No edit summary |
No edit summary |
||
Line 158: | Line 158: | ||
function draw_axes(svg, options) | function draw_axes(svg, options) | ||
local tx, ty = create_transform(options.function_space, options.content_space) | local tx, ty = create_transform(options.function_space, options.content_space) | ||
local min_y = options.content_space.y | local min_y = options.content_space.y | ||
local max_x = options.content_space.w + min_x | local max_x = options.content_space.w + min_x | ||
local origin_x, origin_y = tx(options.origin.x), max_y - ty(options.origin.y) | |||
draw_arrow(svg, origin_x, origin_y, max_x, origin_y) | draw_arrow(svg, origin_x, origin_y, max_x, origin_y) | ||
draw_arrow(svg, origin_x, origin_y, origin_x, min_y) | draw_arrow(svg, origin_x, origin_y, origin_x, min_y) |
Revision as of 21:35, 11 February 2024
Lua error at line 6: Missing 'Functions'.
Arguments
Argument | Description | Example | Default |
---|---|---|---|
Plots | Plots as JSON | See #Example | |
From | Lowest x,y to plot | -pi/2,0 |
0,0
|
To | Highest x,y to plot | 10^2,2 |
|
Origin | X/Y Axis origin | 0.5,0.5 |
0,0
|
GridStep | Coordinate grid step size | pi/2,1 |
1,1
|
Inline | Display the plot inline instead of floating | true |
(absent) |
Axis label (not implemented) | |||
LabelStepX | Label for grid X-axis increments | format("%sπ", dfrac(x/pi, 1)) |
format("%d", x)
|
LabelStepY | Label for grid X-axis increments | format("%.1f", y) |
format("%d", x)
|
ColorX | X-axis color | #44f |
#f44
|
ColorY | Y-axis color | #0f4 |
#0f0
|
Underlined Arguments are required.
Available Functions and Constants in Expressions
printf(format, ...args)
See Lua string.format.dfrac(number, is_factor)
Formats a float as a fraction, if possible. If it is a factor, then the number 1 or -1 will only produce its sign as an output, useful when formatting as a factor of pi, for example. See #Example below.sin,floor,abs,...
Any constant or function from the Lua math library can be used directly (without themath.
prefix).
Example
{{#invoke:ColinTheCat/Plotter|Plot2D |From=-0.2, -1.2 |To=2 * pi + 0.2, 1.2 |GridStep=0.5 * pi, 1 |LabelStepX=format("%sπ", dfrac(x/pi, 1)) |Plots= [ {"Type": "function", "Function": "sin(x)", "Samples": 100, "Label": "sin"}, {"Type": "function", "Function": "cos(x)", "Samples": 100, "Label": "cos"}, {"Type": "function", "Function": "tan(x)", "Samples": 35, "Label": "tan", "Ranges": [ {"To": "(0.5-0.2)*pi"}, {"From": "(0.5+0.2)*pi", "To": "(1.5-0.2)*pi"}, {"From": "(1.5+0.2)*pi"} ]} ] }}
Lua error at line 6: Missing 'Functions'.
local p = {}
local Expression = require("Module:ColinTheCat/Expression")
function p.Plot2D(frame)
assert(frame.args.Functions, "Missing 'Functions'")
assert(frame.args.To, "Missing 'To'")
local options = {
functions = mw.text.jsonDecode(frame.args.Functions),
from = parse_xy(frame.args.From or "0 0"),
to = parse_xy(frame.args.To),
origin = parse_xy(frame.args.Origin or "0 0"),
grid_step = {
x=frame.args.GridStepX or "1",
y=frame.args.GridStepY or "1",
},
axis_labels = {
x=frame.args.LabelX or "x",
y=frame.args.LabelY or "y",
}
}
options.function_space = {
x = options.from.x,
y = options.from.y,
w = options.to.x - options.from.x,
h = options.to.y - options.from.y,
}
options.content_space = {
x = 0,
y = 0,
w = 1000,
h = 1000 * options.function_space.h / options.function_space.w
}
-- make space for axis labels
local label_size = {w = 40, h = 20}
local tx, ty = create_transform(options.function_space, options.content_space)
translate_space(
options.content_space,
math.max(0, label_size.w - tx(options.origin.x)),
0
)
local svg = mw.html.create()
draw_grid(svg, options)
draw_axes(svg, options)
local fig = mw.html.create("div")
:wikitext(frame:callParserFunction("#tag:svg", {
tostring(svg),
viewBox = string.format(
"0 0 %s %s",
options.content_space.w + options.content_space.x,
options.content_space.h + math.max(0, label_size.h - ty(options.origin.y))
),
style = "width: 100%; fill: none;",
}))
fig:wikitext(mw.allToString("content_space:", options.content_space.x, options.content_space.y, options.content_space.w, options.content_space.h))
return tostring(fig)
end
function parse_aspect(str)
local i = str:find(":", 1, false)
if i == nil then return tonumber(str) end
local y = tonumber(str:sub(1, i - 1))
local x = tonumber(str:sub(i + 1))
return x/y
end
function parse_xy(str)
local i = str:find(" ", 1, false)
if i == nil then error("Invalid xy: "..str) end
local x = tonumber(str:sub(1, i - 1))
local y = tonumber(str:sub(i + 1))
return { x=x, y=y }
end
-- Drawing
function create_transform(from, to)
-- (x-from.x) * (to.w/from.w) + to.x
-- <=> x * (to.w/from.w) - from.x * (to.w/from.w) + to.x
-- |----sx-----| |-------------tx--------------|
local sx, sy = to.w/from.w, to.h/from.h
local tx = to.x - from.x * sx
local ty = to.y - from.y * sy
local function transform_x(x) return x * sx + tx end
local function transform_y(y) return y * sy + ty end
return transform_x, transform_y, sx, sy
end
function translate_space(space, x, y)
space.x = space.x + x
space.y = space.y + y
space.w = space.w + x
space.h = space.h + y
end
function draw_grid(svg, options)
local step_x = eval_expr(options.grid_step.x)
local step_y = eval_expr(options.grid_step.y)
if step_x <= 0 then error("Invalid x step size") end
if step_y <= 0 then error("Invalid y step size") end
local tx, ty, sx, sy = create_transform(options.function_space, options.content_space)
step_x = step_x * sx
step_y = step_y * sy
local min_x = options.content_space.x
local min_y = options.content_space.y
local max_x = options.content_space.w + min_x
local max_y = options.content_space.h + min_y
local origin_x, origin_y = tx(options.origin.x), max_y - ty(options.origin.y)
local path = ""
local k
-- +x
k = origin_x + step_x
while(k < max_x) do
path = path.."M"..k.." "..min_y.."V"..max_y
k = k + step_x
end
-- -x
k = origin_x - step_x
while(k > min_x) do
path = path.."M"..k.." "..min_y.."V"..max_y
k = k - step_x
end
-- +y
k = origin_y + step_y
while(k < max_y) do
path = path.."M"..min_x.." "..k.."H"..max_x
k = k + step_x
end
-- -y
k = origin_y - step_y
while(k > min_y) do
path = path.."M"..min_x.." "..k.."H"..max_x
k = k - step_x
end
svg:tag("path")
:attr("stroke", "#666")
:attr("stroke-width", "2")
:attr("stroke-linecap", "butt")
:attr("d", path)
end
function draw_axes(svg, options)
local tx, ty = create_transform(options.function_space, options.content_space)
local min_y = options.content_space.y
local max_x = options.content_space.w + min_x
local origin_x, origin_y = tx(options.origin.x), max_y - ty(options.origin.y)
draw_arrow(svg, origin_x, origin_y, max_x, origin_y)
draw_arrow(svg, origin_x, origin_y, origin_x, min_y)
end
function draw_arrow(svg, x1, y1, x2, y2)
local dx, dy = x2 - x1, y2 - y1
local len = math.sqrt(dx*dx + dy*dy)
dx, dy = dx/len, dy/len
local leftx, lefty = dy, -dx
local arrow_size = 4
svg:tag("path")
:attr("stroke", "#fff")
:attr("stroke-width", "4")
:attr("stroke-linecap", "butt")
:attr("d", string.format(
"M%s %sL%s %sM%s %sl%s %sl%s %sl%s %s",
x1, y1,
x2, y2,
x2 + arrow_size * (leftx - dx), y2 + arrow_size * (lefty - dy),
x2, y2,
x2 + arrow_size * (-leftx - dx), y2 + arrow_size * (-lefty - dy)
))
end
function new_object(self, o)
o = o or {}
setmetatable(o, self)
self.__index = self
return o
end
-- Expression Logic
local ExprCtx = {new=new_object}
function ExprCtx:lookup(name)
return self.vars and self.bars[name] or math[name] or error("Unknown variable: "..name)
end
function parse_expr(str)
local tok = Expression.Tokenizer:new{str=str}
local expr = Expression.parse_expr(tok)
if not expr then error("Invalid expression: "..expr) end
if tok:peek() then error("Leftover tokens: "..tok.str) end
return expr
end
function eval_expr(str, vars)
local cx = ExprCtx:new{vars=vars}
return parse_expr(str):eval(cx)
end
return p