/*--------------------------------------------------------- // // Page states // ---------------------------------------------------------*/ /* Variable intended for dev mode specific output/markings */ const debug = false; /** visual modes * hidden: hides these general elements * shown: unhides these general elements */ const modes = { "welcome": { "hidden": [ "data_container", "chart", "tc_resources" ], "shown": [ "welcome_container", "spec_table" ] }, "data": { "hidden": [ "welcome_container", "spec_table", ], "shown": [ "data_container", "tc_resources", "chart" ] } }; let loaded_data = {}; let chosen_class = ""; let chosen_spec = ""; let chosen_talent_combination = ""; let chosen_azerite_list_type = "trait_stacking"; let chosen_azerite_tier = 1; let dark_mode = true; let bloodyfiller = "mallet"; let language = "EN"; let loaded_languages = {}; /** translate defined IDs based on data */ const translation_IDs = [ "translate_main_paragraph", "navbarSettingsMenu", "translate_dark_mode", "translate_faq", "translate_report_an_error", "translate_language" ]; /** * If content is used multiple times like class names or spec names, add a translation class to the class list. */ const translation_classes = [ "translate_death_knight", "translate_demon_hunter", "translate_druid", "translate_hunter", "translate_mage", "translate_monk", "translate_paladin", "translate_priest", "translate_rogue", "translate_shaman", "translate_warlock", "translate_warrior", "translate_blood", "translate_frost", "translate_unholy", "translate_havoc", "translate_vengeance", "translate_balance", "translate_feral", "translate_guardian", "translate_beast_mastery", "translate_marksmanship", "translate_survival", "translate_arcane", "translate_fire", // frost is already further up, due to death knights "translate_brewmaster", "translate_windwalker", "translate_protection", "translate_retribution", "translate_shadow", "translate_assassination", "translate_outlaw", "translate_subtlety", "translate_elemental", "translate_enhancement", "translate_affliction", "translate_demonology", "translate_destruction", "translate_arms", "translate_fury", "translate_trinkets", "translate_azerite_traits", "translate_races", "translate_secondary_distributions", "translate_patchwerk", "translate_hecticaddcleave", "translate_itemlevel", "translate_trait_stacking", "translate_head", "translate_shoulders", "translate_chest", "translate_link_to_chart", "translate_link_was_copied_to_clipboard" ]; let mode = "welcome"; let fight_style = "patchwerk"; let data_view = "trinkets"; const data_view_IDs = [ "show_trinkets_data", // => trinkets "show_azerite_traits_data", // => azerite_traits "show_races_data", // => races "show_secondary_distributions_data", "talent_combination_selector", "chart_type_itemlevel", "chart_type_trait_stacking", "chart_type_head", "chart_type_shoulders", "chart_type_chest", "copy_link" ]; const fight_style_IDs = [ "fight_style_patchwerk", // "fight_style_beastlord", "fight_style_hecticaddcleave", ]; const azerite_trait_view_type_IDs = [ "chart_type_head", "chart_type_shoulders", "chart_type_chest", "chart_type_itemlevel", "chart_type_trait_stacking", ]; const azerite_trait_tier_IDs = [ "azerite_traits_tier_1", "azerite_traits_tier_2" ]; const light_color = "#eeeeee"; const medium_color = "#999999"; const dark_color = "#343a40"; const font_size = "1.1rem"; const empty_chart = { chart: { type: "bar", backgroundColor: null, style: { fontFamily: "-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,\"Helvetica Neue\",Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\"" } //borderColor: medium_color, //borderWidth: 1 }, colors: [ "#7cb5ec", "#d9d9df", "#90ed7d", "#f7a35c", "#8085e9", "#f15c80", "#e4d354", "#2b908f", "#f45b5b", "#91e8e1" ], legend: { align: "right", backgroundColor: dark_color, borderColor: medium_color, borderWidth: 1, floating: true, itemMarginBottom: 3, itemMarginTop: 3, layout: 'vertical', reversed: true, shadow: false, verticalAlign: "middle", x: 0, y: 0, itemStyle: { color: light_color, }, itemHoverStyle: { color: light_color, } }, plotOptions: { bar: { dataLabels: { enabled: false, }, point: { events: { click: function (event) { var chart = this.series.yAxis; chart.removePlotLine('helperLine'); chart.addPlotLine({ value: this.stackY, color: light_color, width: 2, id: 'helperLine', zIndex: 5, label: { text: this.series.name + ' ' + this.category, style: { color: light_color, fontSize: font_size, }, align: 'left', verticalAlign: 'bottom', rotation: 0, y: -5 } }); } } }, }, series: { stacking: "normal", borderColor: dark_color, events: { legendItemClick: function () { return false; } }, style: { textOutline: false, fontSize: font_size, } } }, series: [ { color: light_color, data: [ 1, 1, 3, 1, 3 ], name: "b main", showInLegend: false }, { color: dark_color, data: [ 0, 0, 0, 1, 0 ], name: "b's emptiness", showInLegend: false }, { color: light_color, data: [ 0, 0, 0, 1, 0 ], name: "b's finishing touch", showInLegend: false } ], subtitle: { text: "Data not found", useHTML: true, style: { color: light_color, fontSize: font_size } }, title: { text: "", //"Title placeholder", useHTML: true, style: { color: light_color, fontSize: "1.2rem" } }, tooltip: { formatter: function () { var s = '
' + this.x + '
' var cumulative_amount = 0; for (var i = this.points.length - 1; i >= 0; i--) { cumulative_amount += this.points[i].y; if (this.points[i].y !== 0) { s += '
' + this.points[i].series.name + ':  ' + Intl.NumberFormat().format(cumulative_amount) + "
"; } } s += '
'; return s; }, headerFormat: "{point.x}", shared: true, backgroundColor: dark_color, borderColor: medium_color, style: { color: light_color, fontSize: font_size, }, useHTML: true, // adding this as a potential tooltip positioning fix. changes tooltip position to be inside the bar rather than at the end positioner: function (boxWidth, boxHeight, point) { return { x: point.plotX, y: point.plotY }; } }, xAxis: { categories: [ "b", "b", "b", "b", "b", ], labels: { useHTML: true, style: { color: light_color, fontSize: font_size, } }, gridLineWidth: 0, gridLineColor: medium_color, lineColor: medium_color, tickColor: medium_color }, yAxis: { labels: { //enabled: true, style: { color: medium_color }, }, min: 0, stackLabels: { enabled: true, formatter: function () { return Intl.NumberFormat().format(this.total); }, style: { color: light_color, textOutline: false, fontSize: font_size, //fontWeight: "normal" } }, title: { text: "\u0394 Damage per second", style: { color: medium_color } }, gridLineWidth: 1, gridLineColor: medium_color } }; const standard_chart = Highcharts.chart('chart', empty_chart); // invalid ilevels to use highcharts base colours but keep the old ones const ilevel_color_table = { "00": "#1f78b4", "10": "#a6cee3", "20": "#33a02c", "30": "#b2df8a", "40": "#e31a1c", "50": "#fb9a99", "60": "#ff7f00", "70": "#cab2d6", "80": "#fdbf6f" }; const class_colors = { "death_knight": "#C41F3B", "demon_hunter": "#A330C9", "druid": "#FF7D0A", "hunter": "#ABD473", "mage": "#69CCF0", "monk": "#00FF96", "paladin": "#F58CBA", "priest": "#FFFFFF", "rogue": "#FFF569", "shaman": "#0070DE", "warlock": "#9482C9", "warrior": "#C79C6E", }; /*--------------------------------------------------------- // // Dark Mode // ---------------------------------------------------------*/ /** add listener to the dark mode checkbox */ document.addEventListener("DOMContentLoaded", function () { if (debug) console.log("addEventListener darkModeCheckbox"); document.getElementById("darkModeCheckbox").addEventListener("change", function (e) { dark_mode = e.target.checked; update_dark_mode(); set_dark_mode_cookie(); }); }); /** Updates dark mode based on dark mode check box. */ function update_dark_mode() { if (debug) console.log("update_dark_mode"); let primary_color; let secondary_color; if (dark_mode) { document.getElementsByTagName("body")[0].classList.remove("bg-light"); document.getElementsByTagName("body")[0].classList.remove("text-dark"); document.getElementsByTagName("body")[0].classList.add("bg-dark"); document.getElementsByTagName("body")[0].classList.add("text-light"); primary_color = light_color; secondary_color = dark_color; } else { document.getElementsByTagName("body")[0].classList.add("bg-light"); document.getElementsByTagName("body")[0].classList.add("text-dark"); document.getElementsByTagName("body")[0].classList.remove("bg-dark"); document.getElementsByTagName("body")[0].classList.remove("text-light"); primary_color = dark_color; secondary_color = light_color; } // update chart base colors standard_chart.update({ legend: { backgroundColor: secondary_color, itemStyle: { color: primary_color, }, itemHoverStyle: { color: primary_color, } }, title: { style: { color: primary_color } }, tooltip: { backgroundColor: secondary_color, style: { color: primary_color, }, }, subtitle: { style: { color: primary_color } }, xAxis: { labels: { style: { color: primary_color } } }, yAxis: { stackLabels: { style: { color: light_color } } } }); scatter_chart.update({ legend: { itemStyle: { color: primary_color, }, itemHoverStyle: { color: primary_color, } }, title: { style: { color: primary_color } }, subtitle: { style: { color: primary_color } }, plotOptions: { series: { dataLabels: { style: { color: primary_color } } } } }); } /** save the current dark_mode value in a cookie */ function set_dark_mode_cookie() { if (debug) console.log("set_dark_mode_cookie"); Cookies.set('bloodmallet_dark_mode', dark_mode, { expires: 31, path: '' }); } /** searches for the dark mode cookie and updates the page if necessary */ function search_dark_mode_cookie() { if (debug) console.log("search_dark_mode_cookie"); if (Cookies.get('bloodmallet_dark_mode')) { dark_mode = ('true' === Cookies.get('bloodmallet_dark_mode')); } document.getElementById("darkModeCheckbox").checked = dark_mode; update_dark_mode(); set_dark_mode_cookie(); } /*--------------------------------------------------------- // // Reroll the patron defined message of the headline // ---------------------------------------------------------*/ const patrons_epic = [ // { // "name": "", // "text": "charts" // } ]; const patrons_rare = [ { "name": "🐕", "text": "🌮" }, ]; const patrons_uncommon = [ { "name": "Fred", "text": "👻" }, { "name": "Barokoshama", "text": "(ノ◕ヮ◕)ノ*:・゚✧" } ]; const patrons = patrons_uncommon.concat( patrons_uncommon, patrons_rare, patrons_rare, patrons_rare, patrons_rare, patrons_rare, patrons_epic, patrons_epic, patrons_epic, patrons_epic, patrons_epic, patrons_epic, patrons_epic, patrons_epic, patrons_epic, patrons_epic ); document.addEventListener("DOMContentLoaded", function () { if (debug) console.log("addEventListener bloodyfiller"); // document.getElementById("bloodyfiller").addEventListener("click", randomize_bloodyfiller); document.getElementById("bloodyheadline").addEventListener("click", randomize_bloodypatrons); if (Math.floor(Math.random() * 2) > 0) { randomize_bloodypatrons(); } else { document.getElementById("bloodyheadline").innerHTML = "bloodmallet"; } }); /** * Way to return the kindness of patrons. * Shows the patron defined message in the title. * And adds a tooltip with their wanted name. * bloody( message ) * T * Tooltip */ function randomize_bloodypatrons() { if (debug) { console.log("randomize_bloodypatrons"); } // if no element 'bloodypatrons' is present, update bloodyheadline let html_element = document.getElementById("bloodypatrons"); if (html_element === null) { let helper = document.getElementById("bloodyheadline"); helper.innerHTML = "bloody(  )"; html_element = document.getElementById("bloodypatrons"); } // roll new patron message and name let old_content = html_element.innerHTML; let new_content = old_content; let roll = 0; while (new_content === old_content) { roll = Math.floor(Math.random() * patrons.length); new_content = patrons[roll]["text"]; } // apply new name to tooltip try { $(function () { $('#bloodypatrons').tooltip('hide') .attr('data-original-title', 'Chosen by patron ' + patrons[roll]['name']) .tooltip('show'); }); } catch (error) { if (debug) { console.log(error); } } // apply new message html_element.innerHTML = new_content; } /*--------------------------------------------------------- // // Switch language // ---------------------------------------------------------*/ document.addEventListener("DOMContentLoaded", function () { if (debug) console.log("addEventListener languageSelector"); document.getElementById("languageSelector").addEventListener("change", function () { switch_language(this.options[this.selectedIndex].value); }); }); /** * Switches the language and calls translate_page and translate_chart to do the actual translation. */ async function switch_language(new_language) { debug && console.log("switch_language"); if (language === new_language) { debug && console.log(`switch_language early exit. new_language: ${new_language}, current language: ${language}`); return; } else { debug && console.log("new language: " + new_language); } // if new language is different to already active language and if it wasn't already loaded if (!loaded_languages[new_language]) { let response = await fetch(`./translations/${new_language.toLowerCase()}.json`); loaded_languages[new_language] = await response.json(); } language = new_language; set_language_cookie(); push_state(); } /** * translate all translation_IDs and translation_classes. Does NOT translate charts. Use translate_chart() for that */ function translate_page() { if (debug) console.log("translate_page"); // get the translation options var language_html_elements = document.getElementById("languageSelector").options; // de-select whatever language option was chosen language_html_elements[document.getElementById("languageSelector").selectedIndex].selected = false; // select the new language in the settings based on data for (let index = 0; index < language_html_elements.length; index++) { const element = language_html_elements[index]; if (element.value === language) { element.selected = true; } } if (typeof loaded_languages[language] === 'undefined') { debug && console.log("translate_page abort, due to missing data"); return; } // translate content of IDs translation_IDs.forEach(element => { translate_element(element); }); // translate content of classes translation_classes.forEach(element => { translate_element(element); }); } function translate_element(element) { if (!loaded_languages[language]) { if (debug) { console.log(`Language package ${language} wasn't loaded`); } return; } const translated_element = loaded_languages[language][element]; [].forEach.call(document.getElementsByClassName(element), function (html_element) { if (!translated_element) { console.log("Language package '" + language + "' doesn't have '" + element + "' added to it or the ID is missing in the page. Help improve the page by submitting a bug report. Or even better: clone the repo, fix the problem, and create a pull request. Any help is greatly appreciated!"); if (debug) { html_element.style.border = "1px solid red"; } return; } if (translated_element === "") { console.log("No translation for '" + element + "' available. Help improve the page by submitting a bug report. Or even better: clone the repo, fix the problem, and create a pull request. Any help is greatly appreciated!"); if (debug) { html_element.style.border = "1px solid red"; } return; } html_element.innerHTML = translated_element; }); } /** Translates the current chart. * assumption: only one chart is present */ function translate_chart() { if (debug) console.log("translate_chart"); if (data_view !== "trinkets" && data_view !== "azerite_traits") { if (debug) console.log("translate_chart early exit"); return; } if (chosen_class === "" || chosen_spec === "") { if (debug) console.log("translate_chart early exit"); return; } if (document.getElementById("translator_helper").childElementCount > 0) { debug && console.log("Another translation seems to be in progress. translate_chart early exit."); return; } // create a dictionary of all created links let link_list = []; clear_translator(); let current_data; if (data_view === "azerite_traits" && ["head", "shoulders", "chest"].includes(chosen_azerite_list_type)) { current_data = loaded_data[chosen_class][chosen_spec][data_view + "_" + chosen_azerite_list_type][fight_style]; } else { current_data = loaded_data[chosen_class][chosen_spec][data_view][fight_style]; } if (!current_data) { debug && console.log("current_data is mysteriously empty."); return; } let appropriate_data_key_list = []; if (data_view === "azerite_traits" && ["itemlevel", "trait_stacking"].includes(chosen_azerite_list_type)) { appropriate_data_key_list = current_data["sorted_azerite_tier_" + chosen_azerite_tier + "_" + chosen_azerite_list_type]; } else { appropriate_data_key_list = current_data["sorted_data_keys"]; } for (let trinket of appropriate_data_key_list) { if (trinket.indexOf("baseline") > -1) { let p = document.createElement("span"); let text_trinket_name = document.createTextNode(trinket); p.appendChild(text_trinket_name); link_list.push(`${trinket}`); if (language !== "EN") translator.appendChild(p); continue; } const lowest_ilvl = current_data["simulated_steps"][current_data["simulated_steps"].length - 1]; // create untranslated link let new_link = document.createElement("a"); // TODO: will need more logic for azerite traits later let link = `https://${language.toLowerCase()}.wowhead.com/`; if (data_view === "azerite_traits" && ["itemlevel", "trait_stacking"].includes(chosen_azerite_list_type)) { link += `spell=${current_data["spell_ids"][trinket]}`; } else { link += `item=${current_data["item_ids"][trinket]}`; } try { link += `&ilvl=${lowest_ilvl.split("1_")[1]}`; } catch (error) { link += `&ilvl=${lowest_ilvl}`; } // add azerite power link portion if (data_view === "azerite_traits" && ["head", "shoulders", "chest"].includes(chosen_azerite_list_type)) { link += `&azerite-powers=${current_data["class_id"]}`; // add azerite traits for (let trait of current_data["used_azerite_traits_per_item"][trinket]) { link += ":" + trait["id"]; } } new_link.href = link; new_link.target = "blank"; let text_trinket_name = document.createTextNode(trinket); new_link.appendChild(text_trinket_name); link_list.push(`${trinket}`); if (language !== "EN") { let translator = document.getElementById("translator_helper"); translator.appendChild(new_link); } } if (debug) { console.log("update categories with link_list (english names, foreign link in translate_chart"); } standard_chart.update({ xAxis: { categories: link_list } }, true); if (debug) console.log("try to trigger wowhead power js"); trigger_wowhead_link_renaming(); setTimeout(function () { update_link_data(link_list) }, 200); } /** * Somewhat saver way to try and retrigger wowhead link translation. */ function trigger_wowhead_link_renaming() { if (debug) { console.log("trigger_wowhead_link_renaming"); } try { $WowheadPower.refreshLinks(); } catch (error) { setTimeout(trigger_wowhead_link_renaming, 50); } } function clear_translator() { if (debug) { console.log("clear_translator"); } let translator = document.getElementById("translator_helper"); while (translator.firstChild) { translator.removeChild(translator.firstChild); } } /** * Awaits the translation of all hidden links. * If translation is done, will apply new links to chart. */ function update_link_data(original_list) { if (debug) console.log("update_link_data"); let all_translated = true; for (let a in original_list) { let original_link = original_list[a]; let new_link; try { new_link = document.getElementById("translator_helper").childNodes[a].outerHTML; } catch (error) { debug && console.log(`update_link_data couldn't find '${original_link}' in the translator_helper. Abort.`); clear_translator(); return; } // wowhead tooltips add span elements into the link, therefore changing the number of the resulting array if (original_link.split(">").length == new_link.split(">").length && original_link.indexOf("baseline") == -1) { all_translated = false; } } if (!all_translated) { setTimeout(function () { update_link_data(original_list) }, 1000); return; } let new_categories = []; for (let link of document.getElementById("translator_helper").childNodes) { new_categories.push(link.outerHTML); } clear_translator(); if (debug) { console.log(original_list); console.log(new_categories); console.log("updating categories with new_categories from update_link_data"); } standard_chart.update({ xAxis: { categories: new_categories } }, true); } /** Save the current language in a cookie. */ function set_language_cookie() { if (debug) console.log("set_language_cookie"); Cookies.set('bloodmallet_language_selection', language, { expires: 31, path: '' }); } /** Searches for the dark mode cookie and updates the page if necessary. */ function search_language_cookie() { if (debug) console.log("search_language_cookie"); switch_language(Cookies.get("bloodmallet_language_selection") || "EN"); } /*--------------------------------------------------------- // // Switch to data mode // ---------------------------------------------------------*/ /** * Apply click events for data manipulation. */ function addDataViewClickEvent(elementId, new_data_view) { document.getElementById(elementId).addEventListener("click", function () { data_view = new_data_view; push_state(); }); } function addAzeriteViewClickEvent(elementId, new_azerite_list_type) { document.getElementById(elementId).addEventListener("click", function () { chosen_azerite_list_type = new_azerite_list_type; push_state(); }); } function addAzeriteTierClickEvent(elementId, new_azerite_tier) { document.getElementById(elementId).addEventListener("click", function () { chosen_azerite_tier = new_azerite_tier; push_state(); }); } function addFightStyleClickEvent(elementId, new_fight_style) { document.getElementById(elementId).addEventListener("click", function () { fight_style = new_fight_style; push_state(); }); } document.addEventListener("DOMContentLoaded", function () { try { addDataViewClickEvent("show_trinkets_data", "trinkets"); addDataViewClickEvent("show_azerite_traits_data", "azerite_traits"); addDataViewClickEvent("show_races_data", "races"); addDataViewClickEvent("show_secondary_distributions_data", "secondary_distributions"); addAzeriteViewClickEvent("chart_type_head", "head"); addAzeriteViewClickEvent("chart_type_shoulders", "shoulders"); addAzeriteViewClickEvent("chart_type_chest", "chest"); addAzeriteViewClickEvent("chart_type_itemlevel", "itemlevel"); addAzeriteViewClickEvent("chart_type_trait_stacking", "trait_stacking"); addAzeriteTierClickEvent("azerite_traits_tier_1", 1); addAzeriteTierClickEvent("azerite_traits_tier_2", 2); addFightStyleClickEvent("fight_style_patchwerk", "patchwerk"); addFightStyleClickEvent("fight_style_hecticaddcleave", "hecticaddcleave"); document.getElementById("copy_link").addEventListener("click", function () { copy_link(); }); } catch (err) { console.log("Couldn't bind click events"); debug && console.log(err); } }); /** * */ window.onhashchange = function () { if (debug) console.log("window.onhashchange"); clear_translator(); get_data_from_link(); switch_mode(); }; document.addEventListener("DOMContentLoaded", function () { document.getElementById("talent_combination_selector").addEventListener("change", function (e) { if (debug) console.log(e); chosen_talent_combination = e.target.value; push_state(); }); }); window.addEventListener('popstate', function (event) { if (history.state) { get_data_from_link(); switch_to_data(); } }, false); /** * Update the global class and spec variables from the current url. */ function get_data_from_link() { if (debug) console.log("get_data_from_link"); let hash = window.location.hash; if (!hash) { // early exit, we got no data, so what shall we do anyway? return; } let combined_class_spec = ""; if (hash.indexOf("?") > -1) { combined_class_spec = hash.slice(1, hash.indexOf("?")); } else { combined_class_spec = hash.slice(1); } if (combined_class_spec) { if (combined_class_spec.indexOf("death_knight") > -1 || combined_class_spec.indexOf("demon_hunter") > -1) { chosen_class = combined_class_spec.slice(0, combined_class_spec.lastIndexOf("_")); chosen_spec = combined_class_spec.slice(combined_class_spec.lastIndexOf("_") + 1); } else { chosen_class = combined_class_spec.slice(0, combined_class_spec.indexOf("_")); chosen_spec = combined_class_spec.slice(combined_class_spec.indexOf("_") + 1); } } if (hash.indexOf("&") === -1) { // rather early exit if no params were provided return; } const params = hash.split("?")[1].split("&"); for (const param of params) { const key = param.split("=")[0]; const value = param.split("=")[1]; if (key === "data_view") { data_view = value; } else if (key === "fight_style") { fight_style = value; } else if (key === "type") { chosen_azerite_list_type = value; } else if (key === "tier") { chosen_azerite_tier = value; } else if (key === "lang") { switch_language(value); } } } /* * Loads spec data (json) according to the already applied settings. Triggers update_chart. */ async function load_data() { if (debug) console.log("load_data"); if (chosen_class === "" || chosen_spec === "") { debug && console.log("load_data aborted. No chosen_class or spec found.") return; } empty_charts(); // necessary to be able to save traits, head, shoulders and chest separately var data_name = data_view; if (data_view === "azerite_traits" && ["head", "shoulders", "chest"].includes(chosen_azerite_list_type)) { data_name += "_" + chosen_azerite_list_type; } if (!loaded_data[chosen_class]) { loaded_data[chosen_class] = {}; } if (!loaded_data[chosen_class][chosen_spec]) { loaded_data[chosen_class][chosen_spec] = {}; } if (!loaded_data[chosen_class][chosen_spec][data_name]) { loaded_data[chosen_class][chosen_spec][data_name] = {}; } if (!loaded_data[chosen_class][chosen_spec][data_name][fight_style]) { var file_name = chosen_class + "_" + chosen_spec; if ((data_view === "azerite_traits") && (["head", "shoulders", "chest"].includes(chosen_azerite_list_type))) { file_name += "_" + chosen_azerite_list_type; } file_name += "_" + fight_style + ".json"; let response = await fetch(`./json/${data_view}/${file_name}`); loaded_data[chosen_class][chosen_spec][data_name][fight_style] = await response.json(); } update_talent_selector(); update_chart(); } /** * Acivates the data mode. * Prepares global chosen_class and chosen_spec variables. * Loads necessary chart data, renders chart, translates page and chart. * Hides welcome-area and shows data area if necessary. */ function switch_mode() { if (debug) console.log("switch_mode"); // hide, unhide stuff if (mode == "welcome") { mode = "data"; $(function () { $('#bloodypatrons').tooltip('hide'); }); make_invisible(modes[mode]["hidden"]); make_visible(modes[mode]["shown"]); } // push new state to history push_state(); } /** * Function to change the url. url change triggers state application, load, and chart updates according to state (class + spec + fight_style + ...). */ function push_state() { if (debug) { console.log("push_state"); console.log(`${chosen_spec} ${chosen_class} ${data_view} ${fight_style}`); } history.pushState({ id: 'data_view' }, chosen_spec + " " + chosen_class + " | " + data_view + " | " + fight_style, create_link()); switch_to_data(); } /** * Function to trigger all possible updates and loads. */ function switch_to_data() { if (debug) { console.log("switch_to_data"); } update_nav(); update_page_content(); update_data_buttons(); update_fight_style_buttons(); update_azerite_buttons(); load_data(); translate_page(); translate_chart(); } /** * Update which data button has the class color background. */ function update_data_buttons() { if (debug) console.log("update_data_buttons"); if (chosen_class === "" || chosen_spec === "") { debug && console.log("update_data_buttons aborted. No chosen_class or spec found.") return; } // reset buttons to standard visual data_view_IDs.forEach(element => { try { document.getElementById(element).className = "btn-data " + chosen_class + "-button"; } catch (err) { console.log(element + " was not found in page."); } }); // set "active" to class color document.getElementById("show_" + data_view + "_data").classList.add(chosen_class + "-border-bottom"); // unhide/hide talent combination selection if necessary document.getElementById("talent_combination_selector").hidden = (data_view !== "secondary_distributions"); let is_azerite = (data_view === "azerite_traits"); document.getElementById("chart_type_head").hidden = !is_azerite; document.getElementById("chart_type_shoulders").hidden = !is_azerite; document.getElementById("chart_type_chest").hidden = !is_azerite; document.getElementById("chart_type_itemlevel").hidden = !is_azerite; document.getElementById("chart_type_trait_stacking").hidden = !is_azerite; let is_traits = (data_view === "azerite_traits" && (chosen_azerite_list_type === "itemlevel" || chosen_azerite_list_type === "trait_stacking")); document.getElementById("azerite_traits_tier_1").hidden = !is_traits; document.getElementById("azerite_traits_tier_2").hidden = !is_traits; } /** * Update the talent list in the talent selector, based on data. Set first talent combination as default. */ function update_talent_selector() { if (debug) console.log("update_talent_selector"); if (data_view !== "secondary_distributions") return; let talent_combinations = Object.keys(loaded_data[chosen_class][chosen_spec][data_view][fight_style]["data"]); let talent_selector = document.getElementById("talent_combination_selector"); talent_selector.innerHTML = ""; chosen_talent_combination = talent_combinations[0]; for (let talent_combination of talent_combinations) { let new_option = document.createElement("option"); new_option.text = talent_combination; if (talent_combination === chosen_talent_combination) new_option.selected = true; talent_selector.add(new_option); } } /** * Resets colors of all fight style buttons and sets active button to class color. */ function update_fight_style_buttons() { if (debug) console.log("update_fight_style_buttons"); if (chosen_class === "" || chosen_spec === "") { debug && console.log("update_fight_style_buttons aborted. No chosen_class or spec found.") return; } // reset buttons to standard visual fight_style_IDs.forEach(element => { document.getElementById(element).className = "btn-data " + chosen_class + "-button"; }); // set "active" to class color document.getElementById("fight_style_" + fight_style).classList.add(chosen_class + "-border-bottom"); } /** * Resets colors of all fight style buttons and sets active button to class color. */ function update_azerite_buttons() { if (debug) console.log("update_azerite_buttons"); if (data_view !== "azerite_traits") { if (debug) console.log("update_azerite_buttons early exit"); return; } if (chosen_class === "" || chosen_spec === "") { debug && console.log("update_azerite_buttons aborted. No chosen_class or spec found.") return; } // reset buttons to standard visual azerite_trait_view_type_IDs.forEach(element => { document.getElementById(element).className = "btn-data " + chosen_class + "-button"; }); azerite_trait_tier_IDs.forEach(element => { document.getElementById(element).className = "btn-data " + chosen_class + "-button"; }); // set "active" to class color document.getElementById("chart_type_" + chosen_azerite_list_type).classList.add(chosen_class + "-border-bottom"); document.getElementById("azerite_traits_tier_" + chosen_azerite_tier).classList.add(chosen_class + "-border-bottom"); } /** * Mark current active chosen class in top navigation. */ function update_nav() { if (debug) console.log("update_nav"); if (chosen_class === "") { return; } var nav_items = document.getElementsByClassName("dropdown-toggle"); for (let index = 0; index < nav_items.length; index++) { const element = nav_items[index]; element.classList.remove("active"); } document.getElementsByClassName("translate_" + chosen_class)[0].classList.add("active"); } /** * Makes all given IDs visible. */ function make_visible(IDs) { if (debug) console.log("make_visible"); IDs.forEach(element => { document.getElementById(element).hidden = false; }); } /** * Makes all given IDs invisible. */ function make_invisible(IDs) { if (debug) console.log("make_invisible"); IDs.forEach(element => { document.getElementById(element).hidden = true; }); } /** * Show and update the chart with currently available data. * Data load is NOT handled by this function. Triggers update_chart! */ function update_chart() { if (debug) console.log("update_chart"); if (data_view == "secondary_distributions") { document.getElementById("scatter_plot_warning").hidden = false; document.getElementById("scatter_plot_chart").hidden = false; document.getElementById("chart").hidden = true; update_scatter_chart(); return; } else { document.getElementById("scatter_plot_warning").hidden = true; document.getElementById("scatter_plot_chart").hidden = true; document.getElementById("chart").hidden = false; } if (data_view === "azerite_traits" && chosen_azerite_list_type === "trait_stacking") { return update_trait_stacking_chart(); } let data_name = data_view; if (data_view == "azerite_traits" && ["head", "shoulders", "chest"].includes(chosen_azerite_list_type)) { data_name += "_" + chosen_azerite_list_type; } // https://stackoverflow.com/questions/25500316/sort-a-dictionary-by-value-in-javascript // create a list of all trinkets with their highest dps value // var dps_ordered_data = Object.keys(loaded_data[chosen_class][chosen_spec][data_view][fight_style]["trinkets"]).map(function (key) { return [key, Math.max(...Object.values(loaded_data[chosen_class][chosen_spec][data_view][fight_style]["trinkets"][key]))] }); // order said list // dps_ordered_data.sort(function (first, second) { return second[1] - first[1]; }); //console.log(dps_ordered_data); // get rid of dps values and keep only the trinket names // dps_ordered_data = dps_ordered_data.map(x => x[0]); // or.... just use the provided sorted list once that is included in fresh data if ("sorted_data_keys" in loaded_data[chosen_class][chosen_spec][data_name][fight_style]) { var dps_ordered_data = []; if (data_view === "azerite_traits" && ["itemlevel", "trait_stacking"].includes(chosen_azerite_list_type)) { dps_ordered_data = loaded_data[chosen_class][chosen_spec][data_name][fight_style]["sorted_azerite_tier_" + chosen_azerite_tier + "_" + chosen_azerite_list_type]; } else { dps_ordered_data = loaded_data[chosen_class][chosen_spec][data_name][fight_style]["sorted_data_keys"]; } } else { debug && console.log("Getting sorted_data_keys from data failed. Set unordered dps_ordered_data"); var dps_ordered_data = Object.keys(loaded_data[chosen_class][chosen_spec][data_view][fight_style]["data"]); } // change item/spell names to wowhead links ordered_trinket_list = []; if (data_view == "trinkets" || data_view == "azerite_traits") { for (let i in dps_ordered_data) { if (dps_ordered_data[i].indexOf("baseline") > -1) { ordered_trinket_list.push(dps_ordered_data[i]); continue; } if (data_view == "azerite_traits" && ["itemlevel", "trait_stacking"].includes(chosen_azerite_list_type)) { let link = "" + dps_ordered_data[i] + ""; ordered_trinket_list.push(link); } else { let string = "" + dps_ordered_data[i] + ""; ordered_trinket_list.push(string); } } // rewrite the trinket names if (debug) { console.log("applying ordered_trinket_list to categories in update_chart"); } standard_chart.update({ xAxis: { categories: ordered_trinket_list } }, false); } else { // rewrite the trinket names if (debug) { console.log("applying ordered_trinket_list to categories in update_chart"); } standard_chart.update({ xAxis: { categories: dps_ordered_data } }, false); } // set title and subtitle let new_title = ""; if (data_view == "azerite_traits" && chosen_azerite_list_type == "itemlevel") new_title = "Different itemlevels; number of each trait: 1"; standard_chart.setTitle({ text: new_title //loaded_data[chosen_class][chosen_spec][data_view][fight_style]["title"] }, { text: loaded_data[chosen_class][chosen_spec][data_name][fight_style]["subtitle"] }, false); // delete all old series data while (standard_chart.series[0]) { standard_chart.series[0].remove(false); } // basically: if something was simmed with multiple itemlevels if ("simulated_steps" in loaded_data[chosen_class][chosen_spec][data_name][fight_style]) { if (debug) console.log("simulated_steps in data found."); for (let itemlevel_position in loaded_data[chosen_class][chosen_spec][data_name][fight_style]["simulated_steps"]) { let itemlevel = loaded_data[chosen_class][chosen_spec][data_name][fight_style]["simulated_steps"][itemlevel_position]; let itemlevel_dps_values = []; if (debug) console.log("handling itemlevel " + itemlevel); // create series input for highcharts for (data of dps_ordered_data) { let dps = loaded_data[chosen_class][chosen_spec][data_name][fight_style]["data"][data][itemlevel]; let min_ilevel = loaded_data[chosen_class][chosen_spec][data_name][fight_style]["simulated_steps"][loaded_data[chosen_class][chosen_spec][data_name][fight_style]["simulated_steps"].length - 1]; let max_ilevel = loaded_data[chosen_class][chosen_spec][data_name][fight_style]["simulated_steps"][0]; // check for zero dps values and don't change them if (dps > 0) { // if lowest itemlevel is looked at, substract baseline if (itemlevel_position === loaded_data[chosen_class][chosen_spec][data_name][fight_style]["simulated_steps"].length - 1) { if (itemlevel in loaded_data[chosen_class][chosen_spec][data_name][fight_style]["data"][data]) { itemlevel_dps_values.push(dps - loaded_data[chosen_class][chosen_spec][data_name][fight_style]["data"]["baseline"][min_ilevel]); } else { itemlevel_dps_values.push(0); } } else { // else substract lower itemlevel value of same item // if lower itemlevel is zero we have to assume that this item needs to be compared now to the baseline if (loaded_data[chosen_class][chosen_spec][data_name][fight_style]["data"][data][loaded_data[chosen_class][chosen_spec][data_name][fight_style]["simulated_steps"][String(Number(itemlevel_position) + 1)]] == 0 || !(loaded_data[chosen_class][chosen_spec][data_name][fight_style]["simulated_steps"][String(Number(itemlevel_position) + 1)] in loaded_data[chosen_class][chosen_spec][data_name][fight_style]["data"][data])) { itemlevel_dps_values.push(dps - loaded_data[chosen_class][chosen_spec][data_name][fight_style]["data"]["baseline"][min_ilevel]); } else { // standard case, next itemlevel is not zero and can be used to substract from the current value itemlevel_dps_values.push(dps - loaded_data[chosen_class][chosen_spec][data_name][fight_style]["data"][data][loaded_data[chosen_class][chosen_spec][data_name][fight_style]["simulated_steps"][String(Number(itemlevel_position) + 1)]]); } } } else { if (itemlevel in loaded_data[chosen_class][chosen_spec][data_name][fight_style]["data"][data]) { itemlevel_dps_values.push(dps); } else { itemlevel_dps_values.push(0); } } } standard_chart.addSeries({ color: ilevel_color_table[itemlevel], data: itemlevel_dps_values, name: itemlevel, showInLegend: true }, false); } } else { // if no itemlevels were used the dps values are exactly at the keys var dps_values = []; for (let category of dps_ordered_data) { dps_values.push(loaded_data[chosen_class][chosen_spec][data_view][fight_style]["data"][category]); } standard_chart.addSeries({ color: class_colors[chosen_class], data: dps_values, name: data_view, showInLegend: true }, false); } document.getElementById("chart").style.height = 200 + dps_ordered_data.length * 30 + "px"; standard_chart.setSize(document.getElementById("chart").style.width, document.getElementById("chart").style.height); standard_chart.redraw(); if (debug) console.log("call translate_chart from update_chart"); translate_chart(); } function update_trait_stacking_chart() { if (debug) console.log("update_trait_stacking_chart"); if ("sorted_data_keys_2" in loaded_data[chosen_class][chosen_spec][data_view][fight_style]) { var dps_ordered_data = []; if (data_view === "azerite_traits" && ["itemlevel", "trait_stacking"].includes(chosen_azerite_list_type)) { dps_ordered_data = loaded_data[chosen_class][chosen_spec][data_view][fight_style]["sorted_azerite_tier_" + chosen_azerite_tier + "_" + chosen_azerite_list_type]; } else { dps_ordered_data = loaded_data[chosen_class][chosen_spec][data_view][fight_style]["sorted_data_keys"]; } } else { if (debug) console.log("Getting sorted_data_keys from data failed. Set unordered dps_ordered_data"); var dps_ordered_data = Object.keys(loaded_data[chosen_class][chosen_spec][data_view][fight_style]["data"]); } // change item/spell names to wowhead links let ordered_trinket_list = []; for (let i in dps_ordered_data) { let string = "" + dps_ordered_data[i] + ""; ordered_trinket_list.push(string); } // rewrite the trinket names if (debug) { console.log("applying ordered_trinket_list to categories in update_trait_stacking_chart"); } standard_chart.update({ xAxis: { categories: ordered_trinket_list } }, false); // set title and subtitle standard_chart.setTitle({ text: "Same itemlevel; different number of traits" }, { text: loaded_data[chosen_class][chosen_spec][data_view][fight_style]["subtitle"] }, false); // delete all old series data while (standard_chart.series[0]) { standard_chart.series[0].remove(false); } // basically: if something was simmed with multiple itemlevels for (let stack_count of [3, 2, 1]) { let max_itemlevel = loaded_data[chosen_class][chosen_spec][data_view][fight_style]["simulated_steps"][0].split("_")[1]; let stack_name = stack_count + "_" + max_itemlevel; let itemlevel_dps_values = []; if (debug) console.log("handling stack_name " + stack_name); // create series input for highcharts for (data of dps_ordered_data) { let dps = loaded_data[chosen_class][chosen_spec][data_view][fight_style]["data"][data][stack_name]; let baseline_dps = loaded_data[chosen_class][chosen_spec][data_view][fight_style]["data"]["baseline"]["1_" + max_itemlevel]; // check for zero dps values and don't change them if (dps > 0) { // if lowest itemlevel is looked at, substract baseline if (stack_count === 1) { itemlevel_dps_values.push(dps - baseline_dps); } else { // else substract lower itemlevel value of same trait itemlevel_dps_values.push(dps - loaded_data[chosen_class][chosen_spec][data_view][fight_style]["data"][data][stack_count - 1 + "_" + max_itemlevel]); } } else { if (stack_name in loaded_data[chosen_class][chosen_spec][data_view][fight_style]["data"][data]) { itemlevel_dps_values.push(dps); } else { itemlevel_dps_values.push(0); } } } standard_chart.addSeries({ color: ilevel_color_table[stack_name], data: itemlevel_dps_values, name: stack_name, showInLegend: true }, false); } document.getElementById("chart").style.height = 200 + dps_ordered_data.length * 30 + "px"; standard_chart.setSize(document.getElementById("chart").style.width, document.getElementById("chart").style.height); standard_chart.redraw(); if (debug) console.log("call translate_chart from update_trait_stacking_chart"); translate_chart(); } function empty_charts() { while (standard_chart.series[0]) { standard_chart.series[0].remove(false); } standard_chart.setTitle({ //text: loaded_data[chosen_class][chosen_spec][data_view][fight_style]["title"] }, { text: "No data available / Loading..." } ); // delete all old series data while (scatter_chart.series[0]) { scatter_chart.series[0].remove(false); } scatter_chart.setTitle({ //text: loaded_data[chosen_class][chosen_spec][data_view][fight_style]["title"] }, { text: "No data available / Loading..." } ); } /** * Capitalize all first letters in a string. * Example: string_test -> String_Test */ function capitalize_first_letters(string) { if (debug) console.log("capitalize_first_letters"); var new_string = string.charAt(0).toUpperCase(); if (string.indexOf("_") > -1) { new_string += string.slice(1, string.indexOf("_") + 1); new_string += capitalize_first_letters(string.slice(string.indexOf("_") + 1)); } else { new_string += string.slice(1); } return new_string; } /** * Update data header, triggers TC area appropriate hide and show. */ function update_page_content() { if (debug) console.log("update_page_content"); if (chosen_class === "" || chosen_spec === "") { debug && console.log("update_page_content aborted. No class or spec found.") return; } // update title var content = "" + capitalize_first_letters(chosen_class).replace("_", " ") + ": " + capitalize_first_letters(chosen_spec).replace("_", " ") + ""; document.getElementById("data_header").innerHTML = content; // update TC resource // hide tc-boxes var boxes = document.getElementsByClassName("tc-box"); for (let index = 0; index < boxes.length; index++) { const element = boxes[index]; element.hidden = true; } // show appropriate tc box document.getElementById("tc_" + chosen_class + "_" + chosen_spec).hidden = false; } /** * Constructs and returns the current state as url-string. */ function create_link() { var path = window.location.origin; path += window.location.pathname; if (chosen_class === "") { return path; } path += "#" + chosen_class; path += "_" + chosen_spec; path += "?data_view=" + data_view; if (data_view == "azerite_traits") { path += "&type=" + chosen_azerite_list_type; } if (chosen_azerite_list_type === "itemlevel" || chosen_azerite_list_type === "trait_stacking") { path += "&tier=" + chosen_azerite_tier; } path += "&fight_style=" + fight_style; path += "&lang=" + language; return path; } // ?data_view=trinkets&fight_style=patchwerk function copy_link() { if (debug) console.log("copy_link"); var path = create_link(); let link_helper = document.getElementById("chart_link_generator"); link_helper.innerHTML = path; link_helper.style.display = "block"; window.getSelection().selectAllChildren(link_helper); document.execCommand("copy"); link_helper.style.display = "none"; let success_message = document.getElementById("copy_success"); success_message.className = "show"; setTimeout(function () { success_message.className = success_message.className.replace("show", ""); }, 3000); } /** * Scatter chart for secondary stat distributions */ var scatter_chart = new Highcharts.Chart({ chart: { renderTo: 'scatter_plot_chart', type: "scatter3d", backgroundColor: null, animation: false, height: 800, width: 800, options3d: { enabled: true, alpha: 10, beta: 30, depth: 800, fitToPlot: false, } }, legend: { enabled: true, backgroundColor: dark_color, borderColor: medium_color, borderWidth: 1, align: "right", verticalAlign: "middle", layout: "vertical", itemStyle: { "color": light_color }, itemHoverStyle: { "color": light_color } }, plotOptions: { series: { dataLabels: { allowOverlap: true, style: { color: light_color, fontSize: font_size, fontWeight: "400", textOutline: "" } }, events: { legendItemClick: function () { return false; } }, }, }, series: [], title: { text: "", //"Title placeholder", useHTML: true, style: { color: light_color } }, subtitle: { text: "Data not found", useHTML: true, style: { color: light_color, fontSize: font_size } }, tooltip: { headerFormat: '', pointFormatter: function () { return '\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \
AbsoluteRelative
DPS' + Intl.NumberFormat().format(this.dps) + '' + Math.round(this.dps / this.dps_max * 10000) / 100 + '%
Crit' + Intl.NumberFormat().format(this.stat_crit) + '' + this.name.split("_")[0] + '%
Haste' + Intl.NumberFormat().format(this.stat_haste) + '' + this.name.split("_")[1] + '%
Mastery' + Intl.NumberFormat().format(this.stat_mastery) + '' + this.name.split("_")[2] + '%
Versatility' + Intl.NumberFormat().format(this.stat_vers) + '' + this.name.split("_")[3] + '%
'; }, useHTML: true, borderColor: dark_color, }, xAxis: { min: 0, max: 80, tickInterval: 20, startOnTick: true, endOnTick: true, title: "", labels: { enabled: false, }, gridLineWidth: 1, gridLineColor: medium_color, }, yAxis: { min: -10, max: 70, tickInterval: 20, startOnTick: true, endOnTick: true, title: "", labels: { enabled: false, }, gridLineWidth: 1, gridLineColor: medium_color, }, zAxis: { min: 10, max: 90, tickInterval: 20, startOnTick: true, endOnTick: true, title: "", labels: { enabled: false, }, reversed: true, gridLineWidth: 1, gridLineColor: medium_color, }, }); // Add mouse and touch events for rotation (function (H) { function dragStart(eStart) { eStart = scatter_chart.pointer.normalize(eStart); var posX = eStart.chartX, posY = eStart.chartY, alpha = scatter_chart.options.chart.options3d.alpha, beta = scatter_chart.options.chart.options3d.beta, sensitivity = 5; // lower is more sensitive function drag(e) { // Get e.chartX and e.chartY e = scatter_chart.pointer.normalize(e); scatter_chart.update({ chart: { options3d: { alpha: alpha + (e.chartY - posY) / sensitivity, beta: beta + (posX - e.chartX) / sensitivity } } }, undefined, undefined, false); } scatter_chart.unbindDragMouse = H.addEvent(document, 'mousemove', drag); scatter_chart.unbindDragTouch = H.addEvent(document, 'touchmove', drag); H.addEvent(document, 'mouseup', scatter_chart.unbindDragMouse); H.addEvent(document, 'touchend', scatter_chart.unbindDragTouch); } H.addEvent(scatter_chart.container, 'mousedown', dragStart); H.addEvent(scatter_chart.container, 'touchstart', dragStart); }(Highcharts)); /** * Creates the rgb color array for the dps of a marker. * * @param {Int} dps * @param {Int} min_dps * @param {Int} max_dps */ function create_color(dps, min_dps, max_dps) { if (debug) console.log("create_color"); // colour of lowest DPS let color_min = [0, 255, 255]; // additional colour step between min and max let color_mid = [255, 255, 0]; // colour of max dps let color_max = [255, 0, 0]; // calculate the position of the mid colour in this relation to ensure a smooth colour transition (colour distance...if something like this exists) between the three let diff_mid_max = 0; let diff_min_mid = 0; for (let i = 0; i < 3; i++) { diff_mid_max += Math.abs(color_max[i] - color_mid[i]); diff_min_mid += Math.abs(color_mid[i] - color_min[i]); } // ratio from min to max to describe the position of the id colour let mid_ratio = diff_min_mid / (diff_min_mid + diff_mid_max); // mid dps resulting from the ratio let mid_dps = min_dps + (max_dps - min_dps) * mid_ratio; // calculate colour based on relative dps if (dps >= mid_dps) { let percent_of_max = (dps - mid_dps) / (max_dps - mid_dps); return [ Math.floor(color_max[0] * percent_of_max + color_mid[0] * (1 - percent_of_max)), Math.floor(color_max[1] * percent_of_max + color_mid[1] * (1 - percent_of_max)), Math.floor(color_max[2] * percent_of_max + color_mid[2] * (1 - percent_of_max)) ]; } else { let percent_of_mid = (dps - min_dps) / (mid_dps - min_dps); return [ Math.floor(color_mid[0] * percent_of_mid + color_min[0] * (1 - percent_of_mid)), Math.floor(color_mid[1] * percent_of_mid + color_min[1] * (1 - percent_of_mid)), Math.floor(color_mid[2] * percent_of_mid + color_min[2] * (1 - percent_of_mid)) ]; } } /** * Creates a series based on the loaded data and pushes it into the scatter chart */ function update_scatter_chart() { if (debug) console.log("update_scatter_chart"); // get max dps of the whole data set let max_dps = loaded_data[chosen_class][chosen_spec][data_view][fight_style]["data"][chosen_talent_combination][loaded_data[chosen_class][chosen_spec][data_view][fight_style]["sorted_data_keys"][chosen_talent_combination][0]]; // get min dps of the whole data set let min_dps = loaded_data[chosen_class][chosen_spec][data_view][fight_style]["data"][chosen_talent_combination][loaded_data[chosen_class][chosen_spec][data_view][fight_style]["sorted_data_keys"][chosen_talent_combination][loaded_data[chosen_class][chosen_spec][data_view][fight_style]["sorted_data_keys"][chosen_talent_combination].length - 1]]; // prepare series with standard data let series = { name: Intl.NumberFormat().format(max_dps) + " DPS", color: "#FF0000", // make sure this matches the value of color_max in create_color(...) data: [] }; // add a marker for each distribution in the data set for (let distribution of Object.keys(loaded_data[chosen_class][chosen_spec][data_view][fight_style]["data"][chosen_talent_combination])) { let talent_data_distribution = loaded_data[chosen_class][chosen_spec][data_view][fight_style]["data"][chosen_talent_combination][distribution]; // get the markers color let color_set = create_color( talent_data_distribution, min_dps, max_dps ); // width of the border of the marker, 0 for all markers but the max, which gets 3 let line_width = 1; let line_color = "#232227"; // adjust marker radius depending on distance to max // worst dps: 2 // max dps: 5 (increased to 8 to fit the additional border) let radius = 2 + 3 * (talent_data_distribution - min_dps) / (max_dps - min_dps); if (max_dps === talent_data_distribution) { line_width = 3; radius = 8; line_color = light_color; } // undefined data label for all markers unless they are the "max" values let data_label = undefined; // 70 is the max possible value in data. would need adjustement if data changes to other max values. But I doubt this'll happen. if (distribution.indexOf("70") > -1) { data_label = { enabled: true, allowOverlap: true, }; switch (distribution.indexOf("70")) { case 0: // "70_10_10_10" data_label.format = "Crit"; data_label.verticalAlign = "top"; break; case 3: // "10_70_10_10" data_label.format = "Haste"; break; case 6: // "10_10_70_10" data_label.format = "Mastery"; data_label.verticalAlign = "top"; break; case 9: // "10_10_10_70" data_label.format = "Versatility"; data_label.verticalAlign = "top"; break; default: // how did we even end up here? break; } } const secondary_sum = loaded_data[chosen_class][chosen_spec][data_view][fight_style]["secondary_sum"]; // push marker data into the series series.data.push({ // formulas slowly snailed together from combining different relations within https://en.wikipedia.org/wiki/Equilateral_triangle and https://en.wikipedia.org/wiki/Pythagorean_theorem x: Math.sqrt(3) / 2 * (parseInt(distribution.split("_")[0]) + 1 / 3 * parseInt(distribution.split("_")[1])), y: Math.sqrt(2 / 3) * parseInt(distribution.split("_")[1]), z: parseInt(distribution.split("_")[2]) + 0.5 * parseInt(distribution.split("_")[0]) + 0.5 * parseInt(distribution.split("_")[1]), name: distribution, // flat markers with dark border (borders are prepared further up) color: "rgb(" + color_set[0] + "," + color_set[1] + "," + color_set[2] + ")", // 3d markers with light area and shadow at the opposite side // color: { // radialGradient: { // cx: 0.4, // cy: 0.3, // r: 0.5 // }, // stops: [ // //[0, "rgb(" + color_set[0] + "," + color_set[1] + "," + color_set[2] + ")"], // [0, Highcharts.Color('rgb(' + color_set[0] + ',' + color_set[1] + ',' + color_set[2] + ')').brighten(0.4).get('rgb')], // [1, Highcharts.Color('rgb(' + color_set[0] + ',' + color_set[1] + ',' + color_set[2] + ')').brighten(-0.4).get('rgb')] // ] // }, // add additional information required for tooltips dps: talent_data_distribution, dps_max: max_dps, dps_min: min_dps, stat_crit: parseInt(distribution.split("_")[0]) * secondary_sum / 100, stat_haste: parseInt(distribution.split("_")[1]) * secondary_sum / 100, stat_mastery: parseInt(distribution.split("_")[2]) * secondary_sum / 100, stat_vers: parseInt(distribution.split("_")[3]) * secondary_sum / 100, stat_sum: secondary_sum, // add marker information marker: { radius: radius, lineColor: line_color, lineWidth: line_width }, // add visible data labels (crit, haste, mastery, vers) dataLabels: data_label, }); } // delete all old series data while (scatter_chart.series[0]) { scatter_chart.series[0].remove(false); } scatter_chart.addSeries(series, false); // make sure this color matches the value of color_min in create_color(...) scatter_chart.addSeries({ name: Intl.NumberFormat().format(min_dps) + " DPS", color: "#00FFFF" }, false); scatter_chart.setTitle({ //text: loaded_data[chosen_class][chosen_spec][data_view][fight_style]["title"] }, { text: loaded_data[chosen_class][chosen_spec][data_view][fight_style]["subtitle"] } ); scatter_chart.redraw(); } /****************************************************************************** * * Last content block. These functions trigger onfinished load. * */ /** Look for the dark mode cookie and update view */ document.addEventListener("DOMContentLoaded", search_dark_mode_cookie); /** Load language from cookie. */ document.addEventListener("DOMContentLoaded", search_language_cookie); /** Load spec and data mode if a spec link was used. */ document.addEventListener("DOMContentLoaded", function () { if (debug) console.log("interprete link"); get_data_from_link(); if (chosen_spec !== "") { switch_mode(); } });