Since asking this question, I've made a small (single script) plotting utility.
This could be made into an add-on however I find it useful as-is.

(Updated, tested to work in versions 3.6 .. 4.0).
Usage:
Paste this script into a text-block named blend_plot.py
and press "Run Script".
Running the script will create a blend_plot_func.py
text-block containing an initial plotting template (if you don't have one already). eg:
def plot(x, **kw):
return x
You could change this to, for example:
def plot(x, **kw):
return 1 - ((1 - x) ** 2)
Playback animation (a curve object will be automatically created if you don't have one active already).
You can duplicate the object and move it around, only visible objects are handled, so hiding or moving to disabled layers can avoid extra computations.
If you want to change blend_plot.py
(curve resolution for eg). Simply edit the values and run the script again.
You may want to turn your scene's FPS down, anywhere from 5 to 15 FPS (to reduce CPU usage).
Some benefits of this method:
- Easy to view and change parameters for multiple plots at once (overlay or side-by-side, objects can be moved around since the script only updates object-data).
- Easy drag values with real-time updates as well as using Blender's animation system.
Other details can be found in the code header.
# All in one plotting utility.
#
# - Uses a frame-change handler
# (play back animation for real-time updates as you type)
# - Expects module `blend_plot_func.py`, to have a function called `plot`
# which takes and returns a float and keyword arguments,
# which are taken from custom properties.
# - Will update all objects in the scene
# (supports multiple plots at the same time).
# - Scene and Object custom properties,
# are passed to the plot function as keyword arguments.
# (Scene properties are like `global` variables,
# Object properties can be different for every object).
# Curve resolution
RESOLUTION = 320
# Range to show
RANGE = -1.0, 2.0
# RANGE = 0.0, 1.0
# Framing
MIN = 0.0, 0.0
MAX = 1.0, 1.0
PLOT_MODULE = "blend_plot_func"
PLOT_TEMPLATE = """
# Plot module template
from math import *
def plot(x, **kw):
return x
"""
# End constants
# -------------
import bpy
from importlib import reload
from math import isfinite
def reinit_curve(context, curve):
""" Initialize or re-initialize curve data.
"""
curve.splines.clear()
curve.fill_mode = 'NONE'
# plot spline
spline = curve.splines.new(type='POLY')
spline.points.add(RESOLUTION)
# frame spline
spline = curve.splines.new(type='POLY')
spline.points.add(3)
spline.points[0].co.xy = MIN[0], MIN[1]
spline.points[1].co.xy = MIN[0], MAX[1]
spline.points[2].co.xy = MAX[0], MAX[1]
spline.points[3].co.xy = MAX[0], MIN[1]
spline.use_cyclic_u = True
def init_object(context):
""" Call when we have no active plot-curve.
"""
curve = bpy.data.curves.new(name="Plot Curve", type='CURVE')
reinit_curve(context, curve)
obj = bpy.data.objects.new(name="Plot Curve", object_data=curve)
collection = context.scene.collection
collection.objects.link(obj)
obj.select_set(True)
context.view_layer.objects.active = obj
return obj
def plot_callback_object(scene, obj, plot_cb, kw):
from bpy import context
curve = obj.data
points = curve.splines[0].points if curve.splines else ()
# re-initialize curve if we change settings
if len(points) != RESOLUTION:
reinit_curve(context, curve)
points = curve.splines[0].points
# print(kw)
i_last = float(RESOLUTION - 1)
span = RANGE[1] - RANGE[0]
for i, p in enumerate(points):
x = i / i_last
x = (x * span + RANGE[0])
try:
y = plot_cb(x, **kw)
if not isfinite(y):
y = -1.0
except Exception as ex:
print(str(ex))
y = -1.0
p.co.xy = x, y
def plot_callback(scene):
""" Runs on frame change, updates all objects in the scene.
"""
try:
plot_module = bpy.data.texts[PLOT_MODULE + ".py"].as_module()
except ImportError as ex:
# Create a new text block if this is the first execution.
text_name = PLOT_MODULE + ".py"
text = bpy.data.texts.get(text_name)
if text is None:
text = bpy.data.texts.new(name=text_name)
text.from_string(PLOT_TEMPLATE)
else:
raise ex
plot_cb = plot_module.plot
from bpy import context
obj = context.active_object
if obj is None:
obj = init_object(context)
# use my keywords for all!
kw_global = dict(scene.items())
for obj in context.visible_objects:
if obj.type == 'CURVE':
kw_local = dict(obj.items())
kw_copy = kw_global.copy()
kw_copy.update(kw_local)
plot_callback_object(context, obj, plot_cb, kw_copy)
bpy.app.handlers.frame_change_pre[:] = [plot_callback]