模組:FunctionGraph
外观
| 此模块已评为alpha版,可接受第三方输入,并可用于少量页面以检查是否存在问题,但需要受到检查。欢迎提供新功能或修改其输入输出机制的建议。 |
本模組為維基百科高效能無 JavaScript 之 SVG + HTML/CSS 混合數學函數繪圖引擎,用於驅動 {{函數圖形}}。
核心功能與子模組架構
- Module:FunctionGraph:主入口與數值取樣管線(支援 2D 顯函數、極座標
#polar:、參數方程、質數語法糖質數(x)/π(x)、samplingMode四種取樣模式、一階與二階微分佇列斷點探測及狄拉克 脈衝合成)。 - Module:FunctionGraph/MathEngine:2D 步進正方形(Marching Squares)隱函數引擎、3D 空間投影、法向量雙面光照(Flat Shading)、畫家演算法(Z-sorting)、3D 隱函數等值面網格(Surface Nets)、六面體截面裁切(Sutherland-Hodgman)與 3D 複變模態曲面圖引擎。
- Module:FunctionGraph/SVGRenderer:2D、極座標、複變定義域著色圖與 3D 圖表之 SVG + HTML 混合排版與智慧節流 Tooltip 產生器。
- Module:FunctionGraph/TextRenderer:數學表達式遞迴下降語法分析器(AST Parser)、圖例 SVG 數學排版引擎與單次快取複用之 MediaWiki
<math>Strip Marker 轉譯器。 - Module:FunctionGraph/RenderUtils:色彩光影運算、連分數有理逼近刻度、單調三次 Hermite 樣條插值與螢幕像素弧長等距取樣工具庫。
- Module:FunctionGraph/style.css:外部 TemplateStyles 樣式表,負責文字疊加定位與無須
pointer-events之父子包裹式:hover互動氣泡框。
函數介面
functionGraph
繪製 2D 顯函數、隱函數、極座標、參數式與 3D 函數圖形:
{{#invoke:FunctionGraph|functionGraph|sin(x)|start=-5|end=5}}
xy圖例 (-4.92): 0.98 (-4.76): 1 (-4.59): 0.99 (-4.43): 0.96 (-4.3): 0.91 (-4.17): 0.86 (-4.06): 0.8 (-3.96): 0.73 (-3.87): 0.67 (-3.78): 0.6 (-3.7): 0.53 (-3.61): 0.46 (-3.54): 0.39 (-3.46): 0.31 (-3.39): 0.24 (-3.31): 0.17 (-3.24): 0.1 (-3.17): 0.02 (-3.09): -0.05 (-3.02): -0.12 (-2.95): -0.19 (-2.87): -0.26 (-2.8): -0.34 (-2.72): -0.41 (-2.64): -0.48 (-2.56): -0.55 (-2.47): -0.62 (-2.39): -0.68 (-2.29): -0.75 (-2.19): -0.81 (-2.07): -0.88 (-1.95): -0.93 (-1.81): -0.97 (-1.64): -1 (-1.47): -1 (-1.32): -0.97 (-1.17): -0.92 (-1.05): -0.87 (-0.94): -0.81 (-0.84): -0.74 (-0.74): -0.68 (-0.65): -0.61 (-0.57): -0.54 (-0.49): -0.47 (-0.41): -0.4 (-0.33): -0.33 (-0.26): -0.25 (-0.18): -0.18 (-0.11): -0.11 (-0.04): -0.04 (0.04): 0.04 (0.11): 0.11 (0.18): 0.18 (0.26): 0.25 (0.33): 0.33 (0.41): 0.4 (0.49): 0.47 (0.57): 0.54 (0.65): 0.61 (0.74): 0.68 (0.84): 0.74 (0.94): 0.81 (1.05): 0.87 (1.17): 0.92 (1.32): 0.97 (1.47): 1 (1.64): 1 (1.81): 0.97 (1.95): 0.93 (2.07): 0.88 (2.19): 0.81 (2.29): 0.75 (2.39): 0.68 (2.47): 0.62 (2.56): 0.55 (2.64): 0.48 (2.72): 0.41 (2.8): 0.34 (2.87): 0.26 (2.95): 0.19 (3.02): 0.12 (3.09): 0.05 (3.17): -0.02 (3.24): -0.1 (3.31): -0.17 (3.39): -0.24 (3.46): -0.31 (3.54): -0.39 (3.61): -0.46 (3.7): -0.53 (3.78): -0.6 (3.87): -0.67 (3.96): -0.73 (4.06): -0.8 (4.17): -0.86 (4.3): -0.91 (4.43): -0.96 (4.59): -0.99 (4.76): -1 (4.92): -0.98
{{#invoke:FunctionGraph|functionGraph|floor(x)|start=-3|end=3|equal_scale=yes}}
xy圖例 (-2.97): -3 (-2.91): -3 (-2.85): -3 (-2.79): -3 (-2.73): -3 (-2.67): -3 (-2.61): -3 (-2.55): -3 (-2.49): -3 (-2.43): -3 (-2.37): -3 (-2.31): -3 (-2.25): -3 (-2.19): -3 (-2.13): -3 (-2.07): -3 (-2.01): -3 (-1.95): -2 (-1.89): -2 (-1.83): -2 (-1.77): -2 (-1.71): -2 (-1.65): -2 (-1.59): -2 (-1.53): -2 (-1.47): -2 (-1.41): -2 (-1.35): -2 (-1.29): -2 (-1.23): -2 (-1.17): -2 (-1.11): -2 (-1.05): -2 (-0.99): -1 (-0.93): -1 (-0.87): -1 (-0.81): -1 (-0.75): -1 (-0.69): -1 (-0.63): -1 (-0.57): -1 (-0.51): -1 (-0.45): -1 (-0.39): -1 (-0.33): -1 (-0.27): -1 (-0.21): -1 (-0.15): -1 (-0.09): -1 (-0.03): -1 (0.03): 0 (0.09): 0 (0.15): 0 (0.21): 0 (0.27): 0 (0.33): 0 (0.39): 0 (0.45): 0 (0.51): 0 (0.57): 0 (0.63): 0 (0.69): 0 (0.75): 0 (0.81): 0 (0.87): 0 (0.93): 0 (0.99): 0 (1.05): 1 (1.11): 1 (1.17): 1 (1.23): 1 (1.29): 1 (1.35): 1 (1.41): 1 (1.47): 1 (1.53): 1 (1.59): 1 (1.65): 1 (1.71): 1 (1.77): 1 (1.83): 1 (1.89): 1 (1.95): 1 (2.01): 2 (2.07): 2 (2.13): 2 (2.19): 2 (2.25): 2 (2.31): 2 (2.37): 2 (2.43): 2 (2.49): 2 (2.55): 2 (2.61): 2 (2.67): 2 (2.73): 2 (2.79): 2 (2.85): 2 (2.91): 2 (2.97): 2
{{#invoke:FunctionGraph|functionGraph|sin(1/x)|start=-0.5|end=0.5|samplingMode=diff}}
xy圖例 (-0.44): -0.75 (-0.34): -0.23 (-0.28): 0.39 (-0.22): 0.99 (-0.17): 0.38 (-0.15): -0.3 (-0.13): -0.97 (-0.11): -0.37 (-0.1): 0.31 (-0.09): 0.99 (-0.08): 0.33 (-0.08): -0.35 (-0.07): -0.97 (-0.06): -0.29 (-0.06): 0.4 (-0.06): 0.93 (-0.05): 0.23 (-0.05): -0.46 (-0.05): -0.89 (-0.05): -0.19 (-0.04): 0.52 (-0.04): 0.86 (-0.04): 0.14 (-0.04): -0.55 (-0.04): -0.87 (-0.04): -0.13 (-0.03): 0.67 (-0.03): 0.8 (-0.03): 0.03 (-0.03): -0.82 (-0.03): -0.67 (-0.03): 0.13 (-0.03): 1 (-0.03): 0.62 (-0.03): -0.2 (-0.02): -0.59 (-0.02): 0.19 (-0.02): 0.87 (-0.02): 0.86 (-0.02): -0.11 (-0.02): 0.71 (-0.02): 0.28 (-0.02): -0.34 (-0.02): -0.92 (-0.01): -0.7 (-0.01): 0.97 (-0.01): -0.52 (-0.01): 0.41 (-0.01): -0.6 (-0.01): -0.84 (0.01): 0.84 (0.01): 0.6 (0.01): -0.41 (0.01): 0.52 (0.01): -0.97 (0.01): 0.7 (0.02): 0.92 (0.02): 0.34 (0.02): -0.28 (0.02): -0.71 (0.02): 0.11 (0.02): -0.86 (0.02): -0.87 (0.02): -0.19 (0.02): 0.59 (0.03): 0.2 (0.03): -0.62 (0.03): -1 (0.03): -0.13 (0.03): 0.67 (0.03): 0.82 (0.03): -0.03 (0.03): -0.8 (0.03): -0.67 (0.04): 0.13 (0.04): 0.87 (0.04): 0.55 (0.04): -0.14 (0.04): -0.86 (0.04): -0.52 (0.05): 0.19 (0.05): 0.89 (0.05): 0.46 (0.05): -0.23 (0.06): -0.93 (0.06): -0.4 (0.06): 0.29 (0.07): 0.97 (0.08): 0.35 (0.08): -0.33 (0.09): -0.99 (0.1): -0.31 (0.11): 0.37 (0.13): 0.97 (0.15): 0.3 (0.17): -0.38 (0.22): -0.99 (0.28): -0.39 (0.34): 0.23 (0.44): 0.75
complex_graph
繪製複變函數 2D 定義域著色圖(預設)或 3D 複變模態曲面圖(加上 3d=yes):
{{#invoke:FunctionGraph|complex_graph|x}}
實軸 (Re)虛軸 (Im) (-4.5+4.5i): -4.50+4.50i (-3.5+4.5i): -3.50+4.50i (-2.5+4.5i): -2.50+4.50i (-1.5+4.5i): -1.50+4.50i (-0.5+4.5i): -0.50+4.50i (0.5+4.5i): 0.50+4.50i (1.5+4.5i): 1.50+4.50i (2.5+4.5i): 2.50+4.50i (3.5+4.5i): 3.50+4.50i (4.5+4.5i): 4.50+4.50i (-4.5+3.5i): -4.50+3.50i (-3.5+3.5i): -3.50+3.50i (-2.5+3.5i): -2.50+3.50i (-1.5+3.5i): -1.50+3.50i (-0.5+3.5i): -0.50+3.50i (0.5+3.5i): 0.50+3.50i (1.5+3.5i): 1.50+3.50i (2.5+3.5i): 2.50+3.50i (3.5+3.5i): 3.50+3.50i (4.5+3.5i): 4.50+3.50i (-4.5+2.5i): -4.50+2.50i (-3.5+2.5i): -3.50+2.50i (-2.5+2.5i): -2.50+2.50i (-1.5+2.5i): -1.50+2.50i (-0.5+2.5i): -0.50+2.50i (0.5+2.5i): 0.50+2.50i (1.5+2.5i): 1.50+2.50i (2.5+2.5i): 2.50+2.50i (3.5+2.5i): 3.50+2.50i (4.5+2.5i): 4.50+2.50i (-4.5+1.5i): -4.50+1.50i (-3.5+1.5i): -3.50+1.50i (-2.5+1.5i): -2.50+1.50i (-1.5+1.5i): -1.50+1.50i (-0.5+1.5i): -0.50+1.50i (0.5+1.5i): 0.50+1.50i (1.5+1.5i): 1.50+1.50i (2.5+1.5i): 2.50+1.50i (3.5+1.5i): 3.50+1.50i (4.5+1.5i): 4.50+1.50i (-4.5+0.5i): -4.50+0.50i (-3.5+0.5i): -3.50+0.50i (-2.5+0.5i): -2.50+0.50i (-1.5+0.5i): -1.50+0.50i (-0.5+0.5i): -0.50+0.50i (0.5+0.5i): 0.50+0.50i (1.5+0.5i): 1.50+0.50i (2.5+0.5i): 2.50+0.50i (3.5+0.5i): 3.50+0.50i (4.5+0.5i): 4.50+0.50i (-4.5-0.5i): -4.50-0.50i (-3.5-0.5i): -3.50-0.50i (-2.5-0.5i): -2.50-0.50i (-1.5-0.5i): -1.50-0.50i (-0.5-0.5i): -0.50-0.50i (0.5-0.5i): 0.50-0.50i (1.5-0.5i): 1.50-0.50i (2.5-0.5i): 2.50-0.50i (3.5-0.5i): 3.50-0.50i (4.5-0.5i): 4.50-0.50i (-4.5-1.5i): -4.50-1.50i (-3.5-1.5i): -3.50-1.50i (-2.5-1.5i): -2.50-1.50i (-1.5-1.5i): -1.50-1.50i (-0.5-1.5i): -0.50-1.50i (0.5-1.5i): 0.50-1.50i (1.5-1.5i): 1.50-1.50i (2.5-1.5i): 2.50-1.50i (3.5-1.5i): 3.50-1.50i (4.5-1.5i): 4.50-1.50i (-4.5-2.5i): -4.50-2.50i (-3.5-2.5i): -3.50-2.50i (-2.5-2.5i): -2.50-2.50i (-1.5-2.5i): -1.50-2.50i (-0.5-2.5i): -0.50-2.50i (0.5-2.5i): 0.50-2.50i (1.5-2.5i): 1.50-2.50i (2.5-2.5i): 2.50-2.50i (3.5-2.5i): 3.50-2.50i (4.5-2.5i): 4.50-2.50i (-4.5-3.5i): -4.50-3.50i (-3.5-3.5i): -3.50-3.50i (-2.5-3.5i): -2.50-3.50i (-1.5-3.5i): -1.50-3.50i (-0.5-3.5i): -0.50-3.50i (0.5-3.5i): 0.50-3.50i (1.5-3.5i): 1.50-3.50i (2.5-3.5i): 2.50-3.50i (3.5-3.5i): 3.50-3.50i (4.5-3.5i): 4.50-3.50i (-4.5-4.5i): -4.50-4.50i (-3.5-4.5i): -3.50-4.50i (-2.5-4.5i): -2.50-4.50i (-1.5-4.5i): -1.50-4.50i (-0.5-4.5i): -0.50-4.50i (0.5-4.5i): 0.50-4.50i (1.5-4.5i): 1.50-4.50i (2.5-4.5i): 2.50-4.50i (3.5-4.5i): 3.50-4.50i (4.5-4.5i): 4.50-4.50i
另外請注意:因要向2個維度取樣(如取樣數設定為100則要運算100×100=10000次,更高的取樣數可能會超出WP:模板限制)因此較耗費效能,請斟酌使用。
{{#invoke:FunctionGraph|complex_graph|z^3 = 1|3d=yes|start=-1.6|end=1.6|zAxisMax=3}}
實軸 (Re)虛軸 (Im)|f(z)|圖例 (-1.1+1.02i): 1.10+2.65i (|f|=2.86) (-0.9+1.26i): 2.58+1.05i (|f|=2.78) (-0.64+1.41i): 2.57-1.06i (|f|=2.78) (-0.33+1.46i): 1.09-2.65i (|f|=2.87) (-1.24+0.08i): -2.90+0.37i (|f|=2.92) (-1.18+0.57i): -1.51+2.20i (|f|=2.66) (-1.06+0.96i): 0.71+2.37i (|f|=2.48) (-0.87+1.21i): 2.20+0.98i (|f|=2.4) (-0.62+1.36i): 2.20-0.99i (|f|=2.41) (-0.29+1.4i): 0.70-2.38i (|f|=2.48) (0.1+1.3i): -1.52-2.18i (|f|=2.66) (0.55+1.12i): -2.90-0.36i (|f|=2.92) (-1.22-0.56i): -1.66-2.31i (|f|=2.84) (-1.17-0.05i): -2.57-0.22i (|f|=2.58) (-1.11+0.45i): -1.71+1.58i (|f|=2.33) (-1.02+0.88i): 0.32+2.08i (|f|=2.1) (-0.84+1.16i): 1.79+0.89i (|f|=2) (-0.58+1.31i): 1.78-0.90i (|f|=2) (-0.25+1.33i): 0.32-2.09i (|f|=2.11) (0.17+1.18i): -1.72-1.55i (|f|=2.32) (0.63+0.98i): -2.58+0.22i (|f|=2.59) (1.09+0.77i): -1.66+2.31i (|f|=2.85) (-1.14-0.69i): -0.84-2.36i (|f|=2.51) (-1.08-0.19i): -2.15-0.68i (|f|=2.26) (-1.02+0.29i): -1.81+0.88i (|f|=2.01) (-0.97+0.79i): -0.08+1.74i (|f|=1.74) (-0.81+1.1i): 1.40+0.81i (|f|=1.62) (-0.55+1.25i): 1.39-0.82i (|f|=1.61) (-0.2+1.24i): -0.09-1.75i (|f|=1.75) (0.26+1.03i): -1.81-0.86i (|f|=2.01) (0.71+0.84i): -2.15+0.69i (|f|=2.25) (1.17+0.64i): -0.81+2.37i (|f|=2.51) (-0.79-1.3i): 2.49-0.25i (|f|=2.51) (-0.36-1.45i): 1.22+2.46i (|f|=2.74) (-0.05-1.4i): -0.72+2.71i (|f|=2.81) (0.23-1.28i): -2.11+1.91i (|f|=2.84) (0.49-1.14i): -2.77+0.66i (|f|=2.85) (0.74-0.99i): -2.78-0.67i (|f|=2.86) (1-0.84i): -2.11-1.92i (|f|=2.86) (1.24-0.66i): -0.72-2.73i (|f|=2.83) (1.43-0.41i): 1.23-2.44i (|f|=2.73) (1.52+0.04i): 2.49+0.27i (|f|=2.51) (-0.41-1.37i): 1.22+1.89i (|f|=2.25) (-0.09-1.32i): -0.52+2.27i (|f|=2.32) (0.18-1.19i): -1.74+1.58i (|f|=2.35) (0.43-1.03i): -2.29+0.54i (|f|=2.35) (0.68-0.88i): -2.29-0.54i (|f|=2.35) (0.95-0.75i): -1.74-1.59i (|f|=2.35) (1.19-0.58i): -0.51-2.27i (|f|=2.33) (1.39-0.33i): 1.23-1.88i (|f|=2.24) (-0.46-1.28i): 1.16+1.31i (|f|=1.74) (-0.14-1.24i): -0.36+1.81i (|f|=1.85) (0.11-1.08i): -1.38+1.21i (|f|=1.84) (0.34-0.89i): -1.77+0.39i (|f|=1.82) (0.6-0.74i): -1.77-0.39i (|f|=1.82) (0.88-0.63i): -1.38-1.22i (|f|=1.84) (1.14-0.49i): -0.35-1.80i (|f|=1.84) (1.34-0.25i): 1.17-1.33i (|f|=1.77) (-0.51-1.18i): 1.02+0.72i (|f|=1.25) (-0.2-1.13i): -0.23+1.31i (|f|=1.33) (0.01-0.91i): -1.04+0.76i (|f|=1.28) (0.14-0.54i): -1.12+0.12i (|f|=1.13) (0.4-0.4i): -1.13-0.13i (|f|=1.13) (0.79-0.47i): -1.04-0.77i (|f|=1.3) (1.08-0.39i): -0.23-1.32i (|f|=1.34) (1.28-0.14i): 1.02-0.70i (|f|=1.24) (-0.6-1.03i): 0.70-0.02i (|f|=0.7) (-0.28-1i): -0.19+0.78i (|f|=0.8) (1.01-0.26i): -0.18-0.78i (|f|=0.81) (1.2-0i): 0.72-0.01i (|f|=0.72)
-- Module:FunctionGraph
local p = {}
local comp_number = nil
local getArgs = require('Module:Arguments').getArgs
local lib_calc = require('Module:Complex_Number/Calculate')
local TrackingCategory = require('Module:TrackingCategory')
local yesno = require('Module:Yesno')
local SVGRenderer = require('Module:FunctionGraph/SVGRenderer')
local MathEngine = require('Module:FunctionGraph/MathEngine')
local noop_func = function() end
local debug_flag, use_ext_mathlib
-- 內部輔助:載入自訂或預設數學庫
local function resolveMathLib(math_class, cmath, qmath)
local mymath = cmath
local mytomath = cmath.toComplexNumber
local resolved_class = math_class or ''
if mw.ustring.sub(resolved_class, 1, 7):upper() == "MODULE:" then
local module_name, math_lib_name = lib_calc.checkModuleClass(resolved_class)
xpcall(function()
local load_module = require("Module:" .. module_name)
if load_module ~= nil then
local load_math_lib = load_module[math_lib_name]
if load_math_lib ~= nil then
local func_type = type(noop_func)
local my_math_lib = (type(load_math_lib.init) == func_type) and load_math_lib.init() or load_math_lib
if type(my_math_lib.constructor) == func_type then
resolved_class = "mymath"
mymath = my_math_lib
mytomath = my_math_lib.constructor
end
end
end
end, noop_func)
end
local lib_map = { cmath = cmath, qmath = qmath, mymath = mymath }
local ctor_map = { cmath = cmath.toComplexNumber, qmath = qmath.toQuaternionNumber, mymath = mytomath }
return lib_map[resolved_class], ctor_map[resolved_class]
end
-- 快速檢查數值字串是否為無效值
local function isInvalidNumStr(s)
if not s then return true end
local lower = string.lower(tostring(s))
return string.find(lower, "nan", 1, true) ~= nil
or string.find(lower, "nil", 1, true) ~= nil
or string.find(lower, "inf", 1, true) ~= nil
end
--subst會壞掉,直接FallBack成舊版圖表不可用警示
function p.renderFallback()
return require('Module:Message box').main( "ombox", { text = "由於[[phab:T405550|技術原因]],此[[mw:Extension:Graph|舊版圖表]]已停用。造成您的不便,我們深表歉意。" .. (mw.isSubsting() and "若您是因為替換引用而看到此消息,請將此改為模板調用,以維持圖表顯示。" or ""), nocat=true})
end
function p.applyFunctionGraph(frame)
return p.functionGraph(frame)
end
function p.functionGraph(frame)
if mw.isSubsting() then return p.renderFallback(frame) end
if comp_number == nil then comp_number = require("Module:Complex Number") end
local cmath, qmath = comp_number.cmath.init(), comp_number.qmath.init()
if not getArgs then getArgs = require('Module:Arguments').getArgs end
local args = getArgs(frame, { parentFirst = true })
local args_is_template_call = yesno(frame.args.is_template_call or 'no') == true
-- 偵測是否經由 {{函數圖形}} 等外層模板呼叫
local parent = frame and frame.getParent and frame:getParent()
local parent_title = (parent and parent:getTitle()) or ""
local is_template_call = false
if mw.ustring.find(parent_title, "函數圖形", 1, true) or mw.ustring.find(parent_title, "FunctionGraph", 1, true) or args_is_template_call then
is_template_call = true
elseif parent and parent.args and parent.args[1] ~= nil and frame.args and frame.args.useOtherModule ~= nil then
is_template_call = true
end
local exprs = {}
local body_args, x_start, x_end, y_min, y_max, sampling = {}, nil, nil, nil, nil, nil
-- 依序收集匿名函數表達式參數
local max_idx = 0
for k, _ in pairs(args) do
local idx = tonumber(k)
if idx and idx > max_idx then max_idx = idx end
end
for idx = 1, max_idx do
if args[idx] ~= nil then
local clean_e, need_mod = MathEngine.preprocessPrimeSugar(lib_calc._remove_strip_marker(args[idx]))
if need_mod then lib_calc.use_other_module = true end
exprs[#exprs + 1] = clean_e
end
end
for arg_name, arg_value in pairs(args) do
if tonumber(arg_name) == nil then
local check_arg_name = string.lower(tostring(arg_name))
if check_arg_name == "start" then x_start = tonumber(arg_value)
elseif check_arg_name == "end" then x_end = tonumber(arg_value)
elseif check_arg_name == "sampling" then sampling = tonumber(arg_value)
elseif check_arg_name == "min" then y_min = tonumber(arg_value)
elseif check_arg_name == "max" then y_max = tonumber(arg_value)
else body_args[arg_name] = arg_value
end
end
end
if yesno(args.useOtherModule or 'no') == true then lib_calc.use_other_module = true end
local math_class = (frame and frame.args and frame.args['class']) or args['class'] or ''
local selected_lib, selected_ctor = resolveMathLib(math_class, cmath, qmath)
body_args.is_template_call = is_template_call
TrackingCategory.append('有SVG圖表的頁面')
if use_ext_mathlib == true then TrackingCategory.append('使用擴充複變函數庫的頁面') end
-- 自動偵測是否為 3D 圖表 (含 #3d: 前綴、雙變數 f(x,y)、6 段 3D 曲線、9 段 3D 參數曲面或 3D 複變模態曲面)
local modeLower = string.lower(mw.text.trim(tostring(body_args.type or body_args.mode or "")))
local isComplexFlag = (yesno(body_args.complex or 'no') == true) or (modeLower == "complex3d")
if isComplexFlag and (MathEngine.is3DExpressionList(exprs, body_args) or modeLower == "complex3d") then
body_args.x_start = x_start or tonumber(body_args.xstart or body_args.x_start) or -5
body_args.x_end = x_end or tonumber(body_args.xend or body_args.x_end) or 5
body_args.y_start = tonumber(body_args.ystart or body_args.y_start) or body_args.x_start
body_args.y_end = tonumber(body_args.yend or body_args.y_end) or body_args.x_end
body_args.z_min = y_min
body_args.z_max = y_max
body_args.sampling = sampling
body_args.width = tonumber(body_args.width) or (is_template_call and 200 or 420)
body_args.height = tonumber(body_args.height) or (is_template_call and 180 or 340)
local data3D = MathEngine.evaluateComplex3DSeries(
exprs, body_args, lib_calc, selected_lib or cmath, selected_ctor or cmath.toComplexNumber
)
return SVGRenderer.render3DGraph(data3D, body_args)
elseif MathEngine.is3DExpressionList(exprs, body_args) then
body_args.x_start = x_start
body_args.x_end = x_end
body_args.z_min = y_min
body_args.z_max = y_max
body_args.sampling = sampling
body_args.width = tonumber(body_args.width) or (is_template_call and 200 or 420)
body_args.height = tonumber(body_args.height) or (is_template_call and 180 or 340)
local rmath = comp_number.math.init()
local data3D = MathEngine.evaluate3DSeries(exprs, body_args, lib_calc, selected_lib or rmath, selected_ctor or tonumber, rmath)
return SVGRenderer.render3DGraph(data3D, body_args)
end
local has2DImplicit = false
for _, ex in ipairs(exprs) do
if MathEngine.is2DImplicitExpression(ex) then
has2DImplicit = true
break
end
end
x_start = x_start or (has2DImplicit and -5 or 0)
x_end = x_end or (has2DImplicit and 5 or 1)
sampling = sampling or 50
-- 計算 2D 函數點位 (相容舊版與新版格式)
local chartData = p._functionGraph(
exprs, x_start, x_end, sampling, y_min, y_max, body_args, selected_lib, selected_ctor
)
body_args.width = tonumber(body_args.width) or (is_template_call and 200 or 400)
body_args.height = tonumber(body_args.height) or (is_template_call and 100 or 300)
body_args.interpolate = body_args.interpolate or (frame and frame.args and frame.args['interpolate']) or "monotone"
body_args.x_start = x_start
body_args.x_end = x_end
body_args.y_min = y_min
body_args.y_max = y_max
return SVGRenderer.renderFunctionGraph(chartData, body_args)
end
function p.graph3d(frame)
return p.functionGraph(frame)
end
-- 保持與 Module:NumberUtil 等外部模組 100% 相容,並支援 #polar:、質數語法糖、智慧取樣模式 (samplingMode)、一階/二階差分佇列斷點與狄拉克脈衝探測
function p._functionGraph(expr_arr, x_start, x_end, sampling, y_min, y_max, body_args, math_lib, number_Constructer)
body_args = body_args or {}
if yesno(body_args.useOtherModule or 'no') == true then lib_calc.use_other_module = true end
if comp_number == nil then comp_number = require("Module:Complex Number") end
local rmath = comp_number.math.init()
lib_calc._randomseed()
local mathlib = math_lib or rmath
local numberConstructer = number_Constructer or tonumber
local postfix = {}
local check_func = {}
local x_arr, y_arr = {}, {}
local raw_x_arr, raw_y_arr, raw_t_arr = {}, {}, {}
local has_last_flags = {}
local series_int_mode = {}
local series_has_impulse = {}
local series_default_interp = {}
local expr_list = type(expr_arr) == "table" and expr_arr or { expr_arr }
local all_polar = (#expr_list > 0)
local globalSamplingMode = string.lower(mw.text.trim(tostring(body_args.samplingMode or body_args.sampling_mode or "auto")))
for i = 1, #expr_list do
local raw_input_expr = mw.text.trim(tostring(expr_list[i] or ''))
local expr_str, need_mod = MathEngine.preprocessPrimeSugar(raw_input_expr)
if need_mod then lib_calc.use_other_module = true end
expr_list[i] = expr_str
local is_polar = false
local polar_body = mw.ustring.match(expr_str, "^#[pP][oO][lL][aA][rR]%s*:%s*(.+)$")
if polar_body then
is_polar = true
expr_str = mw.text.trim(polar_body)
else
all_polar = false
end
-- 若為質數計數函數 π(x) 且使用者未強制指定 interpolate,預設採用 step-after 階梯插值
if string.find(string.lower(expr_str), "primeindexof@", 1, true) and not body_args.interpolate then
series_default_interp[i] = "step-after"
end
local local_func_sign = '↦'
local check_parametric = mw.text.split(expr_str, ';')
if mw.ustring.find(expr_str, local_func_sign) then
check_parametric = mw.text.split(expr_str, '\\;')
end
if #check_parametric == 1 then
local lhs, rhs = nil, nil
if not is_polar then
lhs, rhs = MathEngine.splitTopLevelEquation(expr_str)
end
if lhs and rhs then
if lhs == "y" and not MathEngine.hasFreeVar(rhs, "y") then
local pre_expr, pre_scope = lib_calc._function_preprocessing(rhs, mathlib, numberConstructer, false)
local pf = lib_calc.infixToPostfix(pre_expr, debug_flag)
if type(pf) == "table" then
if pre_scope then pf.scope = pre_scope end
pf.clean_expr = expr_str
end
postfix[#postfix + 1] = pf
elseif rhs == "y" and not MathEngine.hasFreeVar(lhs, "y") then
local pre_expr, pre_scope = lib_calc._function_preprocessing(lhs, mathlib, numberConstructer, false)
local pf = lib_calc.infixToPostfix(pre_expr, debug_flag)
if type(pf) == "table" then
if pre_scope then pf.scope = pre_scope end
pf.clean_expr = expr_str
end
postfix[#postfix + 1] = pf
else
-- 2D 隱函數方程式 LHS = RHS
local diff_expr = (rhs == "0") and lhs or ("(" .. lhs .. ")-(" .. rhs .. ")")
local pre_expr, pre_scope = lib_calc._function_preprocessing(diff_expr, mathlib, numberConstructer, false)
local pf_imp = lib_calc.infixToPostfix(pre_expr, debug_flag)
if type(pf_imp) == "table" and pre_scope then pf_imp.scope = pre_scope end
postfix[#postfix + 1] = {
implicit2d = true,
pf = pf_imp,
clean_expr = expr_str
}
end
else
local eval_expr_str = is_polar and mw.ustring.gsub(expr_str, "θ", "x") or expr_str
local pre_expr, pre_scope = lib_calc._function_preprocessing(eval_expr_str, mathlib, numberConstructer, false)
local pf = lib_calc.infixToPostfix(pre_expr, debug_flag)
if type(pf) == "table" then
if pre_scope then pf.scope = pre_scope end
pf.polar = is_polar
pf.clean_expr = expr_str
end
postfix[#postfix + 1] = pf
end
elseif #check_parametric >= 3 then
local param_obj = { parametric = true, polar = is_polar, clean_expr = expr_str }
param_obj.x_name = mw.text.trim(check_parametric[1] or 't')
param_obj.y_name = mw.text.trim(check_parametric[2] or 't')
param_obj.x = lib_calc.infixToPostfix(param_obj.x_name, debug_flag)
param_obj.y = lib_calc.infixToPostfix(param_obj.y_name, debug_flag)
param_obj.t = mw.text.trim(check_parametric[3] or 't')
param_obj.min = numberConstructer(check_parametric[4]) or numberConstructer(0)
param_obj.max = numberConstructer(check_parametric[5]) or numberConstructer(is_polar and 6.2831853 or 1)
postfix[#postfix + 1] = param_obj
end
has_last_flags[i] = string.find(expr_str, "last", 1, true) ~= nil
y_arr[i], x_arr[i], raw_y_arr[i], raw_x_arr[i], raw_t_arr[i] = {}, {}, {}, {}, {}
end
local round_digits = tonumber(body_args["round number"])
local nonreal_is_nan = (tonumber(body_args["nonreal is nan"]) == 1)
local calc_diff_flags = {}
for j = 1, #expr_list do
local diff_arg = body_args["calc diff " .. tostring(j)]
calc_diff_flags[j] = (tonumber(diff_arg) == 1) or (yesno(diff_arg or 'no') == true)
end
local current_j = 1
local eval_env = {
x = 0,
last = function(num)
local n_offset = tonumber(tostring(num or 1)) or 1
local last_num = body_args['last' .. n_offset] or 0
local arr = y_arr[current_j]
return numberConstructer((arr and arr[#arr - n_offset + 1]) or last_num)
end
}
local param_env = {}
-- 評估未微分的基底函數值 (供 calc diff 模式下探測階梯跳躍並生成狄拉克 δ 脈衝使用)
local function evalRawBaseAt(j, param_val)
current_j = j
local pf = postfix[j]
if not pf or (type(pf) == "table" and (pf.parametric or pf.polar or pf.implicit2d)) then return nil end
local val = nil
xpcall(function()
eval_env.x = param_val
local res = lib_calc.calc_by_postfix(pf, eval_env, mathlib, numberConstructer, false)
if res and res ~= " " then
local nr = mathlib.nonRealPart and mathlib.nonRealPart(res)
if nr and math.abs(tonumber(mathlib.abs(nr)) or 0) > 1e-9 then
if nonreal_is_nan or mathlib == rmath then res = nil end
end
if res then
local re = tonumber(mathlib.re(res))
if re and re == re and math.abs(re) < 1e12 then val = re end
end
end
end, noop_func)
return val
end
-- 單點運算輔助函數 (供取樣迴圈、離散打點與跳躍點極限探測共用)
local function evaluateAt(j, param_val)
current_j = j
local pf = postfix[j]
if not pf then return nil, nil, " ", " " end
local is_param = (type(pf) == "table" and pf.parametric == true)
local is_polar = (type(pf) == "table" and pf.polar == true)
local x_val = param_val
local calc_val = " "
local raw_x_num, raw_y_num = nil, nil
xpcall(function()
if is_param then
param_env[pf.t] = param_val
calc_val = lib_calc.calc_by_postfix(pf.y, param_env, mathlib, numberConstructer, false)
x_val = lib_calc.calc_by_postfix(pf.x, param_env, mathlib, numberConstructer, false)
param_env[pf.t] = nil
else
eval_env.x = param_val
calc_val = lib_calc.calc_by_postfix(pf, eval_env, mathlib, numberConstructer, false)
if calc_diff_flags[j] then
eval_env.x = param_val + 1e-6
local dy = lib_calc.calc_by_postfix(pf, eval_env, mathlib, numberConstructer, false)
calc_val = 1e6 * (dy - calc_val)
end
end
if calc_val and calc_val ~= " " then
local nr = mathlib.nonRealPart and mathlib.nonRealPart(calc_val)
if nr and math.abs(tonumber(mathlib.abs(nr)) or 0) > 1e-9 then
if nonreal_is_nan or mathlib == rmath then calc_val = nil end
end
end
if is_polar then
local r_num, theta_num
if is_param then
r_num = (x_val and x_val ~= " ") and tonumber(mathlib.re(x_val)) or nil
theta_num = (calc_val and calc_val ~= " ") and tonumber(mathlib.re(calc_val)) or nil
else
r_num = (calc_val and calc_val ~= " ") and tonumber(mathlib.re(calc_val)) or nil
theta_num = tonumber(mathlib.re(param_val))
end
if r_num and theta_num and r_num == r_num and theta_num == theta_num then
raw_x_num = r_num * math.cos(theta_num)
raw_y_num = r_num * math.sin(theta_num)
x_val, calc_val = raw_x_num, raw_y_num
else
raw_x_num, raw_y_num, x_val, calc_val = nil, nil, " ", " "
end
else
if calc_val and calc_val ~= " " then raw_y_num = tonumber(mathlib.re(calc_val)) end
if x_val and x_val ~= " " then raw_x_num = tonumber(mathlib.re(x_val)) end
end
end, noop_func)
if round_digits ~= nil and calc_val and calc_val ~= " " then
calc_val = mathlib.round(calc_val, round_digits, 10)
if raw_y_num then raw_y_num = tonumber(mathlib.re(calc_val)) end
end
local y_c = numberConstructer(calc_val)
if isInvalidNumStr(y_c) or not raw_y_num or raw_y_num ~= raw_y_num or math.abs(raw_y_num) == math.huge then
calc_val, raw_y_num = " ", nil
end
local x_c = numberConstructer(x_val)
if isInvalidNumStr(x_c) or not raw_x_num or raw_x_num ~= raw_x_num or math.abs(raw_x_num) == math.huge then
x_val, raw_x_num = " ", nil
end
return raw_x_num, raw_y_num, x_val, calc_val
end
sampling = math.max(1, tonumber(sampling) or 50)
x_start = tonumber(x_start) or 0
x_end = tonumber(x_end) or 1
local dx = (x_end - x_start) / sampling
-- 將單點寫入陣列並同步套用 y_min / y_max 與 x_start / x_end 截斷
local function storeSamplePoint(j, pt_idx, t_val, raw_x_num, raw_y_num, x_val, calc_val, is_param, is_polar)
if calc_val ~= " " and raw_y_num then
if y_max and raw_y_num > y_max then
calc_val = y_max
raw_y_num = y_max
end
if y_min and raw_y_num < y_min then
calc_val = y_min
raw_y_num = y_min
end
end
if x_val ~= " " and raw_x_num and not is_param and not is_polar then
if x_end and raw_x_num > x_end then
x_val = x_end
raw_x_num = x_end
end
if x_start and raw_x_num < x_start then
x_val = x_start
raw_x_num = x_start
end
end
y_arr[j][pt_idx] = tostring(calc_val)
x_arr[j][pt_idx] = tostring(x_val)
raw_y_arr[j][pt_idx] = (calc_val ~= " " and raw_y_num) and raw_y_num or false
raw_x_arr[j][pt_idx] = (x_val ~= " " and raw_x_num) and raw_x_num or false
raw_t_arr[j][pt_idx] = tonumber(t_val) or 0
end
-- 1. 依各系列之取樣模式 (auto / integer / float / diff) 執行取樣
for j = 1, #expr_list do
local pf = postfix[j]
if not (type(pf) == "table" and pf.implicit2d == true) then
local is_param = (type(pf) == "table" and pf.parametric == true)
local is_polar = (type(pf) == "table" and pf.polar == true)
local sMode = string.lower(mw.text.trim(tostring(body_args["samplingMode" .. j] or body_args["sampling_mode_" .. j] or globalSamplingMode)))
-- auto 模式下:若算式為質數函數或化學元素查詢等離散整數函數,自動切換為底層整數取樣
if sMode == "auto" and not is_param and not is_polar and MathEngine.isDiscreteIntegerExpr(expr_list[j]) then
sMode = "integer"
end
if (sMode == "integer" or sMode == "int") and not is_param and not is_polar then
series_int_mode[j] = true
local iStart = math.ceil(math.min(x_start, x_end) - 1e-9)
local iEnd = math.floor(math.max(x_start, x_end) + 1e-9)
if iEnd < iStart then
iStart = math.floor(x_start + 0.5)
iEnd = iStart
end
local pt_idx = 0
for iv = iStart, iEnd, 1 do
pt_idx = pt_idx + 1
local rx, ry, xv, cv = evaluateAt(j, iv)
storeSamplePoint(j, pt_idx, iv, rx, ry, xv, cv, false, false)
end
else
-- 建立基礎取樣參數點清單 (若含 sgn/abs/diff 且跨越 0,確保 0 被精確取樣以捕捉脈衝)
local t_list = {}
local hasZero = false
for i = 0, sampling do
local it = x_start + i * dx
if math.abs(it) < 1e-12 then
it = 0
hasZero = true
end
local t_val = is_param and (pf.min + (i * (pf.max - pf.min) / sampling)) or it
t_list[#t_list + 1] = t_val
end
if not is_param and not is_polar and not has_last_flags[j] and not hasZero and x_start < 0 and x_end > 0 then
t_list[#t_list + 1] = 0
table.sort(t_list)
end
for idx_t = 1, #t_list do
local tv = t_list[idx_t]
local rx, ry, xv, cv = evaluateAt(j, tv)
storeSamplePoint(j, idx_t, tv, rx, ry, xv, cv, is_param, is_polar)
end
-- diff (自適應微分加密取樣) 模式:記錄過往幾次取樣的一階與二階變化量,在變化突增或高頻震盪區自動加密取樣
if (sMode == "diff" or sMode == "adaptive") and not has_last_flags[j] then
local validY = {}
for k = 1, #raw_y_arr[j] do
if raw_y_arr[j][k] ~= false then validY[#validY + 1] = raw_y_arr[j][k] end
end
table.sort(validY)
local vSpanY = (#validY >= 4)
and math.max(1e-6, validY[math.floor(#validY * 0.9)] - validY[math.ceil(#validY * 0.1)])
or ((#validY >= 2) and math.max(1e-6, validY[#validY] - validY[1]) or 1)
-- 有限長度佇列 (Window = 5) 紀錄過往幾輪的一階微分變化量 |Δy|
local d1_queue = {}
local q_size = 5
local extra_pts = {}
local extra_budget = 320
local function subdivideInterval(tA, xA, yA, tB, xB, yB, depth)
if extra_budget <= 0 or depth > 4 then return end
local tM = 0.5 * (tA + tB)
local rxM, ryM, xvM, cvM = evaluateAt(j, tM)
extra_budget = extra_budget - 1
if ryM then
if y_max and ryM > y_max then ryM, cvM = y_max, y_max end
if y_min and ryM < y_min then ryM, cvM = y_min, y_min end
extra_pts[#extra_pts + 1] = { t = tM, rx = rxM, ry = ryM, xv = xvM, cv = cvM }
local dyL = math.abs(ryM - yA)
local dyR = math.abs(yB - ryM)
local d2M = math.abs((yB - ryM) - (ryM - yA))
if dyL > vSpanY * 0.04 or d2M > vSpanY * 0.03 then
subdivideInterval(tA, xA, yA, tM, rxM, ryM, depth + 1)
end
if dyR > vSpanY * 0.04 or d2M > vSpanY * 0.03 then
subdivideInterval(tM, rxM, ryM, tB, xB, yB, depth + 1)
end
else
-- 若中點無效 (如 sin(1/x) 在 0 附近),分別探測左右四分位點
local tQ1, tQ3 = 0.75 * tA + 0.25 * tB, 0.25 * tA + 0.75 * tB
local rx1, ry1, xv1, cv1 = evaluateAt(j, tQ1)
local rx3, ry3, xv3, cv3 = evaluateAt(j, tQ3)
extra_budget = extra_budget - 2
if ry1 then extra_pts[#extra_pts + 1] = { t = tQ1, rx = rx1, ry = ry1, xv = xv1, cv = cv1 } end
if ry3 then extra_pts[#extra_pts + 1] = { t = tQ3, rx = rx3, ry = ry3, xv = xv3, cv = cv3 } end
end
end
local prev_dy = 0
for k = 1, #raw_t_arr[j] - 1 do
local t0, t1 = raw_t_arr[j][k], raw_t_arr[j][k + 1]
local x0, y0 = raw_x_arr[j][k], raw_y_arr[j][k]
local x1, y1 = raw_x_arr[j][k + 1], raw_y_arr[j][k + 1]
if y0 ~= false and y1 ~= false then
local dy = math.abs(y1 - y0)
local d2y = math.abs(dy - prev_dy)
local qSum = 0
for m = 1, #d1_queue do qSum = qSum + d1_queue[m] end
local localAvg = (#d1_queue > 0) and (qSum / #d1_queue) or (vSpanY * 0.02)
-- 當一階變化量或二階變化量相對歷史佇列突增,或區間跨度較大時,觸發自適應加密取樣
local needSub = (dy > vSpanY * 0.045)
or (dy > localAvg * 2.2 and dy > vSpanY * 0.01)
or (d2y > localAvg * 2.0 and d2y > vSpanY * 0.01)
-- 額外探測四分位點以防高頻震盪 (如 sin(1/x)) 剛好落在等值相角上
if not needSub and math.abs(t0) < (x_end - x_start) * 0.25 then
local tProbe = t0 + 0.37 * (t1 - t0)
local rxP, ryP, xvP, cvP = evaluateAt(j, tProbe)
if ryP then
local yLin = y0 + 0.37 * (y1 - y0)
if math.abs(ryP - yLin) > vSpanY * 0.025 then
extra_pts[#extra_pts + 1] = { t = tProbe, rx = rxP, ry = ryP, xv = xvP, cv = cvP }
needSub = true
end
end
end
if needSub then
subdivideInterval(t0, x0, y0, t1, x1, y1, 1)
end
table.insert(d1_queue, dy)
if #d1_queue > q_size then table.remove(d1_queue, 1) end
prev_dy = dy
elseif y0 ~= false or y1 ~= false then
-- 跨越定義域邊界 (如 1/x 或 sqrt(x)) 自動加密邊緣
subdivideInterval(t0, x0 or 0, y0 or y1, t1, x1 or 0, y1 or y0, 2)
end
end
if #extra_pts > 0 then
for k = 1, #raw_t_arr[j] do
extra_pts[#extra_pts + 1] = {
t = raw_t_arr[j][k],
rx = raw_x_arr[j][k],
ry = raw_y_arr[j][k],
xv = x_arr[j][k],
cv = y_arr[j][k]
}
end
table.sort(extra_pts, function(a, b) return a.t < b.t end)
raw_t_arr[j], raw_x_arr[j], raw_y_arr[j], x_arr[j], y_arr[j] = {}, {}, {}, {}, {}
for k = 1, #extra_pts do
local pt = extra_pts[k]
raw_t_arr[j][k] = pt.t
raw_x_arr[j][k] = pt.rx
raw_y_arr[j][k] = pt.ry
x_arr[j][k] = tostring(pt.xv)
y_arr[j][k] = tostring(pt.cv)
end
end
end
end
end
end
-- 1b. 2D 隱函數 Marching Squares 取樣與路徑縫合
local imp_project_fns = {}
local imp_y_bounds = {}
for j = 1, #expr_list do
local pf = postfix[j]
if type(pf) == "table" and pf.implicit2d == true then
local impYMin = tonumber(body_args.yAxisMin or body_args.ystart or body_args.y_start) or y_min or x_start
local impYMax = tonumber(body_args.yAxisMax or body_args.yend or body_args.y_end) or y_max or x_end
if impYMin > impYMax then impYMin, impYMax = impYMax, impYMin end
if impYMin == impYMax then impYMin, impYMax = impYMin - 1, impYMax + 1 end
local impGridN = tonumber(body_args.implicitSampling) or math.max(80, sampling)
local function evalImp2D(vx, vy)
local val = nil
xpcall(function()
eval_env.x = vx
eval_env.y = vy
local res = lib_calc.calc_by_postfix(pf.pf, eval_env, mathlib, numberConstructer, false)
if res and res ~= " " then
local nr = mathlib.nonRealPart and mathlib.nonRealPart(res)
if nr and math.abs(tonumber(mathlib.abs(nr)) or 0) > 1e-8 then
res = nil
end
if res then
local re = tonumber(mathlib.re(res))
if re and re == re and math.abs(re) < 1e12 then
val = re
end
end
end
end, noop_func)
return val
end
local rx_imp, ry_imp, xs_imp, ys_imp, proj_fn = MathEngine.evaluateImplicit2D(
evalImp2D, x_start, x_end, impYMin, impYMax, impGridN
)
raw_x_arr[j], raw_y_arr[j] = rx_imp, ry_imp
x_arr[j], y_arr[j] = xs_imp, ys_imp
imp_project_fns[j] = proj_fn
imp_y_bounds[j] = { min = impYMin, max = impYMax }
end
end
-- 1c. 脈衝函數 (狄拉克 δ 函數 / 階梯函數之微分) 探測與修復
for j = 1, #expr_list do
local pf = postfix[j]
local is_param = (type(pf) == "table" and pf.parametric == true)
local is_polar = (type(pf) == "table" and pf.polar == true)
local is_imp2d = (type(pf) == "table" and pf.implicit2d == true)
if not is_param and not is_polar and not is_imp2d and not series_int_mode[j] then
-- 計算目前平滑區段 (排除 >1e4 的極端脈衝點) 的正常數值範圍
local smoothMin, smoothMax = math.huge, -math.huge
local hasRawSpike = false
for k = 1, #raw_y_arr[j] do
local ry = raw_y_arr[j][k]
if ry ~= false then
if math.abs(ry) >= 1e4 then
hasRawSpike = true
else
if ry < smoothMin then smoothMin = ry end
if ry > smoothMax then smoothMax = ry end
end
end
end
if smoothMin == math.huge then smoothMin, smoothMax = 0, 0 end
local smoothSpan = math.max(1.0, smoothMax - smoothMin)
local impulses = {}
-- (A) 若啟用了 calc diff 微分功能,利用一階與二階差分佇列檢查基底函數 F(x) 是否存在階梯跳躍 (如 sgn(x) 在 0 處跳躍 +2)
if calc_diff_flags[j] then
local baseVals = {}
for k = 1, #raw_x_arr[j] do
local rx = raw_x_arr[j][k]
baseVals[k] = (rx ~= false) and evalRawBaseAt(j, rx) or false
end
for k = 1, #raw_x_arr[j] - 1 do
local x0, x1 = raw_x_arr[j][k], raw_x_arr[j][k + 1]
local b0, b1 = baseVals[k], baseVals[k + 1]
local dy0, dy1 = raw_y_arr[j][k] or 0, raw_y_arr[j][k + 1] or 0
if x0 ~= false and x1 ~= false and b0 ~= false and b1 ~= false then
local dBase = b1 - b0
local expectedChange = 0.5 * ((math.abs(dy0) < 1e4 and dy0 or 0) + (math.abs(dy1) < 1e4 and dy1 or 0)) * (x1 - x0)
-- 若基底函數實際跳躍量遠大於平滑積分變化量,定位跳躍點並轉換為狄拉克 δ 脈衝
if math.abs(dBase - expectedChange) > 0.15 then
local a, b, ba, bb = x0, x1, b0, b1
for _ = 1, 14 do
local mid = 0.5 * (a + b)
local bm = evalRawBaseAt(j, mid)
if not bm then break end
if math.abs(bm - ba) < math.abs(bm - bb) then
a, ba = mid, bm
else
b, bb = mid, bm
end
end
local jumpMag = bb - ba
if math.abs(jumpMag) >= 0.4 * math.abs(dBase) and math.abs(jumpMag) < 1e6 then
local xJ = 0.5 * (a + b)
local snapQ = math.floor(xJ * 4 + 0.5) / 4
if math.abs(xJ - snapQ) < math.max(1e-3, dx * 0.04) then xJ = snapQ end
local dir = (jumpMag > 0) and 1 or -1
local peakY
if dir > 0 then
peakY = y_max or tonumber(body_args.yAxisMax) or (smoothMax + smoothSpan * math.max(1.0, math.abs(jumpMag) * 0.5))
else
peakY = y_min or tonumber(body_args.yAxisMin) or (smoothMin - smoothSpan * math.max(1.0, math.abs(jumpMag) * 0.5))
end
local _, baseLeft = evaluateAt(j, xJ - 1e-4)
local _, baseRight = evaluateAt(j, xJ + 1e-4)
if not baseLeft or math.abs(baseLeft) >= 1e4 then baseLeft = 0 end
if not baseRight or math.abs(baseRight) >= 1e4 then baseRight = 0 end
impulses[#impulses + 1] = { x = xJ, peakY = peakY, yL = baseLeft, yR = baseRight }
end
end
end
end
end
-- (B) 若算式本身直接產生了 >= 1e4 的孤立脈衝極值 (如 diff(:x↦sgn(x);, x) 剛好命中 0),將其正規化為可視脈衝高度
if hasRawSpike or #impulses > 0 then
series_has_impulse[j] = true
local merged = {}
local epsW = math.max(1e-5, (x_end - x_start) * 1e-4)
for k = 1, #raw_x_arr[j] do
local rx, ry = raw_x_arr[j][k], raw_y_arr[j][k]
if rx ~= false and ry ~= false then
if math.abs(ry) >= 1e4 then
local dir = (ry > 0) and 1 or -1
local peakY = (dir > 0)
and (y_max or tonumber(body_args.yAxisMax) or (smoothMax + smoothSpan))
or (y_min or tonumber(body_args.yAxisMin) or (smoothMin - smoothSpan))
local yPrev = (k > 1 and raw_y_arr[j][k - 1] and math.abs(raw_y_arr[j][k - 1]) < 1e4) and raw_y_arr[j][k - 1] or 0
local yNext = (k < #raw_y_arr[j] and raw_y_arr[j][k + 1] and math.abs(raw_y_arr[j][k + 1]) < 1e4) and raw_y_arr[j][k + 1] or 0
merged[#merged + 1] = { x = rx - epsW, y = yPrev }
merged[#merged + 1] = { x = rx, y = peakY }
merged[#merged + 1] = { x = rx + epsW, y = yNext }
else
-- 避免與即將注入的精確脈衝點重疊
local nearImp = false
for _, imp in ipairs(impulses) do
if math.abs(rx - imp.x) <= epsW * 1.5 then nearImp = true; break end
end
if not nearImp then
merged[#merged + 1] = { x = rx, y = ry }
end
end
end
end
for _, imp in ipairs(impulses) do
merged[#merged + 1] = { x = imp.x - epsW, y = imp.yL }
merged[#merged + 1] = { x = imp.x, y = imp.peakY }
merged[#merged + 1] = { x = imp.x + epsW, y = imp.yR }
end
table.sort(merged, function(a, b) return a.x < b.x end)
raw_t_arr[j], raw_x_arr[j], raw_y_arr[j], x_arr[j], y_arr[j] = {}, {}, {}, {}, {}
for k = 1, #merged do
local pt = merged[k]
raw_t_arr[j][k] = pt.x
raw_x_arr[j][k] = pt.x
raw_y_arr[j][k] = pt.y
x_arr[j][k] = tostring(pt.x)
y_arr[j][k] = tostring(pt.y)
end
end
end
end
-- 2. 計算離散打點 (markPoints / markStep) 與跳躍不連續點極限探測 (jumps / markJumps)
local series_marks = {}
local series_jumps = {}
local globalMarkPoints = body_args.markPoints or body_args.mark_points
local globalMarkStep = tonumber(body_args.markStep or body_args.mark_step)
local globalJumps = body_args.jumps
local globalMarkJumps = body_args.markJumps or body_args.mark_jumps
local globalJumpThreshold = tonumber(body_args.jumpThreshold or body_args.jump_threshold)
for j = 1, #expr_list do
local pf = postfix[j]
local is_param = (type(pf) == "table" and pf.parametric == true)
local is_polar = (type(pf) == "table" and pf.polar == true)
local is_imp2d = (type(pf) == "table" and pf.implicit2d == true)
local domMin = is_param and tonumber(pf.min) or x_start
local domMax = is_param and tonumber(pf.max) or x_end
if domMin > domMax then domMin, domMax = domMax, domMin end
-- 2a. 離散打點收集
local mpSpec = body_args["markPoints" .. j] or globalMarkPoints
local msSpec = tonumber(body_args["markStep" .. j]) or globalMarkStep
local mList = {}
if mpSpec then
local mpLower = string.lower(mw.text.trim(tostring(mpSpec)))
if mpLower == "integer" or mpLower == "int" then
for v = math.ceil(domMin - 1e-9), math.floor(domMax + 1e-9), 1 do mList[#mList + 1] = v end
elseif mpLower == "natural" or mpLower == "nat" then
for v = math.max(1, math.ceil(domMin - 1e-9)), math.floor(domMax + 1e-9), 1 do mList[#mList + 1] = v end
elseif mpLower == "nonneg" then
for v = math.max(0, math.ceil(domMin - 1e-9)), math.floor(domMax + 1e-9), 1 do mList[#mList + 1] = v end
elseif mpLower == "all" or yesno(mpLower) == true then
for k = 1, #raw_x_arr[j] do
if raw_x_arr[j][k] ~= false and raw_y_arr[j][k] ~= false then
mList[#mList + 1] = { preX = raw_x_arr[j][k], preY = raw_y_arr[j][k] }
end
end
else
for _, token in ipairs(mw.text.split(tostring(mpSpec), "%s*,%s*")) do
local nv = tonumber(token)
if nv and nv >= domMin - 1e-9 and nv <= domMax + 1e-9 then mList[#mList + 1] = nv end
end
end
elseif msSpec and msSpec > 0 then
local mStart = tonumber(body_args.markStart or body_args.mark_start)
local first = mStart and (mStart + math.ceil((domMin - mStart - 1e-9) / msSpec) * msSpec) or (math.ceil((domMin - 1e-9) / msSpec) * msSpec)
local count = 0
for v = first, domMax + 1e-9, msSpec do
count = count + 1
if count > 200 then break end
if math.abs(v) < 1e-12 then v = 0 end
mList[#mList + 1] = v
end
end
local marks = {}
for _, item in ipairs(mList) do
if type(item) == "table" then
marks[#marks + 1] = { x = item.preX, y = item.preY }
else
local reused = false
if not is_param and not is_polar then
for k = 1, #raw_x_arr[j] do
local rx = raw_x_arr[j][k]
if rx ~= false and math.abs(rx - item) < 1e-9 then
if raw_y_arr[j][k] ~= false then
marks[#marks + 1] = { x = rx, y = raw_y_arr[j][k] }
end
reused = true
break
end
end
end
if not reused then
local mx, my = evaluateAt(j, item)
if mx and my then marks[#marks + 1] = { x = mx, y = my } end
end
end
end
series_marks[j] = marks
-- 2b. 跳躍不連續點探測 (預設自動開啟:使用一階與二階微分有限長度佇列偵測突變,並以 14 步二分法確認不連續點)
local jumps_data = {}
if not is_param and not is_polar and not is_imp2d and not series_int_mode[j] and not series_has_impulse[j] and not has_last_flags[j] then
local jSpec = body_args["jumps" .. j] or globalJumps
local mjRaw = body_args["markJumps" .. j] or globalMarkJumps
local disableJumps = (mjRaw ~= nil and yesno(mjRaw) == false)
local candX = {}
if jSpec then
local jLower = string.lower(mw.text.trim(tostring(jSpec)))
if jLower == "integer" or jLower == "int" then
for v = math.ceil(domMin - 1e-7), math.floor(domMax + 1e-7), 1 do candX[#candX + 1] = v end
else
for _, token in ipairs(mw.text.split(tostring(jSpec), "%s*,%s*")) do
local nv = tonumber(token)
if nv and nv >= domMin - 1e-7 and nv <= domMax + 1e-7 then candX[#candX + 1] = nv end
end
end
elseif not disableJumps then
local minY, maxY = math.huge, -math.huge
for k = 1, #raw_y_arr[j] do
local ry = raw_y_arr[j][k]
if ry ~= false then
if ry < minY then minY = ry end
if ry > maxY then maxY = ry end
end
end
local spanY = (maxY > minY) and (maxY - minY) or 1
local spanX = math.max(1e-9, domMax - domMin)
local minStepTh = globalJumpThreshold or math.max(1e-3, spanY * 0.035)
-- 有限長度佇列 (Window = 6) 儲存過往幾輪的一階微分 |dy/dx| 與二階微分 |d2y/dx2|
local d1_q = {}
local qMax = 6
local prev_d1 = 0
for k = 1, #raw_y_arr[j] - 1 do
local x0, y0 = raw_x_arr[j][k], raw_y_arr[j][k]
local x1, y1 = raw_x_arr[j][k + 1], raw_y_arr[j][k + 1]
if x0 ~= false and y0 ~= false and x1 ~= false and y1 ~= false and x1 > x0 + 1e-12 then
local dy = math.abs(y1 - y0)
local d1 = dy / (x1 - x0)
local d2 = math.abs(d1 - prev_d1)
local qSum = 0
for m = 1, #d1_q do qSum = qSum + d1_q[m] end
local avgD1 = (#d1_q > 0) and (qSum / #d1_q) or 0
-- 當一階微分或二階微分相較於歷史佇列明顯突增 (或超過手動閾值) 時,啟動二分逼近檢驗
local isSurge = (globalJumpThreshold and dy >= globalJumpThreshold)
or (dy >= minStepTh and (d1 > avgD1 * 3.0 + 1e-4 or d2 > avgD1 * 2.5 + 1e-4 or dy >= spanY * 0.20))
if isSurge then
local a, b, ya, yb = x0, x1, y0, y1
local aborted = false
for _ = 1, 14 do
local mid = 0.5 * (a + b)
local _, ym = evaluateAt(j, mid)
if not ym then aborted = true; break end
if math.abs(ym - ya) < math.abs(ym - yb) then
a, ya = mid, ym
else
b, yb = mid, ym
end
end
local finalJump = math.abs(yb - ya)
local subSlope = finalJump / math.max(1e-12, b - a)
-- 確認為有限跳躍不連續點:二分 14 次後落差未收斂為 0 (排除平滑陡坡),且未發散暴衝 (排除 1/x 垂直漸近線)
if not aborted
and finalJump >= math.max(1e-3, dy * 0.45)
and finalJump <= dy * 1.85 + 1e-4
and subSlope > (spanY / spanX) * 40 then
local xJ = 0.5 * (a + b)
-- 整數與四分位數吸附 (Snapping):確保 floor(x)/ceil(x) 精確吸附於整數點
local snapInt = math.floor(xJ + 0.5)
local snapQ = math.floor(xJ * 4 + 0.5) / 4
if math.abs(xJ - snapInt) < math.max(2e-3, (x1 - x0) * 0.05) then
xJ = snapInt
elseif math.abs(xJ - snapQ) < math.max(1e-3, (x1 - x0) * 0.03) then
xJ = snapQ
end
if #candX == 0 or math.abs(xJ - candX[#candX]) > 1e-4 then
candX[#candX + 1] = xJ
end
else
-- 若為平滑陡坡,才將此步的正常斜率推入歷史佇列
table.insert(d1_q, d1)
if #d1_q > qMax then table.remove(d1_q, 1) end
end
else
table.insert(d1_q, d1)
if #d1_q > qMax then table.remove(d1_q, 1) end
end
prev_d1 = d1
end
end
end
local eps = 1e-6
local minJumpDiff = globalJumpThreshold or 1e-3
for _, xJ in ipairs(candX) do
local _, yL = evaluateAt(j, xJ - eps)
local _, yR = evaluateAt(j, xJ + eps)
local _, y0 = evaluateAt(j, xJ)
if yL and yR and math.abs(yR - yL) >= minJumpDiff then
jumps_data[#jumps_data + 1] = { x = xJ, yL = yL, yR = yR, y0 = y0 }
end
end
end
series_jumps[j] = jumps_data
end
local result = { series = {}, all_polar = all_polar }
if #expr_list > 0 then result.legend = "函數" end
for i = 1, #expr_list do
result['x'] = table.concat(x_arr[i], ',')
result['x' .. tostring(i)] = result['x']
result['y' .. tostring(i)] = table.concat(y_arr[i], ',')
local pf = postfix[i]
local is_imp2d = (type(pf) == "table" and pf.implicit2d == true)
local is_param = (type(pf) == "table" and pf.parametric == true) or is_imp2d
local is_polar = (type(pf) == "table" and pf.polar == true)
local clean_expr = (type(pf) == "table" and pf.clean_expr) or tostring(expr_list[i])
local custom_name = body_args[tostring(i) .. " name"]
local yTitle = tostring(custom_name or clean_expr)
if not custom_name then
if is_polar and pf.parametric then
yTitle = "r=" .. pf.x_name .. "; θ=" .. pf.y_name
elseif is_polar then
yTitle = "r=" .. clean_expr
elseif pf.parametric then
yTitle = "x=" .. pf.x_name .. "; y=" .. pf.y_name
elseif calc_diff_flags[i] then
yTitle = '( ' .. yTitle .. " )'"
end
end
if check_func[yTitle] ~= nil then
local new_name = yTitle .. " ,(" .. tostring(check_func[yTitle] + 1) .. ")"
check_func[yTitle] = check_func[yTitle] + 1
yTitle = new_name
else
check_func[yTitle] = 1
end
result['y' .. tostring(i) .. "Title"] = yTitle
local series_idx = i
result.series[i] = {
x = x_arr[i],
y = y_arr[i],
raw_x = raw_x_arr[i],
raw_y = raw_y_arr[i],
raw_t = raw_t_arr[i],
eval_fn = (not has_last_flags[i] and not is_imp2d and not series_has_impulse[i]) and function(t_query)
local rx, ry = evaluateAt(series_idx, t_query)
if rx and ry then
if y_max and ry > y_max then ry = y_max end
if y_min and ry < y_min then ry = y_min end
end
return rx, ry
end or nil,
project_fn = imp_project_fns[i],
implicit = is_imp2d,
integer_mode = series_int_mode[i],
has_impulse = series_has_impulse[i],
default_interp = series_default_interp[i],
imp_y_min = imp_y_bounds[i] and imp_y_bounds[i].min or nil,
imp_y_max = imp_y_bounds[i] and imp_y_bounds[i].max or nil,
marks = series_marks[i],
jumps = series_jumps[i],
title = yTitle,
raw_expr = clean_expr,
parametric = is_param,
polar = is_polar
}
end
return result
end
function p.renderSVG(data, args)
return SVGRenderer.renderFunctionGraph(data, args)
end
-- 保留原有的運算表格產生器 calc_table
function p.calc_table(frame)
local variable_process = require("Module:Number/data")
local args
local can_math = false
local should_math = false
if frame == mw.getCurrentFrame() then
args = getArgs(frame, {
parentFirst = true,
trim = false,
removeBlanks = false
})
can_math = yesno(args['use math'] or args['use_math'])
should_math = yesno(args['should math'] or args['should_math'])
else
args = frame
end
lib_calc._randomseed()
if yesno(args.useOtherModule or 'no') == true then lib_calc.use_other_module = true end
local first_number_list = { "2", "3", "4", "5", "6", "7", "8", "9" }
local second_number_list, first_number_show, second_number_show
local calculate_str, calculate_title = "{{{left}}} * {{{right}}}", "×"
if args['number list'] or args['number_list'] then first_number_list = mw.text.split(args['number list'] or args['number_list'] or '', ',') end
if args['number list2'] or args['number_list2'] then second_number_list = mw.text.split(args['number list2'] or args['number_list2'] or '', ',') end
if args['number list show'] or args['number_list_show'] then
first_number_show = mw.text.split(args['number list show'] or args['number_list_show'] or '', ',')
if #first_number_show == 1 and mw.text.trim(first_number_show[1]) == '' then first_number_show = {} end
end
if args['number list show2'] or args['number_list_show2'] then
second_number_show = mw.text.split(args['number list show2'] or args['number_list_show2'] or '', ',')
if #second_number_show == 1 and mw.text.trim(second_number_show[1]) == '' then second_number_show = {} end
end
if args['calculate'] then calculate_str = mw.text.trim(lib_calc._remove_strip_marker(args['calculate'])) end
if args['calculate title'] or args['calculate_title'] then calculate_title = mw.text.trim(args['calculate title'] or args['calculate_title']) end
if second_number_list == nil or #second_number_list == 0 then second_number_list = first_number_list end
if first_number_show == nil or #first_number_show == 0 then first_number_show = first_number_list end
if second_number_show == nil or #second_number_show == 0 then second_number_show = first_number_show end
if comp_number == nil then comp_number = require("Module:Complex Number") end
local cmath, qmath, bmath = comp_number.cmath.init(), comp_number.qmath.init(), comp_number.bmath.init()
local mathtag = lib_calc.tagmath.init()
local math_class = args['class'] or ''
if mw.text.trim(math_class) == '' then math_class = "cmath" end
local mymath = cmath
local mytomath = cmath.toComplexNumber
if mw.ustring.sub(math_class, 1, 7):upper() == "MODULE:" then
local module_name, math_lib_name = lib_calc.checkModuleClass(math_class)
xpcall(function()
local load_module = require("Module:" .. module_name)
if load_module ~= nil then
local load_math_lib = load_module[math_lib_name]
if load_math_lib ~= nil then
local func_type = type(noop_func)
local my_math_lib = (type(load_math_lib.init) == func_type) and load_math_lib.init() or load_math_lib
if type(my_math_lib.constructor) == func_type then
math_class = "mymath"
mymath = my_math_lib
mytomath = my_math_lib.constructor
end
end
end
end, noop_func)
end
local out_buf = {}
out_buf[#out_buf + 1] = "! " .. (args["main head css"] or args["main_head_css"] or '') .. " | " .. calculate_title .. " \n"
for j = 1, #second_number_list do
local second_it = mw.text.trim(second_number_list[j] or '')
local second_it_show = mw.text.trim(second_number_show[j] or '')
if second_it ~= nil then
local second_num_math = tostring(second_it_show ~= '' and second_it_show or second_it)
if should_math or can_math then
if args.class ~= "mathtag" and yesno(args['show math'] or args['show_math']) then
second_num_math = lib_calc._re_math_output(second_num_math)
end
second_num_math = lib_calc._adj_math_output(second_num_math)
end
if can_math then second_num_math = frame:callParserFunction{ name = "#tag:math", args = { second_num_math } } end
out_buf[#out_buf + 1] = "! " .. (args["head css"] or args["head_css"] or '') .. " | " .. second_num_math .. " \n"
end
end
out_buf[#out_buf + 1] = "\n"
local lib_table = { cmath = cmath, qmath = qmath, bmath = bmath, mathtag = mathtag, mymath = mymath }
local ctor_table = { cmath = cmath.toComplexNumber, qmath = qmath.toQuaternionNumber, bmath = bmath.toBoolean, mathtag = mathtag.toTagMath, mymath = mytomath }
for i = 1, #first_number_list do
local first_it = mw.text.trim(first_number_list[i] or '')
local first_it_show = mw.text.trim(first_number_show[i] or '')
if first_it ~= nil then
out_buf[#out_buf + 1] = "|-\n"
local first_num_math = tostring(first_it_show ~= '' and first_it_show or first_it)
if should_math or can_math then
if args.class ~= "mathtag" and yesno(args['show math'] or args['show_math']) then
first_num_math = lib_calc._re_math_output(first_num_math)
end
first_num_math = lib_calc._adj_math_output(first_num_math)
end
if can_math then first_num_math = frame:callParserFunction{ name = "#tag:math", args = { first_num_math } } end
out_buf[#out_buf + 1] = "! " .. (args["head css"] or args["head_css"] or '') .. " | " .. first_num_math .. " \n"
for j = 1, #second_number_list do
local second_it = mw.text.trim(second_number_list[j] or '')
if second_it ~= nil then
local buffer_str = variable_process._getFormatingStringByArgument(calculate_str, {
left = tostring(first_it), right = tostring(second_it)
})
local exec_result = lib_calc.calc(buffer_str or '', lib_table[math_class], ctor_table[math_class])
local exec_result_str = mw.text.trim(tostring(exec_result) or '')
local exec_check = mw.text.trim((tonumber(exec_result_str) and args[tonumber(exec_result_str)]) or args[exec_result_str] or '')
if exec_check == '' then exec_check = nil end
local cell_css = exec_check or args["number css"] or args["number_css"] or ''
if should_math or can_math then
if args.class ~= "mathtag" and yesno(args['show math'] or args['show_math']) then
exec_result_str = lib_calc._re_math_output(exec_result_str)
end
exec_result_str = lib_calc._adj_math_output(exec_result_str)
end
if can_math then exec_result_str = frame:callParserFunction{ name = "#tag:math", args = { exec_result_str } } end
out_buf[#out_buf + 1] = "|" .. cell_css .. "| " .. mw.text.trim(exec_result_str) .. " \n"
end
end
end
end
return table.concat(out_buf)
end
-- 複變函數圖形:預設為 2D 定義域著色圖,若指定 3d=yes 則切換為 3D 複變模態曲面圖 (支援多函數與複變隱函數)
function p.complex_graph(frame)
if comp_number == nil then comp_number = require("Module:Complex Number") end
local cmath, qmath = comp_number.cmath.init(), comp_number.qmath.init()
if not getArgs then getArgs = require('Module:Arguments').getArgs end
local args = getArgs(frame, { parentFirst = true })
local args_is_template_call = yesno(frame and frame.args and frame.args.is_template_call or 'no') == true
local parent = frame and frame.getParent and frame:getParent()
local parent_title = (parent and parent:getTitle()) or ""
local is_template_call = false
if mw.ustring.find(parent_title, "函數圖形", 1, true) or mw.ustring.find(parent_title, "FunctionGraph", 1, true) or args_is_template_call then
is_template_call = true
end
local exprs = {}
local max_idx = 0
for k, _ in pairs(args) do
local idx = tonumber(k)
if idx and idx > max_idx then max_idx = idx end
end
for idx = 1, max_idx do
if args[idx] ~= nil then
local clean_e, need_mod = MathEngine.preprocessPrimeSugar(lib_calc._remove_strip_marker(args[idx]))
if need_mod then lib_calc.use_other_module = true end
exprs[#exprs + 1] = clean_e
end
end
if #exprs == 0 then exprs[1] = 'x' end
local expr = exprs[1]
local body_args, x_start, x_end, y_start, y_end, sampling, width = {}, -5, 5, nil, nil, nil, nil
for arg_name, arg_value in pairs(args) do
if tonumber(arg_name) == nil then
local check_arg_name = string.gsub(string.lower(tostring(arg_name)), "[ _]", "")
if check_arg_name == "xstart" or check_arg_name == "start" then x_start = tonumber(arg_value) or x_start
elseif check_arg_name == "xend" or check_arg_name == "end" then x_end = tonumber(arg_value) or x_end
elseif check_arg_name == "ystart" then y_start = tonumber(arg_value)
elseif check_arg_name == "yend" then y_end = tonumber(arg_value)
elseif check_arg_name == "sampling" then sampling = tonumber(arg_value)
elseif check_arg_name == "width" then width = tonumber(arg_value)
else body_args[arg_name] = arg_value
end
end
end
y_start = y_start or x_start
y_end = y_end or x_end
if yesno(args.useOtherModule or 'no') == true then lib_calc.use_other_module = true end
local math_class = (frame and frame.args and frame.args['class']) or args['class'] or ''
local selected_lib, selected_ctor = resolveMathLib(math_class, cmath, qmath)
body_args.is_template_call = is_template_call
body_args.x_start = x_start
body_args.x_end = x_end
body_args.y_start = y_start
body_args.y_end = y_end
body_args.expr = expr
TrackingCategory.append('有SVG圖表的頁面')
if use_ext_mathlib == true then TrackingCategory.append('使用擴充複變函數庫的頁面') end
-- 檢查是否啟用 3D 複變模式 (3d=yes 或 type=3d / complex3d)
local modeStr = string.lower(mw.text.trim(tostring(body_args.type or body_args.mode or "")))
local is3DComplex = (yesno(body_args["3d"] or body_args["3D"] or "no") == true)
or (modeStr == "3d" or modeStr == "complex3d" or modeStr == "surface")
or (mw.ustring.match(expr, "^#[3][dD]%s*:") ~= nil)
if is3DComplex then
body_args.sampling = sampling
body_args.width = width or (is_template_call and 200 or 420)
local rawH = tonumber(body_args.height)
body_args.height = (is_template_call and (not rawH or rawH == 100)) and math.max(180, math.floor(body_args.width * 0.9)) or (rawH or 340)
local data3D = MathEngine.evaluateComplex3DSeries(
exprs, body_args, lib_calc, selected_lib or cmath, selected_ctor or cmath.toComplexNumber
)
return SVGRenderer.render3DGraph(data3D, body_args)
end
width = width or 160
sampling = sampling or 75
body_args.width = width
local calc_result = p._complex_graph(
expr, x_start, x_end, y_start, y_end, sampling, width, body_args, selected_lib, selected_ctor
)
return SVGRenderer.renderComplexGraph(calc_result, body_args)
end
function p._complex_graph(expr, x_start, x_end, y_start, y_end, sampling, width, body_args, math_lib, number_Constructer)
body_args = body_args or {}
if yesno(body_args.useOtherModule or 'no') == true then lib_calc.use_other_module = true end
if comp_number == nil then comp_number = require("Module:Complex Number") end
local cmath = comp_number.cmath.init()
lib_calc._randomseed()
local mathlib = math_lib or cmath
local numberConstructer = number_Constructer or cmath.constructor
-- Request-local diagnostic optimization for the standard complex library,
-- which uses mw.ustring too much.
-- Cache literal conversion, including failed conversions; clone returned values
-- because the evaluator calls clean() and may otherwise mutate shared objects.
if mathlib == cmath then
local original_constructor = numberConstructer
local literal_cache = {}
numberConstructer = function(value)
if type(value) ~= 'string' then return original_constructor(value) end
local cached = literal_cache[value]
if cached == nil then
local converted = original_constructor(value)
if converted == nil then
cached = false
else
cached = {real=converted.real, imag=converted.imag}
end
literal_cache[value] = cached
end
if cached == false then return nil end
return cmath.getComplexNumber(cached.real, cached.imag)
end
end
local pre_expr, pre_scope = lib_calc._function_preprocessing(expr, mathlib, numberConstructer, false)
local postfix = lib_calc.infixToPostfix(pre_expr, debug_flag)
if pre_scope then postfix.scope = pre_scope end
local floor, max, min, log, sqrt, atan2, format = math.floor, math.max, math.min, math.log, math.sqrt, math.atan2, string.format
local rad_to_deg = 180 / math.pi
local function HSBToHex(h, s, b)
local k5 = (5 + h / 60) % 6
local k3 = (3 + h / 60) % 6
local k1 = (1 + h / 60) % 6
local f5 = b * (1 - s * max(0, min(k5, 4 - k5, 1)))
local f3 = b * (1 - s * max(0, min(k3, 4 - k3, 1)))
local f1 = b * (1 - s * max(0, min(k1, 4 - k1, 1)))
return format("#%02x%02x%02x", floor(255 * f5 + 0.5), floor(255 * f3 + 0.5), floor(255 * f1 + 0.5))
end
local i_value = (mathlib.elements and mathlib.elements[2]) or mathlib.i
if not i_value then error("繪製失敗:所用數體無非實數單位元") end
sampling = min(max(10, tonumber(sampling) or 75), 120)
local zero_elem = mathlib[0]
local eval_env = { x = nil }
local pixels = {}
local dy = (y_end - y_start) / sampling
local dx = (x_end - x_start) / sampling
for row_idx = 0, sampling do
local it_y = y_end - (row_idx * dy)
local iy_term = i_value * it_y
local calc_row = {}
for col_idx = 0, sampling - 1 do
local it_x = x_start + (col_idx * dx)
eval_env.x = zero_elem + it_x + iy_term
local calc_val = lib_calc.calc_by_postfix(postfix, eval_env, mathlib, numberConstructer, false)
local hex_color = "#7f7f7f"
if calc_val ~= nil then
local re = tonumber(mathlib.re(calc_val))
local im = tonumber((mathlib.im and mathlib.im(calc_val)) or 0)
if re and im then
if re ~= re or im ~= im then
hex_color = "#7f7f7f"
elseif re == math.huge or re == -math.huge or im == math.huge or im == -math.huge then
hex_color = "#ffffff"
else
local abs_val = sqrt(re * re + im * im)
if not mathlib.im and mathlib.abs then
abs_val = tonumber(mathlib.re(mathlib.abs(calc_val))) or abs_val
end
if abs_val ~= abs_val then
hex_color = "#7f7f7f"
elseif abs_val == math.huge then
hex_color = "#ffffff"
else
local arg_rad = (mathlib.im and atan2(im, re)) or tonumber(mathlib.re(mathlib.arg(calc_val))) or 0
local h = (arg_rad * rad_to_deg) % 360
if h < 0 then h = h + 360 end
local log_abs = log(abs_val + 1)
local s = 1 / (log_abs * 0.3 + 1)
local b = 1 - 1 / (log_abs * 5 + 1.1)
hex_color = HSBToHex(h, max(0, min(1, s)), max(0, min(1, b)))
end
end
end
end
calc_row[col_idx + 1] = hex_color
end
pixels[row_idx + 1] = calc_row
end
-- 計算 10x10 均勻網格的複變函數精確值供 HTML Tooltip 使用
local tt_grid = {}
local gridN = 10
for r = 1, gridN do
local gy = y_end - ((r - 0.5) / gridN) * (y_end - y_start)
if math.abs(gy) < 1e-12 then gy = 0 end
local iy_t = i_value * gy
for c = 1, gridN do
local gx = x_start + ((c - 0.5) / gridN) * (x_end - x_start)
if math.abs(gx) < 1e-12 then gx = 0 end
eval_env.x = zero_elem + gx + iy_t
local cval = lib_calc.calc_by_postfix(postfix, eval_env, mathlib, numberConstructer, false)
if cval ~= nil then
local re = tonumber(mathlib.re(cval))
local im = tonumber((mathlib.im and mathlib.im(cval)) or 0)
if re and im and re == re and im == im and math.abs(re) < 1e12 and math.abs(im) < 1e12 then
local abs_v = sqrt(re * re + im * im)
local arg_d = ((mathlib.im and atan2(im, re)) or tonumber(mathlib.re(mathlib.arg(cval))) or 0) * rad_to_deg
local h = arg_d % 360
if h < 0 then h = h + 360 end
local log_a = log(abs_v + 1)
local hex_c = HSBToHex(h, max(0, min(1, 1 / (log_a * 0.3 + 1))), max(0, min(1, 1 - 1 / (log_a * 5 + 1.1))))
tt_grid[#tt_grid + 1] = {
col = c, row = r,
x = gx, y = gy,
re = re, im = im,
color = hex_c
}
end
end
end
end
return {
width = width,
height = width,
pixels = pixels,
tooltips = tt_grid,
sampling = sampling,
x_start = x_start,
x_end = x_end,
y_start = y_start,
y_end = y_end
}
end
return p