Skip to content

Feature: Add command-driven plot(...) that renders to PNG and inserts into Word #1

@JohnnyTheCoder1

Description

@JohnnyTheCoder1

Add a text-based plotting workflow to WordMat so users can type commands like:
plot(sin(x), -2π, 2π)
plot(x^2, -5, 5; title="Parabola", grid=true, width=600, height=400, dpi=150)
plot(sin(x), -10, 10; backend="gnuplot")

When the user runs WordMat → Plot selection (or a keyboard shortcut), WordMat:

Parses the selected text for a plot(...) command.

Calls an external backend (Python/Matplotlib or Gnuplot) to render a PNG.

Inserts the PNG at the cursor (inline image) with optional caption/alt text.

This provides a command-driven experience (like MATLAB/Octave/Python) while keeping deliverables inside Word.

Motivation

Current plotting is GUI-driven (dialogs/GeoGebra). Some users prefer commands over UI.

Reproducibility: the plot(...) text in the doc becomes a source of truth you can re-plot at any time.

Faster iteration for assignments and reports that require many simple function plots.

User stories

As a student, I want to type plot(sin(x), -2π, 2π) and get a clean plot in my document without leaving Word.

As a power user, I want to choose Matplotlib or Gnuplot as the rendering backend and control size/DPI/grid/title via keywords.

As a grader/teacher, I want to re-generate plots from the original plot(...) commands embedded in the doc for verification.

Non-goals (v1)

No 3D plots in v1.

No multi-axis or subplots (one axes per command).

No inline live previews; only generated PNGs.

Not parsing arbitrary OMath/Equation objects (v1 expects plain text selection).

UX / UI

Ribbon: Add Plot selection button under WordMat → Graphs (or new Commands group).

Keyboard shortcut: Alt+W, P (or configurable).

Options dialog (new “Plotting” tab):

Backend: Matplotlib (Python) | Gnuplot

Backend path(s): Python executable path, Gnuplot executable path

Defaults: width (px), height (px), dpi, grid on/off, line width

Timeout (seconds)

Keep temp images (debug) on/off

Context menu: (optional) “Plot selection with WordMat”.

Command syntax (v1)
EBNF (minimal)
plot = "plot" "(" expr "," xmin "," xmax [ ";" options ] ")"
expr = function of x using allowed tokens (see whitelist)
xmin = number | constant | expression using pi
xmax = number | constant | expression using pi
options = option { "," option }
option = key "=" value
key = "title" | "grid" | "width" | "height" | "dpi" | "backend" | "linewidth"
value = string | number | boolean

Examples
plot(sin(x), -2π, 2π)
plot(x^2 + 2x + 1, -10, 10; title="Quadratic", grid=true, dpi=200)
plot(sin(x)/x, -20, 20; linewidth=2)
plot(cos(3
x), 0, 10; backend="gnuplot", width=800, height=400)

Allowed functions/tokens (v1)

Constants: pi (and π), e

Functions: sin, cos, tan, asin, acos, atan, exp, log, ln, sqrt, abs

Operators: + - * / ^

Parentheses and whitespace

Security: No raw eval on user input. Map tokens to a whitelist of math functions and parse safely.

High-level design
Flow

Get selection: If selection text matches plot(…), proceed; else show a helpful error.

Parse: Use a small hand-rolled parser or a simple tokenizer to extract expr, xmin, xmax, options.

Normalize: Replace π with pi, ^ with ** (for Python), etc.

Backend dispatch:

If backend option set, honor it; otherwise use global default from Options.

Render:

Call external process (Python or Gnuplot) with a generated script and a temp output PNG path.

Insert:

Use Word COM: InlineShapes.AddPicture(tempPng, LinkToFile: false, SaveWithDocument: true).

Optionally set width/height in points to match pixels at 96 DPI, or scale proportionally.

Cleanup: Delete temp script (unless “Keep temp” is enabled).

Project structure (suggested)

Plotting/PlotCommandHandler.cs — entry point from Ribbon/shortcut

Plotting/Parser/PlotParser.cs — tokenizer + AST + options

Plotting/Backends/IPlotBackend.cs — interface

Plotting/Backends/MatplotlibBackend.cs

Plotting/Backends/GnuplotBackend.cs

Plotting/Word/WordInserter.cs — image insertion utilities

Options/PlotOptionsPage.cs — options UI

Word integration details (C# / VSTO)
// Pseudocode
void PlotSelection()
{
var sel = Globals.ThisAddIn.Application.Selection;
string text = sel?.Range?.Text?.Trim() ?? string.Empty;

if (!PlotParser.LooksLikePlotCommand(text))
{
    ShowError("Select a plot(...) command, e.g. plot(sin(x), -2π, 2π).");
    return;
}

var cmd = PlotParser.Parse(text); // throws ParseException with good messages
var backend = BackendFactory.Resolve(cmd.Options.Backend ?? Options.DefaultBackend);

string tempPng = Path.Combine(Path.GetTempPath(), $"wordmat_plot_{Guid.NewGuid()}.png");
backend.Render(cmd, tempPng, Options);

// Insert inline and scale
var range = sel.Range;
var shape = range.InlineShapes.AddPicture(tempPng, LinkToFile: false, SaveWithDocument: true);
if (cmd.Options.WidthPx.HasValue)
    shape.Width = PixelsToPoints(cmd.Options.WidthPx.Value);
if (cmd.Options.HeightPx.HasValue)
    shape.Height = PixelsToPoints(cmd.Options.HeightPx.Value);

// Optional: add caption / alt text using cmd.Options.Title
if (!string.IsNullOrWhiteSpace(cmd.Options.Title))
    shape.AlternativeText = cmd.Options.Title;

if (!Options.KeepTempFiles) SafeDelete(tempPng);

}

Backend: Matplotlib (Python)
Invocation strategy

Generate a temporary Python script (or pass via -c), run with user-configured Python.

Dependencies: numpy, matplotlib. Provide detection + helpful error if missing.

Honor width/height/dpi/grid/title/linewidth.

Minimal Python script (template)

generated by WordMat

import math, sys, json
import numpy as np
import matplotlib.pyplot as plt

cfg = json.loads(sys.argv[1]) # expr, xmin, xmax, width, height, dpi, title, grid, linewidth, outfile

Safe namespace

ALLOWED_FUNCS = {
"sin": np.sin, "cos": np.cos, "tan": np.tan,
"asin": np.arcsin, "acos": np.arccos, "atan": np.arctan,
"exp": np.exp, "log": np.log, "ln": np.log, "sqrt": np.sqrt, "abs": np.abs
}
ALLOWED_CONSTS = {"pi": math.pi, "e": math.e}
SAFE_NAMES = {**ALLOWED_FUNCS, **ALLOWED_CONSTS}

def safe_eval(expr, x):
# expr uses np ops; '^' already normalized to '**'
return eval(expr, {"builtins": {}}, {**SAFE_NAMES, "x": x})

xmin, xmax = float(cfg["xmin"]), float(cfg["xmax"])
N = 2000
x = np.linspace(xmin, xmax, N)
y = safe_eval(cfg["expr"], x)

plt.figure(figsize=(cfg["width"]/96.0, cfg["height"]/96.0), dpi=cfg["dpi"])
plt.plot(x, y, linewidth=cfg.get("linewidth", 1.5))
if cfg.get("grid", False): plt.grid(True)
if cfg.get("title"): plt.title(cfg["title"])
plt.tight_layout()
plt.savefig(cfg["outfile"], dpi=cfg["dpi"])

Pass config via JSON argument to avoid shell escaping issues.

Backend: Gnuplot
Invocation strategy

Generate a temporary .gp script and call gnuplot with it.

Map options: set terminal pngcairo size {width},{height} fontscale 1.0

Example script:

set terminal pngcairo size {width},{height} enhanced
set output "{outfile}"
set samples 2000
set grid {gridFlag}
set title "{title}"
set xrange [{xmin}:{xmax}]
plot {expr} with lines linewidth {linewidth}

Expression normalization: sin(x), cos(x), pi are native; convert ^ to ** if needed (gnuplot supports **).

If missing gnuplot, show actionable error with download hint.

Parser implementation notes

Tokenize: identifiers, numbers (allow π → pi), operators, parens, commas, semicolon/options.

Replace ^ → ** for Python; keep ** for gnuplot (works too).

Validate identifiers against whitelist.

Clear error messages:

“Unknown function ‘sinn’. Did you mean ‘sin’?”

“Range must be numbers or use pi (e.g., -2π, 2π).”

“Unbalanced parentheses near …”

Error handling & edge cases

No backend found: “Python not found at …” / “Gnuplot not found …”. Offer to open Options.

Timeout: Kill process; show “Plotting took longer than Xs (configurable).”

Parse errors: Point to the column; include a small caret marker in the message.

Bad math (NaN/inf): Warn if the output contains NaN/inf values; still render if reasonable.

Non-ASCII/pi symbol: Accept both pi and π.

Security: No arbitrary code execution. Strict whitelist.

Performance

Launching Python repeatedly has overhead; acceptable for v1.

Future improvement: keep a small plot worker process running (named pipe/stdio) for batch plots.

Telemetry (optional)

Count backend choice and success/failure codes (no PII).

Toggle in Options.

Accessibility / i18n

Insert alt text from title, or “Function plot of {expr} from {xmin} to {xmax}”.

Localize UI strings; accept both ; and , as option separators if locale demands.

Licensing

Ensure compatibility with Matplotlib/Numpy (BSD) and Gnuplot (various).

Do not bundle Python without checking redistribution terms. Gnuplot may be easier to bundle as a portable binary depending on platform.

Testing plan

Unit tests for parser:

plot(sin(x), -2π, 2π)

plot(x^2 + 2*x + 1, -10, 10; grid=true, dpi=200)

Unknown func, bad range, unbalanced parens.

Integration tests (behind a flag) that stub the backend and assert a PNG is created and inserted.

Manual smoke:

Big ranges, tiny ranges

High DPI

Missing backend

Non-ASCII π

Definition of Done

Ribbon button + shortcut added.

Options page for backend + defaults.

Parser supports v1 grammar + good errors.

Matplotlib backend implemented and documented.

Gnuplot backend implemented and documented.

PNG inserted inline with sizing + optional alt text from title.

Tests for parser + basic integration.

README section: “Command-driven plotting (plot(...))”.

Tasks / Checklist

Add Plot selection command to Ribbon.

Implement PlotParser with whitelist and helpful diagnostics.

Implement IPlotBackend + MatplotlibBackend.

Implement GnuplotBackend.

Add Options UI and persistence.

Implement WordInserter (image insertion + sizing).

Add errors/telemetry hooks.

Parser unit tests.

Backend smoke tests (guarded).

Docs + GIF in README.

Open questions

Should we support multiple curves in v1? e.g., plot(sin(x), -10, 10; also=cos(x))

Should we automatically convert an inserted plot back to the command (round-trip)? (Probably v2)

Provide a “Re-plot” button that reuses last command with current options?

Do we want a “Copy as LaTeX/TikZ” export later?

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions