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.

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, specify thumb=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.

  One (1%)
  Two (33%)
  Three (45%)
  Four (48%)
  Five (50%)
  Six (51%)
   (54%)
   (61%)
   (63%)
   (66%)
   (67%)
   (71%)
   (78%)
   (79%)
   (80%)
   (83%)
   (86%)
   (89%)
   (91%)
  Other (9%)
  Atheists and agnostics (59%)
  Catholics (85.8%)
  Protestants (88.3%)
  Other (11.7%)

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

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