Module:Plotter
This module is rated as pre-alpha. It is unfinished, and may or may not be in active development. It should not be used from article namespace pages. Modules remain pre-alpha until the original editor (or someone who takes one over if it is abandoned for some time) is satisfied with the basic structure. |
Note: now that this module's pie chart capability has been added to Module:Chart, this module is starting to look obsolete by comparison. It is therefore rated pre-alpha and not for general use, probably pending a total takeover by Module:Chart. It's being kept around only in case there are some functions left the latter hasn't matched yet, which I haven't checked.
This module has three plotting capabilities so far. Some of the code is crude as I began back when I'd first heard of Lua. They are all written independently and not even the parameters are standardized between them, nor do they share any subroutines. "Main" needs major work, "bar" is a bit better but not as good as Module:Chart at present, and "piechart" is closely based on Template:Pie chart.
function "piechart"
[সম্পাদনা কৰক]This uses the miter joining of border elements in HTML/CSS to draw pie-shaped slices. I still don't fully understand the code taken from Template:Pie chart but have made it a bit more comprehensible. The main improvement so far is that it is resizable and not limited to 10 slices.
New parameters are
- Radius (default 100) - radius of the pie chart
- nowiki - include a nonblank value to read rather than graph the output
To quote the piechart template
- option "thumb" specifies which side of the page the chart is floated to and defaults to
right
, as with image files. To make the chart appear on the left side of the page, specifythumb=left
. - "caption" is a string of text that appears on a line just before the legend.
- option "
other
", if specified, will cause an "Other" item to appear in the legend. (don't know if I implemented that) - each "labelN" is a string of text that appears in the legend entry for a slice. Omitting it will cause a legend entry to not be shown for that slice.
- each "valueN" is the percentage that the slice represents. Do not include the percent sign. (Need to fix that) Also note that it is shown in the legend as written (just after the label), without any rounding or other reformatting.
- each "colorN" is a CSS color code or name. See Module:Plotter/DefaultColors for the default values. This can be overridden with:
- "colorset" - specifies a Module: space file to get a list of color names and values
Note that the nowiki example given at the beginning of Template:Pie chart's documentation doesn't work, but it doesn't work for that template either. If there was ever some way to specify colors with numbers like 2 or 8, I think it's been forgotten.
function "main"
[সম্পাদনা কৰক]This is a demonstrative scatter plot function that doesn't even have labels added yet.
- icon (an image to display at each point)
- iconradius (default 10 - a rough measurement of the icon's size to help position dashed lines)
- lineicon (cruddy text icon "•" - should be replaced by divs)
- lineiconradius (default 5)
- plotsizex (default 100)
- plotsizey (default 100)
- plotstep (default 10) distance between dashes
- unnamed arguments are x and y coordinates of multiple points
function "bar"
[সম্পাদনা কৰক]This produces a bar chart. It has labels but I'm still working on the axis...
local delimiter = args.delimiter or pargs.delimiter or ","
- width (default 200)
- height (default 200)
- normalize - a list of N numbers corresponding to data series 1 to N. Each number identifies which series a series should be normalized to. So "1 2 1 1 3" will plot series 1,4,5 relative to one another, but 2 and 5 will be on their own scale (with their own axis labels, eventually)
- delimiter (default comma) separates values in group1, group2, etc.
- group1, group2, group3 etc. Each is a list of numeric values separated by delimiter
- xlegend - labels for each position on the x axis separated by delimiter. It is assumed all series use the same x values.
- ylegend - labels for each series of data (group1, group2, etc.)
local p={}
function pick(a,n)
return a[n+1]
end
function loadColorSet(page)
if not(page) then page="" end
if mw.ustring.sub(page,1,7) ~= "Module:" then page="Module:Plotter/DefaultColors" end
local ct=mw.loadData(page)
if not ct then ct=mw.loadData("Module:Plotter/DefaultColors") end
local x=0
local color={}
local name={}
repeat
x=x+1
local n=ct[x*2-1]
local c=ct[x*2]
if not (n and c) then break end
table.insert(color,c)
table.insert(name,n)
until false
return color, name
end
function piechartslice(color,percent,radius,link)
radius=radius or 100
local quadrant=math.floor(percent/25)
local sin=math.floor(radius*math.sin(percent*math.pi/50))
local cos=math.floor(radius*math.cos(percent*math.pi/50))
local tan25=math.floor(-1*radius*math.cos(percent*math.pi/50)/math.sin(percent*math.pi/50))
local output,lr,lrv,tv,bw1,bw2,bw3,bw4,bd,lrB,bw2B
local a={} -- throwaway array to make value matrix more apparent
-- quadrant 1 is upper left, quadrant 2 is lower left
lr=pick({'left','right','right','left','left'},quadrant)
lrv=pick({radius,radius,radius,radius,0},quadrant)
tv=pick({radius-sin,0,radius,radius,0},quadrant)
-- border width:bw1 (top) bw2 (right) bw3 (bottom) bw4 (left)
bw1=pick({0,0,-1*sin,radius,0},quadrant)
bw2=pick({0,tan25,-1*cos,0,2*radius},quadrant)
bw3=pick({sin,radius,0,0,2*radius},quadrant)
bw4=pick({cos,0,0,tan25,0},quadrant)
bd=pick({'bottom-','right-','top-','left-',''},quadrant)
lrB=pick({'n/a','right','left','left','n/a'},quadrant)
-- right border for second div (the bottom border is radius and others are zero)
bw2B=pick({'n/a',radius,2*radius,2*radius,'n/a'},quadrant)
local output='<div style="border:solid transparent;position:absolute;width:'..radius..'px;line-height:0px;'..lr..':'..lrv..'px;top:'..tv..'px;border-width:'..bw1..'px '..bw2..'px '..bw3..'px '..bw4..'px;border-'..bd..'color:'..color..';"></div>'
if quadrant==1 or quadrant==2 or quadrant==3 then
output=output..'<div style="position:absolute;line-height:0px;border-style:solid;'..lrB..':0px;top:0px;border-width:0px '..bw2B..'px '..radius..'px 0px;border-color:'..color..';"></div>'
if quadrant==3 then
output=output.. '<div style="position:absolute;line-height:0px;border-style:solid;left:0px;top:0px;border-width:0px '..radius..'px '..2*radius..'px 0px;border-color:'..color..';"></div>'
end
end
return output
end
function p.piechart(frame)
local parent=frame.getParent(frame) or {}
local color=loadColorSet(frame.args.colorset or parent.args.colorset) or {'red','green','blue','yellow','fuchsia','aqua','brown','orange','purple','sienna'}
local value={}
local label={}
local link={}
local slicecount=0
local thumb,nowiki,radius
if parent.args then
thumb=parent.args.thumb
nowik=parent.args.nowiki
radius=parent.args.radius
end
thumb=frame.args.thumb or thumb
nowiki=frame.args.nowiki or nowiki
radius=frame.args.radius or radius or 100
radius=tonumber(radius)
if radius<1 then radius=100 end
if not(thumb) then thumb="right" end
if not(mw.ustring.match(thumb,"%S")) then thumb="right" end
for i,j in pairs(parent.args or {}) do -- I should look up if there's a way to union parent.args AND frame.args
local k=tonumber(mw.ustring.match(i,"color(%d*)"))
if k then color[k]=j
else k=tonumber(mw.ustring.match(i,"value(%d*)"))
if k then
value[k]=tonumber(j)
if k>slicecount then slicecount=k end -- not using #value to avoid randomness if some values are left out
else k=tonumber(mw.ustring.match(i,"label(%d*)"))
if k then label[k]=j
end
end
end
end
--- innermost absolute div around circle, then a second thumbcaption div around legend. Note (/div)(div) at core between circle and legend. The rest are accreted around this center.
output='<div style="position:absolute;left:0;top:0">[[File:Circle frame.svg|'..(radius*2)..'px|link=]]<Module:Plotter internal imgmap insertion token></div> </div> <!-- Legend --> <div class="thumbcaption"> '
for i,j in pairs(frame.args or {}) do -- supersede parent.args values
local k=tonumber(mw.ustring.match(i,"color(%d*)"))
if k then color[k]=j or ""
else k=tonumber(mw.ustring.match(i,"value(%d*)"))
if k then
value[k]=tonumber(j)
if k>slicecount then slicecount=k end -- not using #value to avoid randomness if some values are left out
else k=tonumber(mw.ustring.match(i,"label(%d*)"))
if k then label[k]=j or ""
else k=tonumber(mw.ustring.match(i,"link(%d*)"))
if k then link[k]=j or ""
end
end
end
end
end
local valuesum=0 -- sum of all slices
local imgmap="" -- beginning of a polygon specification for <imagemap>
for slice=1,slicecount do
if value[slice] then
if link[slice] then
-- center of the circle, NOTE coords are relative to 600 px image before scaling NOT the radius
imgmap=imgmap.."poly 300 300"
for x=valuesum,valuesum+value[slice] do
local sin=math.floor(300*math.sin(x*math.pi/50))
local cos=math.floor(300*math.cos(x*math.pi/50))
imgmap=imgmap.." "..300+cos.." "..300-sin
end
imgmap=imgmap.." [["..link[slice].."]]\n"
end
valuesum=valuesum+value[slice]
output=piechartslice(color[slice],valuesum,radius)..output.."{{legend|"..(color[slice] or "").."|"..(label[slice] or "").." ("..valuesum.."%)}}"
end
end
--- imagemap has its own absolute div to position with a separate transparent image
imgmap='<div style="position:absolute;top:0px;left:0px;width:'..2*radius..'px;height:'..2*radius..'px;z-index:1000;">\n<imagemap>\nFile:transparent600.gif|'..2*radius..'px\n'..imgmap..'desc none\n</imagemap></div>'
if #link==0 then imgmap="" end -- make sure imgmap is blank if no links
--- outer thumb tleft/tright is float/clear left or right
--- thumbinner encapsulates the graph
--- third relative div container ends in the middle of ..output..
--- next third div style "thumbcaption" begins in ..output..
--- all three end at end
output='<div class="thumb t'..thumb..'"><div class="thumbinner" style="width:'..2*radius..'px"> <!-- Graph --> <div style="background-color:white;margin:auto;position:relative;width:'..2*radius..'px;height:'..2*radius..'px;overflow:hidden;"> '..output..'{{legend|white|Other ('..tostring(math.floor((100-valuesum)*1000000)/1000000)..'%)}}</div></div></div>'
output=mw.ustring.gsub(output,"<Module:Plotter internal imgmap insertion token>", imgmap)
if nowiki then return frame.preprocess(frame,"<pre><nowiki>"..output.."</nowiki></pre>") else return frame.preprocess(frame,output) end
end
function p.main(frame)
local args=frame.args
local parent=frame.getParent(frame)
local pargs=parent.args or {}
local icon=args.icon or pargs.icon
local iconradius=args.iconradius or pargs.iconradius or 10
local lineicon=args.lineicon or pargs.lineicon or "•"
local lineiconradius=args.lineiconradius or pargs.lineiconradius or 5
local linefix=iconradius-lineiconradius
local plotsizex = args.plotsizex or pargs.plotsizex or 100
local plotsizey = args.plotsizey or pargs.plotsizey or 100
local plotstep = args.plotstep or pargs.plotstep or 10
local output = [[<div style="position:relative;border-style:solid;border-color: #0077ff;width:]] .. plotsizex+(2*iconradius) .. [[px;height:]] .. plotsizey+(2*iconradius) .. [[px;">]]
if (args[2] or pargs[2]) ~= nil then
local x=(args[1] or pargs[1])+0
local y=(args[2] or pargs[2])+0
local xmin = x
local xmax = x
local ymin = y
local ymax = y
local index = 3
while (args[index+1] or pargs[index+1]) ~= nil do
local x=(args[index]+0 or pargs[index]+0)
local y=(args[index+1]+0 or pargs[index+1]+0)
if (x < xmin) then xmin = x end
if (x > xmax) then xmax = x end
if (y < ymin) then ymin = y end
if (y > ymax) then ymax = y end
index = index + 2
end
local lastx=0
local lasty=0
if args[2] ~= nil then
local x=(args[1] or pargs[1])+0
local y=(args[2] or pargs[2])+0
local plotx=math.floor(plotsizex*(x-xmin)/(xmax-xmin))
local ploty=math.floor((plotsizey-plotsizey*(y-ymin)/(ymax-ymin)))
output = output .. [[<span style="position:absolute;left:]] .. plotx .. [[px; top:]] .. ploty .. [[px;">]] .. icon .. "</span>"
lastx = plotx
lasty = ploty
end
index = 3
while (args[index+1] or pargs[index+1]) ~= nil do
local x=(args[index] or pargs[index])+0
local y=(args[index+1] or pargs[index+1])+0
local plotx=math.floor(plotsizex*(x-xmin)/(xmax-xmin))
local ploty=math.floor((plotsizey-plotsizey*(y-ymin)/(ymax-ymin)))
if plotstep+0 ~= 0 then
local delx=plotx-lastx
local dely=ploty-lasty
plotdist=math.sqrt(delx*delx+dely*dely)
plotparm=plotdist-iconradius-plotstep/2
while plotparm>iconradius+lineiconradius+plotstep/2 do
output = output .. [[<span style="position:absolute;left:]] .. lastx+linefix+math.floor(delx*(plotparm/plotdist)) .. [[px; top:]] .. lasty+linefix+math.floor(dely*(plotparm/plotdist)) .. [[px;">]] .. lineicon .. "</span>"
plotparm = plotparm - plotstep
end
lastx = plotx
lasty = ploty
end
output = output .. [[<span style="position:absolute;left:]] .. plotx .. [[px; top:]] .. ploty .. [[px;">]] .. icon .. "</span>"
index = index + 2
end
else output = "error"
end
output = output .. "</div>"
return output
end
-- data structure is
-- data[y][x].value
-- maxyval[y]
-- data[y].color
-- data[y].legend
-- data.legend[x]
function p.bar(frame)
local debuglog=""
local args=frame.args
local parent=frame.getParent(frame)
local pargs=parent.args or {}
local delimiter = args.delimiter or pargs.delimiter or ","
local width = args.width or pargs.width or 200
local height = args.height or pargs.height or 200
---- Set up the table of "norms". Series 1 to N normalize to (%d+)
local normalize = args.normalize or pargs.normalize or ""
local prowl=mw.ustring.gmatch(normalize,"(%d+)")
norm={}
local ngroup={} -- ngroup[yseries] identifies an index for ymax
local nngroup=0 -- the current maximum ngroup assigned
repeat
local t=prowl()
if not(t) then break end
t=tonumber(t)
table.insert(norm,t)
until false
--- import the actual data in group1 .. groupN
local yseries=0;local x=0
local data={} -- main data storage array
local maxy=0;local maxx=0; local maxyval={}; local minyval={} -- keeping these out of the data array after being driven half mad giving them cutesy names in the array!
repeat
yseries=yseries+1
data[yseries]={}
--- pull in the "groupN" data (delimited) --> text
local text=args["group"..yseries] -- each _group_ is a group of x-values in a y-series
if not (text) then maxy=yseries-1 break end
---- pull in the originN=some number
data[yseries].origin=args["origin"..yseries] or 0
data[yseries].origin=tonumber(data[yseries].origin)
data[yseries].max=data[yseries].origin;data[yseries].min=data[yseries].origin
debuglog=debuglog.."I"..yseries..tostring(norm[yseries])
--- set ngroup[yseries] to whatever its norm points at, or new
if norm[yseries]
then if ngroup[norm[yseries]]
then ngroup[yseries]=ngroup[norm[yseries]]
else nngroup=nngroup+1
ngroup[yseries]=nngroup
end
else ngroup[yseries]=1 -- if no norm specified, just dump to the first series group
end
---- pull in the actual values
prowl=mw.ustring.gmatch(text,"([^" .. delimiter .. "]+)")
x=0
repeat
x=x+1
data[yseries][x]={}
data[yseries][x].value=prowl()
debuglog=debuglog.."V"..x..yseries..tostring(data[yseries][x].value)
if not(data[yseries][x].value) then if x>maxx then maxx = x-1 end; break end
data[yseries][x].value=tonumber(data[yseries][x].value)
if data[yseries].max then if data[yseries][x].value>data[yseries].max then data[yseries].max=data[yseries][x].value end else data[yseries].max=data[yseries][x].value end
if data[yseries].min then if data[yseries][x].value<data[yseries].min then data[yseries].min=data[yseries][x].value end else data[yseries].min=data[yseries][x].value end
until false
---- pull in the colorN="whatever"
data[yseries].color=args["color"..yseries] or "" -- one color for yseries group; can be nil
if data[yseries].color=="" then data[yseries].color="black" end
until false
--- import the xlegends for each group
prowl=mw.ustring.gmatch(args.xlegend,"[^" .. delimiter .. "]+")
x=0
data.legend={} -- for x legends, y="legend"
repeat
x=x+1
data.legend[x]=prowl()
if not (data.legend[x]) then break end
data.legend[x]=data.legend[x]
until false
--- import the ylegends for each group
prowl=mw.ustring.gmatch(args.ylegend,"[^" .. delimiter .. "]+")
yseries=0
repeat
yseries=yseries+1
data[yseries].legend=prowl()
until not (data[yseries].legend)
-- set the maxval[ngroup[(each series)]] = data[(any series in ngroup).max
yseries=0
repeat
yseries=yseries+1
if not(data[yseries].max) then break end
debuglog=debuglog..tostring(yseries)..":"..tostring(ngroup[yseries]) .. ">"..tostring(data[yseries].max)
if maxyval[ngroup[yseries]]
then if data[yseries].max>maxyval[ngroup[yseries]]
then maxyval[ngroup[yseries]]=data[yseries].max
end
else maxyval[ngroup[yseries]]=data[yseries].max;debuglog=debuglog.."A"..tostring(data[yseries].max)..tostring(data[yseries].min)
end
if minyval[ngroup[yseries]]
then if data[yseries].min<minyval[ngroup[yseries]]
then minyval[ngroup[yseries]]=data[yseries].min
end
else minyval[ngroup[yseries]]=data[yseries].min;debuglog=debuglog.."A"..tostring(data[yseries].min)
end
until false
--- Draw the output
local output = [[<div style="position:relative;border-style:solid;border-color: #0077ff;width:]] .. width .. [[px;height:]] .. height .. [[px;">]]
local output='<div style="position:relative;overflow:visible;border-style:solid;border-color: #0077ff;width:' .. width .. 'px;height:' .. height .. 'px;">'
local topreserve=20*(maxy)
local bottomreserve=20
local leftreserve=20
local rightreserve=20
local reducedheight=height-topreserve-bottomreserve
local reducedwidth=width-leftreserve-rightreserve
local ew=math.floor(reducedwidth/((maxx)*(maxy+1)))
for y = 1,maxy do
for x = 1, maxx do
debuglog=debuglog..y..x..tostring(ngroup[y])..tostring(data[y][x]) .. tostring(maxyval[ngroup[y]])..tostring(minyval[ngroup[y]])
if data[y][x] and maxyval[ngroup[y]]
then local pw=(data[y][x].value-data[y].origin)/(maxyval[ngroup[y]]-minyval[ngroup[y]]) -- proportion of value to the max value for that y-series
local po=(data[y].origin-minyval[ngroup[y]])/(maxyval[ngroup[y]]-minyval[ngroup[y]])
local eh=math.floor(pw*reducedheight)
local et=topreserve+math.floor(reducedheight - eh - po*reducedheight)
if eh<0 then eh=-1*eh;et=et-eh end -- pw can be negative; plot "backwards" looks the same
local el=leftreserve+math.floor(((x-1)*(maxy+1) + (y-1) + 0.5)*ew)
output=output..'<div style="position:absolute;background-color:' .. data[y].color .. ';width:' .. ew .. 'px;height:' .. eh .. 'px;top:' .. et .. 'px;left:' .. el .. 'px;"></div>'
end -- if data[y][x] and maxval[ngroup[y]]
end -- for x = 1, maxx
end -- for y=1,maxy
---- draw the ylegends
for x = 1,maxx do
output=output .. '<span style="position:absolute;top:'.. reducedheight+topreserve .. 'px;left:'..leftreserve+math.floor(( (x-1)*(maxy+1)+(maxx/2) )*ew)..'px;">'..data.legend[x]..'</span>'
end
for y = 1,maxy do
output=output .. '<span style="position:absolute;color:'.. data[y].color .. ';top:' .. (y-1)*20 .. 'px;left:'.. (leftreserve+10) ..'px;">'.. (data[y].legend or "") ..'</span>'
local point={minyval[ngroup[y]],data[y].origin,maxyval[ngroup[y]]}
for i,j in ipairs(point) do
local po=(j-minyval[ngroup[y]])/(maxyval[ngroup[y]]-minyval[ngroup[y]])
local et=topreserve+math.floor((1-po)*reducedheight)
debuglog=debuglog.."pass" .. y .. ngroup[y]
if tonumber(ngroup[y])==1
then debuglog=debuglog.."left";output=output .. '<span style="position:absolute;color:'.. data[y].color .. ';'..data[y].color .. ';text-align:right;top:' .. et-10 .. 'px;width:'..leftreserve..'px;left:0px;">'.. j .. '</span>'
else debuglog=debuglog.."right";output=output .. '<span style="position:absolute;color:'.. data[y].color .. ';'..data[y].color .. ';text-align:left;top:' .. et-10 .. 'px;width:'..rightreserve..'px;left:'..leftreserve+reducedwidth..'px;">'.. j .. '</span>'
end
end
end
debuglog=debuglog..tostring(maxyval[1])..tostring(maxyval[2])..tostring(maxyval[3])..tostring(data[1].max)..tostring(data[2].max)..tostring(data[3].max)..tostring(minyval[1])..tostring(minyval[2])..tostring(minyval[3])..tostring(data[1].min)..tostring(data[2].min)..tostring(data[3].min)..data.legend[1]..data.legend[2]..data.legend[3]
output = output .. "</div>\n"
if (args.debug or pargs.debug) then output=output..debuglog end
return output
end
return p