跳转到内容

模組:FunctionGraph

本页使用了标题或全文手工转换
被永久保护的模块
维基百科,自由的百科全书

-- 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