Mòdul:Plotter

De la Viquipèdia, l'enciclopèdia lliure
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 class="transborder" style="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