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(3x), 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?
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(3x), 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;
}
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?