main.js 32 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037
  1. const loadedData = {}; // Data loaded from files
  2. window.onload = function() {
  3. const graphTypeSelector = document.getElementById("graphType");
  4. const selectorSubtypes = document.getElementById("selectorSubtypes");
  5. graphTypeSelector.addEventListener("change", function() {
  6. updateSelectors(graphTypeSelector, selectorSubtypes);
  7. switchPlot();
  8. });
  9. updateSelectors(graphTypeSelector, selectorSubtypes, (new URLSearchParams(window.location.search)).has('type'));
  10. switchPlot();
  11. // Permalink stuff
  12. const permalinkContainer = document.getElementById("permalinkContainer");
  13. const permalinkButton = document.getElementById("permalinkButton");
  14. const permalinkCopyButton = document.getElementById("permalinkCopyButton");
  15. const permalinkOptionsButton = document.getElementById("hideOptions");
  16. const permalinkLatestMonth = document.getElementById("latestMonth");
  17. permalinkButton.addEventListener("click", function(e) {
  18. e.stopPropagation();
  19. const currentDisplay = permalinkContainer.style.display;
  20. if(currentDisplay == "none")
  21. {
  22. permalinkContainer.style.display = "block";
  23. }
  24. else
  25. {
  26. permalinkContainer.style.display = "none";
  27. }
  28. });
  29. document.addEventListener("click", function(e) {
  30. if(!permalinkContainer.contains(e.target) && !permalinkButton.contains(e.target))
  31. {
  32. permalinkContainer.style.display = "none";
  33. }
  34. });
  35. permalinkCopyButton.addEventListener("click", function() {
  36. const permalinkElem = document.getElementById("permalink");
  37. if(permalinkElem.value && permalinkElem.value != "")
  38. {
  39. navigator.clipboard.writeText(permalinkElem.value);
  40. }
  41. });
  42. permalinkOptionsButton.addEventListener("change", function() {
  43. updatePermalink(graphTypeSelector);
  44. });
  45. permalinkLatestMonth.addEventListener("change", function() {
  46. updatePermalink(graphTypeSelector);
  47. });
  48. };
  49. // Update selectors based on graph type
  50. function updateSelectors(graphTypeSelector, selectorSubtypes, useURLParams)
  51. {
  52. const monthsPretty = ["March 3025", "April 3025", "May 3025", "June 3025"];
  53. const months = ["mar25", "apr25", "may25", "jun25"];
  54. const currentMonth = "jun25";
  55. clearChildren(selectorSubtypes);
  56. const urlParams = new URLSearchParams(window.location.search);
  57. if(urlParams.has('hideOptions'))
  58. {
  59. const graphTypeContainer = document.getElementById('graphTypeContainer');
  60. const topTabs = document.getElementById('topTabContainer');
  61. topTabContainer.style.display = 'none';
  62. graphTypeContainer.style.display = 'none';
  63. selectorSubtypes.style.display = 'none';
  64. }
  65. if(useURLParams)
  66. {
  67. graphTypeSelector.value = urlParams.get('type');
  68. }
  69. if(graphTypeSelector.value == "topProduction")
  70. {
  71. selectorSubtypes.appendChild(addInput('select', 'metric', 'Metric: ', [['Volume', 'Profit', 'Deficit'], ['volume', 'profit', 'deficit']], useURLParams && urlParams.get('metric')));
  72. selectorSubtypes.appendChild(addInput('select', 'month', 'Month: ', [monthsPretty, months], useURLParams && urlParams.has('month') ? urlParams.get('month') : currentMonth));
  73. }
  74. else if(graphTypeSelector.value == "topCompanies")
  75. {
  76. selectorSubtypes.appendChild(addInput('select', 'metric', 'Metric: ', [['Volume', 'Profit'], ['volume', 'profit']], useURLParams && urlParams.get('metric')));
  77. selectorSubtypes.appendChild(addInput('select', 'month', 'Month: ', [monthsPretty, months], useURLParams && urlParams.has('month') ? urlParams.get('month') : currentMonth));
  78. }
  79. else if(graphTypeSelector.value == "matHistory")
  80. {
  81. selectorSubtypes.appendChild(addInput('select', 'metric', 'Metric: ', [['Volume', 'Profit', 'Price', 'Produced', 'Consumption', 'Surplus'], ['volume', 'profit', 'price', 'amount', 'consumed', 'surplus']], useURLParams && urlParams.get('metric')));
  82. selectorSubtypes.appendChild(addInput('input', 'ticker', 'Ticker: ', undefined, useURLParams && urlParams.get('ticker')));
  83. }
  84. else if(graphTypeSelector.value == "compTotals")
  85. {
  86. const chartTypeElem = addInput('select', 'chartType', 'Chart Type: ', [['Bar', 'Pie', 'Treemap (Mat)', 'Treemap (Cat)'], ['bar', 'pie', 'treemap', 'treemap-categories']], useURLParams && urlParams.get('chartType'));
  87. chartTypeElem.style.marginLeft = "-30px";
  88. selectorSubtypes.appendChild(chartTypeElem);
  89. selectorSubtypes.appendChild(addInput('select', 'metric', 'Metric: ', [['Volume', 'Profit'], ['volume', 'profit']], useURLParams && urlParams.get('metric')));
  90. selectorSubtypes.appendChild(addInput('select', 'month', 'Month: ', [monthsPretty, months], useURLParams && urlParams.has('month') ? urlParams.get('month') : currentMonth));
  91. // Username input and query button
  92. const usernameInput = addInput('input', 'username', 'Username: ', undefined, useURLParams && urlParams.get('username'));
  93. const submitButton = document.createElement("button");
  94. submitButton.textContent = "Query";
  95. submitButton.classList.add("queryButton");
  96. usernameInput.appendChild(submitButton);
  97. submitButton.addEventListener('click', getCompanyInfo);
  98. usernameInput.style.marginLeft = "33px";
  99. selectorSubtypes.appendChild(usernameInput);
  100. // Hidden company ID input, autofilled by query
  101. const companyIDInput = addInput('input', 'companyID', 'Company ID: ', undefined, useURLParams && urlParams.get('companyID'));
  102. companyIDInput.style.visibility = 'hidden';
  103. selectorSubtypes.appendChild(companyIDInput);
  104. const companyNameInput = addInput('input', 'companyName', 'Company Name: ', undefined, useURLParams && urlParams.get('companyName'));
  105. companyNameInput.style.visibility = 'hidden';
  106. selectorSubtypes.appendChild(companyNameInput);
  107. }
  108. else if(graphTypeSelector.value == "compHistory")
  109. {
  110. selectorSubtypes.appendChild(addInput('select', 'metric', 'Metric: ', [['Volume', 'Profit'], ['volume', 'profit']], useURLParams && urlParams.get('metric')));
  111. // Username input and query button
  112. const usernameInput = addInput('input', 'username', 'Username: ', undefined, useURLParams && urlParams.get('username'));
  113. const submitButton = document.createElement("button");
  114. submitButton.textContent = "Query";
  115. submitButton.classList.add("queryButton");
  116. usernameInput.appendChild(submitButton);
  117. submitButton.addEventListener('click', getCompanyInfo);
  118. usernameInput.style.marginLeft = "33px";
  119. selectorSubtypes.appendChild(usernameInput);
  120. // Hidden company ID input, autofilled by query
  121. const companyIDInput = addInput('input', 'companyID', 'Company ID: ', undefined, useURLParams && urlParams.get('companyID'));
  122. companyIDInput.style.visibility = 'hidden';
  123. selectorSubtypes.appendChild(companyIDInput);
  124. const companyNameInput = addInput('input', 'companyName', 'Company Name: ', undefined, useURLParams && urlParams.get('companyName'));
  125. companyNameInput.style.visibility = 'hidden';
  126. selectorSubtypes.appendChild(companyNameInput);
  127. }
  128. else if(graphTypeSelector.value == "compRank")
  129. {
  130. selectorSubtypes.appendChild(addInput('select', 'month', 'Month: ', [monthsPretty, months], useURLParams && urlParams.has('month') ? urlParams.get('month') : currentMonth));
  131. // Username input and query button
  132. const usernameInput = addInput('input', 'username', 'Username: ', undefined, useURLParams && urlParams.get('username'));
  133. const submitButton = document.createElement("button");
  134. submitButton.textContent = "Query";
  135. submitButton.classList.add("queryButton");
  136. usernameInput.appendChild(submitButton);
  137. submitButton.addEventListener('click', getCompanyInfo);
  138. usernameInput.style.marginLeft = "33px";
  139. selectorSubtypes.appendChild(usernameInput);
  140. // Hidden company ID input, autofilled by query
  141. const companyIDInput = addInput('input', 'companyID', 'Company ID: ', undefined, useURLParams && urlParams.get('companyID'));
  142. companyIDInput.style.visibility = 'hidden';
  143. selectorSubtypes.appendChild(companyIDInput);
  144. const companyNameInput = addInput('input', 'companyName', 'Company Name: ', undefined, useURLParams && urlParams.get('companyName'));
  145. companyNameInput.style.visibility = 'hidden';
  146. selectorSubtypes.appendChild(companyNameInput);
  147. }
  148. }
  149. function switchPlot()
  150. {
  151. const typeElem = document.getElementById("graphType");
  152. var subtypeElem;
  153. var monthElem;
  154. var metricElem;
  155. var matElem;
  156. var nameElem;
  157. var idElem;
  158. const months = ['mar25', 'apr25', 'may25', 'jun25']; // Automate this later
  159. const oldGraph = document.getElementById("mainPlot");
  160. oldGraph.remove();
  161. const newGraph = document.createElement("div");
  162. newGraph.id = "mainPlot";
  163. const graphContainer = document.getElementById("mainPlotContainer");
  164. graphContainer.appendChild(newGraph);
  165. switch(typeElem.value)
  166. {
  167. case "topProduction":
  168. metricElem = document.getElementById("metric");
  169. monthElem = document.getElementById("month");
  170. promiseGenerateTopProdGraph("mainPlot", monthElem.value, metricElem.value);
  171. break;
  172. case "topCompanies":
  173. metricElem = document.getElementById("metric");
  174. monthElem = document.getElementById("month");
  175. promiseGenerateTopCompanyGraph("mainPlot", monthElem.value, metricElem.value);
  176. break;
  177. case "matHistory":
  178. metricElem = document.getElementById("metric");
  179. matElem = document.getElementById("ticker");
  180. promiseGenerateMatGraph("mainPlot", matElem.value, metricElem.value, months);
  181. break;
  182. case "compTotals":
  183. subtypeElem = document.getElementById("chartType");
  184. metricElem = document.getElementById("metric");
  185. monthElem = document.getElementById("month");
  186. nameElem = document.getElementById("companyName");
  187. idElem = document.getElementById("companyID");
  188. promiseGenerateCompanyGraph("mainPlot", subtypeElem.value, nameElem.value, idElem.value, monthElem.value, metricElem.value);
  189. break;
  190. case "compHistory":
  191. metricElem = document.getElementById("metric");
  192. nameElem = document.getElementById("companyName");
  193. idElem = document.getElementById("companyID");
  194. promiseGenerateCompanyHistoryGraph("mainPlot", nameElem.value, idElem.value, metricElem.value, months);
  195. break;
  196. case "compRank":
  197. monthElem = document.getElementById("month");
  198. nameElem = document.getElementById("companyName");
  199. idElem = document.getElementById("companyID");
  200. promiseGenerateRankChart("mainPlot", nameElem.value, idElem.value, monthElem.value, months);
  201. }
  202. updatePermalink(typeElem);
  203. }
  204. function updatePermalink(typeElem)
  205. {
  206. const permalinkInput = document.getElementById("permalink")
  207. const hideOptionsButton = document.getElementById("hideOptions");
  208. const latestMonthButton = document.getElementById("latestMonth");
  209. var permalink = "https://pmmg-products.github.io/reports/?type=" + typeElem.value
  210. const relevantSubtypes = {
  211. "topProduction": ["metric", "month"],
  212. "topCompanies": ["metric", "month"],
  213. "matHistory": ["metric", "ticker"],
  214. "compTotals": ["chartType", "metric", "month", "username", "companyName", "companyID"],
  215. "compHistory": ["metric", "username", "companyName", "companyID"],
  216. "compRank": ["month", "username", "companyName", "companyID"]
  217. }
  218. relevantSubtypes[typeElem.value].forEach(subtype => {
  219. if(subtype == "month" && latestMonthButton.checked){return;}
  220. const inputElem = document.getElementById(subtype);
  221. if(inputElem.value && inputElem.value != "")
  222. {
  223. permalink += "&" + subtype + "=" + inputElem.value
  224. }
  225. });
  226. if(hideOptionsButton.checked)
  227. {
  228. permalink += "&hideOptions"
  229. }
  230. permalinkInput.value = permalink;
  231. return;
  232. }
  233. function promiseGenerateRankChart(container, companyName, companyID, currentMonth, months)
  234. {
  235. const monthIndex = months.indexOf(currentMonth);
  236. const prevMonth = months[monthIndex == 0 ? 0 : monthIndex - 1]; // Get previous month to determine change
  237. (async () => {
  238. if(!loadedData['company-data-' + currentMonth])
  239. {
  240. loadedData['company-data-' + currentMonth] = await fetch('data/company-data-' + currentMonth + '.json?cb=' + Date.now()).then(response => response.json())
  241. }
  242. if(!loadedData['company-data-' + prevMonth])
  243. {
  244. loadedData['company-data-' + prevMonth] = await fetch('data/company-data-' + prevMonth + '.json?cb=' + Date.now()).then(response => response.json())
  245. }
  246. generateRankChart(container, loadedData['company-data-' + currentMonth].individual[companyID], (monthIndex == 0 ? undefined : loadedData['company-data-' + prevMonth].individual[companyID]), companyName, currentMonth); // Use the JSON data
  247. })();
  248. }
  249. function promiseGenerateCompanyHistoryGraph(container, companyName, companyID, metric, months) // Metric is either 'profit' or 'volume'
  250. {
  251. if(!companyID){return;}
  252. const validMonths = [];
  253. const data = []
  254. var hasData = false;
  255. (async () => {
  256. for(const month of months) {
  257. if(!loadedData['company-data-' + month])
  258. {
  259. loadedData['company-data-' + month] = await fetch('data/company-data-' + month + '.json?cb=' + Date.now()).then(response => response.json())
  260. }
  261. const dataPoint = loadedData['company-data-' + month].totals[companyID]
  262. if(dataPoint)
  263. {
  264. data.push(dataPoint[metric])
  265. validMonths.push(month);
  266. hasData = true;
  267. }
  268. };
  269. if(hasData)
  270. {
  271. generateCompanyHistoryGraph(container, months.map(month => prettyMonthName(month)), data, companyName, metric)
  272. }
  273. })();
  274. }
  275. function promiseGenerateCompanyGraph(container, chartType, companyName, companyID, month, metric) // Metric is either 'profit' or 'volume'. chartType is either 'bar' or 'pie'
  276. {
  277. (async () => {
  278. if(!loadedData['company-data-' + month])
  279. {
  280. loadedData['company-data-' + month] = await fetch('data/company-data-' + month + '.json?cb=' + Date.now()).then(response => response.json())
  281. }
  282. generateCompanyGraph(container, chartType, loadedData['company-data-' + month].individual[companyID], companyName, month, metric); // Use the JSON data
  283. })();
  284. }
  285. function promiseGenerateTopCompanyGraph(container, month, metric) // Metric is either 'profit' or 'volume'
  286. {
  287. (async () => {
  288. if(!loadedData['company-data-' + month])
  289. {
  290. loadedData['company-data-' + month] = await fetch('data/company-data-' + month + '.json?cb=' + Date.now()).then(response => response.json())
  291. }
  292. if(!loadedData['known-companies'])
  293. {
  294. loadedData['known-companies'] = await fetch('data/knownCompanies2.json?cb=' + Date.now()).then(response => response.json())
  295. }
  296. generateTopCompanyGraph(container, loadedData['company-data-' + month], loadedData['known-companies'], month, metric); // Use the JSON data
  297. })();
  298. }
  299. function promiseGenerateTopProdGraph(container, month, metric) // Metric is either 'profit' or 'volume'
  300. {
  301. (async () => {
  302. if(!loadedData['prod-data-' + month])
  303. {
  304. loadedData['prod-data-' + month] = await fetch('data/prod-data-' + month + '.json?cb=' + Date.now()).then(response => response.json())
  305. }
  306. const data = loadedData['prod-data-' + month];
  307. if(metric == 'deficit') // Populate deficit into data
  308. {
  309. Object.keys(data).forEach(ticker => {
  310. if(!data[ticker]['amount'] || data[ticker]['amount'] == 0){data[ticker]['deficit'] = 0; return;}
  311. data[ticker]['deficit'] = (data[ticker]['amount'] - (data[ticker]['consumed'] || 0)) * data[ticker]['volume'] / data[ticker]['amount'];
  312. });
  313. }
  314. generateTopProdGraph(container, data, month, metric); // Use the JSON data
  315. })();
  316. }
  317. function promiseGenerateMatGraph(container, ticker, metric, months) // Metric is either 'profit', 'volume', or 'amount'
  318. {
  319. // Validation/sanitizing
  320. if(!ticker){return;}
  321. ticker = ticker.toUpperCase();
  322. const validMonths = [];
  323. const data = [];
  324. (async () => {
  325. for(const month of months) {
  326. if(!loadedData['prod-data-' + month])
  327. {
  328. loadedData['prod-data-' + month] = await fetch('data/prod-data-' + month + '.json?cb=' + Date.now()).then(response => response.json())
  329. }
  330. const dataPoint = loadedData['prod-data-' + month][ticker]
  331. if(dataPoint)
  332. {
  333. if(metric == 'price')
  334. {
  335. data.push(dataPoint['amount'] == 0 ? 0 : dataPoint['volume'] / dataPoint['amount']);
  336. }
  337. else if(metric == 'surplus')
  338. {
  339. data.push(dataPoint['amount'] - dataPoint['consumed'])
  340. }
  341. else
  342. {
  343. data.push(dataPoint[metric])
  344. }
  345. validMonths.push(month)
  346. hasData = true;
  347. }
  348. };
  349. if(hasData)
  350. {
  351. generateMatGraph(container, validMonths.map(month => prettyMonthName(month)), data, ticker, metric)
  352. }
  353. })();
  354. }
  355. function generateTopProdGraph(container, prodData, month, metric)
  356. {
  357. const titles = {
  358. 'profit': 'Profit Materials',
  359. 'volume': 'Production Volumes',
  360. 'deficit': 'Deficits'
  361. }
  362. // Convert the data object into an array of [ticker, volume] pairs
  363. const volumeArray = Object.entries(prodData).map(([ticker, info]) => ({
  364. ticker,
  365. volume: info[metric]
  366. }));
  367. // Sort the array by volume in descending order
  368. if(metric == 'deficit')
  369. {
  370. volumeArray.sort((a, b) => a.volume - b.volume);
  371. }
  372. else
  373. {
  374. volumeArray.sort((a, b) => b.volume - a.volume);
  375. }
  376. // Extract tickers and volumes into separate arrays
  377. const tickers = volumeArray.map(item => item.ticker);
  378. const volumes = volumeArray.map(item => item.volume);
  379. Plotly.newPlot(container, {
  380. data: [{ x: tickers, y: volumes, type: 'bar' , marker: {color: 'rgb(247, 166, 0)'}, hovertemplate: '%{x}: %{y:,.3~s}<extra></extra>'}],
  381. layout: {width: 800, height: 400,
  382. title: {text: 'Top ' + titles[metric] + ' - ' + prettyMonthName(month),
  383. font: {color: '#eee', family: '"Droid Sans", sans-serif'},
  384. },
  385. xaxis: {
  386. title: {
  387. text: 'Ticker',
  388. font: {color: '#bbb', family: '"Droid Sans", sans-serif'}
  389. },
  390. tickfont: {color: '#666', family: '"Droid Sans", sans-serif'},
  391. range: [-0.5, 29.5],
  392. tickangle: -45
  393. },
  394. yaxis: {
  395. title: {
  396. text: prettyModeNames[metric] + ' [$/day]',
  397. font: {color: '#bbb', family: '"Droid Sans", sans-serif'}
  398. },
  399. tickfont: {color: '#666', family: '"Droid Sans", sans-serif'},
  400. range: [(metric == 'deficit' ? null : 0), (metric == 'deficit' ? 0 : null)],
  401. gridcolor: '#323232'
  402. },
  403. plot_bgcolor: '#252525',
  404. paper_bgcolor: '#252525',
  405. dragmode: 'pan'
  406. },
  407. config: {
  408. displayModeBar: true,
  409. modeBarButtonsToRemove: ['lasso2d'], // Remove unwanted buttons
  410. displaylogo: false,
  411. scrollZoom: true
  412. }
  413. });
  414. }
  415. function generateTopCompanyGraph(container, companyData, knownCompanies, month, metric)
  416. {
  417. // Convert the data object into an array of [companyID, volume] pairs
  418. const volumeArray = Object.entries(companyData.totals).map(([companyID, info]) => ({
  419. companyID,
  420. volume: info[metric]
  421. }));
  422. // Sort the array by volume in descending order
  423. volumeArray.sort((a, b) => b.volume - a.volume);
  424. // Extract tickers and volumes into separate arrays
  425. const companyIDs = volumeArray.map(item => item.companyID);
  426. const volumes = volumeArray.map(item => item.volume);
  427. const companyNames = [];
  428. // Print unknown top 40 companies
  429. companyIDs.slice(0,40).forEach(id => {
  430. if(!knownCompanies[id])
  431. {
  432. console.log(id)
  433. }
  434. });
  435. companyIDs.forEach(id => {
  436. companyNames.push(knownCompanies[id] || (id.slice(0, 5) + "..."));
  437. });
  438. Plotly.newPlot(container, {
  439. data: [{ x: companyNames, y: volumes, type: 'bar' , marker: {color: 'rgb(247, 166, 0)'}, hovertemplate: '%{x}: %{y:,.3~s}<extra></extra>'}],
  440. layout: { width: 800, height: 400,
  441. title: {text: 'Top Companies (' + prettyModeNames[metric] + ') - ' + prettyMonthName(month),
  442. font: {color: '#eee', family: '"Droid Sans", sans-serif'},
  443. },
  444. xaxis: {
  445. title: {
  446. text: 'Player',
  447. font: {color: '#bbb', family: '"Droid Sans", sans-serif'}
  448. },
  449. tickfont: {color: '#666', family: '"Droid Sans", sans-serif'},
  450. range: [-0.5, 29.5],
  451. tickangle: -45
  452. },
  453. yaxis: {
  454. title: {
  455. text: prettyModeNames[metric] + ' [$/day]',
  456. font: {color: '#bbb', family: '"Droid Sans", sans-serif'}
  457. },
  458. tickfont: {color: '#666', family: '"Droid Sans", sans-serif'},
  459. range: [0, null],
  460. gridcolor: '#323232'
  461. },
  462. plot_bgcolor: '#252525',
  463. paper_bgcolor: '#252525',
  464. dragmode: 'pan',
  465. margin: {b: 120}
  466. },
  467. config: {
  468. displayModeBar: true,
  469. modeBarButtonsToRemove: ['lasso2d'], // Remove unwanted buttons
  470. displaylogo: false,
  471. scrollZoom: true
  472. }
  473. });
  474. }
  475. function generateMatGraph(container, months, data, ticker, metric)
  476. {
  477. const titles = {
  478. 'profit': 'Production Profit History of ',
  479. 'volume': 'Production Volume History of ',
  480. 'amount': 'Production Amount History of ',
  481. 'price': 'Price History of ',
  482. 'consumed': 'Consumption History of ',
  483. 'surplus': 'Surplus Production History of '
  484. }
  485. const yAxis = {
  486. 'profit': 'Daily Profit [$/day]',
  487. 'volume': 'Daily Volume [$/day]',
  488. 'amount': 'Daily Production [per day]',
  489. 'price': 'Price [$]',
  490. 'consumed': 'Daily Consumption [per day]',
  491. 'surplus': 'Daily Surplus [per day]'
  492. }
  493. Plotly.newPlot(container, {
  494. data: [{ x: months, y: data, type: 'bar' , marker: {color: 'rgb(247, 166, 0)'}, hovertemplate: '%{x}: %{y:,.3~s}<extra></extra>'}],
  495. layout: { width: 800, height: 400,
  496. title: {text: titles[metric] + ticker,
  497. font: {color: '#eee', family: '"Droid Sans", sans-serif'},
  498. },
  499. xaxis: {
  500. title: {
  501. text: 'Month',
  502. font: {color: '#bbb', family: '"Droid Sans", sans-serif'}
  503. },
  504. tickfont: {color: '#666', family: '"Droid Sans", sans-serif'},
  505. tickangle: -45
  506. },
  507. yaxis: {
  508. title: {
  509. text: yAxis[metric],
  510. font: {color: '#bbb', family: '"Droid Sans", sans-serif'}
  511. },
  512. tickfont: {color: '#666', family: '"Droid Sans", sans-serif'},
  513. gridcolor: '#323232'
  514. },
  515. plot_bgcolor: '#252525',
  516. paper_bgcolor: '#252525',
  517. dragmode: 'pan'
  518. },
  519. config: {
  520. displayModeBar: true,
  521. modeBarButtonsToRemove: ['lasso2d'], // Remove unwanted buttons
  522. displaylogo: false,
  523. scrollZoom: true
  524. }
  525. });
  526. }
  527. function generateCompanyGraph(container, chartType, data, companyName, month, metric)
  528. {
  529. if(!data){return;}
  530. const titles = {
  531. 'profit': 'Production Profit Breakdown of ',
  532. 'volume': 'Production Volume Breakdown of ',
  533. }
  534. var mats = Object.keys(data);
  535. var values = mats.map(ticker => data[ticker][metric]);
  536. var indices = values.map((_, i) => i).sort((a, b) => values[b] - values[a]);
  537. mats = indices.map(i => mats[i]);
  538. values = indices.map(i => values[i]);
  539. if(chartType == 'treemap' || chartType == 'treemap-categories')
  540. {
  541. // Filter out negative values
  542. indices = values
  543. .map((v, i) => i)
  544. .filter(i => values[i] >= 0);
  545. mats = indices.map(i => mats[i]);
  546. values = indices.map(i => values[i]);
  547. var colors = mats.map(m => materialsToColors[m] || '#000000');
  548. var parents = chartType == 'treemap-categories'
  549. ? mats.map(m => materialsToCategories[m] || 'Other')
  550. : mats.map(m => 'Total');
  551. var totalValue = 0;
  552. var categoryValues = {};
  553. for (const i of indices) {
  554. totalValue += values[i];
  555. const category = parents[i];
  556. categoryValues[category] = (categoryValues[category] || 0) + values[i];
  557. }
  558. if (chartType == 'treemap-categories') {
  559. for (const category in categoryValues) {
  560. mats.push(category);
  561. values.push(categoryValues[category]);
  562. colors.push(materialCategoryColors[category] || '#000000');
  563. parents.push('Total');
  564. }
  565. }
  566. values.push(totalValue);
  567. mats.push('Total');
  568. parents.push('');
  569. colors.push('#252525');
  570. Plotly.newPlot(container, {
  571. data: [
  572. {
  573. type: 'treemap',
  574. labels: mats,
  575. parents: parents,
  576. values: values,
  577. maxdepth: 2,
  578. branchvalues: 'total',
  579. marker: {
  580. colors: colors,
  581. },
  582. tiling: {
  583. pad: 0,
  584. },
  585. textposition: 'middle center',
  586. hovertemplate: '%{label}<br>$%{value:,.3~s}/day<br>%{percentEntry:.2%}<extra></extra>'
  587. }
  588. ],
  589. layout: { width: 800, height: 600,
  590. title: {text: titles[metric] + companyName + ' - ' + prettyMonthName(month),
  591. font: {color: '#eee', family: '"Droid Sans", sans-serif'},
  592. },
  593. plot_bgcolor: '#252525',
  594. paper_bgcolor: '#252525',
  595. },
  596. config: {
  597. displaylogo: false,
  598. }
  599. });
  600. }
  601. else if(chartType == 'pie')
  602. {
  603. // Filter out negative values
  604. indices = values
  605. .map((v, i) => i)
  606. .filter(i => values[i] >= 0);
  607. mats = indices.map(i => mats[i]);
  608. values = indices.map(i => values[i]);
  609. Plotly.newPlot(container, {
  610. data: [{ labels: mats, values: values, type: 'pie', textinfo: 'label',textposition: 'inside', insidetextorientation: 'none', automargin: false, hovertemplate: '%{label}<br>$%{value:,.3~s}/day<br>%{percent}<extra></extra>'}],
  611. layout: { width: 800, height: 400,
  612. title: {text: titles[metric] + companyName + ' - ' + prettyMonthName(month),
  613. font: {color: '#eee', family: '"Droid Sans", sans-serif'},
  614. },
  615. plot_bgcolor: '#252525',
  616. paper_bgcolor: '#252525',
  617. },
  618. config: {
  619. displayModeBar: true,
  620. modeBarButtonsToRemove: ['lasso2d'], // Remove unwanted buttons
  621. displaylogo: false,
  622. scrollZoom: true
  623. }
  624. });
  625. }
  626. else
  627. {
  628. Plotly.newPlot(container, {
  629. data: [{ x: mats, y: values, type: 'bar' , marker: {color: 'rgb(247, 166, 0)'}, hovertemplate: '%{x}: %{y:,.3~s}<extra></extra>'}],
  630. layout: { width: 800, height: 400,
  631. title: {text: titles[metric] + companyName + ' - ' + prettyMonthName(month),
  632. font: {color: '#eee', family: '"Droid Sans", sans-serif'},
  633. },
  634. xaxis: {
  635. title: {
  636. text: 'Ticker',
  637. font: {color: '#bbb', family: '"Droid Sans", sans-serif'}
  638. },
  639. tickfont: {color: '#666', family: '"Droid Sans", sans-serif'},
  640. range: [-0.5, Math.min(mats.length, 30) - 0.5],
  641. tickangle: -45
  642. },
  643. yaxis: {
  644. title: {
  645. text: prettyModeNames[metric] + ' [$/day]',
  646. font: {color: '#bbb', family: '"Droid Sans", sans-serif'}
  647. },
  648. tickfont: {color: '#666', family: '"Droid Sans", sans-serif'},
  649. range: [0, null],
  650. gridcolor: '#323232'
  651. },
  652. plot_bgcolor: '#252525',
  653. paper_bgcolor: '#252525',
  654. dragmode: 'pan'
  655. },
  656. config: {
  657. displayModeBar: true,
  658. modeBarButtonsToRemove: ['lasso2d'], // Remove unwanted buttons
  659. displaylogo: false,
  660. scrollZoom: true
  661. }
  662. });
  663. }
  664. }
  665. function generateCompanyHistoryGraph(container, months, data, companyName, metric)
  666. {
  667. const titles = {
  668. 'profit': 'Production Profit History of ',
  669. 'volume': 'Production Volume History of ',
  670. }
  671. const yAxis = {
  672. 'profit': 'Daily Profit [$/day]',
  673. 'volume': 'Daily Volume [$/day]'
  674. }
  675. Plotly.newPlot(container, {
  676. data: [{ x: months, y: data, type: 'bar' , marker: {color: 'rgb(247, 166, 0)'}, hovertemplate: '%{x}: %{y:,.3~s}<extra></extra>'}],
  677. layout: { width: 800, height: 400,
  678. title: {text: titles[metric] + companyName,
  679. font: {color: '#eee', family: '"Droid Sans", sans-serif'},
  680. },
  681. xaxis: {
  682. title: {
  683. text: 'Month',
  684. font: {color: '#bbb', family: '"Droid Sans", sans-serif'}
  685. },
  686. tickfont: {color: '#666', family: '"Droid Sans", sans-serif'},
  687. tickangle: -45
  688. },
  689. yaxis: {
  690. title: {
  691. text: yAxis[metric],
  692. font: {color: '#bbb', family: '"Droid Sans", sans-serif'}
  693. },
  694. tickfont: {color: '#666', family: '"Droid Sans", sans-serif'},
  695. gridcolor: '#323232'
  696. },
  697. plot_bgcolor: '#252525',
  698. paper_bgcolor: '#252525',
  699. dragmode: 'pan'
  700. },
  701. config: {
  702. displayModeBar: true,
  703. modeBarButtonsToRemove: ['lasso2d'], // Remove unwanted buttons
  704. displaylogo: false,
  705. scrollZoom: true
  706. }
  707. });
  708. }
  709. function generateRankChart(containerID, currentData, prevData, companyName, currentMonth)
  710. {
  711. if(!currentData){return;}
  712. const container = document.getElementById(containerID);
  713. const tickers = Object.keys(currentData);
  714. const currentRanks = tickers.map(ticker => ({'ticker': ticker, 'currentRank': currentData[ticker].rank, 'data': currentData[ticker]}));
  715. currentRanks.forEach(mat => {
  716. if(prevData && prevData[mat.ticker])
  717. {
  718. mat.prevRank = prevData[mat.ticker].rank;
  719. }
  720. });
  721. currentRanks.sort((x, y) => x.currentRank - y.currentRank);
  722. // Create title
  723. const title = document.createElement("div");
  724. title.textContent = "Production Ranking of " + companyName + " - " + prettyMonthName(currentMonth);
  725. title.classList.add("title");
  726. container.appendChild(title);
  727. // Start creating table
  728. const table = document.createElement("table");
  729. container.appendChild(table);
  730. // Table header
  731. const header = document.createElement("thead");
  732. table.appendChild(header);
  733. const headRow = document.createElement("tr");
  734. header.appendChild(headRow);
  735. const headers = ["Rank", "Ticker", "Amount [/day]", "Volume [$/day]", "Profit [$/day]"]
  736. headers.forEach(label => {
  737. const headerColumn = document.createElement("th");
  738. headerColumn.textContent = label;
  739. headRow.appendChild(headerColumn);
  740. });
  741. const body = document.createElement("tbody");
  742. table.appendChild(body);
  743. currentRanks.forEach(mat =>
  744. {
  745. const row = document.createElement("tr");
  746. body.appendChild(row);
  747. const rankColumn = document.createElement("td");
  748. const rankWrapper = document.createElement("div");
  749. rankWrapper.style.display = "flex";
  750. rankColumn.appendChild(rankWrapper);
  751. if(prevData)
  752. {
  753. const rankSymbol = document.createElement("div");
  754. rankSymbol.style.width = "14px";
  755. rankSymbol.style.minWidth = "14px";
  756. rankSymbol.style.marginRight = "2px";
  757. if(mat.prevRank && mat.prevRank != mat.currentRank)
  758. {
  759. const increasing = mat.prevRank && mat.prevRank < mat.currentRank;
  760. rankSymbol.textContent = increasing ? "▼" : "▲";
  761. rankSymbol.style.color = increasing ? "#d9534f" : "#5cb85c";
  762. }
  763. rankWrapper.appendChild(rankSymbol);
  764. }
  765. const rankNum = document.createElement("div");
  766. rankNum.textContent = mat.currentRank;
  767. rankWrapper.appendChild(rankNum);
  768. row.appendChild(rankColumn);
  769. const tickerColumn = document.createElement("td")
  770. tickerColumn.textContent = mat.ticker;
  771. row.appendChild(tickerColumn);
  772. const amountColumn = document.createElement("td")
  773. amountColumn.textContent = mat.data.amount.toLocaleString(undefined, {maximumFractionDigits: 1});
  774. row.appendChild(amountColumn);
  775. const volumeColumn = document.createElement("td")
  776. volumeColumn.textContent = "$" + mat.data.volume.toLocaleString(undefined, {notation: 'compact', maxixmumSignificantDigits: 3});
  777. row.appendChild(volumeColumn);
  778. const profitColumn = document.createElement("td")
  779. profitColumn.textContent = "$" + mat.data.profit.toLocaleString(undefined, {notation: 'compact', maxixmumSignificantDigits: 3});
  780. row.appendChild(profitColumn);
  781. });
  782. }
  783. // Util functions
  784. function prettyMonthName(monthStr)
  785. {
  786. const monthAbv = monthStr.substring(0,3);
  787. const monthNum = monthStr.substring(3);
  788. return fullMonthNames[monthAbv] + " 30" + monthNum;
  789. }
  790. // Remove all the children of a given element
  791. function clearChildren(elem)
  792. {
  793. elem.textContent = "";
  794. while(elem.children[0])
  795. {
  796. elem.removeChild(elem.children[0]);
  797. }
  798. return;
  799. }
  800. // Add options to a selector
  801. function addOptions(selector, options, values)
  802. {
  803. for(var i = 0; i < options.length; i++)
  804. {
  805. const optionElem = document.createElement("option");
  806. optionElem.textContent = options[i];
  807. optionElem.value = values ? values[i] : options[i];
  808. selector.appendChild(optionElem);
  809. }
  810. }
  811. function wrapInDiv(elem) // Wrap selector element in a div to center it and give it margin
  812. {
  813. const div = document.createElement('div');
  814. div.appendChild(elem);
  815. return div;
  816. }
  817. function addInput(inputType, id, label, values, defaultValue)
  818. {
  819. const labelElem = document.createElement('label');
  820. labelElem.textContent = label;
  821. const inputElem = document.createElement(inputType);
  822. inputElem.id = id;
  823. inputElem.classList.add("plotSelector");
  824. if(inputType == 'select')
  825. {
  826. addOptions(inputElem, values[0], values[1]);
  827. }
  828. if(defaultValue)
  829. {
  830. inputElem.value = defaultValue;
  831. }
  832. inputElem.addEventListener("change", function() {
  833. switchPlot();
  834. });
  835. labelElem.appendChild(inputElem);
  836. return wrapInDiv(labelElem);
  837. }
  838. async function getCompanyInfo()
  839. {
  840. const usernameInput = document.getElementById('username');
  841. var companyID;
  842. var companyName;
  843. if(!usernameInput.value){return;}
  844. (async () => {
  845. if(!loadedData['known-companies'])
  846. {
  847. loadedData['known-companies'] = await fetch('data/knownCompanies2.json?cb=' + Date.now()).then(response => response.json())
  848. }
  849. const match = Object.entries(loadedData['known-companies']).find(([id, username]) => username && (username.toLowerCase() === usernameInput.value.toLowerCase()));
  850. if(match)
  851. {
  852. [companyID, companyName] = match;
  853. }
  854. else
  855. {
  856. const fioResult = fetch('https://rest.fnar.net/user/' + usernameInput.value).then(response => response.json()).catch(error => {alert('Bad Response: Check Username'); console.error(error)});
  857. companyID = fioResult.CompanyId;
  858. companyName = fioResult.UserName;
  859. }
  860. if(!companyID || !companyName){return;}
  861. const companyIDInput = document.getElementById('companyID');
  862. companyIDInput.value = companyID;
  863. const companyNameInput = document.getElementById('companyName');
  864. companyNameInput.value = companyName;
  865. switchPlot();
  866. })();
  867. }
  868. const fullMonthNames = {
  869. "jan": "January",
  870. "feb": "February",
  871. "mar": "March",
  872. "apr": "April",
  873. "may": "May",
  874. "jun": "June",
  875. "jul": "July",
  876. "aug": "August",
  877. "sep": "September",
  878. "oct": "October",
  879. "nov": "November",
  880. "dec": "December"
  881. }
  882. const prettyModeNames = {
  883. "amount": "Amount",
  884. "profit": "Profit",
  885. "volume": "Volume",
  886. "price": "Price"
  887. }