Module:ColinTheCat/Plotter: Difference between revisions

From Resonite Wiki
No edit summary
No edit summary
Line 17: Line 17:
     },
     },
   }
   }
  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.height / options.width}
  options.svg_size = {w = options.content_space.w, h = options.content_space.h}
  if options.from.x > -20 then options.svg_size.w = options.svg_size.w + 20 end
  if options.from.y > -20 then options.svg_size.h = options.svg_size.h + 20 end


   local svg = mw.html.create()
   local svg = mw.html.create()
Line 25: Line 37:
     :wikitext(frame:callParserFunction("#tag:svg", {
     :wikitext(frame:callParserFunction("#tag:svg", {
       tostring(svg),
       tostring(svg),
       viewBox = string.format("%s %s %s %s", options.from.x, options.from.y, options.to.x, options.to.y),
       viewBox = "0 0 1000 "..(1000 * options.height / options.width),
       style = "width: 100%; stroke-width: 0.01px;",
       style = "width: 100%; fill: none;",
     }))
     }))
   return tostring(fig)
   return tostring(fig)
Line 45: Line 57:
   local y = tonumber(str:sub(i + 1))
   local y = tonumber(str:sub(i + 1))
   return { x=x, y=y }
   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 = from.x * (to.w/from.w) + to.x
  local ty = from.y * (to.h/from.h) + to.y
  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
end


Line 52: Line 78:
   if step_x <= 0 then error("Invalid x step size") end
   if step_x <= 0 then error("Invalid x step size") end
   if step_y <= 0 then error("Invalid y 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 tx, ty, sx, sy = create_transform(options.function_space, options.content_space)
   local width = options.to.x - options.from.x
  step_x = step_x * sx
   local height = options.to.y - options.from.y
  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), ty(options.origin.y)
 
   local path = ""
   local path = ""
   local k, op
   local k


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


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


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


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


   svg:tag("path")
   svg:tag("path")
    :attr("fill", "#0000")
     :attr("stroke", "#666")
     :attr("stroke", "#666")
    :attr("stroke-width", "1")
    :attr("stroke-linecat", "butt")
     :attr("d", path)
     :attr("d", path)
     :done()
     :done()
end
end


local ExprBaseCtx = {}
function new_object(self, o)
function ExprBaseCtx:new(o)
   o = o or {}
   o = o or {}
   setmetatable(o, self)
   setmetatable(o, self)
Line 102: Line 135:
end
end


function ExprBaseCtx:lookup(name)
-- 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)
   return self.vars and self.bars[name] or math[name] or error("Unknown variable: "..name)
end
end
Line 115: Line 151:


function eval_expr(str, vars)
function eval_expr(str, vars)
   local cx = ExprBaseCtx:new{vars=vars}
   local cx = ExprCtx:new{vars=vars}
   return parse_expr(str):eval(cx)
   return parse_expr(str):eval(cx)
end
function translate_point(options, x, y)
  return x, options.to.y-y
end
end


return p
return p

Revision as of 20:20, 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)
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 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",
    },
  }

  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.height / options.width}
  options.svg_size = {w = options.content_space.w, h = options.content_space.h}

  if options.from.x > -20 then options.svg_size.w = options.svg_size.w + 20 end
  if options.from.y > -20 then options.svg_size.h = options.svg_size.h + 20 end

  local svg = mw.html.create()

  draw_grid(svg, options)

  local fig = mw.html.create("div")
    :wikitext(frame:callParserFunction("#tag:svg", {
      tostring(svg),
      viewBox = "0 0 1000 "..(1000 * options.height / options.width),
      style = "width: 100%; fill: none;",
    }))
  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 = from.x * (to.w/from.w) + to.x
  local ty = from.y * (to.h/from.h) + to.y
  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 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), 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", "1")
    :attr("stroke-linecat", "butt")
    :attr("d", path)
    :done()
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