模組:Graph
本模組旨在處理一些資料無法與较新的Chart扩展相容的圖表提供基本的圖表顯示功能及簡易資料懸浮框。若需使用圖表原有的互動性功能,建議转换为Chart扩展:如果是{{Graph:Chart}}原有資料結構或圖表性質有在新版圖表支援的範圍內的图表則建議转换为较新的Chart扩展格式。您可以使用Commons上的MediaWiki提供的简单脚本:graphDataImport.js。 |
| 此模块已评为alpha版,可接受第三方输入,并可用于少量页面以检查是否存在问题,但需要受到检查。欢迎提供新功能或修改其输入输出机制的建议。 |
本模組為維基百科圖表系統的核心基礎設施。由於 MediaWiki 官方出於安全因素停用了基於 JavaScript/JSON 的 Extension:Graph (Vega) 擴充功能,本模組利用Scribunto的mw.svg程式庫,在純伺服器端重新實作了高效能的圖表與地圖渲染引擎。
模組架構
本系統採用「高內聚、低耦合」的八模組架構設計:
Module:Graph(主路由器與資料變換器)- 負責接收外部調用、解析參數、載入外部資料、過濾髒數據。
- 將扁平化資料 (Long Data) 轉置為渲染器需要的寬表 (Wide Data) 規格(
ChartSpec)。
Module:Graph/SVGRenderer(SVG渲染引擎)- 接收
ChartSpec,並依據資料屬性派發至Module:Graph/ChartRenderer,或現場繪製,也負責SVG節點生成。
- 接收
Module:Graph/ChartRenderer(2D 圖表渲染引擎)- 接收
ChartSpec,主要負責繪製各種類型的圖表再返回Module:Graph/SVGRenderer輸出。
- 接收
Module:Graph/MapRenderer(地圖渲染引擎)- 負責讀取 TopoJSON,執行麥卡托 (Mercator) 等地理投影,並支援亞像素過濾與靜態 SVG 向量字典快取(WorldMapData)。
Module:Graph/RenderUtils(渲染共用工具集)- 提供
Module:Graph/SVGRenderer、Module:Graph/ChartRenderer、Module:Graph/MapRenderer共用函數和工具集。主要負責處理比例尺(Linear/Log/Pow/Sqrt)、座標軸漂亮刻度計算(Nice Ticks)、單調三次樣條插值(Monotone Cubic Spline)。
- 提供
Module:Graph/MathEngine(數學運算引擎與工具集)- 提供與數學運算相關的共用函數和工具集。
Module:Graph/Expression(閹割版JavaScript引擎)- 負責處理來自舊vega中的JavaScript運算式的圖靈不完全(Turing-incomplete)的單行JavaScript運算式解析器。其僅支援JavaScript求值和閉包函數功能,其餘所有流程控制功能皆不提供。本模組的目的僅為了相容舊vega資料處理語法而設計。若用了不提供的JavaScript語法,將自動降級顯示MediaWiki官方的圖表不可用警示。
Module:Graph/Wikidata(維基數據資料讀取)- 專為
<graph>圖表設計之輕量型 Wikidata SPARQL 直譯與資料萃取引擎。
- 專為
五階段外部資料路由管線 (Data Pipeline)
本模組具備強大的外部資料抓取能力。當偵測到 dataUrl 參數或 JSON 中的 url 屬性時,會透過 fetchExternalData 函數進行安全路由:
- 路由 A(網路 API):若開頭為
wikirest://(如 PageViews)或http://,由於 Lua 沙盒限制無法對外連線,將直接觸發 Fallback (優雅降級),交還給 MediaWiki 系統顯示官方停用警語。 - 路由 B(Data: 命名空間):透過
pcall(mw.ext.data.get)拉取維基共享資源的 Tabular Data。 - 路由 C(Module: 命名空間):透過
pcall(mw.loadData)載入靜態 Lua Table,避免重複解析。 - 路由 D(原生 JSON 快取):檢查頁面的
contentModel == "json",若相符則優先呼叫mw.loadJsonData,直接利用 MediaWiki 內建的 JSON 快取通道。 - 路由 E(一般維基代碼):對於包含維基模板語法的 JSON,先呼叫
frame:preprocess()展開模板參數,接著通過內建的 字串狀態機註解剝離器 抹除//與/* */註解,最後才送入mw.text.jsonDecode,確保解析不崩潰。
針對 Lua 5.1 代理表的防呆設計
由於 MediaWiki 的 Scribunto (Lua 5.1) 中,mw.loadData 與 mw.loadJsonData 回傳的是帶有 Metatable 的唯讀代理表(Proxy Table),直接對其使用 # 運算子(如 #features 或 #data)永遠會得到 0。
本系統的資料遍歷全面改用 while table[idx] ~= nil do 迭代,徹底避開此陷阱,確保資料不會因長度運算子失效而全部遺失。
開發者 API 介面
| 方法名稱 | 說明 |
|---|---|
p.chart(frame)
|
標準圖表入口。負責合併 arguments,抓取外部資料並呼叫 SVGRenderer。 |
p.vega(frame)
|
Vega 相容入口。接受單一無名參數傳入的 Vega JSON 字串,模組會自動將其解析並轉譯為內部參數表,無縫輸出 SVG。 |
p.map(frame)
|
地圖入口。將參數轉交給 Module:Graph/MapRenderer 進行處理。
|
p.chartDebuger(frame)
|
偵錯模式。除了輸出 SVG 圖表外,還會在畫面上以文字方式 Dumping 輸出一份完整的 ChartSpec Table 結構,方便開發者驗證資料變換(Data Wrangling)的結果是否正確。
|
舊版說明文檔
| 此模块已被保护。此高度可见模块具有高風險,或已用于大量頁面或被频繁替换引用。由于破坏或失误影响程度嚴重,因此已被保护,只有錯誤:頁面未被保護。可以編輯。 |
| 相关页面 |
|---|
Module with helper functions for the Graph extension to display graphs and maps. From de:Modul:Graph.
Functions for templates
map
Creates a JSON object for <graph> to display a political map with colored highlights. In the article namespace the template {{Graph:Map}} should be used instead. See its page for use cases.
Maps can be found at Special:Prefixindex/Template:Graph:Map/Inner/ (for example Template:Graph:Map/Inner/Worldmap2c-json with country borders) and new maps should also be saved under Module:Graph/.
Parameters:
- basemap: sets the base map. The map definitions must follow the TopoJSON format and if saved in Wikipedia are available for this module. Maps in the default directory Special:Prefixindex/Template:Graph:Map/Inner/ like Template:Graph:Map/Inner/Worldmap2c-json should only be referenced by their name while omitting the Module:Graph/ prefix to allow better portability. The parameter also accepts URLs, e.g. maps from other Wikipedia versions (the link should follow the scheme of
//en.wikipedia.org/w/index.php?title=mapname&action=raw, i.e. protocol-relative without leading http/s and a trailing action=raw to fetch the raw content only). URLs to maps on external sites should be avoided for the sake of link stability, performance, security, and she be assumed to be blocked by the software or browser anyway. - scale: the scaling factor of the map (default: 100)
- projection: the map projection to use. Supported values are listed at https://github.com/d3/d3-geo-projection. The default value is
equirectangularfor an equirectangular projection. - center: map center (corresponds in the map data to both comma-separated values of the
scalefield) - feature: which geographic objects should be displayed (corresponds in the map data to the name of the field under the
objectsfield). The default is valuecountries. - ids of geographic entities: The actual parameter names depend on the base map and the selected feature. For example, for the above mentioned world map the ids are ISO country codes. The values can be either colors or numbers in case the geographic entities should be associated with numeric data:
DE=lightbluemarks Germany in light blue color, andDE=80.6assigns Germany the value 80.6 (population in millions). In the latter case, the actual color depends on the following parameters. - colorScale: the color palette to use for the color scale. The palette must be provided as a comma-separated list of color values. The color values must be given either as
#rgb/#rrggbbor by a CSS color name. Instead of a list, the built-in color palettescategory10andcategory20can also be used. - scaleType: supported values are
linearfor a linear mapping between the data values and the color scale,logfor a log mapping,powfor a power mapping (the exponent can be provided aspow 0.5),sqrtfor a square-root mapping, andquantizefor a quantized scale, i.e. the data is grouped in as many classes as the color palette has colors. - domainMin: lower boundary of the data values, i.e. smaller data values are mapped to the lower boundary
- domainMax: upper boundary of the data values, i.e. larger data values are mapped to the upper boundary
- legend: show color legend (does not work with
quantize) - defaultValue: default value for unused geographic entities. In case the id values are colors the default value is
silver, in case of numbers it is 0. - formatjson: format JSON object for better legibility
chart
Creates a JSON object for <graph> to display charts. In the article namespace the template Template:Graph:Chart should be used instead. See its page for use cases.
Parameters:
- width: width of the chart
- height: height of the chart
- type: type of the chart:
linefor line charts,areafor area charts, andrectfor (column) bar charts, andpiefor pie charts. Multiple series can stacked using thestackedprefix, e.g.stackedarea. - interpolate: interpolation method for line and area charts. It is recommended to use
monotonefor a monotone cubic interpolation – further supported values are listed at https://github.com/nyurik/vega/wiki/Marks#line. - colors: color palette of the chart as a comma-separated list of colors. The color values must be given either as
#rgb/#rrggbb/#aarrggbbor by a CSS color name. For#aarrggbbtheaacomponent denotes the alpha channel, i.e. FF=100% opacity, 80=50% opacity/transparency, etc. (The default color palette if n <= 10 is Category10: Template:ChartColors else is Category20: Template:ChartColors). See Template:ChartColors for details. - xAxisTitle and yAxisTitle: captions of the x and y axes
- xAxisMin, xAxisMax, yAxisMin, and yAxisMax: minimum and maximum values of the x and y axes (not yet supported for bar charts). These parameters can be used to invert the scale of a numeric axis by setting the lowest value to the Max and highest value to the Min.
- xAxisFormat and yAxisFormat: changes the formatting of the axis labels. Supported values are listed at https://github.com/d3/d3-3.x-api-reference/blob/master/Formatting.md#numbers for numbers. For example, the format
%can be used to output percentages. For date/time specification of supported values is https://github.com/d3/d3-3.x-api-reference/blob/master/Time-Formatting.md , e.g.xAxisFormat=%d-%m-%Yfor result 13-01-1977. - xAxisAngle: rotates the x axis labels by the specified angle. Recommended values are: -45, +45, -90, +90
- xType and yType: data types of the values, e.g.
integerfor integers,numberfor real numbers,datefor dates (e.g. YYYY-MM-DD), andstringfor ordinal values (usestringto prevent axis values from being repeated when there are only a few values). Remarks:Datetype doesn't work for bar graphs. Fordatedata input please use ISO date format (e.g. YYYY-MM-DD) acc. to date and time formats used in HTML. Other date formats may work but not in all browsers. Date is unfortunately displayed only in en-US format for all Wikipedia languages. Workaround is to use xAxisFormat and yAxisFormat with numerical dates format. - xScaleType and yScaleType: scale types of the x and y axes, e.g.
linearfor linear scale (default),logfor logarithmic scale andsqrtfor square root scale.- A logarithmic chart allows only positive values to be plotted. A square root scale chart cannot show negative values.
- x: the x-values as a comma-separated list, for dates and time see remark in xType and yType
- y or y1, y2, …: the y-values for one or several data series, respectively. For pie charts
y2denotes the radius of the corresponding sectors. For dates and time see remark in xType and yType - legend: show legend (only works in case of multiple data series)
- y1Title, y2Title, …: defines the label of the respective data series in the legend
- linewidth: line width for line charts or distance between the pie segments for pie charts. Setting to 0 with
type=linecreates a scatter plot. - linewidths: different line widths may be defined for each series of data with csv, if set to 0 with "showSymbols" results with points graph, eg.:
linewidths=1, 0, 5, 0.2 - showSymbols: show symbol on data point for line graphs, if a number is provided, the symbol size (default 2.5) may be defined for each data series, eg.:
showSymbols=1, 2, 3, 4 - symbolsShape: custom shape for symbol: circle, x, square, cross, diamond, triangle_up, triangle_down, triangle_right, triangle_left. May be defined for each series of data with csv, eg.:
symbolsShape= circle, cross, square - symbolsNoFill: if true symbol will be without fill (only stroke),
- symbolsStroke: if "x" symbol is used or option "symbolsNoFill" symbol stroke width, default 2.5
- showValues: Additionally, output the y values as text. (Currently, only (non-stacked) bar and pie charts are supported.) The output can be configured used the following parameters provided as
name1:value1, name2:value2(e.g.|showValues=fontcolor:blue,angle:0).- format: Format the output according to https://github.com/d3/d3-3.x-api-reference/blob/master/Formatting.md#numbers for numbers and https://github.com/d3/d3-3.x-api-reference/blob/master/Time-Formatting.md for date/time.
- fontcolor: text color
- fontsize: text size
- offset: move text by the given offset. For bar charts and pie charts with
midanglethis also defines if the text is inside or outside the chart. - angle (pie charts only): text angle in degrees or
midangle(default) for dynamic angles based on the mid-angle of the pie sector.
- innerRadius: For pie charts: defines the inner radius to create a doughnut chart.
- xGrid and yGrid: display grid lines on the x and y axes.
- Annotations
- vAnnotationsLine and hAnnotationsLine: display vertical or horizontal annotation lines on specific values e.g.
hAnnotationsLine=4, 5, 6 - vAnnotationsLabel and hAnnotationsLabel: display vertical or horizontal annotation labels for lines e.g.
hAnnotationLabel = label1, label2, label3
- vAnnotationsLine and hAnnotationsLine: display vertical or horizontal annotation lines on specific values e.g.
- formatjson: format JSON object for better legibility
Template wrappers
The functions mapWrapper and chartWrapper are wrappers to pass all parameters of the calling template to the respective map and chart functions.
Note: In the editor preview the graph extension creates a Canvas (HTML元素) with vector graphics. However, when saving the page a PNG raster graphics is generated instead. {{#invoke:Graph|function_wrapper_name}}
-- Module:Graph
-- ATTENTION: Please edit this code at https://de.wikipedia.org/wiki/Modul:Graph
-- This way all wiki languages can stay in sync. Thank you!
-- 2026-10-25 A2569875註: Vega圖表功能早壞了,不必去跟隨https://de.wikipedia.org/wiki/Modul:Graph
--
-- BUGS: X-Axis label format bug? (xAxisFormat =) https://en.wikipedia.org/wiki/Template_talk:Graph:Chart#X-Axis_label_format_bug?_(xAxisFormat_=)
-- linewidths - doesnt work for two values (eg 0, 1) but work if added third value of both are zeros? Same for marksStroke - probably bug in Graph extension
-- clamp - "clamp" used to avoid marks outside marks area, "clip" should be use instead but not working in Graph extension, see https://phabricator.wikimedia.org/T251709
-- TODO:
-- marks:
-- - line strokeDash + serialization,
-- - symStroke serialization
-- - symbolsNoFill serialization
-- - arbitrary SVG path symbol shape as symbolsShape argument
-- - annotations
-- - vertical / horizontal line at specific values [DONE] 2020-09-01
-- - rectangle shape for x,y data range
-- - graph type serialization (deep rebuild reqired)
-- - second axis (deep rebuild required - assignment of series to one of two axies)
-- Version History (_PLEASE UPDATE when modifying anything_):
-- 2026-11-04 支援直接解析<graph>內原生json並直接繪製之(即使用本模組取代<graph>)
-- 2026-10-30 重新支援繁簡轉換
-- 2026-10-25 修改: extractRecordList 支援 statistics 等外層鍵名;相容舊版 content= 參數;
-- 2026-10-24 由於<graph>已停用,因此重寫為基於SVG渲染的模式
-- 2020-09-01 Vertical and horizontal line annotations
-- 2020-08-08 New logic for "nice" for x axis (problem with scale when xType = "date") and grid
-- 2020-06-21 Serializes symbol size
-- transparent symbosls (from line colour) - buggy (incorrect opacity on overlap with line)
-- Linewidth serialized with "linewidths"
-- Variable symbol size and shape of symbols on line charts, default showSymbols = 2, default symbolsShape = circle, symbolsStroke = 0
-- p.chartDebuger(frame) for easy debug and JSON output
-- 2020-06-07 Allow lowercase variables for use with [[Template:Wikidata list]]
-- 2020-05-27 Map: allow specification which feature to display and changing the map center
-- 2020-04-08 Change default showValues.fontcolor from black to persistentGrey
-- 2020-04-06 Logarithmic scale outputs wrong axis labels when "nice"=true
-- 2020-03-11 Allow user-defined scale types, e.g. logarithmic scale
-- 2019-11-08 Apply color-inversion-friendliness to legend title, labels, and xGrid
-- 2019-01-24 Allow comma-separated lists to contain values with commas
-- 2018-10-13 Fix browser color-inversion issues via #54595d per [[mw:Template:Graph:PageViews]]
-- 2018-09-16 Allow disabling the legend for templates
-- 2018-09-10 Allow grid lines
-- 2018-08-26 Use user-defined order for stacked charts
-- 2018-02-11 Force usage of explicitely provided x minimum and/or maximum values, rotation of x labels
-- 2017-08-08 Added showSymbols param to show symbols on line charts
-- 2016-05-16 Added encodeTitleForPath() to help all path-based APIs graphs like pageviews
-- 2016-03-20 Allow omitted data for charts, labels for line charts with string (ordinal) scale at point location
-- 2016-01-28 For maps, always use wikiraw:// protocol. https:// will be disabled soon.
-- Module:Graph
local p = {}
local SVGRenderer = require('Module:Graph/SVGRenderer')
local TrackingCategory = require('Module:TrackingCategory')
local DataProc = require('Module:Graph/DataProcess')
local debuglog = "Debug \n\n"
local persistentGrey = "#54595d"
-- 原版內建符號形狀與路徑定義 (擴充支援 star, wye, ring, plus 與連字號三角形)
local shapes = DataProc.shapes
local parseBool = DataProc.parseBool
local parseShapeArray = DataProc.parseShapeArray
local parseShowValues = DataProc.parseShowValues
local numericArray = DataProc.numericArray
local stringArray = DataProc.stringArray
local maskStripMarkers = DataProc.maskStripMarkers
local stripJsonComments = DataProc.stripJsonComments
local splitTopLevelJsonObjects = DataProc.splitTopLevelJsonObjects
local translateVegaJsonToArgs = DataProc.translateVegaJsonToArgs
local wrangleRecordsToArgs = DataProc.wrangleRecordsToArgs
local fetchExternalData = DataProc.fetchExternalData
local deserializeXData = DataProc.deserializeXData
local deserializeYData = DataProc.deserializeYData
local extractRecordList = DataProc.extractRecordList
local buildColorAndAlphaPalette = DataProc.buildColorAndAlphaPalette
-- 優雅降級 (Fallback):回傳原生 <graph> 擴充標籤,讓系統顯示官方停用警告框
local function triggerFallback(frame, rawContent)
TrackingCategory.append('SVG圖表有錯誤的頁面') --追蹤此類頁面
local wf = (frame and frame.extensionTag) and frame or mw.getCurrentFrame()
if wf and wf.extensionTag then
local ok, res = pcall(function()
return wf:extensionTag{ name = 'graph', content = tostring(rawContent or '{}') }
end)
if ok and res then return res end
end
return p.renderFallback()
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
local function extractArgs(frame)
if not frame then return {} end
if type(frame) == "string" then
return { [1] = frame }
end
if type(frame) == "table" and not frame.args and not frame.getParent then
if frame.marks and frame.data then
return { _vegaTable = frame }
end
return frame
end
local merged = {}
local function copyArgs(src)
if not src then return end
for k, v in pairs(src) do
-- 關鍵修復:若 JSON 因內部含有 !== 或 === 被 MediaWiki 當成「參數名=參數值」切開,自動縫合回 merged[1]
if type(k) == "string" and string.match(mw.text.trim(k), "^%{") then
merged[1] = maskStripMarkers(k .. "=" .. tostring(v))
elseif v ~= "" then
merged[k] = (type(v) == "string") and maskStripMarkers(v) or v
end
end
end
local parent = frame.getParent and frame:getParent()
if parent then copyArgs(parent.args) end
copyArgs(frame.args)
return merged
end
-- 預處理外部資料源與 Vega JSON (回傳 shouldFallback 布林值)
local function prepareExternalDataAndVega(args, frame)
if args._prepared then return false, nil end
args._prepared = true
-- 相容舊版 Graph 擴充功能的 content= 參數:
-- 以 { 開頭視為 Vega JSON,否則視為資料頁名稱 / URL
local contentArg = args.content
if type(contentArg) == "string" and mw.text.trim(contentArg) ~= "" then
local trimmed = mw.text.trim(contentArg)
if string.match(trimmed, "^%{") then
if not args.json and not args.vega then args.json = trimmed end
else
if not args.dataUrl and not args.dataurl and not args.url then
args.dataUrl = trimmed
end
end
end
local rawJsonInput = args.json or args.vega or args[1]
if args._vegaTable then
translateVegaJsonToArgs(args._vegaTable, args)
args._vegaTable = nil
elseif type(rawJsonInput) == "string" and string.match(mw.text.trim(rawJsonInput), "^%{") then
local ok, fallback = translateVegaJsonToArgs(rawJsonInput, args, frame)
if fallback or not ok then
return true, rawJsonInput
end
end
-- [新增] 若確認為 Vega 地圖,直接中斷後續的折線圖資料轉換,交由外層發送給 MapRenderer
if args._isVegaMap then
return false, nil
end
if args._inlineRecords then
-- [新增] 呼叫 Expression 模組處理 formula / filter 等資料變換
if args._vegaTransforms then
local Expr = require('Module:Graph/Expression')
local transformed, shouldAbort = Expr.applyTransforms(args._inlineRecords, args._vegaTransforms, nil, args._vegaSignals)
args._vegaTransforms = nil
if shouldAbort or not transformed then return true, rawJsonInput end
args._inlineRecords = transformed
end
local okWrangle = wrangleRecordsToArgs(args._inlineRecords, args)
args._inlineRecords = nil
if not okWrangle then return true, rawJsonInput end
return false, nil
end
local dataUrl = args.dataUrl or args.dataurl or args.url
if dataUrl and mw.text.trim(tostring(dataUrl)) ~= "" then
local records, shouldFallback = fetchExternalData(dataUrl, frame)
if shouldFallback or not records then
return true, rawJsonInput or dataUrl
end
-- [新增] 呼叫 Expression 模組處理 formula / filter 等資料變換
if args._vegaTransforms then
local Expr = require('Module:Graph/Expression')
local transformed, shouldAbort = Expr.applyTransforms(records, args._vegaTransforms, nil, args._vegaSignals)
args._vegaTransforms = nil
if shouldAbort or not transformed then
return true, rawJsonInput or dataUrl
end
records = transformed
end
local okWrangle = wrangleRecordsToArgs(records, args)
if not okWrangle then
return true, rawJsonInput or dataUrl
end
end
return false, nil
end
-- 解析 colorMap 參數陣列:支援方案 B (如 #000:#f00:#fff 或 #000;#f00;#fff)、中括號包裹 [#000, #f00, #fff] 與尾隨色碼自動合併
local function parseColorMapList(rawStr, nSeriesCount)
if not rawStr or mw.text.trim(tostring(rawStr)) == "" then return nil end
local s = mw.text.trim(tostring(rawStr))
local bracketBlocks = {}
local masked = string.gsub(s, "%[([^%]]+)%]", function(inner)
bracketBlocks[#bracketBlocks + 1] = string.gsub(mw.text.trim(inner), "%s*,%s*", ":")
return "__CMAP_BRACKET_" .. #bracketBlocks .. "__"
end)
local list = stringArray(masked)
if not list then return nil end
for i = 1, #list do
local bIdx = tonumber(string.match(list[i], "^__CMAP_BRACKET_(%d+)__$"))
if bIdx and bracketBlocks[bIdx] then
list[i] = bracketBlocks[bIdx]
end
end
-- 若只有單一數列且傳入多個逗號分隔色碼 (如 colorMap = #000, #f00, #fff),自動合併為單一自訂漸層色階
if nSeriesCount <= 1 and #list >= 2 then
local Utils = require('Module:Graph/RenderUtils')
local firstClean = string.gsub(list[1], "^[%a]+%s*:%s*", "")
if Utils.isValidColorToken(firstClean) and not Utils.isColormapName(list[1]) then
return { table.concat(list, ":") }
end
end
-- 若傳入項目數大於數列數 (如 3 個數列寫 colorMap = jet, blue, #000000, #ff0000, #ffffff),將尾隨色碼合併給最後一個數列
if nSeriesCount > 1 and #list > nSeriesCount then
local Utils = require('Module:Graph/RenderUtils')
local tailColors = {}
local allTailValid = true
for i = nSeriesCount, #list do
local cleanTk = (i == nSeriesCount) and string.gsub(list[i], "^[%a]+%s*:%s*", "") or list[i]
if Utils.isValidColorToken(cleanTk) then
tailColors[#tailColors + 1] = list[i]
else
allTailValid = false
break
end
end
if allTailValid and #tailColors >= 2 then
local merged = {}
for i = 1, nSeriesCount - 1 do merged[i] = list[i] end
merged[nSeriesCount] = table.concat(tailColors, ":")
return merged
end
end
return list
end
function p.buildChartSpec(args, frame)
prepareExternalDataAndVega(args, frame)
-- 支援直接於 x 或 y 傳入 [[x, y, z], ...] 二維陣列字串
local rawTupleStr = nil
if type(args.x) == "string" and string.match(mw.text.trim(args.x), "^%[%s*%[") then
rawTupleStr = mw.text.trim(args.x)
elseif type(args.y) == "string" and string.match(mw.text.trim(args.y), "^%[%s*%[") then
rawTupleStr = mw.text.trim(args.y)
args.y = nil
elseif type(args.y1) == "string" and string.match(mw.text.trim(args.y1), "^%[%s*%[") then
rawTupleStr = mw.text.trim(args.y1)
args.y1 = nil
end
if rawTupleStr then
local okT, tupleList = pcall(mw.text.jsonDecode, stripJsonComments(rawTupleStr), mw.text.JSON_TRY_FIXING)
if okT and type(tupleList) == "table" and type(tupleList[1]) == "table" then
local tx, ty, tz = {}, {}, {}
local hasZCol = false
for idx = 1, #tupleList do
local row = tupleList[idx]
tx[idx] = tostring(row[1] or 0)
ty[idx] = tostring(row[2] or 0)
if row[3] ~= nil then
tz[idx] = tostring(row[3])
hasZCol = true
else
tz[idx] = ""
end
end
args.x = table.concat(tx, ",")
args.y1 = table.concat(ty, ",")
if hasZCol and not args.z and not args.z1 then
args.z1 = table.concat(tz, ",")
end
end
end
local graphwidth = tonumber(args.width) or 200
local graphheight = tonumber(args.height) or 200
local rawChartType = string.lower(mw.text.trim(tostring(args.type or "line")))
local interpolate = args.interpolate and string.lower(mw.text.trim(tostring(args.interpolate))) or "linear"
local colorString = args.colors
local rawColors = colorString and stringArray(colorString) or nil
local linewidth = tonumber(args.linewidth)
local linewidthsString = args.linewidths
local linewidths = (linewidthsString and linewidthsString ~= "") and numericArray(linewidthsString) or nil
local xTitle = args.xAxisTitle or args.xaxistitle
local yTitle = args.yAxisTitle or args.yaxistitle
local chartTitle = args.title
local xType = args.xType or args.xtype
local yType = args.yType or args.ytype
if xType then xType = string.lower(mw.text.trim(xType)) end
if yType then yType = string.lower(mw.text.trim(yType)) end
local xMin = args.xAxisMin or args.xaxismin
local xMax = args.xAxisMax or args.xaxismax
local yMin = args.yAxisMin or args.yaxismin
local yMax = args.yAxisMax or args.yaxismax
local domainMin = tonumber(args.domainMin or args.domainmin)
local domainMax = tonumber(args.domainMax or args.domainmax)
local xAxisFormat = args.xAxisFormat or args.xaxisformat
local yAxisFormat = args.yAxisFormat or args.yaxisformat
local xAxisAngle = tonumber(args.xAxisAngle or args.xaxisangle)
local xScaleType = string.lower(mw.text.trim(tostring(args.xScaleType or args.xscaletype or "linear")))
local yScaleType = string.lower(mw.text.trim(tostring(args.yScaleType or args.yscaletype or "linear")))
local xGrid = parseBool(args.xGrid or args.xgrid, false)
local yGrid = parseBool(args.yGrid or args.ygrid, false)
local showSymbolsRaw = args.showSymbols or args.showsymbols
local symbolsShapeRaw = args.symbolsShape or args.symbolsshape
local symbolsNoFill = parseBool(args.symbolsNoFill or args.symbolsnofill, false)
local symbolsStroke = tonumber(args.symbolsStroke or args.symbolsstroke)
local legendRaw = args.legend
local showLegend = false
local legendTitle = nil
if legendRaw ~= nil and (parseBool(legendRaw, true) ~= false or (type(legendRaw)==type("string") and mw.text.trim(legendRaw) == '')) then
showLegend = true
local trimmedLeg = mw.text.trim(tostring(legendRaw))
if string.lower(trimmedLeg) ~= "true" and string.lower(trimmedLeg) ~= "yes" and string.lower(trimmedLeg) ~= "1" then
legendTitle = trimmedLeg
end
elseif rawChartType == "heatmap" and not parseBool(args.hideAxes or args.hideaxes, false) and legendRaw == nil then
showLegend = true
end
local showValues = parseShowValues(args.showValues or args.showvalues)
local v_annoLineString = args.vAnnotatonsLine or args.vannotatonsline or args.vAnnotationsLine or args.vannotationsline
local h_annoLineString = args.hAnnotatonsLine or args.hannotatonsline or args.hAnnotationsLine or args.hannotationsline
local v_annoLabelString = args.vAnnotatonsLabel or args.vannotatonslabel or args.vAnnotationsLabel or args.vannotationslabel
local h_annoLabelString = args.hAnnotatonsLabel or args.hannotatonslabel or args.hAnnotationsLabel or args.hannotationslabel
local innerRadius = tonumber(args.innerRadius or args.innerradius) or 0
local outerRadius = tonumber(args.outerRadius or args.outerradius)
local startAngle = tonumber(args.startAngle or args.startangle or args.polarStartAngle or args.polarstartangle)
-- 支援使用者傳入 x1, z1 作為 x 與 z 的別名,以及角度/弧度/轉數 (turn) 單位
local rawXInput = args.x or args.x1
local isAngleXType = (xType == "radian" or xType == "rad" or xType == "radians"
or xType == "angle" or xType == "deg" or xType == "degree" or xType == "degrees"
or xType == "turn" or xType == "turns")
local x
x, xType, xMin, xMax = deserializeXData(rawXInput, isAngleXType and "number" or xType, xMin, xMax)
if isAngleXType then
xType = string.lower(mw.text.trim(tostring(args.xType or args.xtype)))
end
local yValues = {}
local rawTitles = {}
local rawSeriesCmaps = {}
local maxYNum = 0
for name, value in pairs(args) do
local yNum
if name == "y" then
yNum = 1
elseif type(name) == "string" then
yNum = tonumber(string.match(name, "^y(%d+)$"))
end
if yNum and value and mw.text.trim(tostring(value)) ~= "" then
yValues[yNum] = value
rawTitles[yNum] = args["y" .. yNum .. "Title"] or args["y" .. yNum .. "title"] or (yNum == 1 and (args.yTitle or args.ytitle)) or ("y" .. yNum)
rawSeriesCmaps[yNum] = args["y" .. yNum .. "ColorMap"] or args["y" .. yNum .. "colormap"] or args["colorMap" .. yNum] or args["colormap" .. yNum]
if yNum > maxYNum then maxYNum = yNum end
end
end
if args.z1 and not args.z then
args.z = args.z1
end
local compactYValues = {}
local seriesTitles = {}
local seriesColorMaps = {}
for idx = 1, maxYNum do
if yValues[idx] ~= nil then
compactYValues[#compactYValues + 1] = yValues[idx]
seriesTitles[#seriesTitles + 1] = rawTitles[idx]
if rawSeriesCmaps[idx] then
-- 方案 A (yNColorMap):若以逗號分隔多個色碼且未使用分號,自動轉為冒號分隔之漸層色階
local sc = mw.text.trim(tostring(rawSeriesCmaps[idx]))
if string.find(sc, ",", 1, true) and not string.find(sc, ";", 1, true) then
sc = string.gsub(sc, "%s*,%s*", ":")
end
seriesColorMaps[#compactYValues] = sc
end
end
end
local y
y, yType, yMin, yMax = deserializeYData(compactYValues, yType, yMin, yMax)
-- 反序列化氣泡圖大小陣列 (size, size1..sizen) 與甘特圖結束座標 (x2)
local sizes = {}
local hasSizes = false
for idx = 1, math.max(1, maxYNum) do
local rawS = args["size" .. idx] or (idx == 1 and args.size) or nil
if rawS and mw.text.trim(tostring(rawS)) ~= "" then
local sArr = numericArray(rawS)
if sArr then
sizes[idx] = sArr
hasSizes = true
end
end
end
if hasSizes and not showSymbolsRaw then
showSymbolsRaw = "3.5"
end
local x2 = nil
if args.x2 and mw.text.trim(tostring(args.x2)) ~= "" then
x2 = deserializeXData(args.x2, xType, nil, nil)
end
local maxYCount = 0
for i = 1, #y do
local len = y[i].n or #y[i]
if len > maxYCount then maxYCount = len end
end
local xCount = x.n or #x
if xCount == 0 and maxYCount > 0 then
for j = 1, maxYCount do x[j] = j end
x.n = maxYCount
xCount = maxYCount
xType = "integer"
end
local stacked = false
local polarHist = false
local isHist = false
local isPolarLine = false
local chartType = rawChartType
if string.sub(chartType, 1, 7) == "stacked" then
chartType = string.sub(chartType, 8)
if #y > 1 then stacked = true end
end
if chartType == "bar" or chartType == "groupedbar" or chartType == "groupedrect" then
chartType = "rect"
elseif chartType == "hist" or chartType == "histogram" or chartType == "groupedhist" then
chartType = "rect"
isHist = true
elseif chartType == "polarbar" or chartType == "polarrect" or chartType == "rose" or chartType == "nightingale" or chartType == "coxcomb" or chartType == "groupedpolarbar" then
chartType = "polarbar"
elseif chartType == "polarhist" or chartType == "polarhistogram" then
chartType = "polarbar"
polarHist = true
isHist = true
elseif chartType == "bubble" or chartType == "scatter" then
chartType = "line"
linewidth = 0
if not showSymbolsRaw then showSymbolsRaw = "3.5" end
elseif chartType == "radar" or chartType == "spider" or chartType == "polararea" then
chartType = "radar"
if showSymbolsRaw == nil then showSymbolsRaw = "2.5" end
elseif chartType == "polarline" then
chartType = "radar"
isPolarLine = true
if showSymbolsRaw == nil then showSymbolsRaw = "2.5" end
elseif chartType == "scatter3d" or chartType == "bubble3d" or chartType == "point3d" then
chartType = "scatter3d"
if showSymbolsRaw == nil then showSymbolsRaw = "4.0" end
elseif chartType == "bar3d" or chartType == "rect3d" or chartType == "column3d" then
chartType = "bar3d"
elseif chartType == "surface3d" or chartType == "mesh3d" then
chartType = "surface3d"
elseif chartType == "line3d" then
chartType = "line3d"
end
local is3D = (chartType == "scatter3d" or chartType == "bar3d" or chartType == "surface3d" or chartType == "line3d")
or ((args.z ~= nil or args.z1 ~= nil) and chartType ~= "polarbar" and chartType ~= "rect")
if is3D and chartType == "line" then
chartType = (linewidth == 0) and "scatter3d" or "line3d"
end
-- 解析科學色階與自訂色階參數 (支援方案 A 與方案 B)
local rawColorMap = args.colormap or args.colorMap or args.colorScale or args.colorscale
local colorMap = nil
local cmList = parseColorMapList(rawColorMap, math.max(1, #y))
if cmList and #cmList > 0 then
colorMap = cmList[1]
for i = 1, math.max(1, #y) do
seriesColorMaps[i] = seriesColorMaps[i] or cmList[(i - 1) % #cmList + 1]
end
end
-- 反序列化 Z 軸資料 (供 3D 圖表高度、或 2D 極座標/直角長條圖 [x, y, z] 之條柱寬度使用)
local z, zType, zMin, zMax = nil, nil, nil, nil
local zTitle, zAxisFormat, zScaleType, zGrid = nil, nil, "linear", true
local viewTheta, viewPhi, viewDistance = nil, nil, nil
if is3D or chartType == "polarbar" or chartType == "rect" or args.z ~= nil or args.z1 ~= nil then
zTitle = args.zAxisTitle or args.zaxistitle or args.zTitle or args.ztitle
zAxisFormat = args.zAxisFormat or args.zaxisformat
zScaleType = string.lower(mw.text.trim(tostring(args.zScaleType or args.zscaletype or "linear")))
zGrid = parseBool(args.zGrid or args.zgrid, true)
zType = args.zType or args.ztype
if zType then zType = string.lower(mw.text.trim(zType)) end
zMin = args.zAxisMin or args.zaxismin or args.zMin or args.zmin
zMax = args.zAxisMax or args.zaxismax or args.zMax or args.zmax
viewTheta = tonumber(args.view_theta or args.viewTheta or args.theta or args.azimuth)
viewPhi = tonumber(args.view_phi or args.viewPhi or args.phi or args.elevation)
viewDistance = tonumber(args.view_distance or args.viewDistance or args.distance)
local zValues = {}
local maxZNum = 0
for name, value in pairs(args) do
local zNum
if name == "z" then
zNum = 1
elseif type(name) == "string" then
zNum = tonumber(string.match(name, "^z(%d+)$"))
end
if zNum and value and mw.text.trim(tostring(value)) ~= "" then
zValues[zNum] = value
if zNum > maxZNum then maxZNum = zNum end
end
end
if maxZNum > 0 then
local compactZ = {}
for idx = 1, maxZNum do
if zValues[idx] ~= nil then compactZ[#compactZ + 1] = zValues[idx] end
end
z, zType, zMin, zMax = deserializeYData(compactZ, zType, zMin, zMax)
else
zMin = tonumber(zMin)
zMax = tonumber(zMax)
end
end
local dirStr = args.direction and string.lower(mw.text.trim(tostring(args.direction))) or nil
local reverseY = parseBool(args.reverseY or args.reversey or args.yReverse or args.yreverse or args._yReverse, false)
or (dirStr == "inward" or dirStr == "in" or dirStr == "reverse")
local barPadding = tonumber(args.barPadding or args.barpadding or args.padding)
local groupPadding = tonumber(args.groupPadding or args.grouppadding or args.innerPadding or args.innerpadding)
local rawAlign = args.align or args.barAlign or args.baralign
local barAlign = nil
if rawAlign and mw.text.trim(tostring(rawAlign)) ~= "" then
local al = string.lower(mw.text.trim(tostring(rawAlign)))
if al == "center" or al == "middle" or al == "mid" then
barAlign = "center"
elseif al == "edge" or al == "left" or al == "start" then
barAlign = "edge"
end
end
if not barAlign then
barAlign = ((z ~= nil and #z > 0) or isHist) and "edge" or "center"
end
local vAnnotations = {}
if v_annoLineString and v_annoLineString ~= "" then
local vLines = (xType == "number" or xType == "integer") and numericArray(v_annoLineString) or stringArray(v_annoLineString)
local vLabels = stringArray(v_annoLabelString) or {}
if vLines then
for i = 1, (vLines.n or #vLines) do
if vLines[i] ~= false and vLines[i] ~= nil then
vAnnotations[#vAnnotations + 1] = { x = vLines[i], label = vLabels[i] or "" }
end
end
end
end
local hAnnotations = {}
if h_annoLineString and h_annoLineString ~= "" then
local hLines = (yType == "number" or yType == "integer") and numericArray(h_annoLineString) or stringArray(h_annoLineString)
local hLabels = stringArray(h_annoLabelString) or {}
if hLines then
for i = 1, (hLines.n or #hLines) do
if hLines[i] ~= false and hLines[i] ~= nil then
hAnnotations[#hAnnotations + 1] = { y = hLines[i], label = hLabels[i] or "" }
end
end
end
end
local colors, alphas = buildColorAndAlphaPalette(rawColors, chartType, xCount, #y)
if symbolsShapeRaw and showSymbolsRaw == nil then
showSymbolsRaw = "3.5"
end
local symbols = nil
if showSymbolsRaw and parseBool(showSymbolsRaw, true) ~= false and chartType ~= "rect" and chartType ~= "polarbar" then
local symRadii = {}
local numList = numericArray(showSymbolsRaw)
if numList then
for i = 1, (numList.n or #numList) do
local v = tonumber(numList[i]) or 2.5
symRadii[i] = math.max(1.5, v * 1.4)
end
elseif tonumber(showSymbolsRaw) then
local r = math.max(1.5, tonumber(showSymbolsRaw) * 1.4)
for i = 1, math.max(1, #y) do symRadii[i] = r end
else
for i = 1, math.max(1, #y) do symRadii[i] = 3.2 end
end
local shapeList = parseShapeArray(symbolsShapeRaw) or { "circle" }
local resolvedShapes = {}
for i = 1, math.max(1, #y) do
local key = shapeList[(i - 1) % #shapeList + 1] or "circle"
resolvedShapes[i] = shapes[key] or key
end
local defaultStroke = (symbolsNoFill or args.symbolsFillWhite or symbolsShapeRaw == "x") and 2.0 or 0
symbols = {
enabled = true,
radii = symRadii,
shapes = resolvedShapes,
noFill = symbolsNoFill or args.symbolsFillWhite,
strokeWidth = symbolsStroke or defaultStroke
}
end
return {
width = graphwidth,
height = graphheight,
chartType = chartType,
isHist = isHist,
polarHist = polarHist,
isPolarLine = isPolarLine,
stacked = stacked,
reverseY = reverseY,
barAlign = barAlign,
barPadding = barPadding,
groupPadding = groupPadding,
startAngle = startAngle,
interpolate = interpolate,
colors = colors,
alphas = alphas,
colorMap = colorMap,
seriesColorMaps = next(seriesColorMaps) and seriesColorMaps or nil,
domainMin = domainMin,
domainMax = domainMax,
linewidth = linewidth,
linewidths = linewidths,
title = chartTitle,
xTitle = xTitle,
yTitle = yTitle,
xType = xType,
yType = yType,
xMin = xMin,
xMax = xMax,
yMin = yMin,
yMax = yMax,
xAxisFormat = xAxisFormat,
yAxisFormat = yAxisFormat,
xAxisAngle = xAxisAngle,
xScaleType = xScaleType,
yScaleType = yScaleType,
xGrid = xGrid,
yGrid = yGrid,
showLegend = showLegend,
legendTitle = legendTitle,
showValues = showValues,
symbols = symbols,
vAnnotations = vAnnotations,
hAnnotations = hAnnotations,
innerRadius = innerRadius,
outerRadius = outerRadius,
rangedRect = args._rangedRectSpec,
heatmap = args._heatmapSpec,
gantt = args._ganttSpec,
boxPlot = args._boxPlotSpec,
hideAxes = parseBool(args.hideAxes or args.hideaxes, false) or (args._isScatterGrid == true),
isScatterGrid = (args._isScatterGrid == true),
interactive = parseBool(args.interactive, true) and not (args._isScatterGrid == true),
animate = parseBool(args.animate, true) and not (args._isScatterGrid == true),
persistentGrey = persistentGrey,
is3D = is3D,
z = z,
zTitle = zTitle,
zType = zType,
zMin = zMin,
zMax = zMax,
zAxisFormat = zAxisFormat,
zScaleType = zScaleType,
zGrid = zGrid,
viewTheta = viewTheta,
viewPhi = viewPhi,
viewDistance = viewDistance,
x = x,
x2 = x2,
y = y,
sizes = hasSizes and sizes or nil,
seriesTitles = seriesTitles
}
end
function p.chart(frame)
--subst必定不可用,直接短路到圖表不可用。
if mw.isSubsting() then return p.renderFallback(frame) end
local args = extractArgs(frame)
local rawJsonInput = args.json or args.vega or args.content or args.code or args[1]
-- 避免自訂形狀在json裡被誤抓
if string.match('<!--svg-->'..(rawJsonInput or ''), "[^=,;%[%]\"'%s]%s*<[sS][vV][gG]%s*([^>]*)>") then
--確定是一個svg後直接短路輸出
return SVGRenderer.RenderSVG({args=args})
end
-- 定義內部渲染或降級處理函數,方便複用
local function renderOrFallback(subArgs, rawInput)
local shouldFallback, fallbackRaw = prepareExternalDataAndVega(subArgs, frame)
if shouldFallback then
return triggerFallback(frame, fallbackRaw or rawInput)
end
if subArgs._isVegaMap then
return require('Module:Graph/MapRenderer').renderMap(subArgs)
end
local spec = p.buildChartSpec(subArgs, frame)
-- 空圖表防呆檢查:若無特殊圖表結構,且 X 軸為空或 Y 軸無任何有效數據,自動降級為圖表不可用
if not (spec.rangedRect or spec.boxPlot or spec.gantt or spec.heatmap) then
local xCount = spec.x and (spec.x.n or #spec.x) or 0
local hasValidY = false
if type(spec.y) == "table" then
for i = 1, #spec.y do
local series = spec.y[i]
local len = series.n or #series
for j = 1, len do
if series[j] ~= false and series[j] ~= nil and series[j] ~= "" then
hasValidY = true
break
end
end
if hasValidY then break end
end
end
if xCount == 0 or not hasValidY then
return triggerFallback(frame, fallbackRaw or rawInput)
end
end
return SVGRenderer.renderChart(spec)
end
-- 若一次傳入多個連寫的 JSON 物件,自動逐一渲染並串接輸出
if type(rawJsonInput) == "string" and string.match(mw.text.trim(rawJsonInput), "^%{") then
local blocks = splitTopLevelJsonObjects(stripJsonComments(rawJsonInput))
if #blocks > 1 then
local out = {}
for i = 1, #blocks do
local subArgs = { [1] = blocks[i] }
out[#out + 1] = renderOrFallback(subArgs, blocks[i])
end
return table.concat(out, " ")
end
end
return renderOrFallback(args, rawJsonInput)
end
function p.chartWrapper(frame)
return p.chart(frame)
end
-- 支援直接傳入舊版 Vega JSON 進行渲染
function p.vega(frame)
return p.chart(frame)
end
function p.chartDebuger(frame)
local args = extractArgs(frame)
local spec = p.buildChartSpec(args, frame)
return "\n\nchart Spec\n " .. mw.dumpObject(spec) .. "\n\n" .. SVGRenderer.renderChart(spec) .. "\n\n" .. debuglog
end
function p.map(frame)
--subst必定不可用,直接短路到圖表不可用。
if mw.isSubsting() then return p.renderFallback(frame) end
local args = extractArgs(frame)
return require('Module:Graph/MapRenderer').renderMap(args)
end
function p.mapWrapper(frame)
return p.map(frame)
end
function p.encodeTitleForPath(frame)
local arg1 = (frame and frame.args and frame.args[1]) or (type(frame) == "table" and frame[1]) or ""
return mw.uri.encode(mw.text.decode(mw.text.trim(tostring(arg1))), 'PATH')
end
-- 診斷用:逐步回報資料讀取在哪一步失敗
function p.dataDebug(frame)
local args = extractArgs(frame)
local out = {}
local function say(s) out[#out + 1] = "* " .. s end
local url = args.dataUrl or args.dataurl or args.url or args.content
if not url or mw.text.trim(tostring(url)) == "" then
return "* 沒有收到 dataUrl / url / content 參數"
end
url = mw.text.trim(tostring(url))
say("收到的資料來源:<code>" .. mw.text.nowiki(url) .. "</code>")
local pageName = url
if string.sub(pageName, 1, 9) == "wikiraw:/" then
pageName = string.gsub(pageName, "^wikiraw:///?", "")
pageName = mw.uri.decode(pageName, "PATH")
end
pageName = mw.text.trim(pageName)
local titleObj = mw.title.new(pageName)
if not titleObj then
say("mw.title.new 失敗:頁面名稱不合法")
return table.concat(out, "\n")
end
say("頁面全名:<code>" .. mw.text.nowiki(titleObj.prefixedText) .. "</code>")
say("頁面是否存在:" .. tostring(titleObj.exists))
say("contentModel:" .. tostring(titleObj.contentModel))
local content = titleObj:getContent()
if not content or content == "" then
say("getContent() 取得空內容(頁面不存在或內容為空)")
return table.concat(out, "\n")
end
say("內容長度:" .. #content .. " 字元")
local cleaned = string.gsub(content, "<noinclude>.-</noinclude>", "")
cleaned = string.gsub(cleaned, "</?includeonly>", "")
cleaned = string.gsub(cleaned, "</?onlyinclude>", "")
local wf = (frame and frame.preprocess) and frame or mw.getCurrentFrame()
if wf and wf.preprocess and string.find(cleaned, "{{", 1, true) then
local okPre, preRes = pcall(function() return wf:preprocess(cleaned) end)
if okPre and preRes then cleaned = preRes end
say("內容含 {{ }},已嘗試展開模板")
end
cleaned = stripJsonComments(mw.text.trim(cleaned))
local okDec, decoded = pcall(mw.text.jsonDecode, cleaned, mw.text.JSON_TRY_FIXING)
if not okDec or type(decoded) ~= "table" then
say("jsonDecode 失敗:<code>" .. mw.text.nowiki(tostring(decoded)) .. "</code>")
return table.concat(out, "\n")
end
say("jsonDecode 成功")
local list = extractRecordList(decoded)
if not list then
say("extractRecordList 回傳 nil:資料外層結構模組不認得")
return table.concat(out, "\n")
end
local count = 0
while list[count + 1] ~= nil do count = count + 1 end
say("解析到的記錄筆數:" .. count)
if list[1] and type(list[1]) == "table" then
local rk = {}
for k, v in pairs(list[1]) do
rk[#rk + 1] = tostring(k) .. "=" .. tostring(v)
end
table.sort(rk)
say("第一筆記錄:<code>" .. mw.text.nowiki(table.concat(rk, ", ")) .. "</code>")
end
local xField = args.xField or args.xfield or "x"
local yField = args.yField or args.yfield or "y"
local groupField = args.groupField or args.groupfield or args.seriesField or args.seriesfield
say("使用的 xField:<code>" .. mw.text.nowiki(tostring(xField)) .. "</code>")
say("使用的 yField:<code>" .. mw.text.nowiki(tostring(yField)) .. "</code>")
say("使用的 groupField:<code>" .. mw.text.nowiki(tostring(groupField)) .. "</code>")
local xFound, idx = 0, 1
while list[idx] ~= nil do
if list[idx][xField] ~= nil then xFound = xFound + 1 end
idx = idx + 1
end
say("xField 有值的記錄數:" .. xFound .. " / " .. count)
local testArgs = { xField = args.xField, yField = args.yField, groupField = args.groupField, type = args.type }
local okW = wrangleRecordsToArgs(list, testArgs)
say("wrangleRecordsToArgs 結果:" .. tostring(okW))
if okW then
local xs = stringArray(testArgs.x or "") or {}
say("轉換後不重複 X 值數量:" .. #xs)
say("X 範圍:<code>" .. mw.text.nowiki(tostring(xs[1]) .. " ~ " .. tostring(xs[#xs])) .. "</code>")
say("y1Title:<code>" .. mw.text.nowiki(tostring(testArgs.y1Title)) .. "</code>")
say("y2Title:<code>" .. mw.text.nowiki(tostring(testArgs.y2Title)) .. "</code>")
end
return table.concat(out, "\n")
end
return p