main.js 34 KB

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