This is a submission for the Hacktoberfest Open-Source AI Challenge: Week 2 (Side Hustle).
Live demo: https://plotkeep-pi.vercel.app/studio
Code: https://github.com/sherotree/plotkeep
Prize category: Best Use of Gemma.
What I built
Plotkeep is a static web app that turns a pasted table into a chart without sending the table anywhere.
You paste CSV, TSV, or cells copied from a spreadsheet — or a sentence if you only have the shape of the question. The app:
- Parses the sheet in the tab (quoted fields,
$4,200, European decimals, tab-separated paste). - Asks an open-weight Gemma model, running on the device, for a chart specification: type, encodings, title, one-or-two-sentence insight.
- Validates that JSON with Zod against the real column names. If the model wraps the object in markdown, trails a comma, or invents a column called
value, the error goes back to the model (up to three tries). After that, a deterministic heuristic draws anyway. - Renders with Apache ECharts. Export PNG or SVG. Follow-up prompts revise the spec (
make it a pie,title to "June collected").
The product name in the brief was ChartLocal. I called it Plotkeep because the job is not “charts, but local” — it is keeping the plate on the desk.
There is no account, no Plotkeep API, and no telemetry on the CSV. The only network the studio uses, if you opt in, is the first download of Gemma weights (WebLLM / Hugging Face) or a call to 127.0.0.1:11434 if you already run Ollama.
Who would pay for this
A freelance operator who already bought a laptop with a GPU, who pastes client names and invoice totals into a chart generator, and who has noticed that “paste your CSV” often means “POST your CSV.”
That is a real irritation even if I am not going to invent a user interview. Aging reports, hours-by-client, a cafe’s Saturday SKUs — the sheet is small, the names are not public, and a $12/month hosted plotter is the wrong object. The silicon is already on the desk. Charging monthly for tokens the GPU can spend locally is a weird shape.
So the license idea is $29 once, per operator, extra personal machines allowed. This repo does not take payment. The Pro surfaces (saved plates in localStorage, 2×/3× PNG) unlock with the demo code PLOTKEEP-PRO so the paid edge is visible, not vapor. Free forever includes the heuristic engine, local Gemma, Ollama, and 1× export.
I am not claiming conversion rates. I am claiming the billing unit matches the cost structure: you pay for a press, not for inference.
Demo
Live: plotkeep-pi.vercel.app/studio. Repo: sherotree/plotkeep. To run locally:
npm install
npm run dev
Open /studio. The invoice sample is preloaded. Draw chart works with no model — that is deliberate. On this build it suggested a three-series line (invoiced / collected / overdue by month) and wrote “May leads invoiced at 6,100; Feb is the low at 3,100.” Load Gemma only if the browser has WebGPU and you can wait out a ~1.35 GB first fetch (cached after that). Screenshots of that flow live in docs/screenshots.
The screenshots below are real runs of Gemma 2 2B IT (gemma-2-2b-it-q4f16_1-MLC-1k) loaded in-tab via WebLLM, in Chrome on a Mac mini.
What went wrong, and how I fixed it
In my first round of testing, Gemma 2 2B misread the freelancer CSV. Asked "which client grew fastest?", it said "Client B grew the most, increasing from 1800 to 2600." That was wrong: client_c went from 900 to 4100, and client_b ended at 3100, not 2600. It also plotted only client_b.
So Plotkeep no longer trusts the model with arithmetic. For a parsed table it now computes the stats on device, in plain code: first and last value, min and max with their labels, absolute and percent change for each series, which series grew fastest, and the best and worst period. Those facts go into the Gemma prompt as ground truth. Gemma chooses the chart type and encoding and writes the wording. Every numeric series is plotted unless you ask for specific ones. After the model replies, a check compares the insight with the computed facts. If it credits the wrong series, names the wrong peak, or quotes a number that isn't in the table or the stats, the insight is replaced with a sentence built from the facts. The status line shows "facts checked on device" either way. In the reshoot above, Gemma's grounded answer passed the check: "Client C experienced the most significant growth, with a 355.6% increase over the period."
If you are on a machine without WebGPU, stay on the heuristic engine or point Ollama at gemma2:2b. The UI says which of those is true; it does not pretend a cloud fallback exists.
How I built it
Stack: Vite, React, TypeScript, Zod, Apache ECharts, @mlc-ai/web-llm. Gemma 2 2B IT is the default WebLLM id (gemma-2-2b-it-q4f16_1-MLC-1k); Gemma 3 1B is on the menu for smaller VRAM. Gemma 2 has no system role, so instructions are folded into the first user turn.
The useful engineering is not the prompt. It is treating the model as an untrusted spec compiler:
-
Schema first. The chart is a JSON object with
type,x,y, optionalseries,title,insight. The table is not echoed back through the model. That keeps numbers honest and tokens short. - Bind, don’t trust. Column names are resolved case-insensitively and with a little fuzzy match. Unknown fields fail closed.
-
Retries are boring and they work. A 2B instruction model often returns “Sure, here is the JSON” plus a fence.
extractJsonpeels that. Trailing commas get stripped. The next user turn is the Zod error, not a rewritten essay. - A fallback that is not a demo fake. Date-like column + numbers → line. Short mix → pie. Two numeric columns and no labels → scatter. Insight is computed from max/min/share so the caption still names a real row when Gemma is absent or rambling. Follow-ups without a model understand type changes and quoted titles; they do not understand “emphasize March” — that is what the weights are for.
-
Offline after first fetch. Fonts are Fontsource in the bundle (COEP would block Google Fonts anyway). WebLLM needs
COOP/COEPforSharedArrayBuffer; Vite,vercel.json, andpublic/_headersset them. Weights cache in the browser. The heuristic path never needed a network.
I used ECharts instead of Vega-Lite because PNG/SVG export from a plate is a one-liner and the default dark plate looked like letterpress ink rather than a dashboard widget. That is a taste choice, not a moral one.
What is still thin: I did not fine-tune Gemma. The 1k-context build will choke if you paste a 200-column dump — the parser will accept it, the prompt truncates to 20 rows, and the spec may pick the wrong y. Pro is a localStorage flag, not Stripe. Ollama from a static host still needs OLLAMA_ORIGINS. Those are limits, not a backlog dressed up as vision.
Why open innovation matters here
A hosted chart-from-CSV feature is easy, and it is also a copy of the sheet on someone else’s GPU. For this job that is the whole product risk.
Open weights change the economics in four concrete ways:
- Offline. After the first Gemma download, a cafe Wi-Fi dropout does not stop an aging chart.
- Privacy. The rows never become a training example at a plotting vendor, because they never arrive.
- Zero inference cost. The meter is electricity you already bought. That is why the paid thing is a license, not a token pack.
-
Swappable model. The spec schema does not care whether WebLLM loaded Gemma 2 2B, Gemma 3 1B, or Ollama’s
gemma2:2b. If a better open-weight JSON-follower shows up inprebuiltAppConfig, it is a dropdown.
Gemma is the right default for this challenge because it is small enough to contemplate in a tab (2B, or 1B if the laptop is modest) and instruction-tuned enough to emit a short JSON object when you show it the schema and an example. It is not reliable enough to skip validation. Plotkeep’s bet is that validation plus a local model is the product, not a chat window with a screenshot of a chart.
If you bill by the hour and you are tired of uploading the hour sheet to draw a bar, the press is at /studio.




Top comments (0)