| 👁 Image | This module is rated as ready for general use. It has reached a mature state, is considered relatively stable and bug-free, and may be used wherever appropriate. It can be mentioned on help pages and other Wikipedia resources as an option for new users. To minimise server load and avoid disruptive output, improvements should be developed through sandbox testing rather than repeated trial-and-error editing. |
| 👁 Page template-protected | This module is currently protected from editing. See the protection policy and protection log for more details. Please discuss any changes on the talk page; you may submit an edit request to ask an administrator to make an edit if it is uncontroversial or supported by consensus. You may also request that this page be unprotected. |
Smooth pie chart module. Accessed via Template:Pie chart.
Usage
Draws charts in HTML with an accessible legend (optional). A list of all features is in the "TODO" section of the main `p.pie` function.
Most of the time you should use with a helper template that adds required CSS: {{Pie chart}}.
Examples
Minimalistic
Note that you don't need to provide the second value as it's calculated (assuming they sum up to 100).
{{Pie chart| [ {"value":33.3}, {} ] |thumb=none}}
Labels and Legend
Here we add some custom labels. Also note that we add a meta option to add legend on the side.
{{Pie chart| [ {"label": "women: $v", "value": 33.3}, {"label": "men: $v"} ] |thumb=none |meta = {"legend":true} }}
- women: 33.3%
- men: 66.7%
Automatic Scaling
In cases where you don't have calculated percentages, you can use automatic scaling. Just provide both values in this case.
{{Pie chart| [ {"label": "women: $v", "value": 750}, {"label": "men: $v", "value": 250} ] |thumb=none |meta = {"legend":true} }}
- women: 750 (75.0%)
- men: 250 (25.0%)
Multiple Values
The module allows displaying multiple values, not just 2.
{{Pie chart| [ {"label": "sweets: $v", "value": 5, "color":"darkred"}, {"label": "sandwiches: $v", "value": 3, "color":"wheat"}, {"label": "cookies: $v", "value": 2, "color":"goldenrod"}, {"label": "drinks: $v", "value": 1, "color":"#ccf"} ] |thumb=none |meta={"autoscale":true, "legend":true} }}
- sweets: 5 (45.5%)
- sandwiches: 3 (27.3%)
- cookies: 2 (18.2%)
- drinks: 1 (9.09%)
Note that in this case, it was necessary to provide the additional option "autoscale":true. This is necessary when the sum is less than 100.
Links
- sweets: 5 (45.5%)
- sandwiches: 3 (27.3%)
- cookies: 2 (18.2%)
- drinks: 1 (9.09%)
Legend and Its Position
The legend is added using the meta property legend as shown. However, you can also change the order using direction. Possible values include:
- row (default) – order is list, chart;
- row-reverse – reverse order, i.e., chart, list;
- column – column layout (vertical).
- column-reverse – column layout, reversed (chart on top).
{{Pie chart| [ {"label": "cookies: $v", "value": 2, "color":"goldenrod"}, {"label": "drinks: $v", "value": 1, "color":"#ccf"}, {"label": "sweets: $v", "value": 5, "color":"darkred"}, {"label": "sandwiches: $v", "value": 3, "color":"wheat"} ] |thumb=none |meta={"autoscale":true, "legend":true, "direction":"row-reverse"} }}
row (default direction)
- cookies: 2 (18.2%)
- drinks: 1 (9.09%)
- sweets: 5 (45.5%)
- sandwiches: 3 (27.3%)
row-reverse
- cookies: 2 (18.2%)
- drinks: 1 (9.09%)
- sweets: 5 (45.5%)
- sandwiches: 3 (27.3%)
column
- cookies: 2 (18.2%)
- drinks: 1 (9.09%)
- sweets: 5 (45.5%)
- sandwiches: 3 (27.3%)
column-reverse
- cookies: 2 (18.2%)
- drinks: 1 (9.09%)
- sweets: 5 (45.5%)
- sandwiches: 3 (27.3%)
Green frames added for clarity in examples. They are not normally added.
Direct functions
In case you want to use without the {{Pie chart}} template, you can use this main functions:
{{#invoke:Piechart|pie|json_data|meta=json_options}}{{#invoke:Piechart|color|number}}
Note that direct calls to the pie function require adding CSS:
<templatestylessrc="Pie chart/style.css"/> {{#invoke:Piechart|pie|[{"value":33.3},{}]}}
Example of json_data:
[ {"label":"pie: $v","color":"wheat","value":40}, {"label":"cheese pizza $v","color":"#fc0","value":20}, {"label":"mixed pizza: $v","color":"#f60","value":20}, {"label":"raw pizza $v","color":"#f30"} ]
- Note that the last value is missing. The last value is optional as long as the values are intended to sum up to 100 (as in 100%).
- Notice
$vlabel, this is a formatted number (see `function prepareLabel`). - Colors are hex or names. Default palette is in shades of green.
Example of meta=json_options:
|meta={"size":200,"autoscale":false,"legend":true}
All meta options are optional (see `function p.setupOptions`).
Feature requests
For feature requests and bugs write to me, the author of the piechart module: Maciej Nux.
localp={} localpriv={}-- private functions scope -- expose private for easy testing/debugging p.__priv=priv -- require exact colors for printing localforPrinting="-webkit-print-color-adjust: exact; print-color-adjust: exact;" --[===[ Smooth piechart module. Draws charts in HTML with an accessible legend (optional). A list of all features is in the "TODO" section of the main `p.pie` function. Module info: - Changelog and TODO: [[:en:User:Nux/pie_chart_-_todo]] (Piechart 1.0, 2.0 and beyond). - Author: [[:en:User:Nux|Maciej Nux]]. Use with a helper template that adds required CSS. {{{1}}}: [ { "label": "pie: $v", "color": "wheat", "value": 40 }, { "label": "cheese pizza $v", "color": "#fc0", "value": 20 }, { "label": "mixed pizza: $v", "color": "#f60", "value": 20 }, { "label": "raw pizza $v", "color": "#f30" } ] Where $v is a formatted number (see `function prepareLabel`). {{{meta}}}: {"size":200, "autoscale":false, "legend":true} All meta options are optional (see `function p.setupOptions`). ]===] --[[ Debug: -- labels and auto-value local json_data = '[{"label": "k: $v", "value": 33.1}, {"label": "m: $v", "value": -1}]' local html = p.renderPie(json_data) mw.logObject(html) -- autoscale values local json_data = '[{"value": 700}, {"value": 300}]' local html = p.renderPie(json_data, options) mw.logObject(html) -- size option local json_data = '[{"label": "k: $v", "value": 33.1}, {"label": "m: $v", "value": -1}]' local options = '{"size":200}' local html = p.renderPie(json_data, options) mw.logObject(html) -- custom colors local json_data = '[{"label": "k: $v", "value": 33.1, "color":"black"}, {"label": "m: $v", "value": -1, "color":"green"}]' local html = p.renderPie(json_data) mw.logObject(html) -- 4-cuts local entries = { '{"label": "ciastka: $v", "value": 2, "color":"goldenrod"}', '{"label": "słodycze: $v", "value": 4, "color":"darkred"}', '{"label": "napoje: $v", "value": 1, "color":"lightblue"}', '{"label": "kanapki: $v", "value": 3, "color":"wheat"}' } local json_data = '['..table.concat(entries, ',')..']' local html = p.renderPie(json_data, '{"autoscale":true}') mw.logObject(html) -- colors local fr = { args = { " 123 " } } local ret = p.color(fr) ]] --[[ Color for a slice (defaults). {{{1}}}: slice number ]] functionp.color(frame) localindex=tonumber(priv.trim(frame.args[1])) return' '..priv.defaultColor(index) end --[===[ Main pie chart function. TODO (maybe): [[:en:User:Nux/pie_chart_-_todo#Hopes_and_dreams]] ]===] functionp.pie(frame) localjson_data=priv.trim(frame.args[1]) localoptions={} if(frame.args.meta)then options.meta=priv.trim(frame.args.meta) end localhtml=p.renderPie(json_data,options) returnpriv.trim(html) end -- Setup chart options. functionp.setupOptions(user_options) localoptions={ -- circle size in [px] size=100, -- autoscale values (otherwise assume they sum up to 100) autoscale=false, -- hide chart for screen readers (when you have a table, forced for legend) ariahidechart=false, -- show legend (defaults to the left side) legend=false, -- direction of legend-chart flexbox (flex-direction) direction="", -- width of the main container -- when direction is used defaults to max-width, otherwise it's not added width="", -- caption above the labels caption="", -- footer below the labels footer="", -- formatting template for labels labelformat="", -- percentage number formatting precision (number of digits; -1 = automatic mode) precision=-1, } -- internals options.style="" ifuser_optionsanduser_options.metathen localdecodeSuccess,rawOptions=pcall(function() returnmw.text.jsonDecode(user_options.meta,mw.text.JSON_TRY_FIXING) end) ifnotdecodeSuccessthen rawOptions=false mw.log('invalid meta parameters') end ifrawOptionsthen iftype(rawOptions.size)=="number"then options.size=math.floor(rawOptions.size) end options.autoscale=rawOptions.autoscaleorfalse ifrawOptions.legendthen options.legend=true end ifrawOptions.ariahidechartthen options.ariahidechart=true end if(type(rawOptions.direction)=="string")then -- Remove unsafe/invalid characters localsanitized=rawOptions.direction:gsub("[^a-z0-9%-]","") -- also adjust width so that row-reverse won't push things to the right options.direction='flex-direction: '..sanitized..';' options.width='width: max-content;' end if(type(rawOptions.width)=="string")then -- note, this intentionaly overwrites what was set for direction localsanitized=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 iftype(rawOptions.precision)=="number"then options.precision=math.floor(rawOptions.precision) end end -- build style ifoptions.width~=""then options.style=options.style..options.width end ifoptions.direction~=""then options.style=options.style..options.direction end end if(options.legend)then options.ariahidechart=true end returnoptions end -- internal for testing legend rendering p.__priv.legendDebug=false --[[ Render piechart. @param json_data JSON string with pie data. ]] functionp.renderPie(json_data,user_options) iftype(json_data)~="string"then error('invalid piechart data type: '..type(json_data)) end if#json_data<2then error('piechart data is empty') end localdecodeSuccess,data=pcall(function() returnmw.text.jsonDecode(json_data,mw.text.JSON_TRY_FIXING) end) -- Handle decode error ifnotdecodeSuccessthen error('invalid piechart data: '..json_data) end localoptions=p.setupOptions(user_options) -- prepare localok,total=p.prepareEntries(data,options) -- init render localhtml="<div class='smooth-pie-container' style='"..options.style.."'>" -- error info ifnotokthen html=html..priv.renderErrors(data) end -- render legend ifoptions.legendthen html=html..p.renderLegend(data,options) end ifp.__priv.legendDebugthen returnhtml end -- render items localheader,items,footer=p.renderEntries(ok,total,data,options) html=html..header..items..footer -- end .smooth-pie-container html=html.."\n</div>" returnhtml end functionpriv.boundaryFormatting(diff) localvalue=0.0 ifdiff<=1.0then value=math.ceil(diff/0.2)*0.2-- 0.2 step else value=math.ceil(diff/0.5)*0.5-- 0.5 step end returnstring.format("%.1f",value) end -- Check if sum will trigger autoscaling functionpriv.willAutoscale(sum) -- Compare with a number larger then 100% to avoid floating-point precision problems --- ...and data precision problems https://en.wikipedia.org/wiki/Template_talk:Pie_chart#c-PrimeHunter-20250420202500-Allow_percentage_sum_slightly_above_100 localdiff=sum-100 localgrace=1 returndiff>grace end -- Tracking errors in data (note: somewhat expensive, similar to a red link) -- In short: ±0.3 is a reasonable deviation; ±1 when the errors accumulate -- https://en.wikipedia.org/wiki/Template_talk:Pie_chart#c-Nux-20250429152000-Nux-20250422224600 functionpriv.sumErrorTracking(sum,items) localdiff=sum-100 ifdiff>=0.4anddiff<=10then localfirstItem=items[1] locallastItem=items[#items] mw.addWarning("pie chart: Σ (value) = "..sum.."% ("..firstItem.value.." + .. .+ "..lastItem.value..")") ifmw.title.getCurrentTitle().namespace==0then localsuffix=priv.boundaryFormatting(diff) _=mw.title.new("Module:Piechart/tracing/diff below "..suffix).id end end end -- Prepare data (slices etc) functionp.prepareEntries(data,options) localsum=priv.sumValues(data); -- force autoscale when over 100 ifpriv.willAutoscale(sum)then options.autoscale=true end -- pre-format entries localok=true localno=0 localtotal=#data forindex,entryinipairs(data)do no=no+1 ifnotpriv.prepareSlice(entry,no,sum,total,options)then no=no-1 ok=false end end total=no-- total valid returnok,total end functionpriv.sumValues(data) localsum=0; for_,entryinipairs(data)do localvalue=entry.value ifnot(type(value)~="number"orvalue<0)then sum=sum+value end end returnsum end -- render error info functionpriv.renderErrors(data) localhtml="\n<ol class='chart-errors' style='display:none'>" for_,entryinipairs(data)do ifentry.errorthen localentryJson=mw.text.jsonEncode(entry) html=html.."\n<li>"..entryJson.."</li>" end end returnhtml.."\n</ol>\n" end -- Prepare single slice data (modifies entry). -- @param no = 1..total functionpriv.prepareSlice(entry,no,sum,total,options) localautoscale=options.autoscale localvalue=entry.value if(type(value)~="number"orvalue<0)then ifautoscalethen entry.error="cannot autoscale unknown value" returnfalse end value=100-sum end -- entry.raw only when scaled ifautoscalethen entry.raw=value value=(value/sum)*100 end entry.value=value -- prepare final label entry.label=priv.prepareLabel(options.labelformat,entry,options.precision) -- background, but also color for MW syntax linter entry.bcolor=priv.backColor(entry,no,total)..";color:#000" returntrue end -- render legend for pre-processed entries functionp.renderLegend(data,options) localhtml="" ifoptions.caption~=""oroptions.footer~=""then html="\n<div class='smooth-pie-legend-container'>" end ifoptions.caption~=""then html=html.."<div class='smooth-pie-caption'>"..options.caption.."</div>" end html=html.."\n<ol class='smooth-pie-legend'>" for_,entryinipairs(data)do ifnotentry.errorthen html=html..priv.renderLegendItem(entry,options) end end html=html.."\n</ol>\n" ifoptions.footer~=""then html=html.."<div class='smooth-pie-footer'>"..options.footer.."</div>" end ifoptions.caption~=""oroptions.footer~=""then html=html.."</div>\n" end returnhtml end -- render legend item functionpriv.renderLegendItem(entry,options) -- invisible value (for a11y reasons this should not be used for important values!) ifentry.visible~=nilandentry.visible==falsethen return"" end locallabel=entry.label localbcolor=entry.bcolor localhtml="\n<li>" ifp.__priv.legendDebugthen forPrinting="" end html=html..'<span class="l-color" style="'..forPrinting..bcolor..'"></span>' html=html..'<span class="l-label">'..label..'</span>' returnhtml.."</li>" end -- Prepare data (slices etc) functionp.renderEntries(ok,total,data,options) -- cache for some items (small slices) p.cuts=mw.loadJsonData('Module:Piechart/cuts.json') localfirst=true localprevious=0 localitems="" forindex,entryinipairs(data)do ifnotentry.errorthen items=items..priv.renderItem(previous,entry,options) previous=previous+entry.value end end localheader=priv.renderHeader(options) localfooter='\n<div class="smooth-pie-border"></div></div>' returnheader,items,footer end -- header of pie-items (class="smooth-pie") functionpriv.renderHeader(options) localbcolor='background:#888;color:#000' localsize=options.size -- hide chart for readers, especially when legend is there localaria="" if(options.ariahidechart)then aria='aria-hidden="true"' end -- slices container and last slice localstyle='width:'..size..'px;height:'..size..'px;'..bcolor..';'..forPrinting localhtml=[[ <div class="smooth-pie" style="]]..style..[[" ]]..aria..[[ >]] returnhtml end -- Render pie-item -- (previous is a sum of previous values) functionpriv.renderItem(previous,entry,options) localvalue=entry.value locallabel=entry.label localbcolor=entry.bcolor -- value too small to see if(value<0.03)then mw.log('value too small',value,label) mw.addWarning("pie chart: Value too small ↆ "..value.."% ("..label..")") return"" end -- minimize transformation defects ifvalue<10then ifprevious>1then previous=previous-0.01 end value=value+0.02 else ifprevious>1then previous=previous-0.1 end value=value+0.2 end -- force sum to be below 100% (needed due to value errors) ifprevious+value>100then ifprevious>=100then mw.log('previous is already at 100',value,label) return"" end value=100-previous end localhtml="" localsize='' -- mw.logObject({'v,p,l', value, previous, label}) 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 -- 0-5% localcutIndex=priv.round(value*10) ifcutIndex<1then cutIndex=1 end localcut=p.cuts[cutIndex] localtransform=priv.rotation(previous) html=priv.sliceX(cut,transform,bcolor,label) end -- mw.log(html) returnhtml end -- round to int functionpriv.round(number) returnmath.floor(number+0.5) end -- render full slice with specific class functionpriv.sliceWithClass(sizeClass,sizeStep,value,previous,bcolor,label) localtransform=priv.rotation(previous) localhtml="" html=html..priv.sliceBase(sizeClass,transform,bcolor,label) -- mw.logObject({'sliceWithClass:', sizeClass, sizeStep, value, previous, bcolor, label}) if(value>sizeStep)then localextra=value-sizeStep transform=priv.rotation(previous+extra) -- mw.logObject({'sliceWithClass; extra, transform', extra, transform}) html=html..priv.sliceBase(sizeClass,transform,bcolor,label) end returnhtml end -- render single slice functionpriv.sliceBase(sizeClass,transform,bcolor,label) localstyle=bcolor iftransform~=""then style=style..'; '..transform end return'\n\t<div class="'..sizeClass..'" style="'..style..'" title="'..p.extract_text(label)..'"></div>' end -- small slice cut to fluid size. -- range in theory: 0 to 24.(9)% reaching 24.(9)% for cut = +inf -- range in practice: 0 to 5% functionpriv.sliceX(cut,transform,bcolor,label) localpath='clip-path: polygon(0% 0%, '..cut..'% 0%, 0 100%)' return'\n\t<div style="'..transform..'; '..bcolor..'; '..path..'" title="'..p.extract_text(label)..'"></div>' end -- translate value to turn rotation (v=100 => 1.0turn) functionpriv.rotation(value) if(value>0.001)then localf=string.format("%.7f",value/100) f=f:gsub("(%d)0+$","%1")-- remove trailing zeros return"transform: rotate("..f.."turn)" end return'' end -- Language sensitive float, small numbers. functionpriv.formatNum(value,precision) precision=precisionor-1 localv="" ifprecision>=0then -- custom format v=string.format("%."..precision.."f",value) else -- automatic if(value<10)then v=string.format("%.2f",value) else v=string.format("%.1f",value) end end locallang=mw.language.getContentLanguage() if(lang:getCode()=='pl')then v=v:gsub("%.",",") end returnv end -- Format large values. functionpriv.formatLargeNum(value) locallang=mw.language.getContentLanguage() -- add thusands separators localv=lang:formatNum(value) returnv end -- Testing formatLargeNum. -- p.__priv.test_formatLargeNum() functionpriv.test_formatLargeNum() mw.log("must not add fractional part") mw.log(p.__priv.formatLargeNum(12)) mw.log(p.__priv.formatLargeNum(123)) mw.log("should preserve fractional part for small numbers") mw.log(p.__priv.formatLargeNum(1.1)) mw.log(p.__priv.formatLargeNum(1.12)) mw.log(p.__priv.formatLargeNum(12.1)) mw.log("can preserve long fractional part") mw.log(p.__priv.formatLargeNum(1.1234)) mw.log(p.__priv.formatLargeNum(1.12345)) mw.log("should add separators above 1k") mw.log(p.__priv.formatLargeNum(999)) mw.log(p.__priv.formatLargeNum(1234)) mw.log(p.__priv.formatLargeNum(12345)) mw.log(p.__priv.formatLargeNum(123456)) mw.log(p.__priv.formatLargeNum(1234567)) mw.log("must handle large float, but might round values") mw.log(p.__priv.formatLargeNum(1234.123)) mw.log(p.__priv.formatLargeNum(12345.123)) mw.log(p.__priv.formatLargeNum(123456.123)) mw.log(p.__priv.formatLargeNum(1234567.123)) end --[[ Prepare final label. Typical tpl: "$L: $v" (same as: "$label: $auto") will result in: "Abc: 23%" -- when values are percentages "Abc: 1234 (23%)" -- when values are autoscaled Advanced tpl: "$L: $d ($p)" e.g. "Abc: 1234 (23%)" for {"label":"Abc", "value":1234} "$L: $v" is the same as above when values are autoscaled Long vs short variable names: $label ($L) $auto ($v) $value ($d) $percent ($p) ]] functionpriv.prepareLabel(tpl,entry,precision) -- setup default tpl ifnottplortpl==""then -- simple if no label ifnotentry.labelorentry.label==""then tpl="$v" else ifentry.label:find("%$[a-z]")then tpl=entry.label else tpl="$L: $v" end end end locallabelLabel=entry.labelandentry.labelorpriv.getLangOther() -- format % value without % localpRaw=priv.formatNum(entry.value,precision) localpp=pRaw.."%%" locallabel=tpl -- aliases label=label :gsub("%$label","$L") :gsub("%$auto","$v") :gsub("%$value","$d") :gsub("%$percent","$p") -- replace variables label=label:gsub("%$L",labelLabel) locald=priv.formatLargeNum(entry.rawandentry.raworentry.value) localv=entry.rawand(d.." ("..pp..")")orpp label=label :gsub("%$p",pp) :gsub("%$d",d) :gsub("%$v",v) -- Report unknown variables forvarinlabel:gmatch("%$[a-zA-Z]+")do -- in preview mw.addWarning("pie chart: Unknown variable (wrong format of your label): "..var) -- tracing links _=mw.title.new("Module:Piechart/tracing/unknown-variable").id end returnlabel end -- default colors -- source: https://colorbrewer2.org/#type=diverging&scheme=PRGn&n=6 localcolorGroupSize=3-- must be at least 3 localcolorGroups=4 localcolorPalette={ -- green (from dark) '#1b7837', '#7fbf7b', '#d9f0d3', -- violet '#762a83', '#af8dc3', '#e7d4e8', -- red '#d73027', '#fc8d59', '#fee090', -- blue '#4575b4', '#91bfdb', '#e0f3f8', } locallastColor='#fff' -- background color from entry or the default colors functionpriv.backColor(entry,no,total) if(type(entry.color)=="string")then -- Remove unsafe characters from entry.color localsanitizedColor=entry.color :gsub('#','#')-- workaround Module:Political_party issue reported on talk :gsub("[^a-zA-Z0-9#%-]","") return'background:'..sanitizedColor else localcolor=priv.defaultColor(no,total) return'background:'..color end end -- color from the default colors functionpriv.defaultColor(no,total) localcolor=lastColor ifno<=0then returncolor end localsize=#colorPalette ifnottotalortotal==0then total=size+1 end localcolorNo=priv.defaultColorNo(no,total,size) ifcolorNo>0then color=colorPalette[colorNo] end returncolor end -- gets color number from default colors -- trys to return a light color as the last one -- 0 means white-ish color should be used functionpriv.defaultColorNo(no,total,size) localcolor=0-- special, lastColor iftotal==1then color=1 elseiftotal<=colorGroupSize*(colorGroups-1)then ifno<totalthen color=no else localgroupIndex=((no-1)%colorGroupSize) ifgroupIndex==0then-- dark color=no+1 elseifgroupIndex==1then-- med color=no+1 else color=no end end elseifno<totalthen color=((no-1)%size)+1 end returncolor end --[[ Testing defaultColorNo: p.__priv.test_defaultColorNo(1, 12) p.__priv.test_defaultColorNo(2, 12) p.__priv.test_defaultColorNo(3, 12) p.__priv.test_defaultColorNo(4, 12) p.__priv.test_defaultColorNo(5, 12) p.__priv.test_defaultColorNo(6, 12) ]] functionpriv.test_defaultColorNo(total,size) forno=1,totaldo localcolor=priv.defaultColorNo(no,total,size) mw.logObject({no=no,color=color}) end end --[[ trim string note: `(s:gsub(...))` returns only a string `s:gsub(...)` returns a string and a number ]] functionpriv.trim(s) return(s:gsub("^%s+",""):gsub("%s+$","")) end --[[ Extract text from simple wikitext. For now only works with links. ]] -- Tests: -- mw.log(p.extract_text("[[candy|sweets]]: $v")) -- mw.log(p.extract_text("[[sandwich]]es: $v")) -- mw.log(p.extract_text("sandwich]]es: $v")) -- mw.log(p.extract_text("sandwiches: $v")) functionp.extract_text(label) label=label -- replace links with pipe (e.g., [[candy|sweets]]) :gsub("%[%[[^|%]]+|(.-)%]%]","%1") -- replace simple links without pipe (e.g., [[sandwich]]) :gsub("%[%[(.-)%]%]","%1") -- remove templates? -- :gsub("{.-}", "") -- remove tags :gsub("<[^>]+>","") -- escape special chars just in case :gsub("<","<"):gsub(">",">") :gsub("'","'"):gsub("\"",""") returnlabel end --[[ Parse classic template params into JSON. From: |label1=cookies: $v |value1=11 |color1=goldenrod |label2=sweets: $v |value2=20 |color2=darkred To: {"value":11,"color":"goldenrod","label":"cookies: $v"}, {"value":20,"color":"darkred","label":"sweets: $v"}, ]] functionp.parseEnumParams(frame) localargs=frame:getParent().args returnpriv.parseEnumParams(args) end functionpriv.parseEnumParams(args) localresult={} locali=1 localsum=0.0 localhasCustomColor=false-- has last custom color whileargs["value"..i]do -- value is required in this mode; it's also assumed to be 0..100 localentry={value=tonumber(args["value"..i])or0} -- label and color is optional locallabel=args["label"..i] iflabelandlabel~=""then entry.label=label end hasCustomColor=false localcolor=args["color"..i] ifcolorandcolor~=""then entry.color=color hasCustomColor=true end table.insert(result,entry) sum=sum+entry.value i=i+1 end -- re-loop to set values in labels localwillAutoscale=priv.willAutoscale(sum) for_,entryinipairs(result)do locallabel=entry.label iflabelandnotlabel:find("%$[a-z]")then -- autoscale will be forced, so use $v in labels ifwillAutoscalethen entry.label=label.." $v" else entry.label=label.." ($p)" end end end -- tracking data errors priv.sumErrorTracking(sum,result) -- support other value mapping locallangOther=priv.getLangOther() localcolorOther="#FEFDFD"-- white-ish for custom colors for best chance and contrast localotherValue=100-sum ifargs["other"]andargs["other"]~=""then ifotherValue<0.001then otherValue=0 end localotherEntry={label=(args["other-label"]orlangOther).." ($p)"} ifargs["other-color"]andargs["other-color"]~=""then otherEntry.color=args["other-color"] else otherEntry.color=colorOther end table.insert(result,otherEntry) elseifotherValue>0.01then ifhasCustomColorthen table.insert(result,{visible=false,label=langOther.." ($v)",color=colorOther}) else table.insert(result,{visible=false,label=langOther.." ($v)"}) end end localjsonString=mw.text.jsonEncode(result) returnjsonString end functionpriv.getLangOther() -- support other value mapping locallang=mw.language.getContentLanguage() if(lang:getCode()=='pl')then return"Inne" end return"Other" end -- Function to check if a value is true-ish localtrueValues={["true"]=true,["1"]=true,["on"]=true,["yes"]=true} functionpriv.isTrueishValue(value) -- should return nil for empty args (i.e. undefined i.e. default) ifnotvalueorvalue==""thenreturnnilend value=priv.trim(value) ifvalue==""thenreturnnilend -- other non-empty are false returntrueValues[value:lower()]orfalse end --[[ Parse classic template params into JSON with chart meta data. ]] functionp.parseMetaParams(frame) localargs=frame:getParent().args localmeta={} -- default meta for value1..n parameters -- ...and for thumb right/left localthumb=args["thumb"] ifargs["value1"]or(thumband(thumb=="right"orthumb=="left"))then meta.size=200 meta.legend=true end -- explicit meta param ifargs["meta"]then localdecodeSuccess,tempMeta=pcall(function() returnmw.text.jsonDecode(args["meta"],mw.text.JSON_TRY_FIXING) end) ifnotdecodeSuccessthen mw.log('invalid meta parameter') else meta=tempMeta end end ifargs["size"]thenmeta.size=tonumber(args["size"])end ifargs["radius"]andtonumber(args["radius"])then meta.size=2*tonumber(args["radius"]) end ifargs["autoscale"]thenmeta.autoscale=priv.isTrueishValue(args["autoscale"])end ifargs["legend"]thenmeta.legend=priv.isTrueishValue(args["legend"])end ifargs["ariahidechart"]thenmeta.ariahidechart=priv.isTrueishValue(args["ariahidechart"])end ifargs["direction"]andargs["direction"]~=""then meta.direction=args["direction"]:gsub("[^a-z0-9%-]","") end ifargs["width"]andargs["width"]~=""then meta.width=args["width"]:gsub("[^a-z0-9%-]","") end ifargs["caption"]andargs["caption"]~=""then meta.caption=args["caption"] end ifargs["footer"]andargs["footer"]~=""then meta.footer=args["footer"] end ifargs["labelformat"]andargs["labelformat"]~=""then meta.labelformat=args["labelformat"] end ifargs["precision"]andargs["precision"]~=""then meta.precision=tonumber(args["precision"]) end returnmw.text.jsonEncode(meta) end returnp
