Module:ColinTheCat/Plotter

From Resonite Wiki
Revision as of 23:36, 10 February 2024 by Colin The Cat (talk | contribs)

Lua error at line 28: 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)
LabelX, LabelY 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 the math. 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 28: Missing 'Functions'.


local p = {}

local functions = {
  ["x^2"] = function(x) return x*x end,
  sin = math.sin,
  cos = math.cos,
  tan = math.tan,
  asin = math.asin,
  acos = math.acos,
  atan = math.atan,
  sinh = math.sinh,
  cosh = math.cosh,
  tanh = math.tanh,
  exp = math.exp,
}

local constants = {
  ["1"] = 1,
  pi = math.pi,
  tau = 2 * math.pi,
  e = math.exp(1),
  deg = math.deg(1),
}

function p.Plot2D(frame)
  local fig = mw.html.create("div")

  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",
    },
  }

  local svg = mw.html.create("svg")
    :cssText("width: 100%; stroke-width: 0.01px;")
    :attr("viewBox", string.format("%s %s %s %s", options.from.x, options.from.y, options.to.x, options.to.y))

  draw_grid(svg, options)

  fig:wikitext(tostring(svg))
  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

function draw_grid(svg, options)
  local step_x = eval_math_expr(options.grid_step.x)
  local step_y = eval_math_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 min_x = options.from.x
  local min_y = options.from.y
  local width = options.to.x - options.from.x
  local height = options.to.y - options.from.y
  local path = ""
  local k, op

  -- +x
  k = options.origin.x + step_x
  while(k < options.to.x) do
    path = path.."M"..k.." "..min_y.."V"..height
    k = k + step_x
  end

  -- -x
  k = options.origin.x - step_x
  while(k > options.from.x) do
    path = path.."M"..k.." "..min_y.."V"..height
    k = k - step_x
  end

  -- +y
  k = options.origin.y + step_y
  while(k < options.to.y) do
    path = path.."M"..min_x.." "..k.."H"..width
    k = k + step_x
  end

  -- -y
  k = options.origin.y - step_y
  while(k > options.to.y) do
    path = path.."M"..min_x.." "..k.."H"..width
    k = k - step_x
  end

  svg:tag("path")
    :attr("fill", "#0000")
    :attr("stroke", "#666")
    :attr("d", path)
    :done()
end

function eval_math_expr(expr, x)
  local fact = expr:match("^%s*(%d+)")
  if fact then
    fact = fact..(expr:sub(#fact+1):match("%.%d+") or "")
  end
  local const = expr:sub(fact and #fact+1 or 1):match("^%s*(.-)%s*$")
  fact = fact and tonumber(fact)
  local func = functions[const]
  const = func and func(x or 0) or constants[const]
  if fact == nil and const == nil then error("Invalid expression: "..expr) end
  return (fact or 1) * (const or 1)
end

function translate_point(options, x, y)
  return x, options.to.y-y
end

return p