Rendering and extending
save hides three stages. This page opens them up: what each stage
computes, how to inspect and reuse its output, how a viewer's settings reach
a figure, and how to write a mark of your own.
open Hugin let steps = Nx.linspace Nx.float64 0. 100. 101 let loss = Nx.add_s (Nx.exp (Nx.mul_s steps (-0.05))) 0.1 let fig = line ~x:(num steps) ~y:(num loss) ()
Three stages
view size, theme density
│ │ │
figure ── resolve ──▶ Resolved.t ── layout ──▶ Layout.t ── draw ──▶ Drawing.t
fitted scales panels, ticks picture, warningsresolvefits every scale to the data and checks the figure's composition. It reads neither size nor theme.layoutmeasures text in the theme's faces, chooses ticks, and places the panels and guides on a page of the given size.drawreads the data, reduces marks with more rows than the output has pixels, and paints the figure as a picture.
save runs the three and writes a file. Each stage is a
pure function of its arguments, so its output can be inspected:
let resolved = resolve fig let y = Resolved.scale resolved (Scale.linear ~name:"y" ())
Scale.domain y;;
- : float Scale.domain = Scale.Floats (0.1, 1.1)
let laid = layout (Size.figure 360. 240.) resolved
Layout.size laid;;
- : float * float = (360., 240.)
Each stage takes its previous output as ~prev and returns it unchanged when
its inputs are equal, so a program that redraws a figure on every event pays
only for what changed:
let again = resolve ~prev:resolved fig
again == resolved;;
- : bool = true
Output without files
Drawing.renderable is the picture on its page. The renderers of
hugin.vg.raster, hugin.vg.svg and hugin.vg.pdf turn it into pixels or
documents, here an RGBA tensor and an SVG string:
let drawing = draw ~density:2. laid let pixels = Hugin_vg_raster.render ~density:2. (Drawing.renderable drawing) let svg = Hugin_vg_svg.render (Drawing.renderable drawing)
Nx.shape pixels;;
- : int array = [|480; 720; 4|]
Drawing.warnings lists the warnings of all three stages.
Views
A view holds values that a viewer sets on a figure, such as a slider's
position or a zoom. A key names a value, and bind builds the part of a
figure that reads it:
let smoothing = View.number "smoothing" ~init:0.5 let noisy = Nx.mul loss (Nx.exp (Nx.mul_s (Nx.Rng.normal ~key:(Nx.Rng.key 0) Nx.float64 [| 101 |]) 0.2)) let live = bind smoothing (fun alpha -> line ~y:(num (Nx.ewma ~alpha noisy)) ()) let () = save ~view:View.(set smoothing 0.1 empty) "smoothed.png" live
Every continuous scale has a zoom key without declaring one. Its value sets the scale's domain in place of the fitted one:
let x_zoom = View.zoom (Scale.linear ~name:"x" ()) let () = save ~view:View.(set x_zoom (Some (0., 20.)) empty) "zoomed.png" fig
Views live outside figures and keys are identified by their names, so a figure rebuilt by the same code reads the values a viewer set on the previous one.
Writing a mark
A mark is a list of channel bindings and a function that draws one panel's
rows as a picture. Mark.v makes one, and the built-in marks are made the
same way. This mark draws a needle at each point, in the direction of an
angle that reads no scale:
let angle = Role.value ~name:"angle" let needles ~x ~y ~angle:a () = Mark.v ~name:"needles" [ Mark.bind Role.x x; Mark.bind Role.y y; Mark.bind angle a ] (fun rows -> let xs, ys = Mark.points rows in let th = Option.get (Mark.get rows angle) in let ink = Theme.ink (Mark.theme rows) in let needle i = Picture.stroke (Stroke.v 1.) ink (Path.polyline [| xs.(i); xs.(i) +. (10. *. cos th.(i)) |] [| ys.(i); ys.(i) -. (10. *. sin th.(i)) |]) in Picture.group (List.init (Mark.length rows) needle)) let () = let gx = Nx.reshape [| 1; 12 |] (Nx.linspace Nx.float64 0. 1. 12) in let gy = Nx.reshape [| 8; 1 |] (Nx.linspace Nx.float64 0. 1. 8) in let theta = Nx.mul_s (Nx.add gx gy) 3. in needles ~x:(num gx) ~y:(num gy) ~angle:(num theta) () |> save "needles.png"
Mark.points gives each row's point on the page, in points, with the y
axis pointing down, which is why the needle's tip subtracts the sine. A
dropped row, or one outside the domain, is at NaN, and a path through NaN
leaves a gap, so the mark needs no check of its own. The needles are a figure
like any other: they layer, facet and fit their x and y scales.
this page is tested with mdxedit this page ↗