|
|
@@ -0,0 +1,1825 @@
|
|
|
+// src/permalink.ts
|
|
|
+function addPermalink() {
|
|
|
+ const permalinkContainer = document.getElementById("permalinkContainer");
|
|
|
+ const permalinkButton = document.getElementById("permalinkButton");
|
|
|
+ const permalinkCopyButton = document.getElementById("permalinkCopyButton");
|
|
|
+ const rprunCopyButton = document.getElementById("permalinkCopyButton-rprun");
|
|
|
+ const permalinkOptionsButton = document.getElementById("hideOptions");
|
|
|
+ const permalinkLatestMonth = document.getElementById("latestMonth");
|
|
|
+ permalinkButton?.addEventListener("click", function(e) {
|
|
|
+ e.stopPropagation();
|
|
|
+ const currentDisplay = permalinkContainer.style.display;
|
|
|
+ if (currentDisplay == "none") {
|
|
|
+ permalinkContainer.style.display = "block";
|
|
|
+ } else {
|
|
|
+ permalinkContainer.style.display = "none";
|
|
|
+ }
|
|
|
+ });
|
|
|
+ document.addEventListener("click", function(e) {
|
|
|
+ if (!permalinkContainer.contains(e.target) && !permalinkButton.contains(e.target)) {
|
|
|
+ permalinkContainer.style.display = "none";
|
|
|
+ }
|
|
|
+ });
|
|
|
+ permalinkCopyButton.addEventListener("click", function() {
|
|
|
+ const permalinkElem = document.getElementById("permalink");
|
|
|
+ if (permalinkElem.value && permalinkElem.value != "") {
|
|
|
+ navigator.clipboard.writeText(permalinkElem.value);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ rprunCopyButton.addEventListener("click", function() {
|
|
|
+ const permalinkElem = document.getElementById("permalink-rprun");
|
|
|
+ if (permalinkElem.value && permalinkElem.value != "") {
|
|
|
+ navigator.clipboard.writeText(permalinkElem.value);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ permalinkOptionsButton.addEventListener("change", function() {
|
|
|
+ updatePermalink();
|
|
|
+ });
|
|
|
+ permalinkLatestMonth.addEventListener("change", function() {
|
|
|
+ updatePermalink();
|
|
|
+ });
|
|
|
+}
|
|
|
+function updatePermalink() {
|
|
|
+ const graphSelect = document.getElementById("graphType");
|
|
|
+ const permalinkInput = document.getElementById("permalink");
|
|
|
+ const rprunInput = document.getElementById("permalink-rprun");
|
|
|
+ const hideOptionsButton = document.getElementById("hideOptions");
|
|
|
+ const latestMonthButton = document.getElementById("latestMonth");
|
|
|
+ var permalink = "https://prun.raylu.net/stats/?type=" + graphSelect.value;
|
|
|
+ var rprunLink = "XIT PRUNSTATS type-" + graphSelect.value;
|
|
|
+ const graph = graphs.find((obj) => obj.id == graphSelect.value);
|
|
|
+ graph?.configFieldIDs.forEach((subtype) => {
|
|
|
+ if (subtype == "month" && latestMonthButton.checked) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const inputElem = document.getElementById(subtype);
|
|
|
+ if (inputElem.value && inputElem.value != "") {
|
|
|
+ permalink += "&" + subtype + "=" + inputElem.value;
|
|
|
+ rprunLink += " " + subtype + "-" + inputElem.value;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ if (hideOptionsButton.checked) {
|
|
|
+ permalink += "&hideOptions";
|
|
|
+ rprunLink += " hideOptions";
|
|
|
+ }
|
|
|
+ permalinkInput.value = permalink;
|
|
|
+ rprunInput.value = rprunLink;
|
|
|
+ return;
|
|
|
+}
|
|
|
+
|
|
|
+// src/staticData/constants.ts
|
|
|
+var months = ["mar25", "apr25", "may25", "jun25", "jul25", "aug25", "sep25", "oct25", "nov25", "dec25", "jan26", "feb26", "mar26", "apr26", "may26", "jun26"];
|
|
|
+var monthsPretty = ["March 3025", "April 3025", "May 3025", "June 3025", "July 3025", "August 3025", "September 3025", "October 3025", "November 3025", "December 3025", "January 3026", "February 3026", "March 3026", "April 3026", "May 3026", "June 3026"];
|
|
|
+var fullMonthNames = {
|
|
|
+ jan: "January",
|
|
|
+ feb: "February",
|
|
|
+ mar: "March",
|
|
|
+ apr: "April",
|
|
|
+ may: "May",
|
|
|
+ jun: "June",
|
|
|
+ jul: "July",
|
|
|
+ aug: "August",
|
|
|
+ sep: "September",
|
|
|
+ oct: "October",
|
|
|
+ nov: "November",
|
|
|
+ dec: "December"
|
|
|
+};
|
|
|
+var prettyModeNames = {
|
|
|
+ amount: "Amount",
|
|
|
+ profit: "Profit",
|
|
|
+ volume: "Volume",
|
|
|
+ price: "Price",
|
|
|
+ deficit: "Deficit",
|
|
|
+ bases: "Bases",
|
|
|
+ ships: "Ships"
|
|
|
+};
|
|
|
+var materialCategories = {
|
|
|
+ "Consumables (Luxury)": [
|
|
|
+ "ALE",
|
|
|
+ "COF",
|
|
|
+ "GIN",
|
|
|
+ "KOM",
|
|
|
+ "NST",
|
|
|
+ "PWO",
|
|
|
+ "REP",
|
|
|
+ "SC",
|
|
|
+ "VG",
|
|
|
+ "WIN"
|
|
|
+ ],
|
|
|
+ "Ship Engines": [
|
|
|
+ "AEN",
|
|
|
+ "AFP",
|
|
|
+ "AFR",
|
|
|
+ "ANZ",
|
|
|
+ "BFP",
|
|
|
+ "BFR",
|
|
|
+ "CHA",
|
|
|
+ "ENG",
|
|
|
+ "FIR",
|
|
|
+ "FSE",
|
|
|
+ "GCH",
|
|
|
+ "GEN",
|
|
|
+ "GNZ",
|
|
|
+ "HNZ",
|
|
|
+ "HPR",
|
|
|
+ "HTE",
|
|
|
+ "HYR",
|
|
|
+ "LFE",
|
|
|
+ "LFP",
|
|
|
+ "MFE",
|
|
|
+ "NOZ",
|
|
|
+ "QCR",
|
|
|
+ "RAG",
|
|
|
+ "RCS",
|
|
|
+ "RCT",
|
|
|
+ "SFE",
|
|
|
+ "VOE",
|
|
|
+ "VOR"
|
|
|
+ ],
|
|
|
+ "Software Tools": [
|
|
|
+ "DA",
|
|
|
+ "DD",
|
|
|
+ "DV",
|
|
|
+ "EDC",
|
|
|
+ "NN",
|
|
|
+ "OS"
|
|
|
+ ],
|
|
|
+ "Construction Parts": [
|
|
|
+ "AEF",
|
|
|
+ "AIR",
|
|
|
+ "DEC",
|
|
|
+ "FC",
|
|
|
+ "FLO",
|
|
|
+ "FLP",
|
|
|
+ "GC",
|
|
|
+ "GV",
|
|
|
+ "LIT",
|
|
|
+ "MGC",
|
|
|
+ "MHL",
|
|
|
+ "PSH",
|
|
|
+ "RSH",
|
|
|
+ "TCS",
|
|
|
+ "TRU",
|
|
|
+ "TSH"
|
|
|
+ ],
|
|
|
+ Alloys: [
|
|
|
+ "ALR",
|
|
|
+ "AST",
|
|
|
+ "BGO",
|
|
|
+ "BOS",
|
|
|
+ "BRO",
|
|
|
+ "FAL",
|
|
|
+ "FET",
|
|
|
+ "RGO",
|
|
|
+ "WAL",
|
|
|
+ "WRH"
|
|
|
+ ],
|
|
|
+ "Consumable Bundles": [
|
|
|
+ "CBU",
|
|
|
+ "EBU",
|
|
|
+ "PBU",
|
|
|
+ "SBU",
|
|
|
+ "TBU"
|
|
|
+ ],
|
|
|
+ "Medical Equipment": [
|
|
|
+ "ADR",
|
|
|
+ "BND",
|
|
|
+ "PK",
|
|
|
+ "SEQ",
|
|
|
+ "STR",
|
|
|
+ "TUB"
|
|
|
+ ],
|
|
|
+ "Electronic Parts": [
|
|
|
+ "CD",
|
|
|
+ "DIS",
|
|
|
+ "FAN",
|
|
|
+ "MB",
|
|
|
+ "MPC",
|
|
|
+ "PCB",
|
|
|
+ "RAM",
|
|
|
+ "ROM",
|
|
|
+ "SEN",
|
|
|
+ "TPU",
|
|
|
+ "TRA"
|
|
|
+ ],
|
|
|
+ "Energy Systems": [
|
|
|
+ "CBL",
|
|
|
+ "CBM",
|
|
|
+ "CBS",
|
|
|
+ "POW",
|
|
|
+ "SOL",
|
|
|
+ "SP"
|
|
|
+ ],
|
|
|
+ Minerals: [
|
|
|
+ "BER",
|
|
|
+ "BOR",
|
|
|
+ "BRM",
|
|
|
+ "CLI",
|
|
|
+ "GAL",
|
|
|
+ "HAL",
|
|
|
+ "LST",
|
|
|
+ "MAG",
|
|
|
+ "MGS",
|
|
|
+ "SCR",
|
|
|
+ "TAI",
|
|
|
+ "TCO",
|
|
|
+ "TS",
|
|
|
+ "ZIR"
|
|
|
+ ],
|
|
|
+ "Construction Materials": [
|
|
|
+ "CMK",
|
|
|
+ "EPO",
|
|
|
+ "GL",
|
|
|
+ "INS",
|
|
|
+ "MCG",
|
|
|
+ "MTC",
|
|
|
+ "NCS",
|
|
|
+ "NFI",
|
|
|
+ "NG",
|
|
|
+ "RG",
|
|
|
+ "SEA"
|
|
|
+ ],
|
|
|
+ "Consumables (Basic)": [
|
|
|
+ "DW",
|
|
|
+ "EXO",
|
|
|
+ "FIM",
|
|
|
+ "HMS",
|
|
|
+ "HSS",
|
|
|
+ "LC",
|
|
|
+ "MEA",
|
|
|
+ "MED",
|
|
|
+ "OVE",
|
|
|
+ "PDA",
|
|
|
+ "PT",
|
|
|
+ "RAT",
|
|
|
+ "SCN",
|
|
|
+ "WS"
|
|
|
+ ],
|
|
|
+ "Software Systems": [
|
|
|
+ "IDC",
|
|
|
+ "IMM",
|
|
|
+ "SNM",
|
|
|
+ "WAI"
|
|
|
+ ],
|
|
|
+ "Electronic Pieces": [
|
|
|
+ "BCO",
|
|
|
+ "BGC",
|
|
|
+ "CAP",
|
|
|
+ "HCC",
|
|
|
+ "LDI",
|
|
|
+ "MFK",
|
|
|
+ "MWF",
|
|
|
+ "SFK",
|
|
|
+ "SWF",
|
|
|
+ "TRN"
|
|
|
+ ],
|
|
|
+ "Software Components": [
|
|
|
+ "BAI",
|
|
|
+ "LD",
|
|
|
+ "MLI",
|
|
|
+ "NF",
|
|
|
+ "SA",
|
|
|
+ "SAL",
|
|
|
+ "WM"
|
|
|
+ ],
|
|
|
+ Ores: [
|
|
|
+ "ALO",
|
|
|
+ "AUO",
|
|
|
+ "CUO",
|
|
|
+ "FEO",
|
|
|
+ "LIO",
|
|
|
+ "REO",
|
|
|
+ "SIO",
|
|
|
+ "TIO",
|
|
|
+ "REO"
|
|
|
+ ],
|
|
|
+ "Unit Prefabs": [
|
|
|
+ "BR1",
|
|
|
+ "BR2",
|
|
|
+ "BRS",
|
|
|
+ "BSU",
|
|
|
+ "CPU",
|
|
|
+ "CQL",
|
|
|
+ "CQM",
|
|
|
+ "CQS",
|
|
|
+ "CQT",
|
|
|
+ "DOU",
|
|
|
+ "FUN",
|
|
|
+ "HAB",
|
|
|
+ "LU",
|
|
|
+ "RDL",
|
|
|
+ "RDS",
|
|
|
+ "SU",
|
|
|
+ "TCU",
|
|
|
+ "WOR",
|
|
|
+ "BSU",
|
|
|
+ "CPU"
|
|
|
+ ],
|
|
|
+ "Ship Shields": [
|
|
|
+ "APT",
|
|
|
+ "ARP",
|
|
|
+ "AWH",
|
|
|
+ "BPT",
|
|
|
+ "BRP",
|
|
|
+ "BWH",
|
|
|
+ "SRP"
|
|
|
+ ],
|
|
|
+ "Electronic Devices": [
|
|
|
+ "AAR",
|
|
|
+ "AWF",
|
|
|
+ "BID",
|
|
|
+ "BMF",
|
|
|
+ "BSC",
|
|
|
+ "BWS",
|
|
|
+ "HD",
|
|
|
+ "HOG",
|
|
|
+ "HPC",
|
|
|
+ "MHP",
|
|
|
+ "RAD",
|
|
|
+ "SAR"
|
|
|
+ ],
|
|
|
+ Metals: [
|
|
|
+ "AL",
|
|
|
+ "AU",
|
|
|
+ "CU",
|
|
|
+ "FE",
|
|
|
+ "LI",
|
|
|
+ "RE",
|
|
|
+ "SI",
|
|
|
+ "STL",
|
|
|
+ "TI",
|
|
|
+ "W",
|
|
|
+ "RE"
|
|
|
+ ],
|
|
|
+ "Electronic Systems": [
|
|
|
+ "ACS",
|
|
|
+ "ADS",
|
|
|
+ "CC",
|
|
|
+ "COM",
|
|
|
+ "CRU",
|
|
|
+ "FFC",
|
|
|
+ "LIS",
|
|
|
+ "LOG",
|
|
|
+ "STS",
|
|
|
+ "TAC",
|
|
|
+ "WR"
|
|
|
+ ],
|
|
|
+ Textiles: [
|
|
|
+ "CF",
|
|
|
+ "COT",
|
|
|
+ "CTF",
|
|
|
+ "KV",
|
|
|
+ "NL",
|
|
|
+ "SIL",
|
|
|
+ "TK"
|
|
|
+ ],
|
|
|
+ Plastics: [
|
|
|
+ "DCL",
|
|
|
+ "DCM",
|
|
|
+ "DCS",
|
|
|
+ "PE",
|
|
|
+ "PG",
|
|
|
+ "PSL",
|
|
|
+ "PSM",
|
|
|
+ "PSS"
|
|
|
+ ],
|
|
|
+ Chemicals: [
|
|
|
+ "BAC",
|
|
|
+ "BL",
|
|
|
+ "BLE",
|
|
|
+ "CST",
|
|
|
+ "DDT",
|
|
|
+ "EES",
|
|
|
+ "ETC",
|
|
|
+ "FLX",
|
|
|
+ "IND",
|
|
|
+ "JUI",
|
|
|
+ "KRE",
|
|
|
+ "LCR",
|
|
|
+ "NAB",
|
|
|
+ "NR",
|
|
|
+ "NS",
|
|
|
+ "OLF",
|
|
|
+ "PFE",
|
|
|
+ "REA",
|
|
|
+ "SOI",
|
|
|
+ "TCL",
|
|
|
+ "THF",
|
|
|
+ "KRE"
|
|
|
+ ],
|
|
|
+ Elements: [
|
|
|
+ "BE",
|
|
|
+ "C",
|
|
|
+ "CA",
|
|
|
+ "CL",
|
|
|
+ "ES",
|
|
|
+ "I",
|
|
|
+ "MG",
|
|
|
+ "NA",
|
|
|
+ "S",
|
|
|
+ "TA",
|
|
|
+ "TC",
|
|
|
+ "ZR"
|
|
|
+ ],
|
|
|
+ Gases: [
|
|
|
+ "AMM",
|
|
|
+ "AR",
|
|
|
+ "F",
|
|
|
+ "H",
|
|
|
+ "HE",
|
|
|
+ "HE3",
|
|
|
+ "KR",
|
|
|
+ "N",
|
|
|
+ "NE",
|
|
|
+ "O",
|
|
|
+ "KR"
|
|
|
+ ],
|
|
|
+ "Ship Parts": [
|
|
|
+ "AGS",
|
|
|
+ "AHP",
|
|
|
+ "ATP",
|
|
|
+ "BGS",
|
|
|
+ "BHP",
|
|
|
+ "HAM",
|
|
|
+ "HHP",
|
|
|
+ "LHP",
|
|
|
+ "NV1",
|
|
|
+ "NV2",
|
|
|
+ "RHP",
|
|
|
+ "SSC",
|
|
|
+ "THP",
|
|
|
+ "HAM"
|
|
|
+ ],
|
|
|
+ Drones: [
|
|
|
+ "CCD",
|
|
|
+ "DCH",
|
|
|
+ "DRF",
|
|
|
+ "RED",
|
|
|
+ "SDR",
|
|
|
+ "SRD",
|
|
|
+ "SUD"
|
|
|
+ ],
|
|
|
+ "Agricultural Products": [
|
|
|
+ "ALG",
|
|
|
+ "BEA",
|
|
|
+ "CAF",
|
|
|
+ "FOD",
|
|
|
+ "GRA",
|
|
|
+ "GRN",
|
|
|
+ "HCP",
|
|
|
+ "HER",
|
|
|
+ "HOP",
|
|
|
+ "MAI",
|
|
|
+ "MTP",
|
|
|
+ "MUS",
|
|
|
+ "NUT",
|
|
|
+ "PIB",
|
|
|
+ "PPA",
|
|
|
+ "RCO",
|
|
|
+ "RSI",
|
|
|
+ "VEG",
|
|
|
+ "VIT"
|
|
|
+ ],
|
|
|
+ "Construction Prefabs": [
|
|
|
+ "ABH",
|
|
|
+ "ADE",
|
|
|
+ "ASE",
|
|
|
+ "ATA",
|
|
|
+ "BBH",
|
|
|
+ "BDE",
|
|
|
+ "BSE",
|
|
|
+ "BTA",
|
|
|
+ "HSE",
|
|
|
+ "LBH",
|
|
|
+ "LDE",
|
|
|
+ "LSE",
|
|
|
+ "LTA",
|
|
|
+ "RBH",
|
|
|
+ "RDE",
|
|
|
+ "RSE",
|
|
|
+ "RTA"
|
|
|
+ ],
|
|
|
+ Fuels: [
|
|
|
+ "FF",
|
|
|
+ "SF",
|
|
|
+ "VF"
|
|
|
+ ],
|
|
|
+ "Ship Kits": [
|
|
|
+ "HCB",
|
|
|
+ "LCB",
|
|
|
+ "LFL",
|
|
|
+ "LSL",
|
|
|
+ "MCB",
|
|
|
+ "MFL",
|
|
|
+ "MSL",
|
|
|
+ "SCB",
|
|
|
+ "SFL",
|
|
|
+ "SSL",
|
|
|
+ "TCB",
|
|
|
+ "VCB",
|
|
|
+ "VFT",
|
|
|
+ "VSC",
|
|
|
+ "WCB",
|
|
|
+ "VFT"
|
|
|
+ ],
|
|
|
+ Liquids: [
|
|
|
+ "BTS",
|
|
|
+ "H2O",
|
|
|
+ "HEX",
|
|
|
+ "LES"
|
|
|
+ ],
|
|
|
+ Utility: [
|
|
|
+ "OFF",
|
|
|
+ "SUN",
|
|
|
+ "UTS"
|
|
|
+ ],
|
|
|
+ Infrastructure: [
|
|
|
+ "GWS",
|
|
|
+ "PFG",
|
|
|
+ "SDM",
|
|
|
+ "SST",
|
|
|
+ "SPT",
|
|
|
+ "TRS",
|
|
|
+ "TOR"
|
|
|
+ ]
|
|
|
+};
|
|
|
+var materialCategoryColors = {
|
|
|
+ "Agricultural Products": "#0a4708",
|
|
|
+ Alloys: "#946537",
|
|
|
+ Chemicals: "#d04774",
|
|
|
+ "Construction Materials": "#3174ec",
|
|
|
+ "Construction Parts": "#426684",
|
|
|
+ "Construction Prefabs": "#28377b",
|
|
|
+ "Consumable Bundles": "#57232a",
|
|
|
+ "Consumables (Basic)": "#ba363c",
|
|
|
+ "Consumables (Luxury)": "#680000",
|
|
|
+ Drones: "#a54d2b",
|
|
|
+ "Electronic Devices": "#6f2dac",
|
|
|
+ "Electronic Parts": "#7447d0",
|
|
|
+ "Electronic Pieces": "#906bd6",
|
|
|
+ "Electronic Systems": "#4c3365",
|
|
|
+ Elements: "#564739",
|
|
|
+ "Energy Systems": "#2e5740",
|
|
|
+ Fuels: "#6ba23c",
|
|
|
+ Gases: "#198284",
|
|
|
+ Liquids: "#6098c3",
|
|
|
+ "Medical Equipment": "#6ec36e",
|
|
|
+ Metals: "#4f4f4f",
|
|
|
+ Minerals: "#b28a62",
|
|
|
+ Ores: "#6b707a",
|
|
|
+ Plastics: "#791f62",
|
|
|
+ "Ship Engines": "#b24219",
|
|
|
+ "Ship Kits": "#b26d19",
|
|
|
+ "Ship Parts": "#b27c19",
|
|
|
+ "Ship Shields": "#d98c21",
|
|
|
+ "Software Components": "#a19248",
|
|
|
+ "Software Systems": "#554e1e",
|
|
|
+ "Software Tools": "#9a7b2c",
|
|
|
+ Textiles: "#6b733a",
|
|
|
+ "Unit Prefabs": "#363435",
|
|
|
+ Utility: "#baada1",
|
|
|
+ Infrastructure: "#252551"
|
|
|
+};
|
|
|
+
|
|
|
+// src/utils.ts
|
|
|
+function addOption(selector, displayName, id) {
|
|
|
+ const optionElem = document.createElement("option");
|
|
|
+ optionElem.textContent = displayName;
|
|
|
+ optionElem.value = id ?? displayName;
|
|
|
+ selector.appendChild(optionElem);
|
|
|
+}
|
|
|
+function clearChildren(elem) {
|
|
|
+ elem.textContent = "";
|
|
|
+ while (elem.children[0]) {
|
|
|
+ elem.removeChild(elem.children[0]);
|
|
|
+ }
|
|
|
+ return;
|
|
|
+}
|
|
|
+function addConfigField(inputType, id, label, value, defaultValue, updateFunc, marginShift) {
|
|
|
+ const labelElem = document.createElement("label");
|
|
|
+ labelElem.textContent = label;
|
|
|
+ labelElem.id = id + "-label";
|
|
|
+ const inputElem = document.createElement(inputType);
|
|
|
+ inputElem.id = id;
|
|
|
+ inputElem.classList.add("plotSelector");
|
|
|
+ if (inputType == "select") {
|
|
|
+ addOptions(inputElem, value.prettyValues, value.values);
|
|
|
+ }
|
|
|
+ if (defaultValue) {
|
|
|
+ inputElem.value = defaultValue;
|
|
|
+ }
|
|
|
+ inputElem.addEventListener("change", updateFunc);
|
|
|
+ labelElem.appendChild(inputElem);
|
|
|
+ const output = wrapInDiv(labelElem);
|
|
|
+ if (marginShift) {
|
|
|
+ output.style.marginLeft = marginShift;
|
|
|
+ }
|
|
|
+ return output;
|
|
|
+}
|
|
|
+function wrapInDiv(elem) {
|
|
|
+ const div = document.createElement("div");
|
|
|
+ div.appendChild(elem);
|
|
|
+ return div;
|
|
|
+}
|
|
|
+function addOptions(selector, prettyValues, values) {
|
|
|
+ for (var i = 0;i < prettyValues.length; i++) {
|
|
|
+ const optionElem = document.createElement("option");
|
|
|
+ optionElem.textContent = prettyValues[i];
|
|
|
+ optionElem.value = values ? values[i] : prettyValues[i];
|
|
|
+ selector.appendChild(optionElem);
|
|
|
+ }
|
|
|
+}
|
|
|
+function deepMerge(target, source) {
|
|
|
+ for (const key in source) {
|
|
|
+ if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key])) {
|
|
|
+ target[key] = deepMerge(target[key] || {}, source[key]);
|
|
|
+ } else {
|
|
|
+ target[key] = source[key];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return target;
|
|
|
+}
|
|
|
+function prettyMonthName(monthStr) {
|
|
|
+ const monthAbv = monthStr.substring(0, 3);
|
|
|
+ const monthNum = monthStr.substring(3);
|
|
|
+ return fullMonthNames[monthAbv] + " 30" + monthNum;
|
|
|
+}
|
|
|
+async function getData(loadedData, dataType, month) {
|
|
|
+ switch (dataType) {
|
|
|
+ case "prod":
|
|
|
+ case "company":
|
|
|
+ case "base":
|
|
|
+ case "ship":
|
|
|
+ if (!loadedData[dataType + "-data-" + month]) {
|
|
|
+ const response = await fetch(`data/${dataType}-data-${month}.json`);
|
|
|
+ if (response.status == 200) {
|
|
|
+ loadedData[dataType + "-data-" + month] = response.json();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return loadedData[dataType + "-data-" + month];
|
|
|
+ case "knownCompanies":
|
|
|
+ if (!loadedData["known-companies"]) {
|
|
|
+ loadedData["known-companies"] = await fetch("data/knownCompanies.json").then((response) => response.json());
|
|
|
+ }
|
|
|
+ return loadedData["known-companies"];
|
|
|
+ case "universe":
|
|
|
+ if (!loadedData["universe-data"]) {
|
|
|
+ loadedData["universe-data"] = await fetch("data/universe-data.json").then((response) => response.json());
|
|
|
+ }
|
|
|
+ return loadedData["universe-data"];
|
|
|
+ case "parentCorps":
|
|
|
+ if (!loadedData["parent-corps"]) {
|
|
|
+ loadedData["parent-corps"] = await fetch("data/parentCorps.json").then((response) => response.json());
|
|
|
+ }
|
|
|
+ return loadedData["parent-corps"];
|
|
|
+ }
|
|
|
+}
|
|
|
+function getMatCategory(ticker) {
|
|
|
+ return Object.entries(materialCategories).find(([_, tickers]) => tickers.includes(ticker))?.[0];
|
|
|
+}
|
|
|
+function getMatColor(ticker) {
|
|
|
+ return materialCategoryColors[getMatCategory(ticker) ?? ""] ?? "#000000";
|
|
|
+}
|
|
|
+async function getCompanyId(companyName, loadedData) {
|
|
|
+ const knownCompanies = await getData(loadedData, "knownCompanies");
|
|
|
+ var companyID = Object.entries(knownCompanies).find(([, v]) => v?.Username?.toLowerCase() === companyName.toLowerCase())?.[0];
|
|
|
+ if (companyID) {
|
|
|
+ return companyID;
|
|
|
+ }
|
|
|
+ console.log("Unknown username, querying FIO.");
|
|
|
+ const fioResult = await fetch("https://rest.fnar.net/user/" + companyName).then((response) => response.json()).catch((error) => {
|
|
|
+ alert("Bad Response: Check Username");
|
|
|
+ console.error(error);
|
|
|
+ });
|
|
|
+ companyID = fioResult?.CompanyId;
|
|
|
+ companyName = fioResult?.UserName;
|
|
|
+ knownCompanies[companyID] = companyName;
|
|
|
+ return companyID;
|
|
|
+}
|
|
|
+function updateUsernameLabel() {
|
|
|
+ const usernameLabel = document.getElementById("companyName-label");
|
|
|
+ const groupElem = document.getElementById("group");
|
|
|
+ if (usernameLabel && usernameLabel.firstChild) {
|
|
|
+ if (groupElem && groupElem.value && groupElem.value != "company") {
|
|
|
+ usernameLabel.firstChild.nodeValue = "Corp Code: ";
|
|
|
+ } else {
|
|
|
+ usernameLabel.firstChild.nodeValue = "Username: ";
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// src/core.ts
|
|
|
+function switchPlot() {
|
|
|
+ const graphSelect = document.getElementById("graphType");
|
|
|
+ const graph = graphs.find((obj) => obj.id == graphSelect.value);
|
|
|
+ const oldGraph = document.getElementById("mainPlot");
|
|
|
+ oldGraph?.remove();
|
|
|
+ const newGraph = document.createElement("div");
|
|
|
+ newGraph.id = "mainPlot";
|
|
|
+ const graphContainer = document.getElementById("mainPlotContainer");
|
|
|
+ graphContainer?.appendChild(newGraph);
|
|
|
+ const configValues = {};
|
|
|
+ graph?.configFieldIDs.forEach((id) => {
|
|
|
+ const inputElem = document.getElementById(id);
|
|
|
+ configValues[id] = inputElem?.value;
|
|
|
+ });
|
|
|
+ updatePermalink();
|
|
|
+ (async () => {
|
|
|
+ graph?.generatePlot(configValues, "mainPlot");
|
|
|
+ })();
|
|
|
+}
|
|
|
+var defaultData = { marker: { color: "rgb(247, 166, 0)" }, hovertemplate: "%{x}: %{y:,.3~s}<extra></extra>" };
|
|
|
+var defaultLayout = {
|
|
|
+ title: {
|
|
|
+ font: { color: "#eee", family: '"Droid Sans", sans-serif' }
|
|
|
+ },
|
|
|
+ xaxis: {
|
|
|
+ title: {
|
|
|
+ font: { color: "#bbb", family: '"Droid Sans", sans-serif' }
|
|
|
+ },
|
|
|
+ tickfont: { color: "#666", family: '"Droid Sans", sans-serif' },
|
|
|
+ tickangle: -45
|
|
|
+ },
|
|
|
+ yaxis: {
|
|
|
+ title: {
|
|
|
+ font: { color: "#bbb", family: '"Droid Sans", sans-serif' }
|
|
|
+ },
|
|
|
+ tickfont: { color: "#666", family: '"Droid Sans", sans-serif' },
|
|
|
+ gridcolor: "#323232"
|
|
|
+ },
|
|
|
+ plot_bgcolor: "#252525",
|
|
|
+ paper_bgcolor: "#252525",
|
|
|
+ dragmode: "pan"
|
|
|
+};
|
|
|
+var defaultConfig = {
|
|
|
+ displayModeBar: true,
|
|
|
+ modeBarButtonsToRemove: ["lasso2d"],
|
|
|
+ displaylogo: false,
|
|
|
+ scrollZoom: true,
|
|
|
+ responsive: true
|
|
|
+};
|
|
|
+function createGraph(plotContainerID, data, layout, config) {
|
|
|
+ for (var i = 0;i < data.length; i++) {
|
|
|
+ data[i] = deepMerge(structuredClone(defaultData), data[i]);
|
|
|
+ }
|
|
|
+ layout = deepMerge(structuredClone(defaultLayout), layout);
|
|
|
+ config = deepMerge(structuredClone(defaultConfig), config);
|
|
|
+ Plotly.newPlot(plotContainerID, { data, layout, config });
|
|
|
+}
|
|
|
+function createTable(plotContainerID, titleText, headers, data, dataDisplay) {
|
|
|
+ const container = document.getElementById(plotContainerID);
|
|
|
+ if (!container) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const title = document.createElement("div");
|
|
|
+ title.textContent = titleText;
|
|
|
+ title.classList.add("title");
|
|
|
+ container.appendChild(title);
|
|
|
+ const table = document.createElement("table");
|
|
|
+ const header = document.createElement("thead");
|
|
|
+ const headRow = document.createElement("tr");
|
|
|
+ headers.forEach((label) => {
|
|
|
+ const column = document.createElement("th");
|
|
|
+ column.textContent = label;
|
|
|
+ headRow.appendChild(column);
|
|
|
+ });
|
|
|
+ header.appendChild(headRow);
|
|
|
+ table.appendChild(header);
|
|
|
+ const body = document.createElement("tbody");
|
|
|
+ dataDisplay.forEach((dataRow) => {
|
|
|
+ const row = document.createElement("tr");
|
|
|
+ dataRow.forEach((dataElem) => {
|
|
|
+ const td = document.createElement("td");
|
|
|
+ td.appendChild(dataElem);
|
|
|
+ row.appendChild(td);
|
|
|
+ });
|
|
|
+ body.appendChild(row);
|
|
|
+ });
|
|
|
+ table.appendChild(body);
|
|
|
+ container.appendChild(table);
|
|
|
+}
|
|
|
+
|
|
|
+// src/graphs/companyHistory.ts
|
|
|
+class CompanyHistory {
|
|
|
+ id = "compHistory";
|
|
|
+ displayName = "Company History";
|
|
|
+ configFieldIDs = ["metric", "group", "companyName"];
|
|
|
+ loadedData;
|
|
|
+ urlParams;
|
|
|
+ constructor(loadedData, urlParams) {
|
|
|
+ this.loadedData = loadedData;
|
|
|
+ this.urlParams = urlParams;
|
|
|
+ }
|
|
|
+ setConfigs(useURLParams) {
|
|
|
+ const updateFunc = function() {
|
|
|
+ switchPlot();
|
|
|
+ };
|
|
|
+ const configDiv = document.getElementById("selectorSubtypes");
|
|
|
+ if (configDiv) {
|
|
|
+ clearChildren(configDiv);
|
|
|
+ }
|
|
|
+ configDiv?.appendChild(addConfigField("select", "metric", "Metric: ", { prettyValues: ["Volume", "Profit", "Bases"], values: ["volume", "profit", "bases"] }, useURLParams ? this.urlParams.metric : undefined, updateFunc));
|
|
|
+ configDiv?.appendChild(addConfigField("select", "group", "Group: ", { prettyValues: ["By Company", "By Corporation"], values: ["company", "corp"] }, useURLParams ? this.urlParams.group : undefined, updateUsernameLabel));
|
|
|
+ configDiv?.appendChild(addConfigField("input", "companyName", "Username: ", undefined, useURLParams ? this.urlParams.companyName : undefined, updateFunc, "-27px"));
|
|
|
+ }
|
|
|
+ async generatePlot(configValues, plotContainerID) {
|
|
|
+ if (!configValues.companyName || configValues.companyName == "") {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const knownCompanies = await getData(this.loadedData, "knownCompanies");
|
|
|
+ var companyName;
|
|
|
+ var companyID;
|
|
|
+ if (configValues.group == "company") {
|
|
|
+ companyID = await getCompanyId(configValues.companyName, this.loadedData);
|
|
|
+ if (!companyID) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ companyName = knownCompanies[companyID]?.Username;
|
|
|
+ } else {
|
|
|
+ companyName = configValues.companyName.toUpperCase();
|
|
|
+ }
|
|
|
+ const fullCompanyData = [];
|
|
|
+ for (var i = 0;i < months.length; i++) {
|
|
|
+ const monthData = await getData(this.loadedData, configValues.metric == "bases" ? "base" : "company", months[i]);
|
|
|
+ const dataset = configValues.metric == "bases" ? monthData : monthData.totals;
|
|
|
+ if (configValues.group == "company") {
|
|
|
+ fullCompanyData.push(dataset[companyID || ""]);
|
|
|
+ } else {
|
|
|
+ const corpData = {};
|
|
|
+ var hasData = false;
|
|
|
+ const parentCorps = await getData(this.loadedData, "parentCorps");
|
|
|
+ Object.keys(dataset).forEach((id) => {
|
|
|
+ const companyObj = knownCompanies[id];
|
|
|
+ if (companyObj && (companyObj.Corporation == companyName || parentCorps[companyObj.Corporation] == companyName)) {
|
|
|
+ hasData = true;
|
|
|
+ Object.keys(dataset[id]).forEach((metric) => {
|
|
|
+ if (!corpData[metric]) {
|
|
|
+ corpData[metric] = 0;
|
|
|
+ }
|
|
|
+ corpData[metric] += dataset[id][metric];
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ if (hasData) {
|
|
|
+ fullCompanyData.push(corpData);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ const validMonths = [];
|
|
|
+ const companyData = [];
|
|
|
+ fullCompanyData.forEach((data, i2) => {
|
|
|
+ if (!data) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ validMonths.push(monthsPretty[i2]);
|
|
|
+ companyData.push(data[configValues.metric]);
|
|
|
+ });
|
|
|
+ const titles = {
|
|
|
+ profit: "Production Profit History of ",
|
|
|
+ volume: "Production Volume History of ",
|
|
|
+ bases: "Base Count History of "
|
|
|
+ };
|
|
|
+ const yAxis = {
|
|
|
+ profit: "Daily Profit [$/day]",
|
|
|
+ volume: "Daily Volume [$/day]",
|
|
|
+ bases: "Bases"
|
|
|
+ };
|
|
|
+ createGraph(plotContainerID, [{ x: validMonths, y: companyData, type: "bar" }], {
|
|
|
+ width: this.urlParams.hideOptions !== undefined ? undefined : 800,
|
|
|
+ height: this.urlParams.hideOptions !== undefined ? undefined : 400,
|
|
|
+ autosize: this.urlParams.hideOptions !== undefined,
|
|
|
+ ...this.urlParams.hideOptions !== undefined ? { margin: {
|
|
|
+ l: 60,
|
|
|
+ r: 10,
|
|
|
+ t: 40,
|
|
|
+ b: 60
|
|
|
+ } } : {},
|
|
|
+ title: { text: titles[configValues.metric] + companyName },
|
|
|
+ xaxis: {
|
|
|
+ title: { text: "Month" }
|
|
|
+ },
|
|
|
+ yaxis: {
|
|
|
+ title: { text: yAxis[configValues.metric] }
|
|
|
+ }
|
|
|
+ }, {});
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// src/graphs/companyRank.ts
|
|
|
+class CompanyRank {
|
|
|
+ id = "compRank";
|
|
|
+ displayName = "Company Rank";
|
|
|
+ configFieldIDs = ["month", "companyName"];
|
|
|
+ loadedData;
|
|
|
+ urlParams;
|
|
|
+ constructor(loadedData, urlParams) {
|
|
|
+ this.loadedData = loadedData;
|
|
|
+ this.urlParams = urlParams;
|
|
|
+ }
|
|
|
+ setConfigs(useURLParams) {
|
|
|
+ const updateFunc = function() {
|
|
|
+ switchPlot();
|
|
|
+ };
|
|
|
+ const configDiv = document.getElementById("selectorSubtypes");
|
|
|
+ if (configDiv) {
|
|
|
+ clearChildren(configDiv);
|
|
|
+ }
|
|
|
+ configDiv?.appendChild(addConfigField("select", "month", "Month: ", { prettyValues: monthsPretty, values: months }, useURLParams && this.urlParams.month ? this.urlParams.month : months[months.length - 1], updateFunc));
|
|
|
+ configDiv?.appendChild(addConfigField("input", "companyName", "Username: ", undefined, useURLParams ? this.urlParams.companyName : undefined, updateFunc, "-27px"));
|
|
|
+ }
|
|
|
+ async generatePlot(configValues, plotContainerID) {
|
|
|
+ if (!configValues.companyName || configValues.companyName == "") {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const knownCompanies = await getData(this.loadedData, "knownCompanies");
|
|
|
+ var companyID = await getCompanyId(configValues.companyName, this.loadedData);
|
|
|
+ if (!companyID) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ var companyName = knownCompanies[companyID]?.Username;
|
|
|
+ const fullCompanyData = await getData(this.loadedData, "company", configValues.month);
|
|
|
+ const fullPrevCompanyData = configValues.month == months[0] ? { individual: {} } : await getData(this.loadedData, "company", months[months.indexOf(configValues.month) - 1]);
|
|
|
+ const companyData = fullCompanyData.individual[companyID];
|
|
|
+ const prevCompanyData = fullPrevCompanyData.individual[companyID];
|
|
|
+ if (!companyData) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ var tableData = [];
|
|
|
+ var tableDisplay = [];
|
|
|
+ Object.keys(companyData).forEach((ticker) => {
|
|
|
+ const tableRow = [companyData[ticker].rank, ticker, companyData[ticker].amount, companyData[ticker].volume, companyData[ticker].profit];
|
|
|
+ const tableDisplayRow = [];
|
|
|
+ if (prevCompanyData) {
|
|
|
+ const outerRankDiv = document.createElement("div");
|
|
|
+ const symbolDiv = document.createElement("div");
|
|
|
+ const rankDiv = document.createElement("div");
|
|
|
+ outerRankDiv.style.display = "flex";
|
|
|
+ symbolDiv.style.width = "14px";
|
|
|
+ symbolDiv.style.minWidth = "14px";
|
|
|
+ symbolDiv.style.marginRight = "2px";
|
|
|
+ const prevRank = prevCompanyData[ticker]?.rank;
|
|
|
+ const increasing = prevRank < companyData[ticker].rank;
|
|
|
+ if (prevRank && prevRank != companyData[ticker].rank) {
|
|
|
+ symbolDiv.textContent = increasing ? "▼" : "▲";
|
|
|
+ symbolDiv.style.color = increasing ? "#d9534f" : "#5cb85c";
|
|
|
+ }
|
|
|
+ rankDiv.textContent = companyData[ticker].rank.toLocaleString();
|
|
|
+ outerRankDiv.appendChild(symbolDiv);
|
|
|
+ outerRankDiv.appendChild(rankDiv);
|
|
|
+ tableDisplayRow.push(outerRankDiv);
|
|
|
+ } else {
|
|
|
+ const rankDiv = document.createElement("div");
|
|
|
+ rankDiv.textContent = companyData[ticker].rank.toLocaleString();
|
|
|
+ tableDisplayRow.push(rankDiv);
|
|
|
+ }
|
|
|
+ const tickerDiv = document.createElement("div");
|
|
|
+ tickerDiv.textContent = ticker;
|
|
|
+ tableDisplayRow.push(tickerDiv);
|
|
|
+ const amountDiv = document.createElement("div");
|
|
|
+ amountDiv.textContent = companyData[ticker].amount.toLocaleString(undefined, { maximumFractionDigits: 1 });
|
|
|
+ tableDisplayRow.push(amountDiv);
|
|
|
+ const volumeDiv = document.createElement("div");
|
|
|
+ volumeDiv.textContent = "$" + companyData[ticker].volume.toLocaleString(undefined, { notation: "compact", maximumSignificantDigits: 3 });
|
|
|
+ tableDisplayRow.push(volumeDiv);
|
|
|
+ const profitDiv = document.createElement("div");
|
|
|
+ const profit = companyData[ticker].profit ?? NaN;
|
|
|
+ let profitText = "";
|
|
|
+ if (profit < 0)
|
|
|
+ profitText = "-$" + (-profit).toLocaleString(undefined, { notation: "compact", maximumSignificantDigits: 3 });
|
|
|
+ else if (profit > 0)
|
|
|
+ profitText = "$" + profit.toLocaleString(undefined, { notation: "compact", maximumSignificantDigits: 3 });
|
|
|
+ profitDiv.textContent = profitText;
|
|
|
+ tableDisplayRow.push(profitDiv);
|
|
|
+ tableData.push(tableRow);
|
|
|
+ tableDisplay.push(tableDisplayRow);
|
|
|
+ });
|
|
|
+ const indices = tableData.map((_, i) => i).sort((a, b) => tableData[a][0] - tableData[b][0]);
|
|
|
+ tableData = indices.map((i) => tableData[i]);
|
|
|
+ tableDisplay = indices.map((i) => tableDisplay[i]);
|
|
|
+ const title = "Production Ranking of " + companyName + " - " + prettyMonthName(configValues.month) + `
|
|
|
+Volume: #` + fullCompanyData.totals[companyID].volumeRank + ", Profit: #" + fullCompanyData.totals[companyID].profitRank;
|
|
|
+ const headers = ["Rank", "Ticker", "Amount [/day]", "Volume [$/day]", "Profit [$/day]"];
|
|
|
+ createTable(plotContainerID, title, headers, tableData, tableDisplay);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// src/graphs/companyTotals.ts
|
|
|
+class CompanyTotals {
|
|
|
+ id = "compTotals";
|
|
|
+ displayName = "Company Totals";
|
|
|
+ configFieldIDs = ["chartType", "metric", "group", "month", "companyName"];
|
|
|
+ loadedData;
|
|
|
+ urlParams;
|
|
|
+ constructor(loadedData, urlParams) {
|
|
|
+ this.loadedData = loadedData;
|
|
|
+ this.urlParams = urlParams;
|
|
|
+ }
|
|
|
+ setConfigs(useURLParams) {
|
|
|
+ const updateFunc = function() {
|
|
|
+ switchPlot();
|
|
|
+ };
|
|
|
+ const configDiv = document.getElementById("selectorSubtypes");
|
|
|
+ if (configDiv) {
|
|
|
+ clearChildren(configDiv);
|
|
|
+ }
|
|
|
+ const usernameLabel = useURLParams && this.urlParams.group && this.urlParams.group != "company" ? "Corp Code: " : "Username: ";
|
|
|
+ configDiv?.appendChild(addConfigField("select", "chartType", "Chart Type: ", { prettyValues: ["Bar", "Pie", "Treemap (Mat)", "Treemap (Cat)"], values: ["bar", "pie", "treemap", "treemap-categories"] }, useURLParams ? this.urlParams.chartType : "treemap", updateFunc, "-30px"));
|
|
|
+ configDiv?.appendChild(addConfigField("select", "metric", "Metric: ", { prettyValues: ["Volume", "Profit"], values: ["volume", "profit"] }, useURLParams ? this.urlParams.metric : undefined, updateFunc));
|
|
|
+ configDiv?.appendChild(addConfigField("select", "group", "Group: ", { prettyValues: ["By Company", "By Corporation"], values: ["company", "corp"] }, useURLParams ? this.urlParams.group : undefined, updateUsernameLabel));
|
|
|
+ configDiv?.appendChild(addConfigField("select", "month", "Month: ", { prettyValues: monthsPretty, values: months }, useURLParams && this.urlParams.month ? this.urlParams.month : months[months.length - 1], updateFunc));
|
|
|
+ configDiv?.appendChild(addConfigField("input", "companyName", usernameLabel, undefined, useURLParams ? this.urlParams.companyName : undefined, updateFunc, "-27px"));
|
|
|
+ }
|
|
|
+ async generatePlot(configValues, plotContainerID) {
|
|
|
+ if (!configValues.companyName || configValues.companyName == "") {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const companyData = await getData(this.loadedData, "company", configValues.month);
|
|
|
+ const knownCompanies = await getData(this.loadedData, "knownCompanies");
|
|
|
+ var companyName;
|
|
|
+ var prodData;
|
|
|
+ if (configValues.group == "company") {
|
|
|
+ var companyID = await getCompanyId(configValues.companyName, this.loadedData);
|
|
|
+ if (!companyID) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ companyName = knownCompanies[companyID]?.Username;
|
|
|
+ if (!companyData.individual[companyID]) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ prodData = companyData.individual[companyID];
|
|
|
+ } else {
|
|
|
+ const parentCorps = await getData(this.loadedData, "parentCorps");
|
|
|
+ companyName = configValues.companyName.toUpperCase();
|
|
|
+ prodData = {};
|
|
|
+ Object.keys(companyData.individual).forEach((id) => {
|
|
|
+ const companyObj = knownCompanies[id];
|
|
|
+ if (companyObj && (companyObj.Corporation == companyName || parentCorps[companyObj.Corporation] == companyName)) {
|
|
|
+ const indivCompanyData = companyData.individual[id];
|
|
|
+ Object.keys(indivCompanyData).forEach((ticker) => {
|
|
|
+ if (!prodData[ticker]) {
|
|
|
+ prodData[ticker] = { volume: 0, profit: 0, amount: 0, rank: 0 };
|
|
|
+ }
|
|
|
+ Object.keys(indivCompanyData[ticker]).forEach((metric) => {
|
|
|
+ prodData[ticker][metric] += indivCompanyData[ticker][metric];
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ if (Object.keys(prodData).length == 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ var catData = [];
|
|
|
+ var categories = [];
|
|
|
+ var totalValue = 0;
|
|
|
+ Object.keys(prodData).forEach((ticker) => {
|
|
|
+ const metric = prodData[ticker][configValues.metric];
|
|
|
+ if (metric < 0 && (configValues.chartType == "treemap" || configValues.chartType == "treemap-categories")) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ totalValue += metric;
|
|
|
+ if (configValues.chartType == "treemap-categories") {
|
|
|
+ const category = getMatCategory(ticker);
|
|
|
+ const catIndex = categories.indexOf(category);
|
|
|
+ if (catIndex == -1) {
|
|
|
+ categories.push(category);
|
|
|
+ catData.push(metric);
|
|
|
+ } else {
|
|
|
+ catData[catIndex] += metric;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ catData.push(metric);
|
|
|
+ categories.push(ticker);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ const indices = Array.from(categories.keys());
|
|
|
+ indices.sort((a, b) => catData[b] - catData[a]);
|
|
|
+ catData = indices.map((i) => catData[i]);
|
|
|
+ categories = indices.map((i) => categories[i]);
|
|
|
+ const titles = {
|
|
|
+ profit: "Production Profit Breakdown of ",
|
|
|
+ volume: "Production Volume Breakdown of "
|
|
|
+ };
|
|
|
+ if (configValues.chartType == "treemap" || configValues.chartType == "treemap-categories") {
|
|
|
+ const colors = [];
|
|
|
+ if (configValues.chartType == "treemap") {
|
|
|
+ categories.forEach((cat) => {
|
|
|
+ colors.push(getMatColor(cat));
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ categories.forEach((cat) => {
|
|
|
+ colors.push(materialCategoryColors[cat] ?? "#000000");
|
|
|
+ });
|
|
|
+ }
|
|
|
+ const parents = categories.map((m) => "Total");
|
|
|
+ categories.push("Total");
|
|
|
+ catData.push(totalValue);
|
|
|
+ parents.push("");
|
|
|
+ colors.push("#252525");
|
|
|
+ createGraph(plotContainerID, [{
|
|
|
+ labels: categories,
|
|
|
+ values: catData,
|
|
|
+ parents,
|
|
|
+ type: "treemap",
|
|
|
+ maxdepth: 2,
|
|
|
+ branchvalues: "total",
|
|
|
+ marker: {
|
|
|
+ colors
|
|
|
+ },
|
|
|
+ tiling: {
|
|
|
+ pad: 0
|
|
|
+ },
|
|
|
+ textposition: "middle center",
|
|
|
+ hovertemplate: "%{label}<br>$%{value:,.3~s}/day<br>%{percentEntry:.2%}<extra></extra>"
|
|
|
+ }], {
|
|
|
+ width: this.urlParams.hideOptions !== undefined ? undefined : 800,
|
|
|
+ height: this.urlParams.hideOptions !== undefined ? undefined : 400,
|
|
|
+ autosize: this.urlParams.hideOptions !== undefined,
|
|
|
+ ...this.urlParams.hideOptions !== undefined ? { margin: {
|
|
|
+ l: 10,
|
|
|
+ r: 10,
|
|
|
+ t: 40,
|
|
|
+ b: 10
|
|
|
+ } } : {},
|
|
|
+ title: { text: titles[configValues.metric] + companyName + " - " + prettyMonthName(configValues.month) }
|
|
|
+ }, {});
|
|
|
+ } else if (configValues.chartType == "bar") {
|
|
|
+ createGraph(plotContainerID, [{ x: categories, y: catData, type: "bar" }], {
|
|
|
+ width: this.urlParams.hideOptions !== undefined ? undefined : 800,
|
|
|
+ height: this.urlParams.hideOptions !== undefined ? undefined : 400,
|
|
|
+ autosize: this.urlParams.hideOptions !== undefined,
|
|
|
+ ...this.urlParams.hideOptions !== undefined ? { margin: {
|
|
|
+ l: 60,
|
|
|
+ r: 10,
|
|
|
+ t: 40,
|
|
|
+ b: 60
|
|
|
+ } } : {},
|
|
|
+ title: { text: titles[configValues.metric] + companyName + " - " + prettyMonthName(configValues.month) },
|
|
|
+ xaxis: {
|
|
|
+ title: { text: "Ticker" },
|
|
|
+ range: [-0.5, Math.min(categories.length, 30) - 0.5]
|
|
|
+ },
|
|
|
+ yaxis: {
|
|
|
+ title: { text: prettyModeNames[configValues.metric] + " [$/day]" },
|
|
|
+ range: [0, null]
|
|
|
+ }
|
|
|
+ }, {});
|
|
|
+ } else if (configValues.chartType == "pie") {
|
|
|
+ createGraph(plotContainerID, [{ labels: categories, values: catData, type: "pie", textinfo: "label", textposition: "inside", insidetextorientation: "none", automargin: false, hovertemplate: "%{label}<br>$%{value:,.3~s}/day<br>%{percent}<extra></extra>" }], {
|
|
|
+ width: this.urlParams.hideOptions !== undefined ? undefined : 800,
|
|
|
+ height: this.urlParams.hideOptions !== undefined ? undefined : 400,
|
|
|
+ autosize: this.urlParams.hideOptions !== undefined,
|
|
|
+ ...this.urlParams.hideOptions !== undefined ? { margin: {
|
|
|
+ l: 10,
|
|
|
+ r: 10,
|
|
|
+ t: 40,
|
|
|
+ b: 10
|
|
|
+ } } : {},
|
|
|
+ title: { text: titles[configValues.metric] + companyName + " - " + prettyMonthName(configValues.month) },
|
|
|
+ xaxis: {
|
|
|
+ title: { text: "Ticker" },
|
|
|
+ range: [-0.5, Math.min(categories.length, 30) - 0.5]
|
|
|
+ },
|
|
|
+ yaxis: {
|
|
|
+ title: { text: prettyModeNames[configValues.metric] + " [$/day]" },
|
|
|
+ range: [0, null]
|
|
|
+ }
|
|
|
+ }, {});
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// src/graphs/matHistory.ts
|
|
|
+class MatHistory {
|
|
|
+ id = "matHistory";
|
|
|
+ displayName = "MAT History";
|
|
|
+ configFieldIDs = ["metric", "ticker"];
|
|
|
+ loadedData;
|
|
|
+ urlParams;
|
|
|
+ constructor(loadedData, urlParams) {
|
|
|
+ this.loadedData = loadedData;
|
|
|
+ this.urlParams = urlParams;
|
|
|
+ }
|
|
|
+ setConfigs(useURLParams) {
|
|
|
+ const updateFunc = function() {
|
|
|
+ switchPlot();
|
|
|
+ };
|
|
|
+ const configDiv = document.getElementById("selectorSubtypes");
|
|
|
+ if (configDiv) {
|
|
|
+ clearChildren(configDiv);
|
|
|
+ }
|
|
|
+ configDiv?.appendChild(addConfigField("select", "metric", "Metric: ", { prettyValues: ["Volume", "Profit", "Price", "Produced", "Consumption", "Surplus"], values: ["volume", "profit", "price", "amount", "consumed", "surplus"] }, useURLParams ? this.urlParams.metric : undefined, updateFunc));
|
|
|
+ configDiv?.appendChild(addConfigField("input", "ticker", "Ticker: ", undefined, useURLParams && this.urlParams.ticker ? this.urlParams.ticker : undefined, updateFunc));
|
|
|
+ }
|
|
|
+ async generatePlot(configValues, plotContainerID) {
|
|
|
+ if (!configValues.ticker || configValues.ticker == "") {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const totalTickerData = [];
|
|
|
+ for (var i = 0;i < months.length; i++) {
|
|
|
+ const monthData = await getData(this.loadedData, "prod", months[i]);
|
|
|
+ totalTickerData.push(monthData[(configValues.ticker ?? "").toUpperCase()]);
|
|
|
+ }
|
|
|
+ const tickerData = [];
|
|
|
+ const validMonths = [];
|
|
|
+ totalTickerData.forEach((data, i2) => {
|
|
|
+ if (!data) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ validMonths.push(monthsPretty[i2]);
|
|
|
+ switch (configValues.metric) {
|
|
|
+ case "volume":
|
|
|
+ tickerData.push(data.volume);
|
|
|
+ break;
|
|
|
+ case "profit":
|
|
|
+ tickerData.push(data.profit);
|
|
|
+ break;
|
|
|
+ case "price":
|
|
|
+ tickerData.push(data.amount == 0 ? 0 : data.volume / data.amount);
|
|
|
+ break;
|
|
|
+ case "amount":
|
|
|
+ tickerData.push(data.amount);
|
|
|
+ break;
|
|
|
+ case "consumed":
|
|
|
+ tickerData.push(data.consumed);
|
|
|
+ break;
|
|
|
+ case "surplus":
|
|
|
+ tickerData.push(data.amount - data.consumed);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ if (validMonths.length == 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const titles = {
|
|
|
+ profit: "Production Profit History of ",
|
|
|
+ volume: "Production Volume History of ",
|
|
|
+ amount: "Production Amount History of ",
|
|
|
+ price: "Price History of ",
|
|
|
+ consumed: "Consumption History of ",
|
|
|
+ surplus: "Surplus Production History of "
|
|
|
+ };
|
|
|
+ const yAxis = {
|
|
|
+ profit: "Daily Profit [$/day]",
|
|
|
+ volume: "Daily Volume [$/day]",
|
|
|
+ amount: "Daily Production [per day]",
|
|
|
+ price: "Price [$]",
|
|
|
+ consumed: "Daily Consumption [per day]",
|
|
|
+ surplus: "Daily Surplus [per day]"
|
|
|
+ };
|
|
|
+ createGraph(plotContainerID, [{ x: validMonths, y: tickerData, type: "bar" }], {
|
|
|
+ width: this.urlParams.hideOptions !== undefined ? undefined : 800,
|
|
|
+ height: this.urlParams.hideOptions !== undefined ? undefined : 400,
|
|
|
+ autosize: this.urlParams.hideOptions !== undefined,
|
|
|
+ ...this.urlParams.hideOptions !== undefined ? { margin: {
|
|
|
+ l: 60,
|
|
|
+ r: 10,
|
|
|
+ t: 40,
|
|
|
+ b: 60
|
|
|
+ } } : {},
|
|
|
+ title: { text: titles[configValues.metric] + (configValues.ticker ?? "").toUpperCase() },
|
|
|
+ xaxis: {
|
|
|
+ title: { text: "Month" }
|
|
|
+ },
|
|
|
+ yaxis: {
|
|
|
+ title: { text: yAxis[configValues.metric] }
|
|
|
+ }
|
|
|
+ }, {});
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// src/graphs/topCompanies.ts
|
|
|
+class TopCompanies {
|
|
|
+ id = "topCompanies";
|
|
|
+ displayName = "Top Companies";
|
|
|
+ configFieldIDs = ["metric", "group", "month"];
|
|
|
+ loadedData;
|
|
|
+ urlParams;
|
|
|
+ constructor(loadedData, urlParams) {
|
|
|
+ this.loadedData = loadedData;
|
|
|
+ this.urlParams = urlParams;
|
|
|
+ }
|
|
|
+ setConfigs(useURLParams) {
|
|
|
+ const updateFunc = function() {
|
|
|
+ switchPlot();
|
|
|
+ };
|
|
|
+ const configDiv = document.getElementById("selectorSubtypes");
|
|
|
+ if (configDiv) {
|
|
|
+ clearChildren(configDiv);
|
|
|
+ }
|
|
|
+ configDiv?.appendChild(addConfigField("select", "metric", "Metric: ", { prettyValues: ["Volume", "Profit", "Bases", "Ships"], values: ["volume", "profit", "bases", "ships"] }, useURLParams ? this.urlParams.metric : undefined, updateFunc));
|
|
|
+ configDiv?.appendChild(addConfigField("select", "group", "Group: ", { prettyValues: ["By Company", "By Corporation"], values: ["company", "corp"] }, useURLParams ? this.urlParams.group : undefined, updateFunc));
|
|
|
+ configDiv?.appendChild(addConfigField("select", "month", "Month: ", { prettyValues: monthsPretty, values: months }, useURLParams && this.urlParams.month ? this.urlParams.month : months[months.length - 1], updateFunc));
|
|
|
+ }
|
|
|
+ async generatePlot(configValues, plotContainerID) {
|
|
|
+ var fileName;
|
|
|
+ switch (configValues.metric) {
|
|
|
+ case "bases":
|
|
|
+ fileName = "base";
|
|
|
+ break;
|
|
|
+ case "ships":
|
|
|
+ fileName = "ship";
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ fileName = "company";
|
|
|
+ }
|
|
|
+ const companyData = await getData(this.loadedData, fileName, configValues.month);
|
|
|
+ const knownCompanies = await getData(this.loadedData, "knownCompanies");
|
|
|
+ const dataset = configValues.metric == "bases" || configValues.metric == "ships" ? companyData : companyData.totals;
|
|
|
+ var companyNames;
|
|
|
+ var volumes;
|
|
|
+ if (!companyData) {
|
|
|
+ console.error("Data type not defined for this month.");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (configValues.group == "corp") {
|
|
|
+ const parentCorps = await getData(this.loadedData, "parentCorps");
|
|
|
+ const corpData = {};
|
|
|
+ Object.keys(dataset).forEach((id) => {
|
|
|
+ const companyObj = knownCompanies[id];
|
|
|
+ if (companyObj && companyObj["Corporation"]) {
|
|
|
+ var corp = companyObj["Corporation"];
|
|
|
+ if (parentCorps[corp]) {
|
|
|
+ corp = parentCorps[corp];
|
|
|
+ }
|
|
|
+ if (corpData[corp]) {
|
|
|
+ corpData[corp] += dataset[id][configValues.metric];
|
|
|
+ } else {
|
|
|
+ corpData[corp] = dataset[id][configValues.metric];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ const volumeArray = Object.entries(corpData).map(([corpCode, info]) => ({
|
|
|
+ corpCode,
|
|
|
+ volume: info
|
|
|
+ }));
|
|
|
+ volumeArray.sort((a, b) => b.volume - a.volume);
|
|
|
+ companyNames = volumeArray.map((item) => item.corpCode);
|
|
|
+ volumes = volumeArray.map((item) => item.volume);
|
|
|
+ } else {
|
|
|
+ const volumeArray = Object.entries(dataset).map(([companyID, info]) => ({
|
|
|
+ companyID,
|
|
|
+ volume: info[configValues.metric]
|
|
|
+ }));
|
|
|
+ volumeArray.sort((a, b) => b.volume - a.volume);
|
|
|
+ const companyIDs = volumeArray.map((item) => item.companyID);
|
|
|
+ volumes = volumeArray.map((item) => item.volume);
|
|
|
+ companyNames = [];
|
|
|
+ companyIDs.forEach((id) => {
|
|
|
+ const companyObj = knownCompanies[id];
|
|
|
+ companyNames.push(companyObj ? companyObj["Username"] : id.slice(0, 5) + "...");
|
|
|
+ });
|
|
|
+ }
|
|
|
+ const prettyGroupNames = { company: "Companies", corp: "Corporations" };
|
|
|
+ createGraph(plotContainerID, [{ x: companyNames, y: volumes, type: "bar" }], {
|
|
|
+ width: this.urlParams.hideOptions !== undefined ? undefined : 800,
|
|
|
+ height: this.urlParams.hideOptions !== undefined ? undefined : 400,
|
|
|
+ autosize: this.urlParams.hideOptions !== undefined,
|
|
|
+ ...this.urlParams.hideOptions !== undefined ? { margin: {
|
|
|
+ l: 60,
|
|
|
+ r: 10,
|
|
|
+ t: 40,
|
|
|
+ b: 100
|
|
|
+ } } : {},
|
|
|
+ title: { text: "Top " + prettyGroupNames[configValues.group] + " (" + prettyModeNames[configValues.metric] + ") - " + prettyMonthName(configValues.month) },
|
|
|
+ xaxis: {
|
|
|
+ title: { text: "Ticker" },
|
|
|
+ range: [-0.5, 29.5]
|
|
|
+ },
|
|
|
+ yaxis: {
|
|
|
+ title: { text: prettyModeNames[configValues.metric] + (configValues.metric == "bases" || configValues.metric == "ships" ? "" : " [$/day]") },
|
|
|
+ range: [0, null]
|
|
|
+ }
|
|
|
+ }, {});
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// src/graphs/topProduction.ts
|
|
|
+class TopProduction {
|
|
|
+ id = "topProduction";
|
|
|
+ displayName = "Top Production";
|
|
|
+ configFieldIDs = ["metric", "month"];
|
|
|
+ loadedData;
|
|
|
+ urlParams;
|
|
|
+ constructor(loadedData, urlParams) {
|
|
|
+ this.loadedData = loadedData;
|
|
|
+ this.urlParams = urlParams;
|
|
|
+ }
|
|
|
+ setConfigs(useURLParams) {
|
|
|
+ const updateFunc = function() {
|
|
|
+ switchPlot();
|
|
|
+ };
|
|
|
+ const configDiv = document.getElementById("selectorSubtypes");
|
|
|
+ if (configDiv) {
|
|
|
+ clearChildren(configDiv);
|
|
|
+ }
|
|
|
+ configDiv?.appendChild(addConfigField("select", "metric", "Metric: ", { prettyValues: ["Volume", "Profit", "Deficit"], values: ["volume", "profit", "deficit"] }, useURLParams ? this.urlParams.metric : undefined, updateFunc));
|
|
|
+ configDiv?.appendChild(addConfigField("select", "month", "Month: ", { prettyValues: monthsPretty, values: months }, useURLParams && this.urlParams.month ? this.urlParams.month : months[months.length - 1], updateFunc));
|
|
|
+ }
|
|
|
+ async generatePlot(configValues, plotContainerID) {
|
|
|
+ const prodData = await getData(this.loadedData, "prod", configValues.month);
|
|
|
+ if (configValues.metric == "deficit") {
|
|
|
+ Object.keys(prodData).forEach((ticker) => {
|
|
|
+ if (!prodData[ticker].amount || prodData[ticker].amount == 0) {
|
|
|
+ prodData[ticker].deficit = 0;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ prodData[ticker].deficit = (prodData[ticker].amount - (prodData[ticker].consumed || 0)) * prodData[ticker].volume / prodData[ticker].amount;
|
|
|
+ });
|
|
|
+ }
|
|
|
+ const titles = {
|
|
|
+ profit: "Profit Materials",
|
|
|
+ volume: "Production Volumes",
|
|
|
+ deficit: "Deficits"
|
|
|
+ };
|
|
|
+ const volumeArray = Object.entries(prodData).map(([ticker, info]) => ({
|
|
|
+ ticker,
|
|
|
+ volume: info[configValues.metric]
|
|
|
+ }));
|
|
|
+ if (configValues.metric == "deficit") {
|
|
|
+ volumeArray.sort((a, b) => a.volume - b.volume);
|
|
|
+ } else {
|
|
|
+ volumeArray.sort((a, b) => b.volume - a.volume);
|
|
|
+ }
|
|
|
+ const tickers = volumeArray.map((item) => item.ticker);
|
|
|
+ const volumes = volumeArray.map((item) => item.volume);
|
|
|
+ createGraph(plotContainerID, [{ x: tickers, y: volumes, type: "bar" }], {
|
|
|
+ width: this.urlParams.hideOptions !== undefined ? undefined : 800,
|
|
|
+ height: this.urlParams.hideOptions !== undefined ? undefined : 400,
|
|
|
+ autosize: this.urlParams.hideOptions !== undefined,
|
|
|
+ ...this.urlParams.hideOptions !== undefined ? { margin: {
|
|
|
+ l: 60,
|
|
|
+ r: 10,
|
|
|
+ t: 40,
|
|
|
+ b: 60
|
|
|
+ } } : {},
|
|
|
+ title: { text: "Top " + titles[configValues.metric] + " - " + prettyMonthName(configValues.month) },
|
|
|
+ xaxis: {
|
|
|
+ title: { text: "Ticker" },
|
|
|
+ range: [-0.5, 29.5]
|
|
|
+ },
|
|
|
+ yaxis: {
|
|
|
+ title: { text: prettyModeNames[configValues.metric] + " [$/day]" },
|
|
|
+ range: [configValues.metric == "deficit" ? null : 0, configValues.metric == "deficit" ? 0 : null]
|
|
|
+ }
|
|
|
+ }, {});
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// src/graphs/universeHistory.ts
|
|
|
+class UniverseHistory {
|
|
|
+ id = "universeHistory";
|
|
|
+ displayName = "Universe History";
|
|
|
+ configFieldIDs = ["metric"];
|
|
|
+ loadedData;
|
|
|
+ urlParams;
|
|
|
+ constructor(loadedData, urlParams) {
|
|
|
+ this.loadedData = loadedData;
|
|
|
+ this.urlParams = urlParams;
|
|
|
+ }
|
|
|
+ setConfigs(useURLParams) {
|
|
|
+ const updateFunc = function() {
|
|
|
+ switchPlot();
|
|
|
+ };
|
|
|
+ const configDiv = document.getElementById("selectorSubtypes");
|
|
|
+ if (configDiv) {
|
|
|
+ clearChildren(configDiv);
|
|
|
+ }
|
|
|
+ configDiv?.appendChild(addConfigField("select", "metric", "Metric: ", { prettyValues: ["Volume", "Profit", "Bases", "Companies"], values: ["volume", "profit", "bases", "companies"] }, useURLParams ? this.urlParams.metric : undefined, updateFunc));
|
|
|
+ }
|
|
|
+ async generatePlot(configValues, plotContainerID) {
|
|
|
+ const data = [];
|
|
|
+ const uniData = await getData(this.loadedData, "universe");
|
|
|
+ for (var i = 0;i < months.length; i++) {
|
|
|
+ data.push(uniData[months[i]][configValues.metric]);
|
|
|
+ }
|
|
|
+ const titles = {
|
|
|
+ profit: "Profit History of the Universe",
|
|
|
+ volume: "Production Volume History of the Universe",
|
|
|
+ bases: "Base Count History of the Universe",
|
|
|
+ companies: "Company Count History of the Universe"
|
|
|
+ };
|
|
|
+ const yAxis = {
|
|
|
+ profit: "Daily Profit [$/day]",
|
|
|
+ volume: "Daily Volume [$/day]",
|
|
|
+ bases: "Bases",
|
|
|
+ companies: "Companies"
|
|
|
+ };
|
|
|
+ createGraph(plotContainerID, [{ x: monthsPretty, y: data, type: "bar" }], {
|
|
|
+ width: this.urlParams.hideOptions !== undefined ? undefined : 800,
|
|
|
+ height: this.urlParams.hideOptions !== undefined ? undefined : 400,
|
|
|
+ autosize: this.urlParams.hideOptions !== undefined,
|
|
|
+ ...this.urlParams.hideOptions !== undefined ? { margin: {
|
|
|
+ l: 60,
|
|
|
+ r: 10,
|
|
|
+ t: 40,
|
|
|
+ b: 60
|
|
|
+ } } : {},
|
|
|
+ title: { text: titles[configValues.metric] },
|
|
|
+ xaxis: {
|
|
|
+ title: { text: "Month" }
|
|
|
+ },
|
|
|
+ yaxis: {
|
|
|
+ title: { text: yAxis[configValues.metric] }
|
|
|
+ }
|
|
|
+ }, {});
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// src/graphs/marketOverview.ts
|
|
|
+class MarketOverview {
|
|
|
+ id = "marketOverview";
|
|
|
+ displayName = "Market Overview";
|
|
|
+ configFieldIDs = ["month", "ticker"];
|
|
|
+ loadedData;
|
|
|
+ urlParams;
|
|
|
+ constructor(loadedData, urlParams) {
|
|
|
+ this.loadedData = loadedData;
|
|
|
+ this.urlParams = urlParams;
|
|
|
+ }
|
|
|
+ setConfigs(useURLParams) {
|
|
|
+ const updateFunc = switchPlot;
|
|
|
+ const configDiv = document.getElementById("selectorSubtypes");
|
|
|
+ if (configDiv) {
|
|
|
+ clearChildren(configDiv);
|
|
|
+ }
|
|
|
+ configDiv?.appendChild(addConfigField("select", "month", "Month: ", {
|
|
|
+ prettyValues: monthsPretty,
|
|
|
+ values: months
|
|
|
+ }, useURLParams && this.urlParams.month ? this.urlParams.month : months[months.length - 1], updateFunc));
|
|
|
+ configDiv?.appendChild(addConfigField("input", "ticker", "Ticker: ", undefined, useURLParams && this.urlParams.ticker ? this.urlParams.ticker : undefined, updateFunc));
|
|
|
+ }
|
|
|
+ async generatePlot(configValues, plotContainerID) {
|
|
|
+ const ticker = configValues.ticker?.toUpperCase();
|
|
|
+ if (!ticker) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const companyData = await getData(this.loadedData, "company", configValues.month);
|
|
|
+ const knownCompanies = await getData(this.loadedData, "knownCompanies");
|
|
|
+ const labels = [];
|
|
|
+ const parents = [];
|
|
|
+ const values = [];
|
|
|
+ let totalAmount = 0;
|
|
|
+ let totalVolume = 0;
|
|
|
+ let totalProfit = 0;
|
|
|
+ for (const key of Object.keys(companyData.individual)) {
|
|
|
+ const individualData = companyData.individual[key];
|
|
|
+ const tickerData = individualData[ticker];
|
|
|
+ if (!tickerData) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ const companyObj = knownCompanies[key];
|
|
|
+ labels.push(companyObj ? companyObj["Username"] : key.slice(0, 5) + "...");
|
|
|
+ parents.push("Total");
|
|
|
+ values.push(tickerData.amount);
|
|
|
+ totalVolume += tickerData.volume;
|
|
|
+ totalProfit += tickerData.profit;
|
|
|
+ totalAmount += tickerData.amount;
|
|
|
+ }
|
|
|
+ if (labels.length === 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ labels.push("Total");
|
|
|
+ parents.push("");
|
|
|
+ values.push(totalAmount);
|
|
|
+ const formatMoney = (num) => "$" + num.toLocaleString(undefined, { maximumFractionDigits: 0 });
|
|
|
+ const title = `${ticker} Market - ${prettyMonthName(configValues.month)}` + "<br>" + `Produced per day: ${Math.round(totalAmount).toLocaleString()} ${ticker}` + "<br>" + `Volume: ${formatMoney(totalVolume)} | Profit: ${formatMoney(totalProfit)}`;
|
|
|
+ createGraph(plotContainerID, [{
|
|
|
+ labels,
|
|
|
+ values,
|
|
|
+ parents,
|
|
|
+ type: "treemap",
|
|
|
+ branchvalues: "total",
|
|
|
+ tiling: {
|
|
|
+ pad: 0
|
|
|
+ },
|
|
|
+ textposition: "middle center",
|
|
|
+ hovertemplate: "%{label}<br>%{value:,.3~s}/day<br>%{percentEntry:.2%}<extra></extra>"
|
|
|
+ }], {
|
|
|
+ title: { text: title },
|
|
|
+ width: this.urlParams.hideOptions !== undefined ? undefined : 800,
|
|
|
+ height: this.urlParams.hideOptions !== undefined ? undefined : 400,
|
|
|
+ autosize: this.urlParams.hideOptions !== undefined,
|
|
|
+ ...this.urlParams.hideOptions !== undefined ? {
|
|
|
+ margin: {
|
|
|
+ l: 10,
|
|
|
+ r: 10,
|
|
|
+ t: 60,
|
|
|
+ b: 10
|
|
|
+ }
|
|
|
+ } : {}
|
|
|
+ }, {});
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// src/graphs/corpBreakdown.ts
|
|
|
+class CorporationBreakdown {
|
|
|
+ id = "corpBreakdown";
|
|
|
+ displayName = "Corp Breakdown";
|
|
|
+ configFieldIDs = ["chartType", "metric", "month", "companyName"];
|
|
|
+ loadedData;
|
|
|
+ urlParams;
|
|
|
+ constructor(loadedData, urlParams) {
|
|
|
+ this.loadedData = loadedData;
|
|
|
+ this.urlParams = urlParams;
|
|
|
+ }
|
|
|
+ setConfigs(useURLParams) {
|
|
|
+ const updateFunc = function() {
|
|
|
+ switchPlot();
|
|
|
+ };
|
|
|
+ const configDiv = document.getElementById("selectorSubtypes");
|
|
|
+ if (configDiv) {
|
|
|
+ clearChildren(configDiv);
|
|
|
+ }
|
|
|
+ configDiv?.appendChild(addConfigField("select", "chartType", "Chart Type: ", { prettyValues: ["Bar", "Pie", "Treemap"], values: ["bar", "pie", "treemap"] }, useURLParams ? this.urlParams.chartType : "treemap", updateFunc, "-30px"));
|
|
|
+ configDiv?.appendChild(addConfigField("select", "metric", "Metric: ", { prettyValues: ["Volume", "Profit", "Bases", "Ships"], values: ["volume", "profit", "bases", "ships"] }, useURLParams ? this.urlParams.metric : undefined, updateFunc));
|
|
|
+ configDiv?.appendChild(addConfigField("select", "month", "Month: ", { prettyValues: monthsPretty, values: months }, useURLParams && this.urlParams.month ? this.urlParams.month : months[months.length - 1], updateFunc));
|
|
|
+ configDiv?.appendChild(addConfigField("input", "companyName", "Corp Code: ", undefined, useURLParams ? this.urlParams.companyName : undefined, updateFunc, "-29px"));
|
|
|
+ }
|
|
|
+ async generatePlot(configValues, plotContainerID) {
|
|
|
+ if (!configValues.companyName || configValues.companyName == "") {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ var fileName;
|
|
|
+ switch (configValues.metric) {
|
|
|
+ case "bases":
|
|
|
+ fileName = "base";
|
|
|
+ break;
|
|
|
+ case "ships":
|
|
|
+ fileName = "ship";
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ fileName = "company";
|
|
|
+ }
|
|
|
+ const companyData = await getData(this.loadedData, fileName, configValues.month);
|
|
|
+ const dataset = configValues.metric == "bases" || configValues.metric == "ships" ? companyData : companyData.totals;
|
|
|
+ const knownCompanies = await getData(this.loadedData, "knownCompanies");
|
|
|
+ var companyName;
|
|
|
+ const parentCorps = await getData(this.loadedData, "parentCorps");
|
|
|
+ companyName = configValues.companyName.toUpperCase();
|
|
|
+ const corpData = {};
|
|
|
+ if (!companyData) {
|
|
|
+ console.error("Data type not defined for this month.");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ Object.keys(dataset).forEach((id) => {
|
|
|
+ const companyObj = knownCompanies[id];
|
|
|
+ if (companyObj && (companyObj.Corporation == companyName || parentCorps[companyObj.Corporation] == companyName)) {
|
|
|
+ const indivCompanyData = dataset[id];
|
|
|
+ const name = companyObj["Username"];
|
|
|
+ corpData[name] = indivCompanyData;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ if (Object.keys(corpData).length == 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ var catData = [];
|
|
|
+ var categories = [];
|
|
|
+ var totalValue = 0;
|
|
|
+ Object.keys(corpData).forEach((name) => {
|
|
|
+ const metric = corpData[name][configValues.metric];
|
|
|
+ if (metric < 0 && configValues.chartType == "treemap") {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ totalValue += metric;
|
|
|
+ catData.push(metric);
|
|
|
+ categories.push(name);
|
|
|
+ });
|
|
|
+ const indices = Array.from(categories.keys());
|
|
|
+ indices.sort((a, b) => catData[b] - catData[a]);
|
|
|
+ catData = indices.map((i) => catData[i]);
|
|
|
+ categories = indices.map((i) => categories[i]);
|
|
|
+ const titles = {
|
|
|
+ profit: "Production Profit Breakdown of ",
|
|
|
+ volume: "Production Volume Breakdown of ",
|
|
|
+ bases: "Base Breakdown of ",
|
|
|
+ ships: "Ship Breakdown of "
|
|
|
+ };
|
|
|
+ const hoverTemplates = {
|
|
|
+ profit: "%{label}<br>$%{value:,.3~s}/day<br>%{percentEntry:.2%}<extra></extra>",
|
|
|
+ volume: "%{label}<br>$%{value:,.3~s}/day<br>%{percentEntry:.2%}<extra></extra>",
|
|
|
+ bases: "%{label}<br>%{value:,.3~s}<br>%{percentEntry:.2%}<extra></extra>",
|
|
|
+ ships: "%{label}<br>%{value:,.3~s}<br>%{percentEntry:.2%}<extra></extra>"
|
|
|
+ };
|
|
|
+ if (configValues.chartType == "treemap") {
|
|
|
+ const parents = categories.map((m) => "Total");
|
|
|
+ categories.push("Total");
|
|
|
+ catData.push(totalValue);
|
|
|
+ parents.push("");
|
|
|
+ createGraph(plotContainerID, [{
|
|
|
+ labels: categories,
|
|
|
+ values: catData,
|
|
|
+ parents,
|
|
|
+ type: "treemap",
|
|
|
+ maxdepth: 2,
|
|
|
+ branchvalues: "total",
|
|
|
+ tiling: {
|
|
|
+ pad: 0
|
|
|
+ },
|
|
|
+ textposition: "middle center",
|
|
|
+ hovertemplate: hoverTemplates[configValues.metric]
|
|
|
+ }], {
|
|
|
+ width: this.urlParams.hideOptions !== undefined ? undefined : 800,
|
|
|
+ height: this.urlParams.hideOptions !== undefined ? undefined : 400,
|
|
|
+ autosize: this.urlParams.hideOptions !== undefined,
|
|
|
+ ...this.urlParams.hideOptions !== undefined ? { margin: {
|
|
|
+ l: 10,
|
|
|
+ r: 10,
|
|
|
+ t: 40,
|
|
|
+ b: 10
|
|
|
+ } } : {},
|
|
|
+ title: { text: titles[configValues.metric] + companyName + " - " + prettyMonthName(configValues.month) }
|
|
|
+ }, {});
|
|
|
+ } else if (configValues.chartType == "bar") {
|
|
|
+ createGraph(plotContainerID, [{ x: categories, y: catData, type: "bar" }], {
|
|
|
+ width: this.urlParams.hideOptions !== undefined ? undefined : 800,
|
|
|
+ height: this.urlParams.hideOptions !== undefined ? undefined : 400,
|
|
|
+ autosize: this.urlParams.hideOptions !== undefined,
|
|
|
+ ...this.urlParams.hideOptions !== undefined ? { margin: {
|
|
|
+ l: 60,
|
|
|
+ r: 10,
|
|
|
+ t: 40,
|
|
|
+ b: 60
|
|
|
+ } } : {},
|
|
|
+ title: { text: titles[configValues.metric] + companyName + " - " + prettyMonthName(configValues.month) },
|
|
|
+ xaxis: {
|
|
|
+ title: { text: "Ticker" },
|
|
|
+ range: [-0.5, Math.min(categories.length, 30) - 0.5]
|
|
|
+ },
|
|
|
+ yaxis: {
|
|
|
+ title: { text: prettyModeNames[configValues.metric] + " [$/day]" },
|
|
|
+ range: [0, null]
|
|
|
+ }
|
|
|
+ }, {});
|
|
|
+ } else if (configValues.chartType == "pie") {
|
|
|
+ createGraph(plotContainerID, [{ labels: categories, values: catData, type: "pie", textinfo: "label", textposition: "inside", insidetextorientation: "none", automargin: false, hovertemplate: "%{label}<br>$%{value:,.3~s}/day<br>%{percent}<extra></extra>" }], {
|
|
|
+ width: this.urlParams.hideOptions !== undefined ? undefined : 800,
|
|
|
+ height: this.urlParams.hideOptions !== undefined ? undefined : 400,
|
|
|
+ autosize: this.urlParams.hideOptions !== undefined,
|
|
|
+ ...this.urlParams.hideOptions !== undefined ? { margin: {
|
|
|
+ l: 10,
|
|
|
+ r: 10,
|
|
|
+ t: 40,
|
|
|
+ b: 10
|
|
|
+ } } : {},
|
|
|
+ title: { text: titles[configValues.metric] + companyName + " - " + prettyMonthName(configValues.month) },
|
|
|
+ xaxis: {
|
|
|
+ title: { text: "Ticker" },
|
|
|
+ range: [-0.5, Math.min(categories.length, 30) - 0.5]
|
|
|
+ },
|
|
|
+ yaxis: {
|
|
|
+ title: { text: prettyModeNames[configValues.metric] + " [$/day]" },
|
|
|
+ range: [0, null]
|
|
|
+ }
|
|
|
+ }, {});
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// src/main.ts
|
|
|
+window.onload = function() {
|
|
|
+ addPermalink();
|
|
|
+ const graphSelect = document.getElementById("graphType");
|
|
|
+ graphs.forEach((graph) => {
|
|
|
+ addOption(graphSelect, graph.displayName, graph.id);
|
|
|
+ });
|
|
|
+ if (urlParams.type) {
|
|
|
+ graphSelect.value = urlParams.type;
|
|
|
+ }
|
|
|
+ graphSelect.addEventListener("change", function() {
|
|
|
+ graphs.find((graph) => graph.id == graphSelect.value)?.setConfigs();
|
|
|
+ switchPlot();
|
|
|
+ });
|
|
|
+ graphs.find((graph) => graph.id == graphSelect.value)?.setConfigs(true);
|
|
|
+ switchPlot();
|
|
|
+ if (urlParams.hideOptions !== undefined) {
|
|
|
+ const graphTypeContainer = document.getElementById("graphTypeContainer");
|
|
|
+ const topTabs = document.getElementById("topTabContainer");
|
|
|
+ const configDiv = document.getElementById("selectorSubtypes");
|
|
|
+ const plotContainer = document.getElementById("mainPlot");
|
|
|
+ if (topTabs && graphTypeContainer && configDiv) {
|
|
|
+ topTabs.style.display = "none";
|
|
|
+ graphTypeContainer.style.display = "none";
|
|
|
+ configDiv.style.display = "none";
|
|
|
+ plotContainer?.classList.add("fullScreen");
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+var urlParams = Object.fromEntries(new URLSearchParams(window.location.search));
|
|
|
+var loadedData = {};
|
|
|
+var graphs = [
|
|
|
+ new TopProduction(loadedData, urlParams),
|
|
|
+ new TopCompanies(loadedData, urlParams),
|
|
|
+ new MatHistory(loadedData, urlParams),
|
|
|
+ new UniverseHistory(loadedData, urlParams),
|
|
|
+ new MarketOverview(loadedData, urlParams),
|
|
|
+ new CompanyTotals(loadedData, urlParams),
|
|
|
+ new CompanyHistory(loadedData, urlParams),
|
|
|
+ new CompanyRank(loadedData, urlParams),
|
|
|
+ new CorporationBreakdown(loadedData, urlParams)
|
|
|
+];
|
|
|
+export {
|
|
|
+ graphs
|
|
|
+};
|
|
|
+
|
|
|
+//# debugId=D6A67DC5C3A8C1C264756E2164756E21
|