Moduli:Chart

--<source lang=lua>--[[ keywords are used for languages: they are the names of the actual parameters of the template]]localkeywords={barChart='bar chart',pieChart='pie chart',width='width',height='height',stack='stack',colors='colors',group='group',xlegend='x legends',tooltip='tooltip',accumulateTooltip='tooltip value accumulation',links='links',defcolor='default color',scalePerGroup='scale per group',unitsPrefix='units prefix',unitsSuffix='units suffix',groupNames='group names',hideGroupLegends='hide group legends',slices='slices',slice='slice',radius='radius',percent='percent',}-- here is what you want to translatelocaldefColors=require"Module:Plotter/DefaultColors"localhideGroupLegendslocalfunctionnulOrWhitespace(s)returnnotsormw.text.trim(s)==''endlocalfunctioncreateGroupList(tab,legends,cols)if#legends>1andnothideGroupLegendsthentable.insert(tab,mw.text.tag('div'))locallist={}localspanStyle="padding:0 1em;background-color:%s;box-shadow:2px -1px 4px 0 silver;margin-right:1em;"forgi=1,#legendsdolocalspan=mw.text.tag('span',{style=string.format(spanStyle,cols[gi])},'&nbsp;')..' '..legends[gi]table.insert(list,mw.text.tag('li',{},span))endtable.insert(tab,mw.text.tag('ul',{style="width:100%;list-style:none;-webkit-column-width:12em;-moz-column-width:12em;column-width:12em;"},table.concat(list,'\n')))table.insert(tab,'</div>')endendfunctionpieChart(frame)localres,imslices,args={},{},frame.argslocalradiuslocalvalues,colors,names,legends,links={},{},{},{},{}localdelimiter=args.delimiteror':'locallang=mw.getContentLanguage()functiongetArg(s,def,subst,with)localresult=args[keywords[s]]ordefor''ifsubstandwiththenresult=mw.ustring.gsub(result,subst,with)endreturnresultendfunctionanalyzeParams()functionaddSlice(i,slice)localvalue,name,color,link=unpack(mw.text.split(slice,'%s*'..delimiter..'%s*'))values[i]=tonumber(lang:parseFormattedNumber(value))orerror(string.format('Slice %d: "%s", first item("%s") could not be parsed as a number',i,valueor'',sliceStr))colors[i]=notnulOrWhitespace(color)andcolorordefColors[i*2]names[i]=nameor''links[i]=linkendradius=getArg('radius',150)hideGroupLegends=notnulOrWhitespace(args[keywords.hideGroupLegends])localslicesStr=getArg('slices')localprefix=getArg('unitsPrefix','','_',' ')localsuffix=getArg('unitsSuffix','','_',' ')localpercent=args[keywords.percent]localsum=0locali,value=0forsliceinmw.ustring.gmatch(slicesStror'',"%b()")doi=i+1addSlice(i,mw.ustring.match(slice,'^%(%s*(.-)%s*%)$'))endfork,vinpairs(args)dolocalind=mw.ustring.match(k,'^'..keywords.slice..'%s+(%d+)$')ifindthenaddSlice(tonumber(ind),v)endendfor_,valinipairs(values)dosum=sum+valendfori,valueinipairs(values)dolocaladdprec=percentandstring.format(' (%0.1f%%)',value/sum*100)or''legends[i]=mw.ustring.format('%s: %s%s%s%s',names[i],prefix,lang:formatNum(value),suffix,addprec)links[i]=mw.text.trim(links[i]ormw.ustring.format('[[#noSuchAnchor|%s]]',legends[i]))endendfunctionaddRes(...)for_,vinpairs({...})dotable.insert(res,v)endendfunctioncreateImageMap()addRes('{{#tag:imagemap|','Image:Circle frame.svg{{!}}'..(radius*2)..'px')addRes(unpack(imslices))addRes('desc none','}}')endfunctiondrawSlice(i,q,start)localcolor=colors[i]localangle=start*2*math.pilocalsin,cos=math.abs(math.sin(angle)),math.abs(math.cos(angle))localwsin,wcos=sin*radius,cos*radiuslocals1,s2,w1,w2,w3,w4,width,borderlocalstyleifq==1thenborder='left'w1,w2,w3,w4=0,0,wsin,wcoss1,s2='bottom','left'elseifq==2thenborder='bottom'w1,w2,w3,w4=0,wcos,wsin,0s1,s2='bottom','right'elseifq==3thenborder='right'w1,w2,w3,w4=wsin,wcos,0,0s1,s2='top','right'elseborder='top'w1,w2,w3,w4=wsin,0,0,wcoss1,s2='top','left'endlocalstyle=string.format('position:absolute;%s:%spx;%s:%spx;width:%spx;height:%spx',s1,radius,s2,radius,radius,radius)ifstart<=(q-1)*0.25thenstyle=string.format('%s;border:0;background-color:%s',style,color)elsestyle=string.format('%s;border-width:%spx %spx %spx %spx;border-%s-color:%s',style,w1,w2,w3,w4,border,color)endaddRes(mw.text.tag('div',{class='transborder',style=style},''))endfunctioncreateSlices()functioncoordsOfAngle(angle)return(100+math.floor(100*math.cos(angle)))..' '..(100-math.floor(100*math.sin(angle)))endlocalsum,start=0,0for_,valueinipairs(values)dosum=sum+valueendfori,valueinipairs(values)dolocalpoly={'poly 100 100'}localstartC,endC=start/sum,(start+value)/sumlocalstartQ,endQ=math.floor(startC*4+1),math.floor(endC*4+1)forq=startQ,math.min(endQ,4)dodrawSlice(i,q,startC)endforangle=startC*2*math.pi,endC*2*math.pi,0.02dotable.insert(poly,coordsOfAngle(angle))endtable.insert(poly,coordsOfAngle(endC*2*math.pi)..' 100 100 '..links[i])table.insert(imslices,table.concat(poly,' '))start=start+values[i]endendanalyzeParams()if#values==0thenerror("no slices found - can't draw pie chart")endaddRes(mw.text.tag('div',{style=string.format("max-width:%spx",radius*2)}))addRes(mw.text.tag('div',{style=string.format('position:relative;min-width:%spx;min-height:%spx;max-width:%spx;overflow:hidden;',radius*2,radius*2,radius*2)}))createSlices()addRes(mw.text.tag('div',{style=string.format('position:absolute;min-width:%spx;min-height:%spx;overflow:hidden;',radius*2,radius*2)}))createImageMap()addRes('</div>')-- close "position:relative" div that contains slices and imagemap.addRes('</div>')-- close "position:relative" div that contains slices and imagemap.createGroupList(res,legends,colors)-- legendsaddRes('</div>')-- close containing divreturnframe:preprocess(table.concat(res,'\n'))endfunctionbarChart(frame)localres={}localargs=frame.args-- can be changed to frame:getParent().argslocalvalues,xlegends,colors,tooltips,yscales={},{},{},{},{},{},{}localgroupNames,unitsSuffix,unitsPrefix,links={},{},{},{}localwidth,height,stack,delimiter=500,350,false,args.delimiteror':'localchartWidth,chartHeight,defcolor,scalePerGroup,accumulateTooltiplocalnumGroups,numValueslocalscaleWidthfunctionvalidate()functionasGroups(name,tab,toDuplicate,emptyOK)if#tab==0andnotemptyOKthenerror("must supply values for "..keywords[name])endif#tab==1andtoDuplicatethenfori=2,numGroupsdotab[i]=tab[1]endendif#tab>0and#tab~=numGroupsthenerror(keywords[name]..' should contain the same number of items as the number of groups ('..numGroups..')')endend-- do all sorts of validation here, so we can assume all params are good from now on.-- among other things, replace numerical values with mw.language:parseFormattedNumber() resultchartHeight=height-80numGroups=#valuesnumValues=#values[1]defcolor=defcoloror'blue'colors[1]=colors[1]ordefcolorscaleWidth=scalePerGroupand80*numGroupsor100chartWidth=width-scaleWidthasGroups('unitsPrefix',unitsPrefix,true,true)asGroups('unitsSuffix',unitsSuffix,true,true)asGroups('colors',colors,true,true)asGroups('groupNames',groupNames,false,false)ifstackandscalePerGroupthenerror(string.format('Illegal settings: %s and %s are incompatible.',keyword.stack,keyword.scalePerGroup))endforgi=2,numGroupsdoif#values[gi]~=numValuesthenerror(keywords.group.." "..gi.." does not have same number of values as "..keywords.group.." 1")endendif#xlegends~=numValuesthenerror('Illegal number of '..keywords.xlegend..'. Should be exatly '..numValues)endendfunctionextractParams()functiontestone(keyword,key,val,tab)i=keyword==keyand0orkey:match(keyword.."%s+(%d+)")ifnotithenreturnendi=tonumber(i)orerror("Expect numerical index for key "..keyword.." instead of '"..key.."'")ifi>0thentab[i]={}endforsinmw.text.gsplit(val,'%s*'..delimiter..'%s*')dotable.insert(i==0andtabortab[i],s)endreturntrueendfork,vinpairs(args)doifk==keywords.widththenwidth=tonumber(v)ifnotwidthorwidth<200thenerror('Illegal width value (must be a number, and at least 200): '..v)endelseifk==keywords.heightthenheight=tonumber(v)ifnotheightorheight<200thenerror('Illegal height value (must be a number, and at least 200): '..v)endelseifk==keywords.stackthenstack=trueelseifk==keywords.scalePerGroupthenscalePerGroup=trueelseifk==keywords.defcolorthendefcolor=velseifk==keywords.accumulateTooltipthenaccumulateTooltip=notnulOrWhitespace(v)elseifk==keywords.hideGroupLegendsthenhideGroupLegends=notnulOrWhitespace(v)elseforkeyword,tabinpairs({group=values,xlegend=xlegends,colors=colors,tooltip=tooltips,unitsPrefix=unitsPrefix,unitsSuffix=unitsSuffix,groupNames=groupNames,links=links,})doiftestone(keywords[keyword],k,v,tab)thenbreakendendendendendfunctionroundup(x)-- returns the next round number: eg., for 30 to 39.999 will return 40, for 3000 to 3999.99 wil return 4000. for 10 - 14.999 will return 15.localordermag=10^math.floor(math.log10(x))localnormalized=x/ordermaglocaltop=normalized>=1.5and(math.floor(normalized+1))or1.5returnordermag*top,top,ordermagendfunctioncalcHeightLimits()-- if limits were passed by user, use them, otherwise calculate. for "stack" there's only one limet.ifstackthenlocalsums={}for_,groupinpairs(values)dofori,valinipairs(group)dosums[i]=(sums[i]or0)+valendendlocalsum=math.max(unpack(sums))fori=1,#valuesdoyscales[i]=sumendelsefori,groupinipairs(values)doyscales[i]=math.max(unpack(group))endendfori,scaleinipairs(yscales)doyscales[i]=roundup(scale*0.9999)endifnotscalePerGroupthenfori=1,#valuesdoyscales[i]=math.max(unpack(yscales))endendendfunctiontooltip(gi,i,val)iftooltipsandtooltips[gi]andnotnulOrWhitespace(tooltips[gi][i])thenreturntooltips[gi][i],trueendlocalgroupName=notnulOrWhitespace(groupNames[gi])andgroupNames[gi]..': 'or''localprefix=unitsPrefix[gi]orunitsPrefix[1]or''localsuffix=unitsSuffix[gi]orunitsSuffix[1]or''returnmw.ustring.gsub(groupName..prefix..mw.getContentLanguage():formatNum(tonumber(val)or0)..suffix,'_',' '),falseendfunctioncalcHeights(gi,i,val)localbarHeight=math.floor(val/yscales[gi]*chartHeight+0.5)-- add half to make it "round" insstead of "trunc"localtop,base=chartHeight-barHeight,0ifstackthenlocalrawbase=0forj=1,gi-1dorawbase=rawbase+values[j][i]end-- sum the "i" value of all the groups below our group, gi.base=math.floor(chartHeight*rawbase/yscales[gi])-- normally, and especially if it's "stack", all the yscales must be equal.endreturnbarHeight,top-baseendfunctiongroupBounds(i)localsetWidth=math.floor(chartWidth/numValues)localsetOffset=(i-1)*setWidthreturnsetOffset,setWidthendfunctioncalcx(gi,i)localsetOffset,setWidth=groupBounds(i)ifstackornumGroups==1thenlocalbarWidth=math.min(38,math.floor(0.8*setWidth))returnsetOffset+(setWidth-barWidth)/2,barWidthendsetWidth=0.85*setWidthlocalbarWidth=math.floor(0.75*setWidth/numGroups)localleft=setOffset+math.floor((gi-1)/numGroups*setWidth)returnleft,barWidthendfunctiondrawbar(gi,i,val,ttval)localcolor,tooltip,custom=colors[gi]ordefcoloror'blue',tooltip(gi,i,ttvalorval)localleft,barWidth=calcx(gi,i)localbarHeight,top=calcHeights(gi,i,val)localstyle=string.format("position:absolute;left:%spx;top:%spx;height:%spx;min-width:%spx;max-width:%spx;background-color:%s;box-shadow:2px -1px 4px 0 silver;overflow:hidden;",left,top,barHeight,barWidth,barWidth,color)locallink=links[gi]andlinks[gi][i]or''localimg=notnulOrWhitespace(link)andmw.ustring.format('[[File:Transparent.png|1000px|link=%s|%s]]',link,customandtooltipor'')or''table.insert(res,mw.text.tag('div',{style=style,title=tooltip,},img))endfunctiondrawYScale()functiondrawSingle(gi,color,width,single)localyscale=yscales[gi]local_,top,ordermag=roundup(yscale*0.999)localnumnotches=top<=1.5andtop*4ortop<4andtop*2ortoplocalvalStyleStr=singleand'position:absolute;height=20px;text-align:right;vertical-align:middle;width:%spx;top:%spx;padding:0 2px'or'position:absolute;height=20px;text-align:right;vertical-align:middle;width:%spx;top:%spx;left:3px;background-color:%s;color:white;font-weight:bold;text-shadow:-1px -1px 0 #000,1px -1px 0 #000,-1px 1px 0 #000,1px 1px 0 #000;padding:0 2px'localnotchStyleStr='position:absolute;height=1px;min-width:5px;top:%spx;left:%spx;border:1px solid %s;'fori=1,numnotchesdolocalval=i/numnotches*yscalelocaly=chartHeight-calcHeights(gi,1,val)localdiv=mw.text.tag('div',{style=string.format(valStyleStr,width-10,y-10,color)},mw.getContentLanguage():formatNum(tonumber(val)or0))table.insert(res,div)div=mw.text.tag('div',{style=string.format(notchStyleStr,y,width-4,color)},'')table.insert(res,div)endendifscalePerGroupthenlocalcolWidth=80localcolStyle="position:absolute;height:%spx;min-width:%spx;left:%spx;border-right:1px solid %s;color:%s"forgi=1,numGroupsdolocalleft=(gi-1)*colWidthlocalcolor=colors[gi]ordefcolortable.insert(res,mw.text.tag('div',{style=string.format(colStyle,chartHeight,colWidth,left,color,color)}))drawSingle(gi,color,colWidth)table.insert(res,'</div>')endelsedrawSingle(1,'black',scaleWidth,true)endendfunctiondrawXlegends()localsetOffset,setWidthlocallegendDivStyleFormat="position:absolute;left:%spx;top:10px;min-width:%spx;max-width:%spx;text-align:center;veritical-align:top;"localtickDivstyleFormat="position:absolute;left:%spx;height:10px;width:1px;border-left:1px solid black;"fori=1,numValuesdoifnotnulOrWhitespace(xlegends[i])thensetOffset,setWidth=groupBounds(i)-- setWidth = 0.85 * setWidthtable.insert(res,mw.text.tag('div',{style=string.format(legendDivStyleFormat,setOffset+5,setWidth-10,setWidth-10)},xlegends[i]or''))table.insert(res,mw.text.tag('div',{style=string.format(tickDivstyleFormat,setOffset+setWidth/2)},''))endendendfunctiondrawChart()table.insert(res,mw.text.tag('div',{style=string.format('max-width:%spx;',width)}))table.insert(res,mw.text.tag('div',{style=string.format("position:relative;min-height:%spx;min-width:%spx;max-width:%spx;",height,width,width)}))table.insert(res,mw.text.tag('div',{style=string.format("float:right;position:relative;min-height:%spx;min-width:%spx;max-width:%spx;border-left:1px black solid;border-bottom:1px black solid;",chartHeight,chartWidth,chartWidth)}))localacum=stackandaccumulateTooltipand{}forgi,groupinpairs(values)dofori,valinipairs(group)doifacumthenacum[i]=(acum[i]or0)+valenddrawbar(gi,i,val,acumandacum[i])endendtable.insert(res,'</div>')table.insert(res,mw.text.tag('div',{style=string.format("position:absolute;height:%spx;min-width:%spx;max-width:%spx;",chartHeight,scaleWidth,scaleWidth,scaleWidth)}))drawYScale()table.insert(res,'</div>')table.insert(res,mw.text.tag('div',{style=string.format("position:absolute;top:%spx;left:%spx;width:%spx;",chartHeight,scaleWidth,chartWidth)}))drawXlegends()table.insert(res,'</div>')table.insert(res,'</div>')createGroupList(res,groupNames,colors)table.insert(res,'</div>')endextractParams()validate()calcHeightLimits()drawChart()returntable.concat(res,"\n")endreturn{['bar-chart']=barChart,[keywords.barChart]=barChart,[keywords.pieChart]=pieChart,}--</source>