ソースを参照

move out of reports/

raylu 1 ヶ月 前
コミット
f46daa32af
94 ファイル変更1890 行追加75 行削除
  1. 0 0
      .gitignore
  2. 0 0
      bun.lock
  3. 1 0
      config.toml
  4. 0 0
      data/base-data-apr25.json
  5. 0 0
      data/base-data-apr26.json
  6. 0 0
      data/base-data-aug25.json
  7. 0 0
      data/base-data-dec25.json
  8. 0 0
      data/base-data-feb26.json
  9. 0 0
      data/base-data-jan26.json
  10. 0 0
      data/base-data-jul25.json
  11. 0 0
      data/base-data-jun25.json
  12. 0 0
      data/base-data-mar25.json
  13. 0 0
      data/base-data-mar26.json
  14. 0 0
      data/base-data-may25.json
  15. 0 0
      data/base-data-nov25.json
  16. 0 0
      data/base-data-oct25.json
  17. 0 0
      data/base-data-sep25.json
  18. 0 0
      data/company-data-apr25.json
  19. 0 0
      data/company-data-apr26.json
  20. 0 0
      data/company-data-aug25.json
  21. 0 0
      data/company-data-dec25.json
  22. 0 0
      data/company-data-feb26.json
  23. 0 0
      data/company-data-jan26.json
  24. 0 0
      data/company-data-jul25.json
  25. 0 0
      data/company-data-jun25.json
  26. 0 0
      data/company-data-mar25.json
  27. 0 0
      data/company-data-mar26.json
  28. 0 0
      data/company-data-may25.json
  29. 0 0
      data/company-data-nov25.json
  30. 0 0
      data/company-data-oct25.json
  31. 0 0
      data/company-data-sep25.json
  32. 0 0
      data/knownCompanies.json
  33. 0 0
      data/parentCorps.json
  34. 0 0
      data/prod-data-apr25.json
  35. 0 0
      data/prod-data-apr26.json
  36. 0 0
      data/prod-data-aug25.json
  37. 0 0
      data/prod-data-dec25.json
  38. 0 0
      data/prod-data-feb26.json
  39. 0 0
      data/prod-data-jan26.json
  40. 0 0
      data/prod-data-jul25.json
  41. 0 0
      data/prod-data-jun25.json
  42. 0 0
      data/prod-data-mar25.json
  43. 0 0
      data/prod-data-mar26.json
  44. 0 0
      data/prod-data-may25.json
  45. 0 0
      data/prod-data-nov25.json
  46. 0 0
      data/prod-data-oct25.json
  47. 0 0
      data/prod-data-sep25.json
  48. 0 0
      data/rawData/PrUN-LEAD-Data-April-2024.csv
  49. 0 0
      data/rawData/PrUN-LEAD-Data-April-2025.csv
  50. 0 0
      data/rawData/PrUN-LEAD-Data-April-2026.csv
  51. 0 0
      data/rawData/PrUN-LEAD-Data-August-2025.csv
  52. 0 0
      data/rawData/PrUN-LEAD-Data-December-2025.csv
  53. 0 0
      data/rawData/PrUN-LEAD-Data-February-2024.csv
  54. 0 0
      data/rawData/PrUN-LEAD-Data-February-2026.csv
  55. 0 0
      data/rawData/PrUN-LEAD-Data-January-2024.csv
  56. 0 0
      data/rawData/PrUN-LEAD-Data-January-2026.csv
  57. 0 0
      data/rawData/PrUN-LEAD-Data-July-2025.csv
  58. 0 0
      data/rawData/PrUN-LEAD-Data-June-2025.csv
  59. 0 0
      data/rawData/PrUN-LEAD-Data-March-2024.csv
  60. 0 0
      data/rawData/PrUN-LEAD-Data-March-2025.csv
  61. 0 0
      data/rawData/PrUN-LEAD-Data-March-2026.csv
  62. 0 0
      data/rawData/PrUN-LEAD-Data-May-2025.csv
  63. 0 0
      data/rawData/PrUN-LEAD-Data-November-2025.csv
  64. 0 0
      data/rawData/PrUN-LEAD-Data-October-2025.csv
  65. 0 0
      data/rawData/PrUN-LEAD-Data-September-2025.csv
  66. 0 0
      data/ship-data-apr26.json
  67. 0 0
      data/ship-data-feb26.json
  68. 0 0
      data/ship-data-mar26.json
  69. 0 0
      data/universe-data.json
  70. 0 0
      icon128.png
  71. 60 7
      index.html
  72. 0 0
      main.js
  73. 0 1
      package.json
  74. 0 67
      reports/index.html
  75. 0 0
      src/core.ts
  76. 0 0
      src/graphs/companyHistory.ts
  77. 0 0
      src/graphs/companyRank.ts
  78. 0 0
      src/graphs/companyTotals.ts
  79. 0 0
      src/graphs/corpBreakdown.ts
  80. 0 0
      src/graphs/graph.ts
  81. 0 0
      src/graphs/marketOverview.ts
  82. 0 0
      src/graphs/matHistory.ts
  83. 0 0
      src/graphs/topCompanies.ts
  84. 0 0
      src/graphs/topProduction.ts
  85. 0 0
      src/graphs/universeHistory.ts
  86. 0 0
      src/main.ts
  87. 0 0
      src/permalink.ts
  88. 0 0
      src/staticData/constants.ts
  89. 0 0
      src/utils.ts
  90. 0 0
      styles.css
  91. 0 0
      tsconfig.json
  92. 0 0
      webpack.config.js
  93. 1825 0
      www/main.js
  94. 4 0
      www/main.js.map

+ 0 - 0
reports/.gitignore → .gitignore


+ 0 - 0
reports/bun.lock → bun.lock


+ 1 - 0
config.toml

@@ -0,0 +1 @@
+fio_api_key = 'd7bfdd26-db89-43d9-87e4-0c4aa59237ba'

+ 0 - 0
reports/data/base-data-apr25.json → data/base-data-apr25.json


+ 0 - 0
reports/data/base-data-apr26.json → data/base-data-apr26.json


+ 0 - 0
reports/data/base-data-aug25.json → data/base-data-aug25.json


+ 0 - 0
reports/data/base-data-dec25.json → data/base-data-dec25.json


+ 0 - 0
reports/data/base-data-feb26.json → data/base-data-feb26.json


+ 0 - 0
reports/data/base-data-jan26.json → data/base-data-jan26.json


+ 0 - 0
reports/data/base-data-jul25.json → data/base-data-jul25.json


+ 0 - 0
reports/data/base-data-jun25.json → data/base-data-jun25.json


+ 0 - 0
reports/data/base-data-mar25.json → data/base-data-mar25.json


+ 0 - 0
reports/data/base-data-mar26.json → data/base-data-mar26.json


+ 0 - 0
reports/data/base-data-may25.json → data/base-data-may25.json


+ 0 - 0
reports/data/base-data-nov25.json → data/base-data-nov25.json


+ 0 - 0
reports/data/base-data-oct25.json → data/base-data-oct25.json


+ 0 - 0
reports/data/base-data-sep25.json → data/base-data-sep25.json


+ 0 - 0
reports/data/company-data-apr25.json → data/company-data-apr25.json


+ 0 - 0
reports/data/company-data-apr26.json → data/company-data-apr26.json


+ 0 - 0
reports/data/company-data-aug25.json → data/company-data-aug25.json


+ 0 - 0
reports/data/company-data-dec25.json → data/company-data-dec25.json


+ 0 - 0
reports/data/company-data-feb26.json → data/company-data-feb26.json


+ 0 - 0
reports/data/company-data-jan26.json → data/company-data-jan26.json


+ 0 - 0
reports/data/company-data-jul25.json → data/company-data-jul25.json


+ 0 - 0
reports/data/company-data-jun25.json → data/company-data-jun25.json


+ 0 - 0
reports/data/company-data-mar25.json → data/company-data-mar25.json


+ 0 - 0
reports/data/company-data-mar26.json → data/company-data-mar26.json


+ 0 - 0
reports/data/company-data-may25.json → data/company-data-may25.json


+ 0 - 0
reports/data/company-data-nov25.json → data/company-data-nov25.json


+ 0 - 0
reports/data/company-data-oct25.json → data/company-data-oct25.json


+ 0 - 0
reports/data/company-data-sep25.json → data/company-data-sep25.json


+ 0 - 0
reports/data/knownCompanies.json → data/knownCompanies.json


+ 0 - 0
reports/data/parentCorps.json → data/parentCorps.json


+ 0 - 0
reports/data/prod-data-apr25.json → data/prod-data-apr25.json


+ 0 - 0
reports/data/prod-data-apr26.json → data/prod-data-apr26.json


+ 0 - 0
reports/data/prod-data-aug25.json → data/prod-data-aug25.json


+ 0 - 0
reports/data/prod-data-dec25.json → data/prod-data-dec25.json


+ 0 - 0
reports/data/prod-data-feb26.json → data/prod-data-feb26.json


+ 0 - 0
reports/data/prod-data-jan26.json → data/prod-data-jan26.json


+ 0 - 0
reports/data/prod-data-jul25.json → data/prod-data-jul25.json


+ 0 - 0
reports/data/prod-data-jun25.json → data/prod-data-jun25.json


+ 0 - 0
reports/data/prod-data-mar25.json → data/prod-data-mar25.json


+ 0 - 0
reports/data/prod-data-mar26.json → data/prod-data-mar26.json


+ 0 - 0
reports/data/prod-data-may25.json → data/prod-data-may25.json


+ 0 - 0
reports/data/prod-data-nov25.json → data/prod-data-nov25.json


+ 0 - 0
reports/data/prod-data-oct25.json → data/prod-data-oct25.json


+ 0 - 0
reports/data/prod-data-sep25.json → data/prod-data-sep25.json


+ 0 - 0
reports/data/rawData/PrUN-LEAD-Data-April-2024.csv → data/rawData/PrUN-LEAD-Data-April-2024.csv


+ 0 - 0
reports/data/rawData/PrUN-LEAD-Data-April-2025.csv → data/rawData/PrUN-LEAD-Data-April-2025.csv


+ 0 - 0
reports/data/rawData/PrUN-LEAD-Data-April-2026.csv → data/rawData/PrUN-LEAD-Data-April-2026.csv


+ 0 - 0
reports/data/rawData/PrUN-LEAD-Data-August-2025.csv → data/rawData/PrUN-LEAD-Data-August-2025.csv


+ 0 - 0
reports/data/rawData/PrUN-LEAD-Data-December-2025.csv → data/rawData/PrUN-LEAD-Data-December-2025.csv


+ 0 - 0
reports/data/rawData/PrUN-LEAD-Data-February-2024.csv → data/rawData/PrUN-LEAD-Data-February-2024.csv


+ 0 - 0
reports/data/rawData/PrUN-LEAD-Data-February-2026.csv → data/rawData/PrUN-LEAD-Data-February-2026.csv


+ 0 - 0
reports/data/rawData/PrUN-LEAD-Data-January-2024.csv → data/rawData/PrUN-LEAD-Data-January-2024.csv


+ 0 - 0
reports/data/rawData/PrUN-LEAD-Data-January-2026.csv → data/rawData/PrUN-LEAD-Data-January-2026.csv


+ 0 - 0
reports/data/rawData/PrUN-LEAD-Data-July-2025.csv → data/rawData/PrUN-LEAD-Data-July-2025.csv


+ 0 - 0
reports/data/rawData/PrUN-LEAD-Data-June-2025.csv → data/rawData/PrUN-LEAD-Data-June-2025.csv


+ 0 - 0
reports/data/rawData/PrUN-LEAD-Data-March-2024.csv → data/rawData/PrUN-LEAD-Data-March-2024.csv


+ 0 - 0
reports/data/rawData/PrUN-LEAD-Data-March-2025.csv → data/rawData/PrUN-LEAD-Data-March-2025.csv


+ 0 - 0
reports/data/rawData/PrUN-LEAD-Data-March-2026.csv → data/rawData/PrUN-LEAD-Data-March-2026.csv


+ 0 - 0
reports/data/rawData/PrUN-LEAD-Data-May-2025.csv → data/rawData/PrUN-LEAD-Data-May-2025.csv


+ 0 - 0
reports/data/rawData/PrUN-LEAD-Data-November-2025.csv → data/rawData/PrUN-LEAD-Data-November-2025.csv


+ 0 - 0
reports/data/rawData/PrUN-LEAD-Data-October-2025.csv → data/rawData/PrUN-LEAD-Data-October-2025.csv


+ 0 - 0
reports/data/rawData/PrUN-LEAD-Data-September-2025.csv → data/rawData/PrUN-LEAD-Data-September-2025.csv


+ 0 - 0
reports/data/ship-data-apr26.json → data/ship-data-apr26.json


+ 0 - 0
reports/data/ship-data-feb26.json → data/ship-data-feb26.json


+ 0 - 0
reports/data/ship-data-mar26.json → data/ship-data-mar26.json


+ 0 - 0
reports/data/universe-data.json → data/universe-data.json


+ 0 - 0
reports/icon128.png → icon128.png


+ 60 - 7
index.html

@@ -1,14 +1,67 @@
 <!DOCTYPE html>
 <html lang="en">
 <head>
-  <meta charset="UTF-8">
-  <title>Redirecting...</title>
-  <script>
-    window.location.href = "https://pmmg-products.github.io/reports/";
-  </script>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>PrUn Financial Reports</title>
+	<link rel="stylesheet" href="styles.css">
+	<link rel="icon" type="image/x-icon" href="icon128.png">
+	<script src="main.js"></script>
+	<script src="https://cdn.plot.ly/plotly-3.0.1.min.js" charset="utf-8"></script>
 </head>
 <body>
-  <p>If you're not redirected, <a href="https://pmmg-products.github.io/reports">click here</a>.</p>
+
+<div class="topTabContainer" id="topTabContainer">
+	<div>
+		<div class="topTab">
+			<a class="topTabLink" href="https://github.com/PMMG-Products/pmmg-products.github.io">GitHub</a>
+			<div class="toggleIndicator toggleIndicatorActive"></div>
+		</div>
+	</div>
+	<div>
+		<div class="topTab" id="permalinkButton">
+			<a class="topTabLink">Get Permalink</a>
+			<div class="toggleIndicator toggleIndicatorActive"></div>
+		</div>
+		<div class="permalinkContainer" id="permalinkContainer" style="display: none">
+				<div class="permalinkCaret"></div>
+				<div class="permalinkInner">
+					<div style="margin-bottom: 8px">
+						<input class="permalink" id="permalink"></input>
+						<button id="permalinkCopyButton">🔗</button>
+					</div>
+					<div style="margin-bottom: 6px">
+						<input class="permalink" id="permalink-rprun"></input>
+						<button id="permalinkCopyButton-rprun">🔗</button>
+					</div>
+					<div>
+						<input type="checkbox" id="hideOptions"></input>
+						<div>Hide Options?</div>
+					</div>
+					<div>
+						<input type="checkbox" id="latestMonth" checked=true></input>
+						<div>Show Latest Month?</div>
+					</div>
+				</div>
+		</div>
+	</div>
+</div>
+
+<div class="mainContainer">
+	<div class="plotSelectorContainer" id="graphTypeContainer">
+		<label>
+			Graph: 
+			<select class="plotSelector" id="graphType">	
+			</select>
+		</label>
+	</div>
+	<div class="selectorSubtypes" id="selectorSubtypes">
+
+	</div>
+	<div class="mainPlot" id="mainPlotContainer">
+		<div id="mainPlot"></div>
+	</div>
+</div>
+
 </body>
 </html>
-

+ 0 - 0
reports/main.js → main.js


+ 0 - 1
reports/package.json → package.json

@@ -5,7 +5,6 @@
   "main": "main.js",
   "scripts": {
     "build": "bun build src/main.ts --target browser --sourcemap=external",
-    "typecheck": "tsgo --noEmit",
     "serve": "python3 -m http.server 8000"
   },
   "keywords": [],

+ 0 - 67
reports/index.html

@@ -1,67 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>PrUn Financial Reports</title>
-	<link rel="stylesheet" href="styles.css">
-	<link rel="icon" type="image/x-icon" href="icon128.png">
-	<script src="main.js"></script>
-	<script src="https://cdn.plot.ly/plotly-3.0.1.min.js" charset="utf-8"></script>
-</head>
-<body>
-
-<div class="topTabContainer" id="topTabContainer">
-	<div>
-		<div class="topTab">
-			<a class="topTabLink" href="https://github.com/PMMG-Products/pmmg-products.github.io">GitHub</a>
-			<div class="toggleIndicator toggleIndicatorActive"></div>
-		</div>
-	</div>
-	<div>
-		<div class="topTab" id="permalinkButton">
-			<a class="topTabLink">Get Permalink</a>
-			<div class="toggleIndicator toggleIndicatorActive"></div>
-		</div>
-		<div class="permalinkContainer" id="permalinkContainer" style="display: none">
-				<div class="permalinkCaret"></div>
-				<div class="permalinkInner">
-					<div style="margin-bottom: 8px">
-						<input class="permalink" id="permalink"></input>
-						<button id="permalinkCopyButton">🔗</button>
-					</div>
-					<div style="margin-bottom: 6px">
-						<input class="permalink" id="permalink-rprun"></input>
-						<button id="permalinkCopyButton-rprun">🔗</button>
-					</div>
-					<div>
-						<input type="checkbox" id="hideOptions"></input>
-						<div>Hide Options?</div>
-					</div>
-					<div>
-						<input type="checkbox" id="latestMonth" checked=true></input>
-						<div>Show Latest Month?</div>
-					</div>
-				</div>
-		</div>
-	</div>
-</div>
-
-<div class="mainContainer">
-	<div class="plotSelectorContainer" id="graphTypeContainer">
-		<label>
-			Graph: 
-			<select class="plotSelector" id="graphType">	
-			</select>
-		</label>
-	</div>
-	<div class="selectorSubtypes" id="selectorSubtypes">
-
-	</div>
-	<div class="mainPlot" id="mainPlotContainer">
-		<div id="mainPlot"></div>
-	</div>
-</div>
-
-</body>
-</html>

+ 0 - 0
reports/src/core.ts → src/core.ts


+ 0 - 0
reports/src/graphs/companyHistory.ts → src/graphs/companyHistory.ts


+ 0 - 0
reports/src/graphs/companyRank.ts → src/graphs/companyRank.ts


+ 0 - 0
reports/src/graphs/companyTotals.ts → src/graphs/companyTotals.ts


+ 0 - 0
reports/src/graphs/corpBreakdown.ts → src/graphs/corpBreakdown.ts


+ 0 - 0
reports/src/graphs/graph.ts → src/graphs/graph.ts


+ 0 - 0
reports/src/graphs/marketOverview.ts → src/graphs/marketOverview.ts


+ 0 - 0
reports/src/graphs/matHistory.ts → src/graphs/matHistory.ts


+ 0 - 0
reports/src/graphs/topCompanies.ts → src/graphs/topCompanies.ts


+ 0 - 0
reports/src/graphs/topProduction.ts → src/graphs/topProduction.ts


+ 0 - 0
reports/src/graphs/universeHistory.ts → src/graphs/universeHistory.ts


+ 0 - 0
reports/src/main.ts → src/main.ts


+ 0 - 0
reports/src/permalink.ts → src/permalink.ts


+ 0 - 0
reports/src/staticData/constants.ts → src/staticData/constants.ts


+ 0 - 0
reports/src/utils.ts → src/utils.ts


+ 0 - 0
reports/styles.css → styles.css


+ 0 - 0
reports/tsconfig.json → tsconfig.json


+ 0 - 0
reports/webpack.config.js → webpack.config.js


+ 1825 - 0
www/main.js

@@ -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

ファイルの差分が大きいため隠しています
+ 4 - 0
www/main.js.map


この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません