Module:BubbleGraph

From IdleOn MMO Wiki
Revision as of 19:42, 31 December 2024 by BHY4A (talk | contribs) (Reverted edits by BHY4A (talk) to last revision by Butt Toast)

Documentation for this module may be created at Module:BubbleGraph/doc

local BubbleGraph = {}

local DEFAULTS = {
    MAX_LEVELS = 1000,
    STEP = 20,
    GRAPH_WIDTH = 1000,
    GRAPH_HEIGHT = 250,
    DECIMAL_PLACES = 3,
    FONT_FAMILY = "Idleon",
    COLUMN_MIN_WIDTH = 2,
    MAJOR_GRID_LINES = 10,
    SHOW_GRID = true
}

local function round(num, places)
    if not num then
        return 0
    end
    local mult = 10 ^ (places or 0)
    return math.floor(num * mult + 0.5) / mult
end

local function calculate_threshold_level(x2)
    return x2 * 90 / (100 - 90)
end

local bubble_colors = {
    orange = {
        column_start = "#ffe787ff",
        column_end = "#690e0eff",
        background_start = "#ffffffcc",
        background_end = "#ef7500cc",
        image = "OrangeBubble"
    },
    green = {
        column_start = "#bfffabff",
        column_end = "#09591aff",
        background_start = "#f7f7ffcc",
        background_end = "#3fe855cc",
        image = "GreenBubble"
    },
    purple = {
        column_start = "#fcc1ffff",
        column_end = "#350b6aff",
        background_start = "#fffdfacc",
        background_end = "#ca51eecc",
        image = "PurpleBubble"
    },
    yellow = {
        column_start = "#f7ffbdff",
        column_end = "#714200ff",
        background_start = "#f7fffacc",
        background_end = "#ecc200cc",
        image = "YellowBubble"
    },
    other = {
        column_start = "#a94bc1ff",
        column_end = "#7e38aaff",
        background_start = "#ffcffacc",
        background_end = "#ee75ffcc"
    }
}

BubbleGraph.page_color_mapping = {
    patterns = {
        ["Alchemy/Power_Cauldron/Chart"] = "orange",
        ["Alchemy/Quicc_Cauldron/Chart"] = "green",
        ["Alchemy/High%-Iq_Cauldron/Chart"] = "purple",
        ["Alchemy/Kazam_Cauldron/Chart"] = "yellow"
    },
    getColor = function(page_name)
        for pattern, color in pairs(BubbleGraph.page_color_mapping.patterns) do
            if string.match(page_name, pattern) then
                return color
            end
        end
        return "other"
    end
}

function BubbleGraph._render_grid(container, width, height, major_lines)
    local grid =
        mw.html.create("div"):addClass("bubble-graph-grid"):css(
        {
            position = "absolute",
            top = "0",
            left = "0",
            width = width .. "px",
            height = height .. "px",
            ["pointer-events"] = "none",
            ["z-index"] = "1",
            ["background-image"] = string.format(
                "linear-gradient(to right, rgba(0,0,0,0.1) 1px, transparent 1px), " ..
                    "linear-gradient(to bottom, rgba(0,0,0,0.1) 1px, transparent 1px)",
                width / major_lines,
                height / major_lines
            ),
            ["background-size"] = string.format("%dpx 100%%, 100%% %dpx", width / major_lines, height / major_lines)
        }
    )

    for i = 0, major_lines do
        local position = (i / major_lines) * 100

        grid:tag("div"):css(
            {
                position = "absolute",
                left = position .. "%",
                top = "0",
                height = "100%",
                width = "1px",
                ["background-color"] = "rgba(0,0,0,0.2)"
            }
        )

        grid:tag("div"):css(
            {
                position = "absolute",
                top = position .. "%",
                left = "0",
                width = "100%",
                height = "1px",
                ["background-color"] = "rgba(0,0,0,0.2)"
            }
        )
    end

    return grid
end

function BubbleGraph._interpolate_color(color1, color2, ratio)
    local function hex_to_rgb(hex)
        local r = tonumber(hex:sub(2, 3), 16)
        local g = tonumber(hex:sub(4, 5), 16)
        local b = tonumber(hex:sub(6, 7), 16)
        local a = tonumber(hex:sub(8, 9) or "ff", 16)
        return {r, g, b, a}
    end

    local rgb1, rgb2 = hex_to_rgb(color1), hex_to_rgb(color2)
    local interpolated_rgb = {}

    for i = 1, 4 do
        interpolated_rgb[i] = math.floor(rgb1[i] + ratio * (rgb2[i] - rgb1[i]))
    end

    local result =
        string.format(
        "#%02x%02x%02x%02x",
        interpolated_rgb[1],
        interpolated_rgb[2],
        interpolated_rgb[3],
        interpolated_rgb[4]
    )

    return result
end

local bonus_calculations = {
    add = function(level, x1, x2)
        if not x1 or not x2 then
            return 0
        end
        return x2 ~= 0 and (((x1 + x2) / x2 + 0.5 * (level - 1)) / (x1 / x2)) * level * x1 or level * x1
    end,
    decay = function(level, x1, x2)
        if not x1 or not x2 then
            return 0
        end
        return (level * x1) / (level + x2)
    end,
    intervalAdd = function(level, x1, x2)
        if not x1 or not x2 then
            return 0
        end
        return x1 + math.floor(level / x2)
    end,
    decayMulti = function(level, x1, x2)
        if not x1 or not x2 then
            return 0
        end
        return 1 + (level * x1) / (level + x2)
    end,
    bigBase = function(level, x1, x2)
        if not x1 or not x2 then
            return 0
        end
        return x1 + x2 * level
    end,
    addLower = function(level, x1, x2)
        if not x1 or not x2 then
            return 0
        end
        return x1 + x2 * (level + 1)
    end,
    decayLower = function(level, x1, x2)
        if not x1 or not x2 then
            return 0
        end
        return x1 * (level + 1) / (level + 1 + x2) - x1 * level / (level + x2)
    end,
    decayMultiLower = function(level, x1, x2)
        if not x1 or not x2 then
            return 0
        end
        return x1 * (level + 1) / (level + 1 + x2) - x1 * level / (level + x2)
    end,
    bigBaseLower = function(_, _, x2)
        return x2 or 0
    end,
    intervalAddLower = function(level, _, x2)
        if not x2 then
            return 0
        end
        return math.max(math.floor((level + 1) / x2), 0) - math.max(math.floor(level / x2), 0)
    end,
    reduce = function(level, x1, x2)
        if not x1 or not x2 then
            return 0
        end
        return x1 - x2 * level
    end,
    reduceLower = function(level, x1, x2)
        if not x1 or not x2 then
            return 0
        end
        return x1 - x2 * (level + 1)
    end
}

local function calculate_utility(level, x1, x2, func)
    local actual_max_level = max_levels or 1000

    if not x1 or not x2 then
        return 0
    end

    if func == "decayMulti" then
        local current = 1 + (level * x1) / (level + x2)
        local limit = 1 + x1
        local base = 1
        return ((current - base) / (limit - base)) * 100
    elseif func == "decay" then
        local current = (level * x1) / (level + x2)
        local limit = x1
        return (current / limit) * 100
    elseif func == "add" then
        local current = x2 ~= 0 and (((x1 + x2) / x2 + 0.5 * (level - 1)) / (x1 / x2)) * level * x1 or level * x1
        local max_value =
            x2 ~= 0 and (((x1 + x2) / x2 + 0.5 * (actual_max_level - 1)) / (x1 / x2)) * actual_max_level * x1 or
            actual_max_level * x1
        return (current / max_value) * 100
    elseif func == "intervalAdd" then
        local current = x1 + math.floor(level / x2)
        local max_intervals = math.floor(1000 / x2)
        local max_value = x1 + max_intervals
        return (current / max_value) * 100
    elseif func == "bigBase" then
        local current = x1 + x2 * level
        local max_value = x1 + x2 * 1000
        return (current / max_value) * 100
    elseif func == "addLower" then
        local current = x1 + x2 * (level + 1)
        local max_value = x1 + x2 * (1000 + 1)
        return (current / max_value) * 100
    elseif func == "decayLower" or func == "decayMultiLower" then
        local current = x1 * (level + 1) / (level + 1 + x2) - x1 * level / (level + x2)
        local limit = x1 / x2
        return (current / limit) * 100
    elseif func == "bigBaseLower" then
        return 100
    elseif func == "intervalAddLower" then
        local current = math.max(math.floor((level + 1) / x2), 0) - math.max(math.floor(level / x2), 0)
        local max_intervals = math.max(math.floor(1001 / x2), 0) - math.max(math.floor(1000 / x2), 0)
        return current > 0 and 100 or 0
    elseif func == "reduce" then
        local current = x1 - x2 * level
        local min_value = x1 - x2 * 1000
        local max_value = x1
        return 100 - ((current - min_value) / (max_value - min_value)) * 100
    elseif func == "reduceLower" then
        local current = x1 - x2 * (level + 1)
        local min_value = x1 - x2 * 1001
        local max_value = x1 - x2
        return 100 - ((current - min_value) / (max_value - min_value)) * 100
    end

    return 0
end

function BubbleGraph.calculate_bonus(func, level, x1, x2)
    local calcFunc = bonus_calculations[func] or function()
            return 0
        end
    local result = round(calcFunc(level, x1, x2), DEFAULTS.DECIMAL_PLACES)
    return result
end

local function round_format(num)
    if not num then
        return "0"
    end
    local str = tostring(num)
    return str:find("%.") and str:gsub("0+$", ""):gsub("%.$", "") or str
end

local function calculate_efficiency_threshold_level(x1, x2, func, target_efficiency)
    local min_level = 1
    local max_level = 100000
    local tolerance = 0.0001

    while min_level < max_level do
        local level = math.floor((min_level + max_level) / 2)
        local utility = calculate_utility(level, x1, x2, func)

        if math.abs(utility - target_efficiency) < tolerance then
            return level
        elseif utility < target_efficiency then
            min_level = level + 1
        else
            max_level = level - 1
        end
    end

    return min_level
end

local function generate_graph_points(x1, x2, func, max_levels, step, options)
    local max_bonus = 0
    local min_level = math.max(1, options.min_levels or 1)
    local display_levels = {}
    local threshold_level = math.floor(calculate_threshold_level(x2))

    local base_level = min_level - (min_level % 100)
    if min_level % 100 ~= 0 then
        base_level = base_level + 100
    end

    table.insert(display_levels, min_level)

    for level = base_level, max_levels, 100 do
        if level > min_level and level <= max_levels then
            table.insert(display_levels, level)
        end
    end

    if max_levels % 100 ~= 0 then
        table.insert(display_levels, max_levels)
    end

    local threshold_bonus = BubbleGraph.calculate_bonus(func, threshold_level, x1, x2)
    local threshold_aligned = (threshold_level % step) == 0
    local points_with_threshold = {}

    local threshold_in_range = threshold_level >= min_level and threshold_level <= max_levels

    for level = min_level - (min_level % step), max_levels, step do
        if level >= min_level then
            local actual_level = level
            local raw_bonus = BubbleGraph.calculate_bonus(func, actual_level, x1, x2)

            local utility_ratio = calculate_utility(actual_level, x1, x2, func) / 100

            local display_bonus
            if actual_level <= threshold_level then
                display_bonus = raw_bonus
            else
                display_bonus = threshold_bonus * (threshold_level / actual_level)
            end

            local point = {
                level = actual_level,
                bonus = display_bonus,
                real_bonus = raw_bonus,
                formatted_bonus = round_format(round(raw_bonus, DEFAULTS.DECIMAL_PLACES)),
                show_label = false,
                is_threshold = math.abs(actual_level - threshold_level) < step / 2,
                utility_ratio = utility_ratio
            }

            for _, display_level in ipairs(display_levels) do
                if actual_level == display_level then
                    point.show_label = true
                    break
                end
            end

            table.insert(points_with_threshold, point)
            max_bonus = math.max(max_bonus, display_bonus)

            if not threshold_aligned and actual_level < threshold_level and (actual_level + step) > threshold_level then
                local threshold_point = {
                    level = threshold_level,
                    bonus = threshold_bonus,
                    real_bonus = threshold_bonus,
                    formatted_bonus = round_format(round(threshold_bonus, DEFAULTS.DECIMAL_PLACES)),
                    show_label = true,
                    is_threshold = true,
                    utility_ratio = calculate_utility(threshold_level, x1, x2, func) / 100
                }
                table.insert(points_with_threshold, threshold_point)
                max_bonus = math.max(max_bonus, threshold_bonus)
            end
        end
    end

    table.sort(
        points_with_threshold,
        function(a, b)
            return a.level < b.level
        end
    )

    return points_with_threshold, max_bonus, threshold_level
end

local function format_description(description, bonus)
    return description:gsub(
        "{",
        '<span class="bonus-value" style="color: rgba(255,0,0,1); text-shadow: -.065em 0 rgba(255,0,0,0.5), 0 .065em rgba(255,0,0,0.5), .065em 0 rgba(255,0,0,0.5), 0 -.065em rgba(255,0,0,0.5); font-size: 1.2em;">' ..
            bonus .. "</span>"
    )
end

local function generate_graph_points_efficiency(x1, x2, func, options)
    local points = {}
    local max_bonus = 0
    local prev_efficiency = 0

    local efficiencies = {}
    for eff = 2, 98, 2 do
        table.insert(efficiencies, eff)
    end
    table.insert(efficiencies, 99)

    for _, efficiency in ipairs(efficiencies) do
        local level = calculate_efficiency_threshold_level(x1, x2, func, efficiency)
        local raw_bonus = BubbleGraph.calculate_bonus(func, level, x1, x2)
        local efficiency_gain = efficiency - prev_efficiency

        local point = {
            level = level,
            bonus = raw_bonus,
            real_bonus = raw_bonus,
            formatted_bonus = round_format(round(raw_bonus, DEFAULTS.DECIMAL_PLACES)),
            show_label = true,
            is_threshold = false,
            utility_ratio = efficiency / 100,
            efficiency = efficiency,
            efficiency_gain = efficiency_gain
        }

        table.insert(points, point)
        max_bonus = math.max(max_bonus, raw_bonus)
        prev_efficiency = efficiency
    end

    local prev_bonus = 0
    for i, point in ipairs(points) do
        local bonus_gain = point.bonus - prev_bonus
        point.bonus_gain = bonus_gain
        prev_bonus = point.bonus
    end

    local max_gain = 0
    for _, point in ipairs(points) do
        max_gain = math.max(max_gain, point.bonus_gain)
    end

    for _, point in ipairs(points) do
        point.relative_height = point.bonus_gain / max_gain
    end

    return points, max_bonus
end

function BubbleGraph.generate_graph(x1, x2, func, bubble_color, bubble_name, bubble_number, description, options)
    assert(type(x1) == "number", "x1 must be a number")
    assert(type(x2) == "number", "x2 must be a number")
    assert(type(func) == "string", "func must be a string")

    options = options or {}
    local graph_width = options.graph_width or DEFAULTS.GRAPH_WIDTH
    local graph_height = options.graph_height or DEFAULTS.GRAPH_HEIGHT

    local points, max_bonus
    if options.display_mode == "efficiency" then
        points, max_bonus = generate_graph_points_efficiency(x1, x2, func, options)
    else
        points, max_bonus =
            generate_graph_points(
            x1,
            x2,
            func,
            options.max_levels or DEFAULTS.MAX_LEVELS,
            options.step or DEFAULTS.STEP,
            options
        )
    end

    local container_height = options.display_mode == "efficiency" and graph_height or (graph_height + 50)

    local container =
        mw.html.create("div"):addClass("bubble-graph-container"):attr("data-x1", x1):attr("data-x2", x2):attr(
        "data-func",
        func
    ):attr("data-color", bubble_color):attr("data-name", bubble_name):attr("data-number", bubble_number):attr(
        "data-description",
        description
    ):css(
        {
            width = (graph_width) .. "px",
            height = container_height .. "px",
            position = "relative",
            ["font-family"] = DEFAULTS.FONT_FAMILY,
            ["text-align"] = "center",
            ["margin-bottom"] = "50px",
            ["padding-left"] = "40px"
        }
    )

    local scheme = bubble_colors[bubble_color] or bubble_colors.other
    local bubble_image_name = scheme.image and (scheme.image .. bubble_number) or ""

    local graph_container =
        mw.html.create("div"):css(
        {
            position = "relative",
            width = graph_width .. "px",
            height = graph_height .. "px",
            ["margin-top"] = "60px",
            ["z-index"] = "1"
        }
    )

    if options.show_grid then
        graph_container:node(
            BubbleGraph._render_grid(
                graph_container,
                graph_width,
                graph_height,
                options.major_grid_lines or DEFAULTS.MAJOR_GRID_LINES
            )
        )
    end

    local graph_body =
        mw.html.create("div"):css(
        {
            width = graph_width .. "px",
            height = graph_height .. "px",
            display = "flex",
            ["align-items"] = "flex-end",
            position = "absolute",
            top = "0",
            left = "0",
            ["z-index"] = "2",
            ["background-image"] = string.format(
                "linear-gradient(%s, %s)",
                scheme.background_start,
                scheme.background_end
            ),
            border = "1px solid #000",
            ["border-radius"] = "3px",
            padding = "3px"
        }
    )

    for i, point in ipairs(points) do
        local height

        if options.display_mode == "efficiency" then
            height = math.floor(point.relative_height * 5 * graph_height)
        else
            height = math.floor((point.bonus / max_bonus) * graph_height)
        end

        local ratio = i / #points
        local color
        if options.display_mode == "efficiency" then
            local ratio = point.efficiency / 99
            color = BubbleGraph._interpolate_color(scheme.column_start, scheme.column_end, ratio)
        else
            local threshold_level = math.floor(calculate_threshold_level(x2))
            if point.level <= threshold_level then
                local ratio = point.level / threshold_level
                color = BubbleGraph._interpolate_color(scheme.column_start, scheme.column_end, ratio)
            else
                local ratio = (point.level - threshold_level) / (options.max_levels - threshold_level)
                color = BubbleGraph._interpolate_color(scheme.column_end, scheme.column_start, ratio)
            end
        end

        local column = mw.html.create("div"):addClass("bubble-graph-column")
        local column_css = {
            flex = "1",
            height = height .. "px",
            ["background-color"] = color,
            position = "relative",
            ["min-width"] = DEFAULTS.COLUMN_MIN_WIDTH .. "px"
        }

        column:css(column_css)

        local formatted_description = format_description(description, point.formatted_bonus)

        local tooltip_text
        if options.display_mode == "efficiency" then
            tooltip_text =
                string.format(
                "Level: %s<br>Efficiency: %d%%<br><span class='description'>%s</span>",
                tostring(point.level),
                point.efficiency or 0,
                formatted_description
            )
        else
            tooltip_text =
                string.format(
                "Level: %s<br>Efficiency: %.2f%%<br><span class='description'>%s</span>",
                tostring(point.level),
                (point.utility_ratio or 0) * 100,
                formatted_description
            )
        end

        local tooltip =
            mw.html.create("div"):addClass("bubble-graph-tooltip"):css(
            {
                ["background-color"] = "#000142cc",
                ["border"] = "1px solid #000142",
                ["padding"] = "5px",
                ["color"] = "#fff",
                ["text-align"] = "center",
                ["z-index"] = "10",
                ["width"] = "150px"
            }
        ):wikitext(tooltip_text)

        column:node(tooltip)

        if point.is_threshold then
            column:tag("div"):addClass("bubble-graph-threshold"):css(
                {
                    position = "absolute",
                    top = "-5px",
                    width = "100%",
                    height = "3px",
                    ["background-color"] = "#ff0000",
                    ["z-index"] = "4"
                }
            )
        end

        if point.formatted_bonus ~= "0" then
            if options.display_mode == "efficiency" then
                height = math.floor(point.relative_height * 5 * graph_height)
            else
                height = math.floor((point.bonus / max_bonus) * graph_height)
            end

            if height < 20 then
                column:tag("span"):addClass("bubble-graph-value"):css(
                    {
                        position = "absolute",
                        bottom = "100%",
                        left = "50%",
                        transform = "translate(-50%, -2px) rotate(-90deg)",
                        color = "#fff",
                        ["text-shadow"] = "-.065em 0 #000, 0 .065em #000, .065em 0 #000, 0 -.065em #000",
                        ["white-space"] = "nowrap"
                    }
                ):wikitext(point.formatted_bonus)
            else
                local text_y_pos = math.min(height / 2, height - 15)
                column:tag("span"):addClass("bubble-graph-value"):css(
                    {
                        position = "absolute",
                        bottom = text_y_pos .. "px",
                        left = "50%",
                        transform = "translate(-50%, 50%) rotate(-90deg)",
                        color = "#fff",
                        ["text-shadow"] = "-.065em 0 #000, 0 .065em #000, .065em 0 #000, 0 -.065em #000",
                        ["white-space"] = "nowrap"
                    }
                ):wikitext(point.formatted_bonus)
            end
        end

        if point.show_label then
            local label_text =
                options.display_mode == "efficiency" and tostring(point.efficiency) .. "%" or tostring(point.level)

            column:tag("div"):addClass("bubble-graph-level"):css(
                {
                    position = "absolute",
                    bottom = "-20px",
                    width = "100%",
                    ["text-align"] = "center",
                    ["font-size"] = "10px",
                    ["z-index"] = "3",
                    ["text-wrap"] = "nowrap"
                }
            ):wikitext(label_text)
        end

        graph_body:node(column)
    end

    local y_axis =
        mw.html.create("div"):addClass("bubble-graph-y-axis"):css(
        {
            position = "absolute",
            top = "0",
            left = "-40px",
            width = "40px",
            height = graph_height .. "px",
            ["pointer-events"] = "none",
            ["z-index"] = "3"
        }
    )

    for i = 0, 10 do
        local value = round(max_bonus * (10 - i) / 10, 2)
        local label =
            mw.html.create("div"):addClass("bubble-graph-y-value"):css(
            {
                position = "absolute",
                top = (i * 10) .. "%",
                right = "5px",
                ["text-align"] = "right",
                ["font-size"] = "12px",
                ["line-height"] = "1",
                transform = "translateY(-50%)"
            }
        ):wikitext(round_format(value))
        y_axis:node(label)
    end

    if grid then
        graph_container:node(grid)
    end

    local pagination =
        mw.html.create("div"):addClass("bubble-graph-pagination"):css(
        {
            position = "absolute",
            bottom = "0",
            left = "0",
            width = "100%",
            height = "40px",
            display = "flex",
            ["justify-content"] = "center",
            ["align-items"] = "center",
            gap = "10px"
        }
    )

    if not options.display_mode == "efficiency" then
        local pagination =
            mw.html.create("div"):addClass("bubble-graph-pagination"):css(
            {
                position = "absolute",
                bottom = "0",
                left = "0",
                width = "100%",
                height = "40px",
                display = "flex",
                ["justify-content"] = "center",
                ["align-items"] = "center",
                gap = "10px"
            }
        )

        local prev_page =
            mw.html.create("div"):addClass("bubble-graph-page-button prev-page"):css(
            {
                width = "30px",
                height = "30px",
                ["background-color"] = "#eee",
                ["border-radius"] = "5px",
                cursor = "pointer",
                display = "flex",
                ["justify-content"] = "center",
                ["align-items"] = "center"
            }
        ):wikitext("[[File:Bubble Graph Left.png|link=]]")

        local next_page =
            mw.html.create("div"):addClass("bubble-graph-page-button next-page"):css(
            {
                width = "30px",
                height = "30px",
                ["background-color"] = "#eee",
                ["border-radius"] = "5px",
                cursor = "pointer",
                display = "flex",
                ["justify-content"] = "center",
                ["align-items"] = "center"
            }
        ):wikitext("[[File:Bubble Graph Right.png|link=]]")

        local page_info =
            mw.html.create("div"):addClass("bubble-graph-page-info"):css(
            {
                ["font-size"] = "14px"
            }
        ):wikitext("Levels 1-1000")

        pagination:node(prev_page)
        pagination:node(page_info)
        pagination:node(next_page)
        container:node(pagination)
    end
    graph_container:node(graph_body)
    graph_container:node(y_axis)
    container:node(graph_container)

    return tostring(container)
end

function BubbleGraph.render(frame)
    local args = frame.args or frame:getParent().args

    local x1 = tonumber(args.x1)
    local x2 = tonumber(args.x2)
    assert(x1 and x2 and args.func, "x1, x2 and func are missing")

    local options = {
        max_levels = tonumber(args.max_levels) or DEFAULTS.MAX_LEVELS,
        min_levels = tonumber(args.min_levels) or 1,
        step = tonumber(args.step) or DEFAULTS.STEP,
        graph_width = tonumber(args.width) or DEFAULTS.GRAPH_WIDTH,
        graph_height = tonumber(args.height) or DEFAULTS.GRAPH_HEIGHT,
        show_grid = args.show_grid == nil and DEFAULTS.SHOW_GRID or (args.show_grid == "true" or args.show_grid == true),
        major_grid_lines = tonumber(args.major_grid_lines) or DEFAULTS.MAJOR_GRID_LINES,
        display_mode = args.display_mode or "normal"
    }

    local description = args.description or ""

    return BubbleGraph.generate_graph(
        x1,
        x2,
        args.func,
        args.bubble_color or "other",
        args.bubble_name or args.name or "",
        args.bubble_number or args.number or "",
        description,
        options
    )
end

return BubbleGraph