Vai al contenuto

Modulo:Graph/sandbox

Da Wikipedia, l'enciclopedia libera.
Versione del 7 lug 2015 alle 06:39 di Moroboshi (discussione | contributi) (Nuova pagina: local p = {} local cfg = mw.loadData( 'Modulo:Chart/sandbox/Configurazione' ); local function dump(t, ...) local args = {...} for _, s in ipairs(args) do...)
(diff) ← Versione meno recente | Versione attuale (diff) | Versione più recente → (diff)
local p = {}

local cfg = mw.loadData( 'Modulo:Chart/sandbox/Configurazione' );

local function dump(t, ...)
    local args = {...}
    for _, s in ipairs(args) do
        table.insert(t, s)
    end
end

-- ===============================================================================
-- Costruisce una struttura dati da codificare in json per realizzare un grafico
-- a torta visualizzabile utilizzando il tag graph
-- https://www.mediawiki.org/wiki/Extension:Graph
-- i nomi dei parametri sono localizzati in base a chart/Config
--
-- ===============================================================================
function build_pie_chart_json(data, args)
  local graph = {
        name = args[cfg.localization.name] or 'grafico a torta',
        data = {
            {  
                name = "table",
                values = data,
                transform = { { type = "pie", value = "data.x" } }
            }
        },
        marks = {
            {
                type = "arc",
                from = { data = "table"},
                properties = {
                    enter = {
                        x = { field = "data.x", group = "width", mult = 0.5 },
                        y = { field = "data.x", group = "height", mult = 0.5 },
                        startAngle = {field = "startAngle"},
                        endAngle = {field = "endAngle"},
                        innerRadius = {value = 0},
                        outerRadius = {value = tonumber(args[cfg.localization.radius]) or cfg.radius_default },
                        stroke = {value = "#fff"},                       
                    },
                    update = { fill = { field = "data.color"} },
                    hover = { fill = {value = "pink"} }
                },
             }
        }
    }
    if args[cfg.localization.show_label] then
        graph.marks[#graph.marks+1] = {type = "text",
                from = { data = "table"},
                properties = {  
                    enter = {
                        x = { field = "data.x", group = "width", mult = 0.5 },
                        y = { field = "data.x", group = "height", mult = 0.5 },
                        radius = { value = tonumber(args[cfg.localization.radius]) or cfg.radius_default, offset = 8 },
                        theta = { field = "midAngle"},
                        fill = { value = "#000" },
                        align = { value = "center" },
                        baseline = { value = "middle" },
                        text = { field = "data.label"}
                    }
                }
            }
    end
    return graph
end


local function legend_item(color, text)
    local item = mw.html.create('p'):cssText('margin:0px;font-size:100%;text-align:left')
    item:tag('span'):cssText(string.format('border:none;background-color:%s;color:%s;', color, color)):wikitext("██")
    item:wikitext(string.format(" %s", text))
    return item
end

local function build_legend(data, args)
    local legend = mw.html.create('div'):addClass('thumbcaption')
    legend:wikitext(args[cfg.localization.caption] or '')
    for _,datum in ipairs(data) do
        legend:node(legend_item(datum.color, mw.ustring.format('%s (%s %%)', datum.label, mw.getContentLanguage():formatNum(datum.x))))
    end
    return legend
end


function p.pie_chart(frame)
    args = require('Modulo:Arguments').getArgs(frame)
    local value_string = cfg.localization.value
    local label_string = cfg.localization.label
    local color_string = cfg.localization.color
    local index = 1
    local data = {}
    local colors = {}
    local labels = {}
    while true do
        local index_s = tostring(index)
        local value = tonumber(args[value_string .. index_s])
        if  value then
            data[index] = {
                x = value,
                label = args[label_string .. index_s] or '',
                color = args[color_string .. index_s] or cfg.default_colors[index] or cfg.default_color
            }
            index = index + 1
        else
            break
        end
    end
    -- Se è definito 'other' assumo che sia calcolato su base %, calcolo il suo valore e l'aggiungo alla tabella dati
    if args[cfg.localization.other] then
        local total = 0
        for _,datum in ipairs(data) do
            total = total + datum.x
        end
        local other_value = math.max(0, 100 - total)
        data[index] = {
            x = other_value,
            label = args[cfg.localization.label_other] or 'altri',
            color = args[cfg.localization.color_other] or cfg.default_colors[index] or cfg.default_color
        }
    end
    if #data == 0 then
        return ''
    end
    local graph = build_pie_chart_json(data, args)
    local json
    -- se il parametro debug_json è stato valorizzato ritorna il codice json generato invece di generare il grafico
    if args[cfg.localization.debug_json] then
        return  frame:extensionTag('syntaxhighlight', mw.text.jsonEncode(graph, mw.text.JSON_PRETTY), {lang='json'})
    end
    local legend = build_legend(data, args)
    local html = mw.html.create('div')
        :addClass(string.format('thumb t%s', args[cfg.localization.thumb] or 'right'))     
    html:tag('div')
        :addClass('thumbinner')
        :cssText('width:202px')
        :tag('div')
            :cssText("background-color:white;margin:auto;position:relative;width:200px;height:200px;overflow:hidden")
            :wikitext(frame:extensionTag( 'graph', mw.text.jsonEncode(graph) ))
            :done()
        :node(legend)
    return html
end

function p._chart(args)
    
    -- chart width
    local graphwidth = tonumber(args[cfg.localization.width])
    -- chart height
    local graphheight = tonumber(args[cfg.localization.height])
    -- chart type
    local type = args[cfg.localizatio.type] or "line"
    -- interpolation mode: linear, step-before, step-after, basis, basis-open, basis-closed (type=line only), bundle (type=line only), cardinal, cardinal-open, cardinal-closed (type=line only), monotone
    local interpolate = args[cfg.localization.interpolate]
    -- mark colors (if no colors are given, the default 10 color palette is used)
    local colors = stringArray(args[cfg.colors]) or "category10"
    -- x and y axis caption
    local xTitle = args[cfg.localizazion.xAxisTitle]
    local yTitle = args[cfg.localizazion.yAxisTitle]
    -- override x and y axis minimum and maximum
    local xMin = tonumber(args[cfg.localizazion.xAxisMin])
    local xMax = tonumber(args[cfg.localizazion.xAxisMax])
    local yMin = tonumber(args[cfg.localizazion.yAxisMin])
    local yMax = tonumber(args[cfg.localizazion.yAxisMax])
    -- override x and y axis label formatting
    local xFormat = args[cfg.localizazion.xAxisFormat]
    local yFormat = args[cfg.localizazion.yAxisFormat]
    -- show legend, optionally caption
    local legend = args[cfg.localizazion.legend]
    -- format JSON output
    local formatJSON = args[cfg.localizazion.formatjson]
 
    -- get x values
    local x = numericArray(frame.args.x)
 
    -- get y values (series)
    local y = {}
    local index = 1
    local seriesTitles = {}
    if args[y] then
        y[1] = NumericArray(value)
        seriesTitles[1] = args[string.gsub(cfg.localization.yTitle, '#', '')] or args[string.gsub(cfg.localization.yTitle, '1', '')] or "y"
        index = 2
    end
    while true do
        if args[y..index] then
            y[#y+1] = numericArray(args[y..index])
            seriesTitles[#seriesTitles+1] = args[string.gsub(cfg.localization.yTitle, index, '')] or (y .. index)    
        end
    end
    -- create data tuples, consisting of series index, x value, y value
    local data = { name = "chart", values = {} }
    for i = 1, #y do
        for j = 1, #x do
            if j <= #y[i] then data.values[#data.values + 1] = { series = seriesTitles[i], x = x[j], y = y[i][j] } end
        end
    end
 
    -- use stacked charts
    local stacked = false
    local stats
    if string.sub(type, 1, 7) == "stacked" then
        type = string.sub(type, 8)
        if #y > 1 then -- ignore stacked charts if there is only one series
            stacked = true
            -- calculate statistics of data as stacking requires cumulative y values
            stats =
            {
                name = "stats", source = "chart", transform =
                {
                    { type = "facet", keys = { "data.x" } },
                    { type = "stats", value = "data.y" }
                }
        }
        end
    end
 
    -- create scales
    local xscale =
    {
        name = "x",
        type = "linear",
        range = "width",
        zero = false, -- do not include zero value
        nice = true,  -- force round numbers for y scale
        domain = { data = "chart", field = "data.x" }
    }
    if xMin then xscale.domainMin = xMin end
    if xMax then xscale.domainMax = xMax end
    if xMin or xMax then xscale.clamp = true end
    if type == "rect" then xscale.type = "ordinal" end
 
    local yscale =
    {
        name = "y",
        type = "linear",
        range = "height",
        -- area charts have the lower boundary of their filling at y=0 (see marks.properties.enter.y2), therefore these need to start at zero
        zero = type ~= "line",
        nice = true
    }
    if yMin then yscale.domainMin = yMin end
    if yMax then yscale.domainMax = yMax end
    if yMin or yMax then yscale.clamp = true end
    if stacked then
        yscale.domain = { data = "stats", field = "sum"  }
    else
        yscale.domain = { data = "chart", field = "data.y" }
    end
    local colorScale =
    {
        name = "color",
        type = "ordinal",
        range = colors
    }
    local alphaScale
    -- if there is at least one color in the format "#aarrggbb", create a transparency (alpha) scale
    if isTable(colors) then
        local alphas = {}
        local hasAlpha = false
        for i = 1, #colors do
            local a, rgb = string.match(colors[i], "#(%x%x)(%x%x%x%x%x%x)")
            if a then
                hasAlpha = true
                alphas[i] = tostring(tonumber(a, 16) / 255.0)
                colors[i] = "#" .. rgb
            else
                alphas[i] = "1"
            end
        end
        for i = #colors + 1, #y do alphas[i] = "1" end
        if hasAlpha then alphaScale = { name = "transparency", type = "ordinal", range = alphas } end
    end
    -- for bar charts with multiple series: each series is grouped by the x value, therefore the series need their own scale within each x group
    local groupScale
    if type == "rect" and not stacked and #y > 1 then
        groupScale =
        {
            name = "series",
            type = "ordinal",
            range = "width",
            domain = { field = "data.series" }
        }
        xscale.padding = 0.2 -- pad each bar group
    end
 
    -- decide if lines (strokes) or areas (fills) should be drawn
    local colorField
    if type == "line" then colorField = "stroke" else colorField = "fill" end
 
    -- create chart markings
    local marks =
    {
        type = type,
        properties =
        {
            -- chart creation event handler
            enter =
            {
                x = { scale = "x", field = "data.x" },
                y = { scale = "y", field = "data.y" }
            },
            -- chart update event handler
            update = { },
            -- chart hover event handler
            hover = { }
        }
    }
    marks.properties.update[colorField] = { scale = "color" }
    marks.properties.hover[colorField] = { value = "red" }
    if alphaScale then marks.properties.update[colorField .. "Opacity"] = { scale = "transparency" } end
    -- for bars and area charts set the lower bound of their areas
    if type == "rect" or type == "area" then
        if stacked then
            -- for stacked charts this lower bound is cumulative/stacking
            marks.properties.enter.y2 = { scale = "y", field = "y2" }
        else
            --[[
            for non-stacking charts the lower bound is y=0
            TODO: "yscale.zero" is currently set to "true" for this case, but "false" for all other cases.
            For the similar behavior "y2" should actually be set to where y axis crosses the x axis,
            if there are only positive or negative values in the data ]]
            marks.properties.enter.y2 = { scale = "y", value = 0 }
        end
    end
    -- for bar charts ...
    if type == "rect" then
        -- set 1 pixel width between the bars
        marks.properties.enter.width = { scale = "x", band = true, offset = -1 }
        -- for multiple series the bar marking need to use the "inner" series scale, whereas the "outer" x scale is used by the grouping
        if not stacked and #y > 1 then
            marks.properties.enter.x.scale = "series"
            marks.properties.enter.x.field = "data.series"
            marks.properties.enter.width.scale = "series"
        end
    end
    -- stacked charts have their own (stacked) y values
    if stacked then marks.properties.enter.y.field = "y" end
 
    -- set interpolation mode
    if interpolate then marks.properties.enter.interpolate = { value = interpolate } end
 
    if #y == 1 then marks.from = { data = "chart" } else
        -- if there are multiple series, connect colors to series
        marks.properties.update[colorField].field = "data.series"
        if alphaScale then marks.properties.update[colorField .. "Opacity"].field = "data.series" end
        -- apply a grouping (facetting) transformation
        marks =
        {
            type = "group",
            marks = { marks },
            from =
            {
                data = "chart",
                transform =
                {
                    {
                        type = "facet",
                        keys = { "data.series" }
                    }
                }
            }
        }
        -- for stacked charts apply a stacking transformation
        if stacked then
            marks.from.transform[2] = { type = "stack", point = "data.x", height = "data.y" }
        else
            -- for bar charts the series are side-by-side grouped by x
            if type == "rect" then
                marks.from.transform[1].keys = "data.x"
                marks.scales = { groupScale }
                marks.properties = { enter = { x = { field = "key", scale = "x" }, width = { scale = "x", band = true } } }
            end
        end
    end
 
    -- create legend
    if legend then
        legend =
        {
            {
                fill = "color",
                stroke = "color",
                title = legend
            }
        }
    end
 
    -- construct final output object
    local output =
    {
        width = graphwidth,
        height = graphheight,
        data = { data, stats },
        scales = { xscale, yscale, colorScale, alphaScale },
        axes =
        {
            {
                type = "x",
                scale = "x",
                title = xTitle,
                format = xFormat
            },
            {
                type = "y",
                scale = "y",
                title = yTitle,
                format = yFormat
            }
        },
        marks = { marks },
        legends = legend
    }
 
    local flags
    if args[cfg.localization.debug_json] then flags = mw.text.JSON_PRETTY end
    return mw.text.jsonEncode(output, flags)
end

function p.chart(frame)
    local args = getArgs(frame, {parentOnly = true})
    local chart = p._chart(args)
    if args[cfg.localization.debug_json] then
        return  frame:extensionTag('syntaxhighlight', mw.text.jsonEncode(graph, mw.text.JSON_PRETTY), {lang='json'})
    end
    return frame:extensionTag('graph', chart)
end

return p