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
python
1from mlx_lm import load, generate
2from mlx_lm.sample_utils import make_sampler
34# Load base model + chart-type adapter5model, tokenizer = load(6"mlx-community/gemma-3-12b-it-4bit",7 adapter_path="John-Williams-ATL/svg-chart-lora/adapters_funnel"8)910# Build prompt (see per_type_system_prompts.py for full system prompt)11messages =[12{"role":"system","content":"You are a funnel chart specialist..."},13{"role":"user","content":"""Chart type: funnel
14Heading: Sales Pipeline
15Pre-computed bars (centered at x=250, use coordinates exactly):
16 'Leads' (1000): x=50.0 y=40 width=400.0 height=44 label at x=250 y=66
17 'Qualified' (600): x=110.0 y=90 width=280.0 height=44 label at x=250 y=116
18 'Proposal' (300): x=170.0 y=140 width=160.0 height=44 label at x=250 y=166
19 'Closed' (120): x=202.0 y=190 width=96.0 height=44 label at x=250 y=216
2021Return ONLY the SVG code."""}22]2324prompt = tokenizer.apply_chat_template(25 messages, tokenize=False, add_generation_prompt=True26)2728sampler = make_sampler(temp=0.1, top_p=0.95)29svg_output = generate(model, tokenizer, prompt=prompt,30 max_tokens=2000, sampler=sampler, verbose=False)3132print(svg_output)# Valid SVG — write to file or pipe to renderer
Switch chart types by changing the adapter path and system prompt:
python
1# Bar chart2model, tokenizer = load("mlx-community/gemma-3-12b-it-4bit",3 adapter_path="John-Williams-ATL/svg-chart-lora/adapters_bar")45# Line chart6model, tokenizer = load("mlx-community/gemma-3-12b-it-4bit",7 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:
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 dy coordinates and a Gemma 4 spatial critic
Spatial critic layer: Gemma 4 26B validates coordinate geometry before training data inclusion
Waterfall: relative dy coordinates 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.