RectSLDataVizProof.kt
src/jvmMain/kotlin/afield/examples/rectsl/RectSLDataVizProof.kt · 300 lines
package afield.examples.rectsl
import afield.graphics.color.Color4
import afield.graphics.color.Colormaps
import afield.lang.rectsl.PaintSpec
import afield.lang.rectsl.RectAnimationOracle
import afield.lang.rectsl.SampledRect
import afield.lang.rectsl.SceneProgram
import java.awt.BasicStroke
import java.awt.Color
import java.awt.Font
import java.awt.RenderingHints
import java.awt.geom.AffineTransform
import java.awt.geom.Rectangle2D
import java.awt.image.BufferedImage
import java.io.File
import javax.imageio.ImageIO
import kotlin.math.roundToInt
FIELD_TEST id: rectsl.dataviz.proof runner: jvm-main entrypoint: afield.examples.rectsl.RectSLDataVizProof tags: [smoke, examples, rectsl, dataviz] timeoutSec: 180 expect: exitCode: 0 stdoutContains: - "dataviz proof rendered"
object RectSLDataVizProof
Pixel proof for the RectSL data-visualization family. Two kinds of PNG:
colormaps.png— every ramp drawn straight from Colormaps, so the gradient you look at is the published control-point table (viridis dark-purple → teal → yellow, etc.).<program>.png— the exact worked programs fromrectsl_examples.mdcompiled through the realRectAnimationOracle→SceneProgram.sample(t)pipeline and CPU-rasterized (fill colour + geometry are the real sampled output; only the GPU shader is skipped, which these words never touch).
As a FIELD_TEST it renders to a temp dir and asserts colours/coverage (no committed
writes). Pass an output dir as args[0] (or RECTSL_PROOF_DIR) to regenerate the
committed proof PNGs. Runs headless and exits naturally.
object RectSLDataVizProof {
private val PROGRAMS: List<Pair<String, Double>> = listOf(
"colormap-legend" to 2.0,
"data-bars" to 3.0,
"heatmap-field" to 2.0,
"scatter-plot" to 2.0,
"data-inspect" to 1.0,
"dataviz-demo" to 3.0,
"plot-axes" to 1.0,
)
val PLOT_SOURCE: String
The plotting-layer demo (kept identical to the transcript rectsl_plot_axes.yaml).
val PLOT_SOURCE: String = """
#rectsl
->wall 16 10 abstract
->duration 3
->fps 30
# shared coordinate ->frame: explicit ->domain + auto ->margins, then axes underneath
0 10 0 1 ->domain
->frame
axes
# three series overlaid in the ONE shared ->domain
0 10 40 linspace dup '( 0.7 * sin 1 + 2 / ) data-map "viridis" area
0 10 40 linspace dup '( 0.55 * cos 1 + 2 / ) data-map "plasma" line
0 10 12 linspace dup '( 0.9 * sin 1 + 2 / ) data-map "turbo" points
# a colour-bar legend (numeric labels) and a discrete swatch key
"viridis" colorbar
5 "turbo" legend
""".trimIndent()
val DEMO_SOURCE: String
The loadable-demo program (kept identical to dev_docs/agents/transcripts/rectsl_dataviz.yaml).
body — 69 lines, from line 83
val DEMO_SOURCE: String = """
#rectsl
->wall 20 10 meters
->duration 6
->fps 30
# colour legend across the top: 40 swatches sweeping viridis
"leg" rect 0.5 0.8 ->wh
40 '( fc 9.6 * 4.3 ->offset "viridis" f colormap solid ->fill ) array
;
# bar chart from data, viridis by value, staggered grow-in
'( 4 7 2 9 5 8 3 6 1 6 5 9 4 7 3 8 ) data normalize "viridis" bars
'( '( 0=0 0.7=1 ) ease-out ->alpha ) each
0.05 ->stagger-time
""".trimIndent()
private fun source(name: String): String = when (name) {
"colormap-legend" -> """
#rectsl
->wall 24 4 abstract
->duration 4
->fps 30
"swatch" rect 0.7 3 ->wh
24 '( fc 11 * 0 ->offset "viridis" f colormap solid ->fill ) array
;
""".trimIndent()
"data-bars" -> """
#rectsl
->wall 18 8 abstract
->duration 5
->fps 30
'( 3 7 2 9 5 8 4 6 1 6 ) data normalize "magma" bars
'( '( 0=0 0.6=1 ) ease-out ->alpha ) each
0.07 ->stagger-time
""".trimIndent()
"heatmap-field" -> """
#rectsl
->wall 12 8 abstract
->duration 4
->fps 30
0 1 48 linspace '( 6 * sin 1 + ) data-map 8 6 "turbo" heat
'( '( 0=0 0.5=1 ) ease-in-out ->alpha ) each
0.03 ->stagger-time
""".trimIndent()
"scatter-plot" -> """
#rectsl
->wall 10 10 abstract
->duration 4
->fps 30
0 1 40 linspace dup '( 7 * sin ) data-map "plasma" scatter
""".trimIndent()
"data-inspect" -> """
#rectsl
->wall 16 6 abstract
->duration 2
->fps 30
'( 12 5 27 3 19 8 ) data
dup extent drop drop
dup dmin drop
dup dmax drop
dup data-len drop
dup 2 data-at drop
"coolwarm" bars
""".trimIndent()
"dataviz-demo" -> DEMO_SOURCE
"plot-axes" -> PLOT_SOURCE
else -> error("unknown program $name")
}
fun main(args: Array<String>)
@JvmStatic
fun main(args: Array<String>) {
System.setProperty("java.awt.headless", "true")
val outDir = File(args.getOrNull(0) ?: System.getenv("RECTSL_PROOF_DIR")
?: File(System.getProperty("java.io.tmpdir"), "rectsl-dataviz-proof").path)
outDir.mkdirs()
// 1. Colormap strips — straight from the tables.
val strip = renderColormaps()
val stripFile = File(outDir, "colormaps.png")
ImageIO.write(strip, "png", stripFile)
println("wrote ${stripFile.absolutePath} (${strip.width}x${strip.height})")
assertColormapSanity()
// 2. The real worked programs, sampled + CPU-rasterized.
PROGRAMS.forEach { (name, t) ->
val result = RectAnimationOracle.compile(source(name))
val program = result.programOrNull
check(result.ok && program != null) {
"$name failed to compile:\n" + result.diagnostics.joinToString("\n")
}
val img = renderProgram(program, t)
val file = File(outDir, "$name.png")
ImageIO.write(img, "png", file)
val painted = paintedFraction(img, program.background)
check(painted > 0.01) { "$name rasterized to a near-empty image (painted=$painted)" }
println("wrote ${file.absolutePath} (${img.width}x${img.height}, painted=${"%.3f".format(painted)})")
}
println("dataviz proof rendered to ${outDir.absolutePath}")
}
colormap strip
source — 47 lines, from line 186
private fun renderColormaps(): BufferedImage {
val names = Colormaps.names
val w = 640
val labelW = 96
val rowH = 40
val gap = 10
val pad = 12
val h = pad * 2 + names.size * rowH + (names.size - 1) * gap
val img = BufferedImage(w, h, BufferedImage.TYPE_INT_RGB)
val g = img.createGraphics()
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
g.color = Color(24, 24, 28)
g.fillRect(0, 0, w, h)
g.font = Font(Font.SANS_SERIF, Font.PLAIN, 13)
names.forEachIndexed { row, name ->
val y = pad + row * (rowH + gap)
g.color = Color.WHITE
g.drawString(name, 8, y + rowH / 2 + 5)
val barX = labelW
val barW = w - labelW - pad
for (px in 0 until barW) {
val t = px / (barW - 1).toDouble()
g.color = awt(Colormaps.sample(name, t))
g.fillRect(barX + px, y, 1, rowH)
}
}
g.dispose()
return img
}
private fun assertColormapSanity() {
// viridis: dark purple at 0 → yellow at 1.
val v0 = Colormaps.sample("viridis", 0.0)
val v1 = Colormaps.sample("viridis", 1.0)
check(v0.r < 0.4 && v0.g < 0.2 && v0.b in 0.25..0.5) { "viridis(0) not dark-purple: $v0" }
check(v1.r > 0.8 && v1.g > 0.7 && v1.b < 0.35) { "viridis(1) not yellow: $v1" }
// magma/inferno start near-black.
check(Colormaps.sample("magma", 0.0).let { it.r < 0.1 && it.g < 0.1 && it.b < 0.1 }) { "magma(0) not near-black" }
// coolwarm diverging: blue at 0, ~grey at 0.5, red at 1.
val c0 = Colormaps.sample("coolwarm", 0.0)
val c1 = Colormaps.sample("coolwarm", 1.0)
check(c0.b > c0.r && c0.b > 0.6) { "coolwarm(0) not blue: $c0" }
check(c1.r > c1.b && c1.r > 0.6) { "coolwarm(1) not red: $c1" }
// turbo starts dark blue-purple, ends dark red.
check(Colormaps.sample("turbo", 0.0).b > Colormaps.sample("turbo", 0.0).g) { "turbo(0) not blue-ish" }
println("colormap sanity: viridis/magma/coolwarm/turbo endpoints correct")
}
CPU rasterizer for a sampled scene
source — 65 lines, from line 235
private fun renderProgram(program: SceneProgram, time: Double): BufferedImage {
val aspect = program.wall.width / program.wall.height
val longSide = 900
val (w, h) = if (aspect >= 1.0) longSide to (longSide / aspect).roundToInt()
else (longSide * aspect).roundToInt() to longSide
val img = BufferedImage(w.coerceAtLeast(2), h.coerceAtLeast(2), BufferedImage.TYPE_INT_RGB)
val g = img.createGraphics()
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
g.color = awt(program.background)
g.fillRect(0, 0, img.width, img.height)
g.stroke = BasicStroke(1f)
val sx = img.width / program.wall.width
val sy = img.height / program.wall.height
program.sample(time).rects.forEach { r ->
drawRect(g, r, img.width, img.height, sx, sy)
}
g.dispose()
return img
}
private fun drawRect(g: java.awt.Graphics2D, r: SampledRect, w: Int, h: Int, sx: Double, sy: Double) {
val pw = r.size.x * r.scale.x * sx
val ph = r.size.y * r.scale.y * sy
if (pw <= 0.0 || ph <= 0.0) return
// wall origin at centre, +y up → flip vertical for image space.
val cx = w / 2.0 + r.center.x * sx
val cy = h / 2.0 - r.center.y * sy
val old = g.transform
val tf = AffineTransform()
tf.translate(cx, cy)
tf.rotate(-r.rotation) // negated: image y runs downward
g.transform = tf
g.paint = when (val fill = r.fill) {
is PaintSpec.Solid -> awt(fill.color, r.alpha)
is PaintSpec.LinearGradient -> awt(fill.c0, r.alpha) // approximate with the start colour
}
g.fill(Rectangle2D.Double(-pw / 2.0, -ph / 2.0, pw, ph))
g.transform = old
}
private fun paintedFraction(img: BufferedImage, background: Color4): Double {
val bg = awt(background).rgb and 0xFFFFFF
var painted = 0
val total = img.width * img.height
var i = 0
val step = if (total > 200_000) 4 else 1 // subsample large images
var y = 0
while (y < img.height) {
var x = 0
while (x < img.width) {
if ((img.getRGB(x, y) and 0xFFFFFF) != bg) painted++
x += step
i++
}
y += step
}
return painted.toDouble() / i.coerceAtLeast(1)
}
private fun awt(c: Color4, alphaMul: Double = 1.0): Color {
fun b(v: Double) = (v.coerceIn(0.0, 1.0) * 255.0).roundToInt().coerceIn(0, 255)
return Color(b(c.r), b(c.g), b(c.b), b(c.a * alphaMul))
}
}