Skip to content

Commit e22b4ab

Browse files
committed
feat: Add recursive SVG rendering for styled/overlay graphics
- Add renderGraphicToSvg helper that recursively handles all GraphicTag variants - Fix SVG export for scatter plots wrapped in .styled containers (e.g., via .title) - Add SVG support for overlay composites and area plots - Make all pattern matches explicitly exhaustive without fallthrough
1 parent c47623b commit e22b4ab

22 files changed

+909
-292
lines changed

LeanPlot.lean

Lines changed: 42 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,13 @@
1-
/-!
2-
# LeanPlot
3-
4-
Interactive plotting for Lean 4.
5-
6-
## Main API
7-
8-
Import `LeanPlot.Graphic` for first-class algebraic graphics:
9-
- `plot f` - Create a function plot
10-
- `scatter pts` - Scatter plot from points
11-
- `bar pts` - Bar chart from points
12-
- Algebraic operators: `+` (overlay), `|||` (horizontal facet), `/` (vertical facet)
13-
- Fluent combinators: `.domain`, `.samples`, `.color`, `.title`, etc.
14-
15-
Import `LeanPlot.API` and `LeanPlot.DSL` for legacy plotting functions:
16-
- `plotMany` - Multiple function comparison
17-
- `#plot` - Convenient syntax for quick visualization
18-
19-
## PNG/SVG Export
20-
21-
Import `LeanPlot.Render.Export` for file export:
22-
- `g.savePNG "path.png"` - Save graphic to PNG
23-
- `g.saveSVG "path.svg"` - Save graphic to SVG
24-
25-
## Advanced Features
26-
27-
- `LeanPlot.GrammarOfGraphics` - Grammar of Graphics DSL
28-
- `LeanPlot.Interactive` - Two-way slider widgets
29-
- `LeanPlot.PlotComposition` - Subplot grids and composition
30-
- `LeanPlot.Transform` - Data transformations (log, sqrt, etc.)
31-
- `LeanPlot.Faceting` - Small multiples layouts
32-
33-
## Demos
34-
35-
See `LeanPlot.Demos.*` for example usage.
36-
-/
1+
-- Plot specification system (import first to allow overrides)
2+
import LeanPlot.Specification
3+
import LeanPlot.Plot
374

385
-- Core API (what users should import)
39-
import LeanPlot.Graphic -- First-class algebraic graphics
6+
import LeanPlot.Graphic -- First-class algebraic graphics (overrides some Specification names)
407
import LeanPlot.Interactive -- Two-way slider widgets
418
import LeanPlot.API
429
import LeanPlot.DSL
4310
import LeanPlot.ToFloat
44-
45-
-- Plot specification system
46-
import LeanPlot.Specification
47-
import LeanPlot.Plot
4811
import LeanPlot.Algebra
4912

5013
-- Components layer (Tier-1)
@@ -75,7 +38,7 @@ import LeanPlot.Render.Export
7538
import LeanPlot.Series
7639
import LeanPlot.Core
7740
import LeanPlot.Axis
78-
import LeanPlot.Axes
41+
-- import LeanPlot.Axes -- Duplicate of LeanPlot.Axis, causes conflict
7942
import LeanPlot.Legend
8043
import LeanPlot.AutoDomain
8144
import LeanPlot.Recharts
@@ -84,3 +47,40 @@ import LeanPlot.Utils
8447
import LeanPlot.AssertKeys
8548
import LeanPlot.WarningBanner
8649
import LeanPlot.LegacyLayer
50+
51+
/-!
52+
# LeanPlot
53+
54+
Interactive plotting for Lean 4.
55+
56+
## Main API
57+
58+
Import `LeanPlot.Graphic` for first-class algebraic graphics:
59+
- `plot f` - Create a function plot
60+
- `scatter pts` - Scatter plot from points
61+
- `bar pts` - Bar chart from points
62+
- Algebraic operators: `+` (overlay), `|||` (horizontal facet), `/` (vertical facet)
63+
- Fluent combinators: `.domain`, `.samples`, `.color`, `.title`, etc.
64+
65+
Import `LeanPlot.API` and `LeanPlot.DSL` for legacy plotting functions:
66+
- `plotMany` - Multiple function comparison
67+
- `#plot` - Convenient syntax for quick visualization
68+
69+
## PNG/SVG Export
70+
71+
Import `LeanPlot.Render.Export` for file export:
72+
- `g.savePNG "path.png"` - Save graphic to PNG
73+
- `g.saveSVG "path.svg"` - Save graphic to SVG
74+
75+
## Advanced Features
76+
77+
- `LeanPlot.GrammarOfGraphics` - Grammar of Graphics DSL
78+
- `LeanPlot.Interactive` - Two-way slider widgets
79+
- `LeanPlot.PlotComposition` - Subplot grids and composition
80+
- `LeanPlot.Transform` - Data transformations (log, sqrt, etc.)
81+
- `LeanPlot.Faceting` - Small multiples layouts
82+
83+
## Demos
84+
85+
See `LeanPlot.Demos.*` for example usage.
86+
-/

LeanPlot/CLI/ExportMain.lean

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ open LeanPlot.CLI
88
`leanplot-export` — tiny CLI to dump sampled function data as JSON.
99
1010
Usage:
11-
leanplot-export --fn sin --out out.json [--steps 200] [--min 0.0] [--max 1.0]
12-
-/
11+
leanplot-export --fn sin --out out.json (--steps 200) (--min 0.0) (--max 1.0)
12+
-/
1313
def main (args : List String) : IO Unit := do
1414
-- Minimal float parser supporting `-? [0-9]+ (.[0-9]+)?`.
1515
let parseFloat? (s : String) : Option Float :=

LeanPlot/DSL.lean

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import LeanPlot.Plot
22
import LeanPlot.API
3+
import LeanPlot.Constants
34
import Lean
45
import ProofWidgets.Component.HtmlDisplay
56

@@ -11,6 +12,7 @@ This module implements the ergonomic plotting syntax:
1112
```
1213
#plot (fun x => x^2) -- default 200 samples
1314
#plot (fun t => Float.sin t) using 400
15+
#plot (fun x => x^2) domain=(-2, 2) steps=100 -- named params
1416
```
1517
1618
With doc comments as captions:
@@ -20,6 +22,13 @@ With doc comments as captions:
2022
#plot (fun t => Float.exp (-t) * Float.sin (5 * t)) using 200
2123
```
2224
25+
## Named Parameters
26+
27+
The `#plot` command supports named parameters for full control:
28+
- `domain=(lo, hi)` : x-axis range (default: 0 to 1)
29+
- `steps=n` : number of sample points (default: 200)
30+
- `size=(w, h)` : chart dimensions in pixels (default: 400×300)
31+
2332
We intercept the #plot command and check if the argument looks like
2433
a function. If so, we wrap it with {name}`LeanPlot.API.plot` automatically.
2534
-/
@@ -29,6 +38,7 @@ namespace LeanPlot.DSL
2938
open Lean Elab Command Term
3039
open ProofWidgets
3140
open LeanPlot.PlotCommand (withCaption)
41+
open LeanPlot.Constants
3242

3343
-- Store the original elaborator before removing it
3444
private def originalElabPlotCmd := LeanPlot.PlotCommand.elabPlotCmd
@@ -39,6 +49,61 @@ attribute [-command_elab] LeanPlot.PlotCommand.elabPlotCmd
3949
/-- Syntax for `#plot` with explicit sample count: `#plot f using 400` -/
4050
syntax (name := plotCmdUsing) (docComment)? "#plot " term " using " num : command
4151

52+
/-- Syntax for `#plot` with named parameters: `#plot f domain=(-2, 2) steps=100 size=(500, 300)` -/
53+
syntax (name := plotCmdNamed) (docComment)? "#plot " term
54+
("domain=" "(" term "," term ")")?
55+
("steps=" num)?
56+
("size=" "(" num "," num ")")? : command
57+
58+
/-- Parse a term as a Float, handling negative numbers and various formats -/
59+
private def termToFloat (t : TSyntax `term) (default : Float := 0.0) : Float :=
60+
-- Try natural literal first
61+
match t.raw.isNatLit? with
62+
| some n => n.toFloat
63+
| none =>
64+
-- Try negative number (like -2)
65+
if t.raw.isOfKind `Lean.Parser.Term.app then
66+
-- Check if it's a negation: (- n)
67+
let args := t.raw.getArgs
68+
if args.size >= 2 then
69+
let fn := args[0]!
70+
let arg := args[1]!
71+
if fn.isOfKind `Lean.Parser.Term.paren then
72+
-- Check for prefix negation
73+
match arg.isNatLit? with
74+
| some n => - (n.toFloat)
75+
| none => default
76+
else if fn.getId == ``Neg.neg || toString fn == "-" then
77+
match arg.isNatLit? with
78+
| some n => - (n.toFloat)
79+
| none => default
80+
else default
81+
else default
82+
else if t.raw.isOfKind `Lean.Parser.Term.negNum then
83+
-- Direct negNum syntax (rare but possible)
84+
match t.raw[1]!.isNatLit? with
85+
| some n => - (n.toFloat)
86+
| none => default
87+
else if t.raw.isOfKind `Lean.Parser.Term.paren then
88+
-- Parenthesized expression - try to extract inner
89+
let inner := t.raw[1]!
90+
match inner.isNatLit? with
91+
| some n => n.toFloat
92+
| none => default
93+
else
94+
-- Try to get it as a string and parse manually
95+
let s := t.raw.reprint.getD ""
96+
let trimmed := s.trim
97+
-- Simple integer parsing fallback
98+
if trimmed.startsWith "-" then
99+
match trimmed.drop 1 |>.toNat? with
100+
| some n => - (n.toFloat)
101+
| none => default
102+
else
103+
match trimmed.toNat? with
104+
| some n => n.toFloat
105+
| none => default
106+
42107
/-- Elaborator for the basic `#plot` command. Wraps functions with `LeanPlot.API.plot`. -/
43108
@[command_elab LeanPlot.PlotCommand.plotCmd]
44109
def elabPlotNew : CommandElab := fun stx => do
@@ -88,6 +153,58 @@ def elabPlotUsing : CommandElab := fun stx => do
88153
(return json% { html: $(← Server.rpcEncode finalHtml) })
89154
stx
90155

156+
/-- Elaborator for the `#plot` command with named parameters. -/
157+
@[command_elab plotCmdNamed]
158+
def elabPlotNamed : CommandElab := fun stx => do
159+
-- Extract doc comment, term, and named parameters from syntax
160+
let (doc?, term, loT?, hiT?, stepsN?, widthN?, heightN?) ← match stx with
161+
| `($doc:docComment #plot $t:term $[domain=($lo:term, $hi:term)]? $[steps=$n:num]? $[size=($w:num, $h:num)]?) =>
162+
pure (some doc, t, lo, hi, n, w, h)
163+
| `(#plot $t:term $[domain=($lo:term, $hi:term)]? $[steps=$n:num]? $[size=($w:num, $h:num)]?) =>
164+
pure (none, t, lo, hi, n, w, h)
165+
| _ => throwUnsupportedSyntax
166+
167+
-- Parse domain if provided
168+
let domainOpt : Option (Float × Float) := match loT?, hiT? with
169+
| some lo, some hi => some (termToFloat lo, termToFloat hi)
170+
| _, _ => none
171+
172+
-- Parse steps
173+
let steps := stepsN?.map (·.getNat) |>.getD 200
174+
175+
-- Parse size
176+
let width := widthN?.map (·.getNat) |>.getD defaultW
177+
let height := heightN?.map (·.getNat) |>.getD defaultH
178+
179+
-- Build the wrapped term based on what's provided
180+
let wrappedStx ← match domainOpt with
181+
| some (lo, hi) =>
182+
let loLit := Syntax.mkNumLit (toString lo)
183+
let hiLit := Syntax.mkNumLit (toString hi)
184+
let stepsLit := Syntax.mkNumLit (toString steps)
185+
let wLit := Syntax.mkNumLit (toString width)
186+
let hLit := Syntax.mkNumLit (toString height)
187+
`(LeanPlot.API.plot $term (steps := $stepsLit) (domain := some ($loLit, $hiLit)) (w := $wLit) (h := $hLit))
188+
| none =>
189+
let stepsLit := Syntax.mkNumLit (toString steps)
190+
let wLit := Syntax.mkNumLit (toString width)
191+
let hLit := Syntax.mkNumLit (toString height)
192+
`(LeanPlot.API.plot $term (steps := $stepsLit) (w := $wLit) (h := $hLit))
193+
194+
-- Evaluate it directly as Html
195+
let htX ← liftTermElabM <| HtmlCommand.evalCommandMHtml <| ← ``(ProofWidgets.HtmlEval.eval $wrappedStx)
196+
let ht ← htX
197+
198+
-- Wrap with caption if doc comment present
199+
let finalHtml := match doc? with
200+
| some doc => withCaption doc.getDocString ht
201+
| none => ht
202+
203+
liftCoreM <| Widget.savePanelWidgetInfo
204+
(hash ProofWidgets.HtmlDisplayPanel.javascript)
205+
(return json% { html: $(← Server.rpcEncode finalHtml) })
206+
stx
207+
91208
end LeanPlot.DSL
92209

93210
-- Re-export for convenience

LeanPlot/Demos/AutoAxisLabelsDemo.lean

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import Lean
88
Super simple examples showing how to get beautiful plots with zero effort.
99
Just call `smartLabels` and `smartNames` - that's it!
1010
11-
## TL;DR
12-
- `smartLabels yourFunction` → get nice axis labels
11+
**TL;DR**
12+
- `smartLabels yourFunction` → get nice axis labels
1313
- `smartNames yourFunction` → get enhanced parameter names
1414
- `fixDuplicates yourArray` → fix duplicate names
1515
- Done! 🎉
@@ -41,10 +41,10 @@ def physicsData : Array Json := #[
4141
]
4242

4343
/-- Traditional way - manual axis labels -/
44-
def manualPlot := mkLineChartWithLabels
45-
physicsData
46-
#[("position", "#2563eb")]
47-
(some "Time (s)")
44+
def manualPlot := mkLineChartWithLabels
45+
physicsData
46+
#[("position", "#2563eb")]
47+
(some "Time (s)")
4848
(some "Position (m)")
4949

5050
/-- Demo function for auto axis labels -/
@@ -67,11 +67,11 @@ section SuperEasyExamples
6767

6868
-- Just watch the magic happen:
6969
def myFunction : Expr := Expr.lam `t (Expr.const ``Float []) (Expr.bvar 0) BinderInfo.default
70-
#eval smartNames myFunction -- 👀 Watch: `t` becomes "time"!
70+
#eval smartNames myFunction -- 👀 Watch: `t` becomes "time"!
7171
#eval smartLabels myFunction -- 👀 Watch: Get perfect axis labels!
7272

7373
-- Handle duplicates like a boss:
74-
def messyFunction : Expr :=
74+
def messyFunction : Expr :=
7575
Expr.lam `x (Expr.const ``Float [])
7676
(Expr.lam `y (Expr.const ``Float [])
7777
(Expr.lam `x (Expr.const ``Float []) (Expr.bvar 0) BinderInfo.default)
@@ -113,7 +113,7 @@ def economicsData : Array Json := #[
113113
]
114114

115115
/-- Economics plot with meaningful labels -/
116-
def economicsPlot :=
116+
def economicsPlot :=
117117
mkLineChartWithLabels economicsData #[("demand", "#059669")] (some "Price ($)") (some "Demand")
118118

119119
#html economicsPlot
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import LeanPlot.Interactive
2+
3+
/-!
4+
# Interactive Slider Demo
5+
6+
This demo showcases the 2-way slider widgets for interactive plot parameters.
7+
8+
Each parameter section has sliders that write back to source code when adjusted.
9+
If a parameter is at its default value, clicking "+ Add to source" explicitly
10+
adds it to the command.
11+
12+
## Usage
13+
14+
Hover over the widget output to see the interactive panel with sliders for:
15+
- **Domain** (min, max) - x-axis range
16+
- **Steps** - number of sample points
17+
- **Size** (width, height) - chart dimensions
18+
19+
Dragging any slider automatically updates the source code!
20+
-/
21+
22+
open LeanPlot.Interactive
23+
24+
-- Basic: Just a function, all defaults
25+
-- Try hovering and adjusting sliders - they'll write back to source
26+
#iplot (fun x => x * x)
27+
28+
-- With explicit domain
29+
-- Notice the "explicit" badge on Domain - it's in the source
30+
#iplot (fun x => x * x) domain=(-2, 2)
31+
32+
-- With explicit domain and steps
33+
#iplot (fun x => x * x) domain=(-3, 3) steps=100
34+
35+
-- Fully explicit: all parameters
36+
-- All sections show "explicit" badge
37+
#iplot (fun x => x * x) domain=(-1, 1) steps=200 size=(500, 300)
38+
39+
-- Try with a sine-like pattern (represented as x^3 - x for demo)
40+
#iplot (fun x => x^3 - x) domain=(-2, 2) steps=150
41+
42+
-- With explicit color
43+
#iplot (fun x => x * x) domain=(-1, 1) color="#ff7043"
44+
45+
/-!
46+
## How It Works
47+
48+
1. **Parameter Tracking**: The widget tracks which parameters are explicitly
49+
set in source vs using defaults.
50+
51+
2. **2-Way Binding**: When you drag a slider:
52+
- The widget state updates immediately for responsive feedback
53+
- The source code is updated via `applyEdit` API
54+
- The file re-elaborates, producing fresh data
55+
56+
3. **Progressive Disclosure**: Parameters start with defaults. Clicking
57+
"+ Add to source" makes them explicit, giving you fine control.
58+
59+
This pattern is inspired by the ImageViewer widget in Aloklib, which uses
60+
the same technique for image resize sliders.
61+
-/

0 commit comments

Comments
 (0)