svg-chart-lora v1.0 β SVG Chart Generation LoRA Adapters
12 per-type LoRA adapters for SVG business chart generation, fine-tuned on Gemma 3 12B
Built and published by John Williams / fxops.ai
What This Is
A set of 12 specialist LoRA adapters β one per chart type β that turn Gemma 3 12B into a deterministic SVG chart generator. Given structured JSON input (headings, labels, values), the model produces a valid, renderable SVG chart with correct coordinate geometry, proportional data representation, and consistent visual style.
The entire stack runs on-device. No API calls at inference time. Trained and deployed on an Apple M4 Mac mini with 24GB unified memory.
Chart types: bar Β· line Β· area Β· pie Β· donut Β· funnel Β· scatter Β· bubble Β· grouped_bar Β· stacked_bar Β· waterfall Β· horizontal_bar
Quick Start
from mlx_lm import load, generate
from mlx_lm.sample_utils import make_sampler
# Load base model + chart-type adapter
model, tokenizer = load(
"mlx-community/gemma-3-12b-it-4bit",
adapter_path="John-Williams-ATL/svg-chart-lora/adapters_funnel"
)
# Build prompt (see per_type_system_prompts.py for full system prompt)
messages = [
{"role": "system", "content": "You are a funnel chart specialist..."},
{"role": "user", "content": """Chart type: funnel
Heading: Sales Pipeline
Pre-computed bars (centered at x=250, use coordinates exactly):
'Leads' (1000): x=50.0 y=40 width=400.0 height=44 label at x=250 y=66
'Qualified' (600): x=110.0 y=90 width=280.0 height=44 label at x=250 y=116
'Proposal' (300): x=170.0 y=140 width=160.0 height=44 label at x=250 y=166
'Closed' (120): x=202.0 y=190 width=96.0 height=44 label at x=250 y=216
Return ONLY the SVG code."""}
]
prompt = tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True
)
sampler = make_sampler(temp=0.1, top_p=0.95)
svg_output = generate(model, tokenizer, prompt=prompt,
max_tokens=2000, sampler=sampler, verbose=False)
print(svg_output) # Valid SVG β write to file or pipe to renderer
Switch chart types by changing the adapter path and system prompt:
# Bar chart
model, tokenizer = load("mlx-community/gemma-3-12b-it-4bit",
adapter_path="John-Williams-ATL/svg-chart-lora/adapters_bar")
# Line chart
model, tokenizer = load("mlx-community/gemma-3-12b-it-4bit",
adapter_path="John-Williams-ATL/svg-chart-lora/adapters_line")
Full inference pipeline with geometry pre-computation and 4-stage validation:
β See gemma3_svg_chart_generation_v2_1.ipynb in this repository.
v1.0 Evaluation Results
Evaluated across 20 runs per type at temp=0.1 using an automated scoring harness.
Pre-training baseline included for retrained types (marked *).
| Type | Pass Rate | Before* | Delta | Status |
|---|---|---|---|---|
| area | 100% | 100% | = | β Production |
| bubble | 100% | 100% | = | β Production |
| donut | 100% | 100% | = | β Production |
| funnel | 100% | 100% | = | β Production |
| grouped_bar | 100% | 100% | = | β Production |
| line * | 100% | 0% | +100% | β Production |
| stacked_bar * | 95% | 65% | +30% | β Near production |
| horizontal_bar | 90% | 100% | -10% | β Near production |
| pie | 80% | 90% | -10% | β Near production |
| bar * | 0% | 0% | = | π v2.0 target |
| scatter * | 0% | 0% | = | π v2.0 target |
| waterfall * | 0% | 0% | = | π v2.0 target |
* Retrained in v1.0. Line adapter is the standout result β 0%β100% in one training pass.
Architecture
Why per-type adapters?
Each chart type has distinct failure modes. A single multi-type adapter spreads training signal across 12 geometrically different patterns. Per-type adapters focus each adapter's capacity on one layout β coordinate arithmetic, element ordering, label placement β specific to that chart.
Geometry pre-computation
The inference pipeline pre-computes all coordinate arithmetic in Python before sending to the model. For complex types (donut, pie, scatter, bubble, waterfall), the prompt includes exact SVG path strings, circle positions, and bar coordinates. The model's job is SVG assembly, not trigonometry.
This separates concerns cleanly:
- Python geometry layer β correct math, guaranteed
- Model layer β correct SVG structure and element assembly
4-Stage Validation Pipeline
Every generated SVG passes through GemmaChartValidator before use:
| Gate | Check |
|---|---|
| Gate 0 | Guard against empty output |
| Gate 1 | SVG extraction β strip think blocks, markdown fences |
| Gate 2 | XML parse validation |
| Gate 3 | Content rules β viewBox, coordinate bounds, no expressions |
| Gate 4 | Text overflow repair (x-clamp) |
| Gate 4b | Rect coordinate repair (bar baseline) |
| Gate 4c | Axis line stroke injection |
Base Model & Training
| Parameter | Value |
|---|---|
| Base model | google/gemma-3-12b-it (text variant) |
| Quantisation | BF16 training / 4-bit inference |
| Framework | MLX + mlx-lm |
| Hardware | Apple M4 Mac mini 24GB |
| Adapter type | LoRA, 16 layers |
| Iterations | 300 per type |
| Sequence length | 4096 tokens |
| Training examples | ~156 per type (v1.0, merged original + retrained) |
| Dataset | John-Williams-ATL/svg-chart-training-data |
| Temperature | 0.1 (deterministic at this setting) |
Training data generated using Claude (Anthropic) for initial examples, then migrated to local gemma3:12b via Ollama for scale, and finally to MLX direct inference for the v1.0 retraining pass.
Canvas Specification
All 12 adapters share a fixed output canvas:
viewBox="0 0 500 300" width="500" height="300"
Background : #f5f5f5
Palette : #4a90d9 Β· #e67e22 Β· #27ae60 Β· #8e44ad
Text color : #333333
Font : sans-serif
Origin : top-left (0,0). x rightward, y downward.
Chart area : x=50..480, y=30..270
Baseline : y=270 (vertical bar and waterfall charts)
Repository Structure
adapters_area/
adapters_bar/
adapters_bubble/
adapters_donut/
adapters_funnel/
adapters_grouped_bar/
adapters_horizontal_bar/
adapters_line/
adapters_pie/
adapters_scatter/
adapters_stacked_bar/
adapters_waterfall/
adapter_config.json β LoRA config (rank, alpha, base model path)
adapters.safetensors β trained adapter weights
scripts/
per_type_system_prompts.py β system prompts for all 12 types
generate_training_data.py β dataset generation pipeline (MLX backend)
chart_registry.py β chart type registry
run_per_type_training.sh β MLX LoRA training runner
eval_harness.py β automated 20-run evaluation harness
gemma3_svg_chart_generation_v2_1.ipynb β demo notebook
Known Limitations (v1.0)
- Fixed 500Γ300px canvas β not suitable for responsive outputs without post-processing
- Bar, scatter, waterfall β pass rate 0% in automated eval; geometry is partially correct but fails specific coordinate invariants. Targeted for v2.0 with Gemma 4 teacher/student pipeline
- Grouped bar capped at 3 series β 4+ series overflows the 500px canvas
- Waterfall running total arithmetic is the hardest pattern for autoregressive generation β v2.0 will use relative
dycoordinates and a Gemma 4 spatial critic
Roadmap
v2.0 β in development, targeting Gemma 4:
- Base model: Gemma 4 E4B (student) + Gemma 4 27B MoE (teacher/critic)
- Spatial critic layer: Gemma 4 26B validates coordinate geometry before training data inclusion
- Waterfall: relative
dycoordinates replacing absolute running totals - Canvas: 1000Γ1000 coordinate system aligned with Gemma 4 spatial reasoning
- Eval harness: autoresearch-style temperature sweep per type
Author
John Williams β AI transformation practitioner and independent operator.
fxops.ai Β· HuggingFace Β· X @fxopsAI
Built in public as part of the fxops.ai private AI infrastructure stack. Training methodology, architecture decisions, and operational history documented in Substack series: Teaching a Small Model to Do One Thing Well.
License
Apache 2.0. Base model (Gemma 3) is subject to Google's Gemma Terms of Use.
Quantized