Mô đun:Piechart
| Mô đun này được xếp loại là đã sẵn sàng để sử dụng rộng rãi. Nó đã đạt đến mức độ hoàn thiện, được coi là khá ổn định và không có lỗi, và có thể được sử dụng bất kỳ chỗ nào nếu phù hợp. Nó có thể được nêu trên các trang trợ giúp cũng như các tài liệu Wikipedia khác làm tùy chọn tìm hiểu cho người dùng mới. Để giảm tải tài nguyên máy chủ và tránh tạo đầu ra gây hại, mọi cải tiến nên được thực hiện thông qua việc kiểm thử tại chỗ thử thay vì sửa đổi "thử và sai" lặp đi lặp lại liên tục. |
Mô đun tạo biểu đồ tròn mượt mà (smooth pie chart). Được truy cập thông qua Bản mẫu:Pie chart.
Cách sử dụng
Vẽ biểu đồ bằng HTML với chú giải dễ tiếp cận (tùy chọn). Danh sách tất cả các tính năng nằm trong phần "TODO" của hàm chính `p.pie`.
Trong hầu hết các trường hợp, bạn nên sử dụng cùng với bản mẫu hỗ trợ để thêm CSS cần thiết: {{Pie chart}}.
Ví dụ
Tối giản
Lưu ý rằng bạn không cần cung cấp giá trị thứ hai vì nó được tự động tính toán (giả sử tổng cộng là 100).
{{Pie chart| [ {"value":33.3}, {} ] |thumb=none}}Nhãn và Chú giải
Ở đây chúng ta thêm một số nhãn tùy chỉnh. Cũng lưu ý rằng chúng ta thêm tùy chọn meta để hiển thị chú giải ở bên cạnh.
{{Pie chart| [ {"label": "nữ: $v", "value": 33.3}, {"label": "nam: $v"}]|thumb=none|meta = {"legend":true}}}Tự động chia tỷ lệ
Trong trường hợp bạn không có tỷ lệ phần trăm đã tính sẵn, bạn có thể sử dụng tính năng tự động chia tỷ lệ. Chỉ cần cung cấp cả hai giá trị trong trường hợp này.
{{Pie chart| [ {"label": "nữ: $v", "value": 750}, {"label": "nam: $v", "value": 250}]|thumb=none|meta = {"legend":true}}}Nhiều giá trị
Mô đun cho phép hiển thị nhiều giá trị, không chỉ 2.
{{Pie chart| [ {"label": "kẹo: $v", "value": 5, "color":"darkred"}, {"label": "bánh kẹp: $v", "value": 3, "color":"wheat"}, {"label": "bánh quy: $v", "value": 2, "color":"goldenrod"}, {"label": "đồ uống: $v", "value": 1, "color":"#ccf"}]|thumb=none|meta={"autoscale":true, "legend":true}}}Lưu ý rằng trong trường hợp này, cần phải cung cấp thêm tùy chọn "autoscale":true. Điều này là cần thiết khi tổng số nhỏ hơn 100.
Liên kết
Chú giải và vị trí của nó
Chú giải được thêm vào bằng cách sử dụng thuộc tính meta legend như đã hiển thị. Tuy nhiên, bạn cũng có thể thay đổi thứ tự bằng cách sử dụng direction. Các giá trị có thể bao gồm:
- row (mặc định) – thứ tự là danh sách, biểu đồ;
- row-reverse – thứ tự ngược lại, tức là biểu đồ, danh sách;
- column – bố cục cột (dọc).
- column-reverse – bố cục cột, đảo ngược (biểu đồ ở trên cùng).
{{Pie chart| [ {"label": "bánh quy: $v", "value": 2, "color":"goldenrod"}, {"label": "đồ uống: $v", "value": 1, "color":"#ccf"}, {"label": "kẹo: $v", "value": 5, "color":"darkred"}, {"label": "bánh kẹp: $v", "value": 3, "color":"wheat"}]|thumb=none|meta={"autoscale":true, "legend":true, "direction":"row-reverse"}}}row (hướng mặc định)
row-reverse
column
column-reverse
Khung viền xanh lá được thêm vào để làm rõ trong các ví dụ. Chúng thường không được thêm vào.
Các hàm trực tiếp
Trong trường hợp bạn muốn sử dụng mà không cần bản mẫu {{Pie chart}}, bạn có thể sử dụng các hàm chính này:
{{#invoke:Piechart|pie|json_data|meta=json_options}}{{#invoke:Piechart|color|number}}
Lưu ý rằng các cuộc gọi trực tiếp đến hàm pie yêu cầu thêm CSS:
<templatestyles src="Bản mẫu:Pie chart/styles.css"/>{{#invoke:Piechart|pie| [ {"value":33.3}, {} ] }}Ví dụ về json_data:
[ { "label": "bánh: $v", "color": "wheat", "value": 40 }, { "label": "bánh pizza phô mai $v", "color": "#fc0", "value": 20 }, { "label": "bánh pizza thập cẩm: $v", "color": "#f60", "value": 20 }, { "label": "bánh pizza sống $v", "color": "#f30" }]- Lưu ý rằng giá trị cuối cùng bị thiếu. Giá trị cuối cùng là tùy chọn miễn là các giá trị dự định cộng lại thành 100 (tức là 100%).
- Chú ý nhãn
$v, đây là một số được định dạng (xem `function prepareLabel`). - Màu sắc là mã hex hoặc tên màu tiếng Anh. Bảng màu mặc định có tông màu xanh lá cây.
Ví dụ về meta=json_options:
|meta = {"size":200, "autoscale":false, "legend":true}Tất cả các tùy chọn meta là tùy chọn (xem `function p.setupOptions`).
local p = {}local priv = {} p.__priv = privlocal forPrinting = "-webkit-print-color-adjust: exact; print-color-adjust: exact;"function p.color(frame) local index = tonumber(priv.trim(frame.args[1])) return ' ' .. priv.defaultColor(index)endfunction p.pie(frame) local json_data = priv.trim(frame.args[1]) local options = {} if (frame.args.meta) then options.meta = priv.trim(frame.args.meta) end local html = p.renderPie(json_data, options) return priv.trim(html)endfunction p.setupOptions(user_options) local options = { size = 100, autoscale = false, ariahidechart = false, legend = false, direction = "", width = "", caption = "", footer = "", labelformat = "", } options.style = "" if user_options and user_options.meta then local decodeSuccess, rawOptions = pcall(function() return mw.text.jsonDecode(user_options.meta, mw.text.JSON_TRY_FIXING) end) if not decodeSuccess then rawOptions = false end if rawOptions then if type(rawOptions.size) == "number" then options.size = math.floor(rawOptions.size) end options.autoscale = rawOptions.autoscale or false if rawOptions.legend then options.legend = true end if rawOptions.ariahidechart then options.ariahidechart = true end if (type(rawOptions.direction) == "string") then local sanitized = rawOptions.direction:gsub("[^a-z0-9%-]", "") options.direction = 'flex-direction: ' .. sanitized .. ';' options.width = 'width: max-content;' end if (type(rawOptions.width) == "string") then local sanitized = rawOptions.width:gsub("[^a-z0-9%-]", "") options.width = 'width: ' .. sanitized .. ';' end if (type(rawOptions.caption) == "string") then options.caption = rawOptions.caption end if (type(rawOptions.footer) == "string") then options.footer = rawOptions.footer end if (type(rawOptions.labelformat) == "string") then options.labelformat = rawOptions.labelformat end end if options.width ~= "" then options.style = options.style .. options.width end if options.direction ~= "" then options.style = options.style .. options.direction end end if (options.legend) then options.ariahidechart = true end return optionsendp.__priv.legendDebug = falsefunction p.renderPie(json_data, user_options) if type(json_data) ~= "string" or #json_data < 2 then error('invalid piechart data') end local decodeSuccess, data = pcall(function() return mw.text.jsonDecode(json_data, mw.text.JSON_TRY_FIXING) end) if not decodeSuccess then error('invalid piechart data: '..json_data) end local options = p.setupOptions(user_options) local ok, total = p.prepareEntries(data, options) local html = "<div class='smooth-pie-container' style='"..options.style.."'>" if not ok then html = html .. priv.renderErrors(data) end if options.legend then html = html .. p.renderLegend(data, options) end if p.__priv.legendDebug then return html end local header, items, footer = p.renderEntries(ok, total, data, options) html = html .. header .. items .. footer html = html .. "\n</div>" return htmlendfunction priv.boundaryFormatting(diff) local value = 0.0 if diff <= 1.0 then value = math.ceil(diff / 0.2) * 0.2 else value = math.ceil(diff / 0.5) * 0.5 end return string.format("%.1f", value)endfunction priv.willAutoscale(sum) local diff = sum - 100 local grace = 1 return diff > graceendfunction priv.sumErrorTracking(sum, items) -- Disabled on viwiki to reduce category clutterendfunction p.prepareEntries(data, options) local sum = priv.sumValues(data); if priv.willAutoscale(sum) then options.autoscale = true end local ok = true local no = 0 local total = #data for index, entry in ipairs(data) do no = no + 1 if not priv.prepareSlice(entry, no, sum, total, options) then no = no - 1 ok = false end end total = no return ok, totalendfunction priv.sumValues(data) local sum = 0; for _, entry in ipairs(data) do local value = entry.value if not (type(value) ~= "number" or value < 0) then sum = sum + value end end return sumendfunction priv.renderErrors(data) local html = "\n<ol class='chart-errors' style='display:none'>" for _, entry in ipairs(data) do if entry.error then local entryJson = mw.text.jsonEncode(entry) html = html .. "\n<li>".. entryJson .."</li>" end end return html .. "\n</ol>\n"endfunction priv.prepareSlice(entry, no, sum, total, options) local autoscale = options.autoscale local value = entry.value if (type(value) ~= "number" or value < 0) then if autoscale then entry.error = "cannot autoscale unknown value" return false end value = 100 - sum end if autoscale then entry.raw = value value = (value / sum) * 100 end entry.value = value entry.label = priv.prepareLabel(options.labelformat, entry) entry.bcolor = priv.backColor(entry, no, total) .. ";color:#000" return trueendfunction p.renderLegend(data, options) local html = "" if options.caption ~= "" or options.footer ~= "" then html = "\n<div class='smooth-pie-legend-container'>" end if options.caption ~= "" then html = html .. "<div class='smooth-pie-caption'>" .. options.caption .. "</div>" end html = html .. "\n<ol class='smooth-pie-legend'>" for _, entry in ipairs(data) do if not entry.error then html = html .. priv.renderLegendItem(entry, options) end end html = html .. "\n</ol>\n" if options.footer ~= "" then html = html .. "<div class='smooth-pie-footer'>" .. options.footer .. "</div>" end if options.caption ~= "" or options.footer ~= "" then html = html .. "</div>\n" end return htmlendfunction priv.renderLegendItem(entry, options) if entry.visible ~= nil and entry.visible == false then return "" end local label = entry.label local bcolor = entry.bcolor local html = "\n<li>" if p.__priv.legendDebug then forPrinting = "" end html = html .. '<span class="l-color" style="'..forPrinting..bcolor..'"></span>' html = html .. '<span class="l-label">'..label..'</span>' return html .. "</li>"endfunction p.renderEntries(ok, total, data, options) -- Fallback cuts if json load fails p.cuts = {0.000000,0.062832,0.125664,0.188496,0.251327,0.314159,0.376991,0.439823,0.502655} local success, loadedCuts = pcall(mw.loadJsonData, 'Module:Piechart/cuts.json') if success then p.cuts = loadedCuts end local first = true local previous = 0 local items = "" for index, entry in ipairs(data) do if not entry.error then items = items .. priv.renderItem(previous, entry, options) previous = previous + entry.value end end local header = priv.renderHeader(options) local footer = '\n<div class="smooth-pie-border"></div></div>' return header, items, footerendfunction priv.renderHeader(options) local bcolor = 'background:#888;color:#000' local size = options.size local aria = "" if (options.ariahidechart) then aria = 'aria-hidden="true"' end local style = 'width:'..size..'px;height:'..size..'px;'..bcolor..';'..forPrinting local html = [[<div class="smooth-pie" style="]]..style..[[" ]]..aria..[[>]] return htmlendfunction priv.renderItem(previous, entry, options) local value = entry.value local label = entry.label local bcolor = entry.bcolor if (value < 0.03) then return "" end if value < 10 then if previous > 1 then previous = previous - 0.01 end value = value + 0.02 else if previous > 1 then previous = previous - 0.1 end value = value + 0.2 end if previous + value > 100 then if previous >= 100 then return "" end value = 100 - previous end local html = "" if (value >= 50) then html = priv.sliceWithClass('pie50', 50, value, previous, bcolor, label) elseif (value >= 25) then html = priv.sliceWithClass('pie25', 25, value, previous, bcolor, label) elseif (value >= 12.5) then html = priv.sliceWithClass('pie12-5', 12.5, value, previous, bcolor, label) elseif (value >= 7) then html = priv.sliceWithClass('pie7', 7, value, previous, bcolor, label) elseif (value >= 5) then html = priv.sliceWithClass('pie5', 5, value, previous, bcolor, label) else local cutIndex = priv.round(value*10) if cutIndex < 1 then cutIndex = 1 end local cut = p.cuts[cutIndex] or 0 local transform = priv.rotation(previous) html = priv.sliceX(cut, transform, bcolor, label) end return htmlendfunction priv.round(number) return math.floor(number + 0.5) endfunction priv.sliceWithClass(sizeClass, sizeStep, value, previous, bcolor, label) local transform = priv.rotation(previous) local html = "" html = html .. priv.sliceBase(sizeClass, transform, bcolor, label) if (value > sizeStep) then local extra = value - sizeStep transform = priv.rotation(previous + extra) html = html .. priv.sliceBase(sizeClass, transform, bcolor, label) end return htmlendfunction priv.sliceBase(sizeClass, transform, bcolor, label) local style = bcolor if transform ~= "" then style = style .. '; ' .. transform end return '\n\t<div class="'..sizeClass..'" style="'..style..'" title="'..p.extract_text(label)..'"></div>'endfunction priv.sliceX(cut, transform, bcolor, label) local path = 'clip-path: polygon(0% 0%, '..cut..'% 0%, 0 100%)' return '\n\t<div style="'..transform..'; '..bcolor..'; '..path..'" title="'..p.extract_text(label)..'"></div>'endfunction priv.rotation(value) if (value > 0.001) then local f = string.format("%.7f", value / 100) f = f:gsub("(%d)0+$", "%1") return "transform: rotate("..f.."turn)" end return ''endfunction priv.formatNum(value) local v = "" if (value < 10) then v = string.format("%.2f", value) else v = string.format("%.1f", value) end -- Vietnamese uses comma for decimal separator v = v:gsub("%.", ",") return vendfunction priv.formatLargeNum(value) local lang = mw.language.getContentLanguage() local v = lang:formatNum(value) return vendfunction priv.prepareLabel(tpl, entry) if not tpl or tpl == "" then if not entry.label or entry.label == "" then tpl = "$v" else if entry.label:find("%$[a-z]") then tpl = entry.label else tpl = "$L: $v" end end end local labelLabel = entry.label and entry.label or priv.getLangOther() local pRaw = priv.formatNum(entry.value) local pp = pRaw .. "%%" local label = tpl label = label:gsub("%$label", "$L"):gsub("%$auto", "$v"):gsub("%$value", "$d"):gsub("%$percent", "$p") label = label:gsub("%$L", labelLabel) local d = priv.formatLargeNum(entry.raw and entry.raw or entry.value) local v = entry.raw and (d .. " (" .. pp .. ")") or pp label = label:gsub("%$p", pp):gsub("%$d", d):gsub("%$v", v) return labelendlocal colorPalette = {'#1b7837','#7fbf7b','#d9f0d3','#762a83','#af8dc3','#e7d4e8','#d73027','#fc8d59','#fee090','#4575b4','#91bfdb','#e0f3f8',}local lastColor = '#fff'function priv.backColor(entry, no, total) if (type(entry.color) == "string") then local sanitizedColor = entry.color:gsub('#', '#'):gsub("[^a-zA-Z0-9#%-]", "") return 'background:' .. sanitizedColor else local color = priv.defaultColor(no, total) return 'background:' .. color endendfunction priv.defaultColor(no, total) local color = lastColor if no <= 0 then return color end local size = #colorPalette if not total or total == 0 then total = size + 1 end local colorNo = priv.defaultColorNo(no, total, size) if colorNo > 0 then color = colorPalette[colorNo] end return colorendfunction priv.defaultColorNo(no, total, size) local color = 0 local colorGroupSize = 3 local colorGroups = 4 if total == 1 then color = 1 elseif total <= colorGroupSize * (colorGroups - 1) then if no < total then color = no else local groupIndex = ((no - 1) % colorGroupSize) if groupIndex == 0 or groupIndex == 1 then color = no+1 else color = no end end elseif no < total then color = ((no - 1) % size) + 1 end return colorendfunction priv.trim(s) return (s:gsub("^%s+", ""):gsub("%s+$", ""))endfunction p.extract_text(label) label = label:gsub("%[%[[^|%]]+|(.-)%]%]", "%1"):gsub("%[%[(.-)%]%]", "%1"):gsub("<[^>]+>", ""):gsub("<", "<"):gsub(">", ">"):gsub("'", "'"):gsub("\"", """) return labelendfunction p.parseEnumParams(frame) local args = frame:getParent().args return priv.parseEnumParams(args)endfunction priv.parseEnumParams(args) local result = {} local i = 1 local sum = 0.0 local hasCustomColor = false while args["value" .. i] do local entry = { value = tonumber(args["value" .. i]) or 0 } local label = args["label" .. i] if label and label ~= "" then entry.label = label end hasCustomColor = false local color = args["color" .. i] if color and color ~= "" then entry.color = color hasCustomColor = true end table.insert(result, entry) sum = sum + entry.value i = i + 1 end local willAutoscale = priv.willAutoscale(sum) for _, entry in ipairs(result) do local label = entry.label if label and not label:find("%$[a-z]") then if willAutoscale then entry.label = label .. " $v" else entry.label = label .. " ($p)" end end end -- Support for 'other' or 'khác' local langOther = priv.getLangOther() local colorOther = "#FEFDFD" local otherValue = 100 - sum local otherParam = args["other"] or args["khác"] local otherLabelParam = args["other-label"] or args["nhãn khác"] local otherColorParam = args["other-color"] or args["màu khác"] if otherParam and otherParam ~= "" then if otherValue < 0.001 then otherValue = 0 end local otherEntry = { label = (otherLabelParam or langOther) .. " ($p)" } if otherColorParam and otherColorParam ~= "" then otherEntry.color = otherColorParam else otherEntry.color = colorOther end table.insert(result, otherEntry) elseif otherValue > 0.01 then if hasCustomColor then table.insert(result, {visible = false, label = langOther .. " ($v)", color = colorOther}) else table.insert(result, {visible = false, label = langOther .. " ($v)"}) end end return mw.text.jsonEncode(result)endfunction priv.getLangOther() return "Khác"endlocal trueValues = { ["true"] = true, ["1"] = true, ["on"] = true, ["yes"] = true, ["có"] = true }function priv.isTrueishValue(value) if not value or value == "" then return nil end value = priv.trim(value) if value == "" then return nil end return trueValues[value:lower()] or falseendfunction p.parseMetaParams(frame) local args = frame:getParent().args local meta = {} local thumb = args["thumb"] if args["value1"] or (thumb and (thumb == "right" or thumb == "left")) then meta.size = 200 meta.legend = true end if args["meta"] then local decodeSuccess, tempMeta = pcall(function() return mw.text.jsonDecode(args["meta"], mw.text.JSON_TRY_FIXING) end) if decodeSuccess then meta = tempMeta end end if args["size"] then meta.size = tonumber(args["size"]) end if args["radius"] and tonumber(args["radius"]) then meta.size = 2 * tonumber(args["radius"]) end if args["autoscale"] then meta.autoscale = priv.isTrueishValue(args["autoscale"]) end if args["legend"] then meta.legend = priv.isTrueishValue(args["legend"]) end if args["ariahidechart"] then meta.ariahidechart = priv.isTrueishValue(args["ariahidechart"]) end if args["direction"] and args["direction"] ~= "" then meta.direction = args["direction"]:gsub("[^a-z0-9%-]", "") end if args["width"] and args["width"] ~= "" then meta.width = args["width"]:gsub("[^a-z0-9%-]", "") end if args["caption"] and args["caption"] ~= "" then meta.caption = args["caption"] end if args["chú thích"] and args["chú thích"] ~= "" then meta.caption = args["chú thích"] end if args["footer"] and args["footer"] ~= "" then meta.footer = args["footer"] end if args["labelformat"] and args["labelformat"] ~= "" then meta.labelformat = args["labelformat"] end return mw.text.jsonEncode(meta)endreturn p