main.js 35 KB

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