[Ipe-discuss] plot trigonometric functions

Jan Hlavacek jhlavace at svsu.edu
Thu Nov 5 16:58:26 CET 2009


On Thu, Nov 05, 2009 at 10:14:49AM +0000, Tim Hutt wrote:
> 
>    You can also use matlab if you have it. I wrote a page on. The wiki
>    explaining how to export matlap figures to ipe. Alternatively it's
>    probably quite simple to write an ipelet to plot a funtion directly.

I am attaching a first version of an ipelet that does that.  It is my first
attempt at lua programming, so there are probably many places where things can
be done better.  Also, the code could use quite a bit of reorganization, at
this stage I just wanted to quickly have something working. 

There are brief instructions in the file.  Hopefully this will be useful for
somebody. 

-- 
Jan Hlavacek (jhlavace at svsu.edu, (989) 964-2004)
Department of Mathematical Sciences, Saginaw Valley State University
http://www.svsu.edu/~jhlavace/
-------------- next part --------------
----------------------------------------------------------------------
-- plot ipelet
----------------------------------------------------------------------
--[[
 
   This file is an extension of the drawing editor Ipe (ipe7.sourceforge.net)

   Copyright (c) 2009 Jan Hlavacek

   This file can be distributed and modified under the terms of the GNU General
   Public License as published by the Free Software Foundation; either version
   3, or (at your option) any later version.

   This file is distributed in the hope that it will be useful, but WITHOUT ANY
   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
   FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
   details.

   You can find a copy of the GNU General Public License at
   "http://www.gnu.org/copyleft/gpl.html", or write to the Free
   Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

   Basic documentation (bug: better documentation needs to be written)

   All functions provided by this ipelet use a selection to describe the plot
   "viewport" on the page.  The only thing that is used from the selection is
   its bounding rectangle.  This rectangle will represent the "viewport" of the
   plot on your page. Each function will present a dialog where you, in
   addition to other things, specify the corresponding plot coordinates for
   this viewport. 

   For example, assume that you start with a selection that is a rectangle.
   You choose Parametric plot, type in cos(t) and sin(t) for x and y, t from
   -3.14 to 3.14, and viewport coordinates as x from -1 to 1, y from -1 to 1. 

   This will make the originally selected rectangle represent a part of
   coordinate plane with corners (-1,-1), (-1,1), (1,1) and (1,-1), and, in
   effect, draw an inscribed ellipse into the rectangle. 

   There is no clipping going on right now, so if your plot is larger than the
   specified viewport, it will simply stick out of the rectangle. 

   After creating a coordinate system with the "Coordinate system" menu item,
   you can use this coordinate system as your initial selection for your plots,
   and they will be correctly placed on this coordinate system. 

   When creating coordinate system, you can specify location of ticks on both
   axes.  Here you can use expressions such as pi, 2*pi, etc.  If you leave
   this empty, ticks will be placed at every integer. Ticks will only be drawn
   if the tick size is not 0. 

   I am planning to write a more expensive documentation for this.  In the mean
   time, if you have questions, please contact me at jhlavace at svsu.edu. Also
   please contact me if you have any suggestions for improvement. 

   This is my first attempt to write something in lua, and it has been put
   together quite in a hurry, so I am sure there are lot of places where things
   can be done better. 

   Future plans:  Add coordinate grid generating function, make general lua
   math expressions work at other places than just tick lists (so you don't
   have to enter things like 3.14 for t), add polar plots, ...

--]]

label = "Plots"

about = [[
Parametric curves, plots of functions, coordinate systems
]]

local function isfinite(x)
   return (x > - math.huge) and (x < math.huge)
end

local function bounding_box(p)
  local box = ipe.Rect()
  for i,obj,sel,layer in p:objects() do
    if sel then box:add(p:bbox(i)) end
  end
  return box
end

local function get_number (model,string, error_msg) -- this should eventually use loadstring to parse things like "2*pi" and "arcsin(.7)" etc. 
   if string == "" then
      model:warning ("You need to specify " .. error_msg)
      return
   end
   local num = tonumber(string)
   if not num then
      model:warning(string .. " is not a valid value for " .. error_msg)
      return
   end
   return num
end

function curve(model)
  local box = bounding_box(model:page())
  if box:isEmpty() then 
    model:warning("Selection seems to be empty")
    return
  end

  -- canvas coordinates
  local cstart = box:bottomLeft()
  local cend = box:topRight()
  local cdif = cend-cstart
  local cxlen = cdif.x
  local cylen = cdif.y

  local d = ipeui.Dialog(model.ui, "Parametric plot")
  d:add("label1", "label", {label="Enter parametric equations. Use t as a parameter."}, 1, 1, 1, 4)
  d:add("label2", "label", {label="x="}, 2, 1)
  d:add("xeq", "input", {}, 2, 2, 1, 3)
  d:add("label3", "label", {label="y="}, 3, 1)
  d:add("yeq", "input", {}, 3, 2, 1, 3)
  d:add("label4", "label", {label="Set the domain for t:"}, 4, 1, 1, 4)
  d:add("label5", "label", {label="from:"}, 5, 1, 1, 1)
  d:add("tfrom", "input", {}, 5, 2, 1, 1)
  d:add("label6", "label", {label="to:"}, 5, 3, 1, 1)
  d:add("tto", "input", {}, 5, 4, 1, 1)
  d:add("label7", "label", {label="Set coordinates for viewport:"}, 6, 1, 1, 4)
  d:add("label8", "label", {label="from x="}, 7, 1, 1, 1)
  d:add("xfrom", "input", {}, 7, 2, 1, 1)
  d:add("label9", "label", {label="to x="}, 7, 3, 1, 1)
  d:add("xto", "input", {}, 7, 4, 1, 1)
  d:add("label10", "label", {label="from y="}, 8, 1, 1, 1)
  d:add("yfrom", "input", {}, 8, 2, 1, 1)
  d:add("label11", "label", {label="to y="}, 8, 3, 1, 1)
  d:add("yto", "input", {}, 8, 4, 1, 1)
  d:add("ok", "button", {label="&Ok", action="accept"}, 9, 4)
  d:add("cancel", "button", {label="&Cancel", action="reject"}, 9, 3)
  d:setStretch("column", 2, 1)
  d:setStretch("row", 5, 1)
  if xeqstore then d:set("xeq",xeqstore) end
  if yeqstore then d:set("yeq",yeqstore) end
  if x0store then d:set("xfrom",x0store) end
  if x1store then d:set("xto",x1store) end
  if y0store then d:set("yfrom",y0store) end
  if y1store then d:set("yto",y1store) end
  if t0store then d:set("tfrom",t0store) end
  if t1store then d:set("tto",t1store) end
  if not d:execute() then return end
  local s1 = d:get("xeq")
  local s2 = d:get("yeq")
  xeqstore = s1
  yeqstore = s2
  x0store = d:get("xfrom")
  x1store = d:get("xto")
  y0store = d:get("yfrom")
  y1store = d:get("yto")
  t0store = d:get("tfrom")
  t1store = d:get("tto")

  -- real coordinates
  local x0 = get_number(model,x0store,"lower x limit")
  if not x0 then return end
  local x1 = get_number(model,x1store,"upper x limit")
  if not x1 then return end
  local y0 = get_number(model,y0store,"lower y limit")
  if not y0 then return end
  local y1 = get_number(model,y1store,"upper y limit")
  if not y1 then return end

  -- parameter
  local t0 = get_number(model,t0store,"initial value of t")
  if not t0 then return end
  local t1 = get_number(model,t1store,"final value of t")
  if not t0 then return end

  -- check validity of t limits:
  if t0 > t1 then
     t0, t1 = t1, t0
  end
  if t0 == t1 then
     model:warning("Limits for t cannot be equal")
     return
  end

  -- check validity of x and y limits:
  if x0 > x1 then
     x0, x1 = x1, x0
  end
  if x0 == x1 then
     model:warning("Limits for x cannot be equal")
     return
  end
  if y0 > y1 then
     y0, y1 = y1, y0
  end
  if y0 == y1 then
     model:warning("Limits for y cannot be equal")
     return
  end

  -- scaling calculations
  local start = ipe.Vector(x0,y0)
  local xlen = x1-x0
  local ylen = y1-y0
  local scalem = ipe.Matrix(cxlen/xlen,0,0,cylen/ylen)
  local trans = ipe.Translation(cstart-scalem*start) * scalem
  local tlen = t1-t0
  local t = t0

  -- create user function
  local coordstr = s1 .. "," .. s2
  local mathdefs = "local abs = math.abs; local acos = math.acos; local asin = math.asin; local atan = math.atan; local atan2 = math.atan2; local ceil = math.ceil; local cos = math.cos; local cosh = math.cosh; local deg = math.deg; local exp = math.exp; local floor = math.floor; local fmod = math.fmod; local log = math.log; local log10 = math.log10; local max = math.max; local min = math.min; local modf = math.modf; local pi = math.pi; local pow = math.pow; local rad = math.rad; local sin = math.sin; local sinh = math.sinh; local sqrt = math.sqrt; local tan = math.tan; local tanh = math.tanh;"

  coordstr =  mathdefs .. "return function (t) local v = ipe.Vector(" .. coordstr .. "); return v end"
  local f,err = _G.loadstring(coordstr,"parametric_plot")
  if not f then
     model:warning("Could not compile coordinate functions")
     return
  end

  local curve = { type="curve", closed=false }
  local v0 = trans*f()(t)
  local v1 = v0
  for i = 1,100 do
     t = t + tlen/100
     v1 = trans*f()(t)
     curve[#curve + 1] = { type="segment", v0, v1 }
     v0 = v1
  end

  local graph = ipe.Path(model.attributes, { curve } )
  model:creation("create graph", graph)
end

function func_plot(model)
   local box = bounding_box(model:page())
   if box:isEmpty() then 
      model:warning("Selection seems to be empty")
      return
   end

   -- canvas coordinates
   local cstart = box:bottomLeft()
   local cend = box:topRight()
   local cdif = cend-cstart
   local cxlen = cdif.x
   local cylen = cdif.y

   local d = ipeui.Dialog(model.ui, "Function Plot")
   d:add("label1", "label", {label="Enter y as a function of x"}, 1, 1, 1, 4)
   d:add("label2", "label", {label="y="}, 2, 1)
   d:add("xeq", "input", {}, 2, 2, 1, 3)
   d:add("label4", "label", {label="Set the domain for x:"}, 3, 1, 1, 4)
   d:add("label5", "label", {label="from:"}, 4, 1, 1, 1)
   d:add("tfrom", "input", {}, 4, 2, 1, 1)
   d:add("label6", "label", {label="to:"}, 4, 3, 1, 1)
   d:add("tto", "input", {}, 4, 4, 1, 1)
   d:add("label7", "label", {label="Set coordinates for viewport:"}, 5, 1, 1, 4)
   d:add("label8", "label", {label="from x="}, 6, 1, 1, 1)
   d:add("xfrom", "input", {}, 6, 2, 1, 1)
   d:add("label9", "label", {label="to x="}, 6, 3, 1, 1)
   d:add("xto", "input", {}, 6, 4, 1, 1)
   d:add("label10", "label", {label="from y="}, 7, 1, 1, 1)
   d:add("yfrom", "input", {}, 7, 2, 1, 1)
   d:add("label11", "label", {label="to y="}, 7, 3, 1, 1)
   d:add("yto", "input", {}, 7, 4, 1, 1)
   d:add("ok", "button", {label="&Ok", action="accept"}, 8, 4)
   d:add("cancel", "button", {label="&Cancel", action="reject"}, 8, 3)
   d:setStretch("column", 2, 1)
   d:setStretch("row", 5, 1)
   if fstore then d:set("xeq",fstore) end
   if x0store then d:set("xfrom",x0store) end
   if x1store then d:set("xto",x1store) end
   if y0store then d:set("yfrom",y0store) end
   if y1store then d:set("yto",y1store) end
   if dom0store then d:set("tfrom",dom0store) end
   if dom1store then d:set("tto",dom1store) end
   if not d:execute() then return end
   local s1 = d:get("xeq")
   fstore = s1
   x0store = d:get("xfrom")
   x1store = d:get("xto")
   y0store = d:get("yfrom")
   y1store = d:get("yto")
   dom0store = d:get("tfrom")
   dom1store = d:get("tto")

   -- real coordinates
   local x0 = get_number(model,x0store,"lower x limit")
   if not x0 then return end
   local x1 = get_number(model,x1store,"upper x limit")
   if not x1 then return end
   local y0 = get_number(model,y0store,"lower y limit")
   if not y0 then return end
   local y1 = get_number(model,y1store,"upper y limit")
   if not y1 then return end

   -- independent variable
   local t0 = get_number(model,dom0store,"initial value of x")
   if not t0 then return end
   local t1 = get_number(model,dom1store,"final value of x")
   if not t0 then return end

   -- check validity of t limits:
   if t0 > t1 then
      t0, t1 = t1, t0
   end
   if t0 == t1 then
      model:warning("Limits for x cannot be equal")
      return
   end

   -- check validity of x and y limits:
   if x0 > x1 then
      x0, x1 = x1, x0
   end
   if x0 == x1 then
      model:warning("Limits for x cannot be equal")
      return
   end
   if y0 > y1 then
      y0, y1 = y1, y0
   end
   if y0 == y1 then
      model:warning("Limits for y cannot be equal")
      return
   end

   -- scaling calculations
   local start = ipe.Vector(x0,y0)
   local xlen = x1-x0
   local ylen = y1-y0
   local scalem = ipe.Matrix(cxlen/xlen,0,0,cylen/ylen)
   local trans = ipe.Translation(cstart-scalem*start) * scalem
   local tlen = t1-t0
   local t = t0

   -- create user function
   local coordstr = s1
   local mathdefs = "local abs = math.abs; local acos = math.acos; local asin = math.asin; local atan = math.atan; local atan2 = math.atan2; local ceil = math.ceil; local cos = math.cos; local cosh = math.cosh; local deg = math.deg; local exp = math.exp; local floor = math.floor; local fmod = math.fmod; local log = math.log; local log10 = math.log10; local max = math.max; local min = math.min; local modf = math.modf; local pi = math.pi; local pow = math.pow; local rad = math.rad; local sin = math.sin; local sinh = math.sinh; local sqrt = math.sqrt; local tan = math.tan; local tanh = math.tanh;"

   coordstr =  mathdefs .. "return function (x) local v = ipe.Vector(x," .. coordstr .. "); return v end"
   -- attempt to load this string.  Give a warning and quit if it fails. 
   local f,err = _G.loadstring(coordstr,"function plot")
   if not f then
      model:warning(err) -- bug: error messages will be cryptic
      return
   end
   -- execute the function obtained from the string.  That should create the
   -- actual function usable for our calculations.  Warn and quit if it fails.
   stat,f = _G.pcall(f)
   if not stat then
      model:warning(f) -- bug: error messages will be cryptic
      return
   end

   local curve = { type="curve", closed=false }
   local v0
   -- try to evaluate the function.  Warn and quit if it fails. 
   stat, v0 = _G.pcall(f,t)
   if not stat then
      model:warning(v0) -- bug: error messages will be cryptic
      return
   end
   if not isfinite(v0.x*v0.y) then
      model:warning("domain error")
      return
   end
   v0 = trans*v0
   local v1 = v0
   for i = 1,99 do
      t = t + tlen/99
      stat, v1 = _G.pcall(f,t)
      if not stat then
	 model:warning(v1) -- bug: error messages will be cryptic
	 return
      end
      if not isfinite(v1.x*v1.y) then
	 model:warning("domain error")
	 return
      end
      v1 = trans*v1
      curve[#curve + 1] = { type="segment", v0, v1 }
      v0 = v1
   end

   local graph = ipe.Path(model.attributes, { curve } )
   model:creation("create graph", graph)
end

function make_axes(model)
   local box = bounding_box(model:page())
   if box:isEmpty() then 
      model:warning("Selection seems to be empty")
      return
   end

   -- canvas coordinates
   local cstart = box:bottomLeft()
   local cend = box:topRight()
   local cdif = cend-cstart
   local cxlen = cdif.x
   local cylen = cdif.y

   local d = ipeui.Dialog(model.ui, "Coordinate System")
   d:add("label3", "label", {label="Set coordinates for viewport:"}, 1, 1, 1, 4)
   d:add("label8", "label", {label="from x="}, 2, 1, 1, 1)
   d:add("xfrom", "input", {}, 2, 2, 1, 1)
   d:add("label9", "label", {label="to x="}, 2, 3, 1, 1)
   d:add("xto", "input", {}, 2, 4, 1, 1)
   d:add("label10", "label", {label="from y="}, 3, 1, 1, 1)
   d:add("yfrom", "input", {}, 3, 2, 1, 1)
   d:add("label11", "label", {label="to y="}, 3, 3, 1, 1)
   d:add("yto", "input", {}, 3, 4, 1, 1)
   d:add("label27", "label", {label="Size of x-ticks (in pt):"}, 4, 1, 1, 1)
   d:add("xticksize", "input", {}, 4, 2, 1, 1)
   d:add("label28", "label", {label="Size of y-ticks (in pt):"}, 4, 3, 1, 1)
   d:add("yticksize", "input", {}, 4, 4, 1, 1)
   d:add("label84", "label", {label="Locations of x-ticks:"},5,1,1,1)
   d:add("xticklist", "input", {}, 5, 2, 1, 3)
   d:add("label85", "label", {label="Locations of y-ticks:"},6,1,1,1)
   d:add("yticklist", "input", {}, 6, 2, 1, 3)
   d:add("ok", "button", {label="&Ok", action="accept"}, 7, 4)
   d:add("cancel", "button", {label="&Cancel", action="reject"}, 7, 3)
   d:setStretch("column", 2, 1)
   d:setStretch("row", 5, 1)
   if x0store then d:set("xfrom",x0store) end
   if x1store then d:set("xto",x1store) end
   if y0store then d:set("yfrom",y0store) end
   if y1store then d:set("yto",y1store) end
   if not xticksizestore then xticksizestore = 0 end
   d:set("xticksize", xticksizestore)
   if not yticksizestore then yticksizestore = 0 end
   d:set("yticksize", yticksizestore)
   if xticksstore then d:set("xticklist", xtickstore) end
   if yticksstore then d:set("yticklist", ytickstore) end
   if not d:execute() then return end
   x0store = d:get("xfrom")
   x1store = d:get("xto")
   y0store = d:get("yfrom")
   y1store = d:get("yto")
   xticksizestore = d:get("xticksize")
   yticksizestore = d:get("yticksize")
   xtickstore = d:get("xticklist")
   ytickstore = d:get("yticklist")

   -- real coordinates
   local x0 = get_number(model,x0store,"lower x limit")
   if not x0 then return end
   local x1 = get_number(model,x1store,"upper x limit")
   if not x1 then return end
   local y0 = get_number(model,y0store,"lower y limit")
   if not y0 then return end
   local y1 = get_number(model,y1store,"upper y limit")
   if not y1 then return end

   -- check validity of x and y limits:
   if x0 > x1 then
      x0, x1 = x1, x0
   end
   if x0 == x1 then
      model:warning("Limits for x cannot be equal")
      return
   end
   if y0 > y1 then
      y0, y1 = y1, y0
   end
   if y0 == y1 then
      model:warning("Limits for y cannot be equal")
      return
   end

   -- scaling calculations
   local start = ipe.Vector(x0,y0)
   local xlen = x1-x0
   local ylen = y1-y0
   local scalem = ipe.Matrix(cxlen/xlen,0,0,cylen/ylen)
   local trans = ipe.Translation(cstart-scalem*start) * scalem

   -- ticks:
   xticksize = tonumber(xticksizestore)
   if not xticksize then xticksize = 0 end
   yticksize = tonumber(yticksizestore)
   if not yticksize then yticksize = 0 end

   -- tick locations:
   local mathdefs = "local abs = math.abs; local acos = math.acos; local asin = math.asin; local atan = math.atan; local atan2 = math.atan2; local ceil = math.ceil; local cos = math.cos; local cosh = math.cosh; local deg = math.deg; local exp = math.exp; local floor = math.floor; local fmod = math.fmod; local log = math.log; local log10 = math.log10; local max = math.max; local min = math.min; local modf = math.modf; local pi = math.pi; local pow = math.pow; local rad = math.rad; local sin = math.sin; local sinh = math.sinh; local sqrt = math.sqrt; local tan = math.tan; local tanh = math.tanh;"


   local xticks = {}
   local yticks = {}

   if xtickstore and (xtickstore ~= "") then
      local tickliststr = mathdefs .. "return {" .. xtickstore .. "}"
      -- attempt to load this string.  Give a warning and quit if it fails. 
      local f,err = _G.loadstring(tickliststr,"x-ticks")
      if not f then
	 model:warning(err) -- bug: error messages will be cryptic
	 return
      end
      local xticklist 
      stat, xticklist = _G.pcall(f,t)
      if not stat then
	 model:warning(xticklist) -- bug: error messages will be cryptic
	 return
      end
      for i,x in pairs(xticklist) do
	 if isfinite(x) then
	    if (x > x0) and (x < x1) then
	       xticks[#xticks + 1] = x
	    end
	 end
      end
   else -- place ticks at every integer
      for i = math.floor(x0) + 1, math.ceil(x1)-1 do
	 xticks[#xticks + 1] = i
      end
   end

   -- do the same for y-ticks
   if ytickstore and (ytickstore ~= "") then
      local tickliststr = mathdefs .. "return {" .. ytickstore .. "}"
      -- attempt to load this string.  Give a warning and quit if it fails. 
      local f,err = _G.loadstring(tickliststr,"y-ticks")
      if not f then
	 model:warning(err) -- bug: error messages will be cryptic
	 return
      end
      local yticklist 
      stat, yticklist = _G.pcall(f,t)
      if not stat then
	 model:warning(yticklist) -- bug: error messages will be cryptic
	 return
      end
      for i,y in pairs(yticklist) do
	 if isfinite(y) then
	    if (y > y0) and (y < y1) then
	       yticks[#yticks + 1] = y
	    end
	 end
      end
   else -- place ticks at every integer
      for i = math.floor(y0) + 1, math.ceil(y1)-1 do
	 yticks[#yticks + 1] = i
      end
   end

   local axes = { }

   -- only make x-axis if y0<=0<=y1
   if y0*y1 <= 0 then
      local v0 = trans*ipe.Vector(x0,0)
      local v1 = trans*ipe.Vector(x1,0)
      local curve = { type="curve", closed=false; { type="segment", v0, v1 }}
      local xaxis = ipe.Path(model.attributes, {curve})
      xaxis:set("farrow",true)
      xaxis:set("pen","fat")
      axes[#axes + 1] = xaxis
      if xticksize ~= 0 then
	 local half_tick = ipe.Vector(0,xticksize/2)
	 for n,i in pairs(xticks) do
	    v0 = trans*ipe.Vector(i,0)
	    local tick = { type="curve", closed=false; { type="segment", v0+half_tick, v0-half_tick }}
	    axes[#axes + 1] = ipe.Path(model.attributes, {tick})
	 end
      end
   end
   if x0*x1 <= 0 then
      local v0 = trans*ipe.Vector(0,y0)
      local v1 = trans*ipe.Vector(0,y1)
      curve = { type="curve", closed=false; { type="segment", v0, v1 }}
      local yaxis = ipe.Path(model.attributes, {curve})
      yaxis:set("farrow",true)
      yaxis:set("pen","fat")
      axes[#axes + 1] = yaxis
      if yticksize ~= 0 then
	 local half_tick = ipe.Vector(yticksize/2,0)
	 for n,i in pairs(yticks) do
	    v0 = trans*ipe.Vector(0,i)
	    local tick = { type="curve", closed=false; { type="segment", v0+half_tick, v0-half_tick }}
	    axes[#axes + 1] = ipe.Path(model.attributes, {tick})
	 end
      end
   end

   if #axes > 0 then 
      local coordsys = ipe.Group(axes)
      model:creation("create coordinate system", coordsys)
   end
end

methods = {
   { label = "Parametric plot", run=curve },
   { label = "Function plot", run=func_plot },
   { label = "Coordinate system", run=make_axes },
}

----------------------------------------------------------------------


More information about the Ipe-discuss mailing list