Module:BubbleGraph: Difference between revisions
From IdleOn MMO Wiki
mNo edit summary Tag: Reverted |
mNo edit summary Tag: Reverted |
||
Line 89: | Line 89: | ||
height = height .. "px", | height = height .. "px", | ||
["pointer-events"] = "none", | ["pointer-events"] = "none", | ||
["z-index"] = " | ["z-index"] = "1", | ||
["background-image"] = string.format( | ["background-image"] = string.format( | ||
"linear-gradient(to right, rgba(0,0,0,0.1) 1px, transparent 1px), " .. | "linear-gradient(to right, rgba(0,0,0,0.1) 1px, transparent 1px), " .. | ||
Line 603: | Line 603: | ||
["background-color"] = color, | ["background-color"] = color, | ||
position = "relative", | position = "relative", | ||
["z-index"] = " | ["z-index"] = "3", | ||
["min-width"] = DEFAULTS.COLUMN_MIN_WIDTH .. "px" | ["min-width"] = DEFAULTS.COLUMN_MIN_WIDTH .. "px" | ||
} | } |
Revision as of 19:35, 31 December 2024
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 page_name = options.page_name or mw.title.getCurrentTitle().fullText
if not bubble_color or bubble_color == "other" then
bubble_color = BubbleGraph.page_color_mapping.getColor(page_name)
end
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",
["z-index"] = "3",
["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"] = "9",
["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",
page_name = args.page_name or mw.title.getCurrentTitle().fullText
}
local description = args.description or ""
return BubbleGraph.generate_graph(
x1,
x2,
args.func,
args.bubble_color or args.color or "other",
args.bubble_name or args.name or "",
args.bubble_number or args.number or "",
description,
options
)
end
return BubbleGraph