diff --git a/__tests__/serverjs/analytics.test.js b/__tests__/serverjs/analytics.test.js deleted file mode 100644 index cbe8cbf8f..000000000 --- a/__tests__/serverjs/analytics.test.js +++ /dev/null @@ -1,175 +0,0 @@ -const analytics = require('../../serverjs/analytics'); -const carddb = require('../../serverjs/cards'); -const cubefixture = require('../../fixtures/examplecube'); - -const fixturesPath = 'fixtures'; - -beforeEach(() => {}); - -afterEach(() => {}); - -test('GetColorCat returns the expected results', () => { - expect(analytics.GetColorCat('land', [])).toBe('l'); - expect(analytics.GetColorCat('creature', [])).toBe('c'); - expect(analytics.GetColorCat('creature', ['G', 'R'])).toBe('m'); - expect(analytics.GetColorCat('creature', ['G'])).toBe('g'); -}); - -test('GetColorIdentity returns the expected results', () => { - expect(analytics.GetColorIdentity([])).toBe('Colorless'); - expect(analytics.GetColorIdentity(['G', 'R'])).toBe('Multicolored'); - expect(analytics.GetColorIdentity(['G'])).toBe('Green'); -}); - -test('GetTypeByColorIdentity returns valid counts', () => { - expect.assertions(1); - var promise = carddb.initializeCardDb(fixturesPath, true); - return promise.then(function() { - var expected = { - Artifacts: { - Black: 0, - Blue: 2, - Colorless: 1, - Green: 0, - Multi: 0, - Red: 1, - Total: 5, - White: 1, - }, - Creatures: { - Black: 7, - Blue: 7, - Colorless: 1, - Green: 6, - Multi: 4, - Red: 6, - Total: 40, - White: 9, - }, - Enchantments: { - Black: 0, - Blue: 1, - Colorless: 0, - Green: 1, - Multi: 3, - Red: 1, - Total: 7, - White: 1, - }, - Instants: { - Black: 0, - Blue: 0, - Colorless: 0, - Green: 0, - Multi: 0, - Red: 0, - Total: 1, - White: 1, - }, - Lands: { - Black: 1, - Blue: 1, - Colorless: 2, - Green: 1, - Multi: 0, - Red: 1, - Total: 7, - White: 1, - }, - Planeswalkers: { - Black: 0, - Blue: 1, - Colorless: 0, - Green: 0, - Multi: 1, - Red: 0, - Total: 2, - White: 0, - }, - Sorceries: { - Black: 0, - Blue: 0, - Colorless: 0, - Green: 0, - Multi: 2, - Red: 1, - Total: 3, - White: 0, - }, - Total: { - Black: 8, - Blue: 12, - Colorless: 4, - Green: 8, - Multi: 10, - Red: 10, - Total: 65, - White: 13, - }, - }; - var result = analytics.GetTypeByColorIdentity(cubefixture.exampleCube.cards, carddb); - expect(result).toEqual(expected); - }); -}); - -test('GetColorIdentityCounts returns valid counts', () => { - expect.assertions(1); - var expected = { - Abzan: 0, - Azorius: 1, - Bant: 0, - Black: 11, - Blue: 15, - Boros: 2, - Colorless: 4, - Dimir: 1, - Esper: 0, - FiveColor: 0, - Golgari: 1, - Green: 12, - Grixis: 0, - Gruul: 1, - Izzet: 1, - Jeskai: 0, - Jund: 0, - Mardu: 0, - Naya: 0, - NonBlack: 0, - NonBlue: 0, - NonGreen: 0, - NonRed: 0, - NonWhite: 0, - Orzhov: 1, - Rakdos: 0, - Red: 14, - Selesnya: 2, - Simic: 0, - Sultai: 0, - Temur: 0, - White: 19, - }; - var promise = carddb.initializeCardDb(fixturesPath, true); - return promise.then(function() { - var result = analytics.GetColorIdentityCounts(cubefixture.exampleCube.cards, carddb); - expect(result).toEqual(expected); - }); -}); - -test('GetCurve returns a valid curve structure', () => { - expect.assertions(1); - var expected = { - black: [0, 1, 2, 3, 0, 1, 0, 0, 0, 0], - blue: [0, 1, 3, 7, 0, 0, 0, 0, 0, 0], - colorless: [0, 0, 1, 0, 0, 0, 0, 1, 0, 0], - green: [0, 2, 2, 1, 1, 0, 1, 0, 0, 0], - multi: [0, 0, 3, 1, 3, 1, 2, 0, 0, 0], - red: [0, 1, 1, 3, 3, 0, 1, 0, 0, 0], - total: [0, 7, 16, 19, 7, 3, 5, 1, 0, 0], - white: [0, 2, 4, 4, 0, 1, 1, 0, 0, 0], - }; - var promise = carddb.initializeCardDb(fixturesPath, true); - return promise.then(function() { - var result = analytics.GetCurve(cubefixture.exampleCube.cards, carddb); - expect(result).toEqual(expected); - }); -}); diff --git a/package-lock.json b/package-lock.json index 3bc449c44..702a5b14b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -241,7 +241,6 @@ "version": "7.7.0", "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.7.0.tgz", "integrity": "sha512-LSln3cexwInTMYYoFeVLKnYPPMfWNJ8PubTBs3hkh7wCu9iBaqq1OOyW+xGmEdLxT1nhsl+9SJ+h2oUDYz0l2A==", - "dev": true, "requires": { "@babel/types": "^7.7.0", "esutils": "^2.0.0" @@ -251,7 +250,6 @@ "version": "7.7.2", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.7.2.tgz", "integrity": "sha512-YTf6PXoh3+eZgRCBzzP25Bugd2ngmpQVrk7kXX0i5N9BO7TFBtIgZYs7WtxtOGs8e6A4ZI7ECkbBCEHeXocvOA==", - "dev": true, "requires": { "esutils": "^2.0.2", "lodash": "^4.17.13", @@ -261,8 +259,7 @@ "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" } } }, @@ -788,8 +785,7 @@ "@babel/helper-plugin-utils": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz", - "integrity": "sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==", - "dev": true + "integrity": "sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==" }, "@babel/helper-regex": { "version": "7.5.5", @@ -1472,7 +1468,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.2.0.tgz", "integrity": "sha512-VyN4QANJkRW6lDBmENzRszvZf3/4AXaj9YR7GwrWeeN9tEBPuXbmDYVU9bYBN0D70zCWVwUy0HWq2553VCb6Hw==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -1860,7 +1855,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.2.0.tgz", "integrity": "sha512-Htf/tPa5haZvRMiNSQSFifK12gtr/8vwfr+A9y69uF0QcU77AVu4K7MiHEkTxF7lQoHOL0F9ErqgfNEAKgXj7A==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0" } @@ -1869,7 +1863,6 @@ "version": "7.7.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.7.0.tgz", "integrity": "sha512-mXhBtyVB1Ujfy+0L6934jeJcSXj/VCg6whZzEcgiiZHNS0PGC7vUCsZDQCxxztkpIdF+dY1fUMcjAgEOC3ZOMQ==", - "dev": true, "requires": { "@babel/helper-builder-react-jsx": "^7.7.0", "@babel/helper-plugin-utils": "^7.0.0", @@ -1880,7 +1873,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.2.0.tgz", "integrity": "sha512-v6S5L/myicZEy+jr6ielB0OR8h+EH/1QFx/YJ7c7Ua+7lqsjj/vW6fD5FR9hB/6y7mGbfT4vAURn3xqBxsUcdg==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/plugin-syntax-jsx": "^7.2.0" @@ -1890,7 +1882,6 @@ "version": "7.5.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.5.0.tgz", "integrity": "sha512-58Q+Jsy4IDCZx7kqEZuSDdam/1oW8OdDX8f+Loo6xyxdfg1yF0GE2XNJQSTZCaMol93+FBzpWiPEwtbMloAcPg==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/plugin-syntax-jsx": "^7.2.0" @@ -2053,7 +2044,6 @@ "version": "7.7.0", "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.7.0.tgz", "integrity": "sha512-IXXgSUYBPHUGhUkH+89TR6faMcBtuMW0h5OHbMuVbL3/5wK2g6a2M2BBpkLa+Kw0sAHiZ9dNVgqJMDP/O4GRBA==", - "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/plugin-transform-react-display-name": "^7.0.0", @@ -8337,8 +8327,7 @@ "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "js-yaml": { "version": "3.13.1", @@ -8811,7 +8800,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "requires": { "js-tokens": "^3.0.0 || ^4.0.0" } @@ -10427,7 +10415,6 @@ "version": "15.7.2", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "dev": true, "requires": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -10662,6 +10649,11 @@ "safe-buffer": "^5.1.0" } }, + "randomcolor": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/randomcolor/-/randomcolor-0.5.4.tgz", + "integrity": "sha512-nYd4nmTuuwMFzHL6W+UWR5fNERGZeVauho8mrJDUSXdNDbao4rbrUwhuLgKC/j8VCS5+34Ria8CsTDuBjrIrQA==" + }, "randomfill": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", @@ -10799,8 +10791,7 @@ "react-is": { "version": "16.9.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.9.0.tgz", - "integrity": "sha512-tJBzzzIgnnRfEm046qRcURvwQnZVXmuCbscxUO5RWrGTXpon2d4c8mI0D8WE6ydVIm29JiLB6+RslkIvym9Rjw==", - "dev": true + "integrity": "sha512-tJBzzzIgnnRfEm046qRcURvwQnZVXmuCbscxUO5RWrGTXpon2d4c8mI0D8WE6ydVIm29JiLB6+RslkIvym9Rjw==" }, "react-lifecycles-compat": { "version": "3.0.4", @@ -10854,6 +10845,17 @@ } } }, + "react-tagcloud": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/react-tagcloud/-/react-tagcloud-2.0.0.tgz", + "integrity": "sha512-SQLAvSDx35by4xYQtcJEYZW3294jicw7yMEmhJ70ZBpGBA/MDqvPki/hyr37YHIDr1w1lEW0aJoIO7WEjRjUdw==", + "requires": { + "@babel/preset-react": "^7.6.3", + "prop-types": "^15.6.2", + "randomcolor": "^0.5.4", + "shuffle-array": "^1.0.1" + } + }, "react-transition-group": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz", @@ -11633,6 +11635,11 @@ "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", "dev": true }, + "shuffle-array": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/shuffle-array/-/shuffle-array-1.0.1.tgz", + "integrity": "sha1-xP88/nTRb5NzBZIwGyXmV3sSiYs=" + }, "shuffle-seed": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/shuffle-seed/-/shuffle-seed-1.1.6.tgz", diff --git a/package.json b/package.json index 6103c50db..7aa4ddfa6 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "passport-local": "^1.0.0", "pug": "^2.0.3", "quickselect": "^2.0.0", + "react-tagcloud": "^2.0.0", "request": "^2.88.0", "rss": "^1.2.2", "sanitize-html": "^1.20.1", diff --git a/public/js/analytics/colorCount.js b/public/js/analytics/colorCount.js new file mode 100644 index 000000000..f2c44571c --- /dev/null +++ b/public/js/analytics/colorCount.js @@ -0,0 +1,96 @@ +function areArraysEqualSets(a1, a2) { + if (a1.length != a2.length) return false; + let superSet = {}; + for (let i = 0; i < a1.length; i++) { + const e = a1[i] + typeof a1[i]; + superSet[e] = 1; + } + + for (let i = 0; i < a2.length; i++) { + const e = a2[i] + typeof a2[i]; + if (!superSet[e]) { + return false; + } + superSet[e] = 2; + } + + for (let e in superSet) { + if (superSet[e] === 1) { + return false; + } + } + + return true; +} + +onmessage = (e) => { + if (!e) return; + var cards = e.data; + var colorCombinations = [ + [], + ['W'], + ['U'], + ['B'], + ['R'], + ['G'], + ['W', 'U'], + ['U', 'B'], + ['B', 'R'], + ['R', 'G'], + ['G', 'W'], + ['W', 'B'], + ['U', 'R'], + ['B', 'G'], + ['R', 'W'], + ['G', 'U'], + ['G', 'W', 'U'], + ['W', 'U', 'B'], + ['U', 'B', 'R'], + ['B', 'R', 'G'], + ['R', 'G', 'W'], + ['R', 'W', 'B'], + ['G', 'U', 'R'], + ['W', 'B', 'G'], + ['U', 'R', 'W'], + ['B', 'G', 'U'], + ['U', 'B', 'R', 'G'], + ['B', 'R', 'G', 'W'], + ['R', 'G', 'W', 'U'], + ['G', 'W', 'U', 'B'], + ['W', 'U', 'B', 'R'], + ['W', 'U', 'B', 'R', 'G'], + ]; + var ColorCounts = Array.from(colorCombinations, (label) => 0); + var ColorAsfans = Array.from(colorCombinations, (label) => 0); + var cardColors; + var totalCount = 0; + var totalAsfan = 0; + cards.forEach((card, index) => { + asfan = card.asfan || 15 / cards.length; + cardColors = card.colors || card.details.colors || []; + + totalCount += 1; + totalAsfan += asfan; + colorCombinations.forEach((combination, idx) => { + if (areArraysEqualSets(combination, cardColors)) { + ColorCounts[idx] += 1; + ColorAsfans[idx] += asfan; + } + }); + }); + datapoints = Array.from(colorCombinations, (combination, idx) => ({ + label: combination.length == 0 ? '{c}' : combination.map((c) => '{' + c.toLowerCase() + '}').join(''), + asfan: ColorAsfans[idx].toFixed(2), + count: ColorCounts[idx], + })); + datapoints.push({ key: 'total', label: 'Total', asfan: totalAsfan.toFixed(2), count: totalCount }); + postMessage({ + type: 'table', + columns: [ + { header: 'Color Combination', key: 'label', rowHeader: true }, + { header: 'Expected Count of Exact Matches in Pool', key: 'asfan' }, + { header: 'Count of Exact Match', key: 'count' }, + ], + data: datapoints, + }); +}; diff --git a/public/js/analytics/colorCurve.js b/public/js/analytics/colorCurve.js new file mode 100644 index 000000000..225c5ea07 --- /dev/null +++ b/public/js/analytics/colorCurve.js @@ -0,0 +1,140 @@ +function GetColorCat(type, colors) { + if (type.toLowerCase().includes('land')) { + return 'l'; + } else if (colors.length == 0) { + return 'c'; + } else if (colors.length > 1) { + return 'm'; + } else if (colors.length == 1) { + switch (colors[0]) { + case 'W': + return 'w'; + break; + case 'U': + return 'u'; + break; + case 'B': + return 'b'; + break; + case 'R': + return 'r'; + break; + case 'G': + return 'g'; + break; + case 'C': + return 'c'; + break; + } + } +} + +onmessage = (e) => { + if (!e) return; + var cards = e.data; + var curve = { + white: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + blue: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + black: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + red: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + green: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + colorless: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + multi: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + total: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + }; + + cards.forEach(function(card, index) { + var category; + switch (GetColorCat(card.details.type, card.colors)) { + case 'w': + category = curve.white; + break; + case 'u': + category = curve.blue; + break; + case 'b': + category = curve.black; + break; + case 'r': + category = curve.red; + break; + case 'g': + category = curve.green; + break; + case 'c': + category = curve.colorless; + break; + case 'm': + category = curve.multi; + break; + } + // const asfan = card.asfan || 15 / cards.length; + const asfan = 1; + if (category) { + if (card.cmc >= 9) { + category[9] += asfan; + curve.total[9] += asfan; + } else { + category[Math.floor(card.cmc)] += asfan; + curve.total[Math.floor(card.cmc)] += asfan; + } + } + }); + const datasets = { + labels: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9+'], + datasets: [ + ['White', curve.white, '#D8CEAB'], + ['Blue', curve.blue, '#67A6D3'], + ['Black', curve.black, '#8C7A91'], + ['Red', curve.red, '#D85F69'], + ['Green', curve.green, '#6AB572'], + ['Colorless', curve.colorless, '#ADADAD'], + ['Multicolored', curve.multi, '#DBC467'], + ['Total', curve.total, '#000000'], + ].map((color) => ({ + label: color[0], + data: color[1].map((af) => af.toFixed(2)), + fill: false, + backgroundColor: color[2], + borderColor: color[2], + })), + }; + const options = { + responsive: true, + tooltips: { + mode: 'index', + intersect: false, + }, + hover: { + mode: 'nearest', + intersect: true, + }, + scales: { + xAxes: [ + { + display: true, + scaleLabel: { + display: true, + labelString: 'CMC', + }, + }, + ], + yAxes: [ + { + display: true, + scaleLabel: { + display: true, + labelString: 'Count', + }, + }, + ], + }, + }; + postMessage({ + type: 'chart', + chartType: 'bar', + datasets: datasets, + options: options, + description: 'Click the labels to filter the datasets. Lands are omitted for the curve chart.', + }); +}; diff --git a/public/js/analytics/cumulativeColorCount.js b/public/js/analytics/cumulativeColorCount.js new file mode 100644 index 000000000..fede844e3 --- /dev/null +++ b/public/js/analytics/cumulativeColorCount.js @@ -0,0 +1,101 @@ +function areArraysEqualSets(a1, a2) { + if (a1.length != a2.length) return false; + let superSet = {}; + for (let i = 0; i < a1.length; i++) { + const e = a1[i] + typeof a1[i]; + superSet[e] = 1; + } + + for (let i = 0; i < a2.length; i++) { + const e = a2[i] + typeof a2[i]; + if (!superSet[e]) { + return false; + } + superSet[e] = 2; + } + + for (let e in superSet) { + if (superSet[e] === 1) { + return false; + } + } + + return true; +} + +function arrayContainsOtherArray(arr1, arr2) { + return arr2.every((v) => arr1.includes(v)); +} + +onmessage = (e) => { + if (!e) return; + var cards = e.data; + var colorCombinations = [ + [], + ['W'], + ['U'], + ['B'], + ['R'], + ['G'], + ['W', 'U'], + ['U', 'B'], + ['B', 'R'], + ['R', 'G'], + ['G', 'W'], + ['W', 'B'], + ['U', 'R'], + ['B', 'G'], + ['R', 'W'], + ['G', 'U'], + ['G', 'W', 'U'], + ['W', 'U', 'B'], + ['U', 'B', 'R'], + ['B', 'R', 'G'], + ['R', 'G', 'W'], + ['R', 'W', 'B'], + ['G', 'U', 'R'], + ['W', 'B', 'G'], + ['U', 'R', 'W'], + ['B', 'G', 'U'], + ['U', 'B', 'R', 'G'], + ['B', 'R', 'G', 'W'], + ['R', 'G', 'W', 'U'], + ['G', 'W', 'U', 'B'], + ['W', 'U', 'B', 'R'], + ['W', 'U', 'B', 'R', 'G'], + ]; + var ColorCounts = Array.from(colorCombinations, (label) => 0); + var ColorAsfans = Array.from(colorCombinations, (label) => 0); + var totalCount = 0; + var totalAsfan = 0; + var cardColors; + cards.forEach((card, index) => { + // Hack until asfan can be properly added to cards + asfan = card.asfan || 15 / cards.length; + cardColors = card.colors || card.details.colors || []; + + totalCount += 1; + totalAsfan += asfan; + colorCombinations.forEach((combination, idx) => { + if (arrayContainsOtherArray(combination, cardColors)) { + ColorCounts[idx] += 1; + ColorAsfans[idx] += asfan; + } + }); + }); + datapoints = Array.from(colorCombinations, (combination, idx) => ({ + label: combination.length == 0 ? '{c}' : combination.map((c) => '{' + c.toLowerCase() + '}').join(''), + asfan: ColorAsfans[idx].toFixed(2), + count: ColorCounts[idx], + })); + datapoints.push({ key: 'total', label: 'Total', asfan: totalAsfan.toFixed(2), count: totalCount }); + postMessage({ + type: 'table', + columns: [ + { header: 'Color Combination', key: 'label', rowHeader: true }, + { header: 'Expected Count in Poll Contained In', key: 'asfan' }, + { header: 'Count of Contained In', key: 'count' }, + ], + data: datapoints, + }); +}; diff --git a/public/js/analytics/tagCloud.js b/public/js/analytics/tagCloud.js new file mode 100644 index 000000000..609b33157 --- /dev/null +++ b/public/js/analytics/tagCloud.js @@ -0,0 +1,19 @@ +onmessage = (e) => { + if (!e) return; + const cards = e.data; + + var tags = {}; + cards.forEach((card) => + card.tags.forEach((tag) => { + if (tags[tag]) { + tags[tag] += card.asfan; + } else { + tags[tag] = card.asfan; + } + }), + ); + const words = Object.keys(tags).map((key) => { + return { value: key, count: tags[key] }; + }); + postMessage({ type: 'cloud', words: words }); +}; diff --git a/public/js/analytics/tokenGrid.js b/public/js/analytics/tokenGrid.js new file mode 100644 index 000000000..611a0a986 --- /dev/null +++ b/public/js/analytics/tokenGrid.js @@ -0,0 +1,55 @@ +const compareCards = (x, y) => { + if (x.details.name === y.details.name) { + return 0; + } else { + return x.details.name < y.details.name ? -1 : 1; + } +}; + +const compareTokens = (x, y) => compareCards(x.token, y.token); + +const sortTokens = (tokens) => [...tokens].sort(compareTokens); +const sortCards = (cards) => [...cards].sort(compareCards); + +const dedupeCards = (cards) => { + const map = new Map(); + for (const card of [...cards].reverse()) { + map.set(card.details.name, card); + } + return [...map.values()]; +}; + +onmessage = (e) => { + if (!e) return; + const cards = e.data; + + var mentionedTokens = []; + cards.forEach((card, position) => { + card.position = position; + if (card.details.tokens) { + mentionedTokens.push(...card.details.tokens.map(({ token }) => ({ token: token, sourceCard: { ...card } }))); + } + }); + + let resultingTokens = []; + mentionedTokens.forEach((element) => { + var relevantIndex = resultingTokens.findIndex(({ token }) => token.cardID == element.token.cardID); + if (relevantIndex >= 0) { + resultingTokens[relevantIndex].cards.push(element.sourceCard); + } else { + var tokenData = { token: element.token, cards: [element.sourceCard] }; + resultingTokens.push(tokenData); + } + }); + const data = sortTokens(resultingTokens).map(({ token, cards }) => ({ + card: token, + cardDescription: sortCards(dedupeCards(cards)) + .map(({ position }) => `[[${position}]]`) + .join('\n\n'), + })); + postMessage({ + type: 'cardGrid', + massBuyLabel: 'Buy all tokens', + cards: data, + }); +}; diff --git a/public/js/analytics/typeBreakdown.js b/public/js/analytics/typeBreakdown.js new file mode 100644 index 000000000..76263415d --- /dev/null +++ b/public/js/analytics/typeBreakdown.js @@ -0,0 +1,206 @@ +function GetColorCat(colors) { + if (colors.length == 0) { + return 'Colorless'; + } else if (colors.length > 1) { + return 'Multi'; + } else if (colors.length == 1) { + switch (colors[0]) { + case 'W': + return 'White'; + break; + case 'U': + return 'Blue'; + break; + case 'B': + return 'Black'; + break; + case 'R': + return 'Red'; + break; + case 'G': + return 'Green'; + break; + case 'C': + return 'Colorless'; + break; + } + } +} + +onmessage = (e) => { + if (!e) return; + var cards = e.data; + + var TypeByColor = { + Creatures: { + label: 'Creatures', + White: { asfan: 0, count: 0 }, + Blue: { asfan: 0, count: 0 }, + Black: { asfan: 0, count: 0 }, + Red: { asfan: 0, count: 0 }, + Green: { asfan: 0, count: 0 }, + Colorless: { asfan: 0, count: 0 }, + Multi: { asfan: 0, count: 0 }, + Total: { asfan: 0, count: 0 }, + }, + Enchantments: { + label: 'Enchantments', + White: { asfan: 0, count: 0 }, + Blue: { asfan: 0, count: 0 }, + Black: { asfan: 0, count: 0 }, + Red: { asfan: 0, count: 0 }, + Green: { asfan: 0, count: 0 }, + Colorless: { asfan: 0, count: 0 }, + Multi: { asfan: 0, count: 0 }, + Total: { asfan: 0, count: 0 }, + }, + Lands: { + label: 'Lands', + White: { asfan: 0, count: 0 }, + Blue: { asfan: 0, count: 0 }, + Black: { asfan: 0, count: 0 }, + Red: { asfan: 0, count: 0 }, + Green: { asfan: 0, count: 0 }, + Colorless: { asfan: 0, count: 0 }, + Multi: { asfan: 0, count: 0 }, + Total: { asfan: 0, count: 0 }, + }, + Planeswalkers: { + label: 'Planeswalkers', + White: { asfan: 0, count: 0 }, + Blue: { asfan: 0, count: 0 }, + Black: { asfan: 0, count: 0 }, + Red: { asfan: 0, count: 0 }, + Green: { asfan: 0, count: 0 }, + Colorless: { asfan: 0, count: 0 }, + Multi: { asfan: 0, count: 0 }, + Total: { asfan: 0, count: 0 }, + }, + Instants: { + label: 'Instants', + White: { asfan: 0, count: 0 }, + Blue: { asfan: 0, count: 0 }, + Black: { asfan: 0, count: 0 }, + Red: { asfan: 0, count: 0 }, + Green: { asfan: 0, count: 0 }, + Colorless: { asfan: 0, count: 0 }, + Multi: { asfan: 0, count: 0 }, + Total: { asfan: 0, count: 0 }, + }, + Sorceries: { + label: 'Sorceries', + White: { asfan: 0, count: 0 }, + Blue: { asfan: 0, count: 0 }, + Black: { asfan: 0, count: 0 }, + Red: { asfan: 0, count: 0 }, + Green: { asfan: 0, count: 0 }, + Colorless: { asfan: 0, count: 0 }, + Multi: { asfan: 0, count: 0 }, + Total: { asfan: 0, count: 0 }, + }, + Artifacts: { + label: 'Artifacts', + White: { asfan: 0, count: 0 }, + Blue: { asfan: 0, count: 0 }, + Black: { asfan: 0, count: 0 }, + Red: { asfan: 0, count: 0 }, + Green: { asfan: 0, count: 0 }, + Colorless: { asfan: 0, count: 0 }, + Multi: { asfan: 0, count: 0 }, + Total: { asfan: 0, count: 0 }, + }, + Total: { + label: 'Total', + White: { asfan: 0, count: 0 }, + Blue: { asfan: 0, count: 0 }, + Black: { asfan: 0, count: 0 }, + Red: { asfan: 0, count: 0 }, + Green: { asfan: 0, count: 0 }, + Colorless: { asfan: 0, count: 0 }, + Multi: { asfan: 0, count: 0 }, + Total: { asfan: 0, count: 0 }, + }, + }; + cards.forEach(function(card, index) { + var asfan = card.asfan || 15 / cards.length; + var colorCategory = GetColorCat(card.colors); + + TypeByColor['Total'][colorCategory].count += 1; + TypeByColor['Total'][colorCategory].asfan += asfan; + TypeByColor['Total']['Total'].count += 1; + TypeByColor['Total']['Total'].asfan += asfan; + + var type = null; + if (card.details.type.toLowerCase().includes('creature')) { + type = TypeByColor['Creatures']; + } else if (card.details.type.toLowerCase().includes('enchantment')) { + type = TypeByColor['Enchantments']; + } else if (card.details.type.toLowerCase().includes('land')) { + type = TypeByColor['Lands']; + } else if (card.details.type.toLowerCase().includes('planeswalker')) { + type = TypeByColor['Planeswalkers']; + } else if (card.details.type.toLowerCase().includes('instant')) { + type = TypeByColor['Instants']; + } else if (card.details.type.toLowerCase().includes('sorcery')) { + type = TypeByColor['Sorceries']; + } else if (card.details.type.toLowerCase().includes('artifact')) { + type = TypeByColor['Artifacts']; + } else { + console.warn(`Unrecognized type: ${card.details.type} from ${card.details.name}`); + return; + } + type[colorCategory].count += 1; + type[colorCategory].asfan += asfan; + type['Total'].count += 1; + type['Total'].asfan += asfan; + }); + + for (let type in TypeByColor) { + const typed = TypeByColor[type]; + for (let color in typed) { + if (color == 'label') continue; + const totalCount = TypeByColor['Total'][color].count; + const totalAsfan = TypeByColor['Total'][color].asfan; + const count = TypeByColor[type][color].count; + const asfan = TypeByColor[type][color].asfan; + const asfanText = asfan.toFixed(2); + let countPercentageStr = ''; + if (totalCount > 0 && type != 'Total') { + const countPercentage = Math.round((100.0 * count) / totalCount); + countPercentageStr = ` %%${countPercentage}%%`; + } + let asfanPercentageStr = ''; + if (totalAsfan > 0 && type != 'Total') { + const asfanPercentage = Math.round((100.0 * asfan) / totalAsfan); + asfanPercentageStr = ` %%${asfanPercentage}%%`; + } + TypeByColor[type][ + color + ] = `${count}${countPercentageStr} Count / ${asfanText}${asfanPercentageStr} Expected in Pool`; + } + } + postMessage({ + type: 'table', + columns: [ + { header: '', key: 'label', rowHeader: true }, + { header: '{w}', key: 'White' }, + { header: '{u}', key: 'Blue' }, + { header: '{b}', key: 'Black' }, + { header: '{r}', key: 'Red' }, + { header: '{g}', key: 'Green' }, + { header: '{c}', key: 'Colorless' }, + { header: '{m}', key: 'Multi' }, + { header: 'Total', key: 'Total' }, + ], + data: [ + TypeByColor['Creatures'], + TypeByColor['Instants'], + TypeByColor['Sorceries'], + TypeByColor['Enchantments'], + TypeByColor['Artifacts'], + TypeByColor['Planeswalkers'], + TypeByColor['Lands'], + TypeByColor['Total'], + ], + }); +}; diff --git a/routes/cube_routes.js b/routes/cube_routes.js index bc8adccf8..3e012cac8 100644 --- a/routes/cube_routes.js +++ b/routes/cube_routes.js @@ -12,7 +12,6 @@ var { build_id_query, get_cube_id, } = require('../serverjs/cubefn.js'); -const analytics = require('../serverjs/analytics.js'); const draftutil = require('../dist/util/draftutil.js'); const cardutil = require('../dist/util/Card.js'); const carddb = require('../serverjs/cards.js'); @@ -924,45 +923,79 @@ router.get('/analysis/:id', function(req, res) { username: 'unknown', }; } - if (err) { - res.render('cube/cube_analysis', { - cube: cube, - cube_id: req.params.id, - owner: user.username, - activeLink: 'analysis', - title: `${abbreviate(cube.name)} - Analysis`, - TypeByColor: analytics.GetTypeByColorIdentity(cube.cards, carddb), - MulticoloredCounts: analytics.GetColorIdentityCounts(cube.cards, carddb), - curve: JSON.stringify(analytics.GetCurve(cube.cards, carddb)), - GeneratedTokensCounts: analytics.GetTokens(cube.cards, carddb), - metadata: generateMeta( - `Cube Cobra Analysis: ${cube.name}`, - cube.type ? `${cube.card_count} Card ${cube.type} Cube` : `${cube.card_count} Card Cube`, - cube.image_uri, - `https://cubecobra.com/cube/analysis/${req.params.id}`, - ), - loginCallback: '/cube/analysis/' + req.params.id, - }); - } else { - res.render('cube/cube_analysis', { - cube: cube, - cube_id: req.params.id, - owner: user.username, - activeLink: 'analysis', - title: `${abbreviate(cube.name)} - Analysis`, - TypeByColor: analytics.GetTypeByColorIdentity(cube.cards, carddb), - MulticoloredCounts: analytics.GetColorIdentityCounts(cube.cards, carddb), - curve: JSON.stringify(analytics.GetCurve(cube.cards, carddb)), - GeneratedTokensCounts: analytics.GetTokens(cube.cards, carddb), - metadata: generateMeta( - `Cube Cobra Analysis: ${cube.name}`, - cube.type ? `${cube.card_count} Card ${cube.type} Cube` : `${cube.card_count} Card Cube`, - cube.image_uri, - `https://cubecobra.com/cube/analysis/${req.params.id}`, - ), - loginCallback: '/cube/analysis/' + req.params.id, + var pids = []; + cube.cards.forEach(function(card, index) { + card.details = { + ...carddb.cardFromId(card.cardID), + }; + card.details.display_image = util.getCardImageURL(card); + if (!card.type_line) { + card.type_line = card.details.type; + } + if (card.details.tcgplayer_id && !pids.includes(card.details.tcgplayer_id)) { + pids.push(card.details.tcgplayer_id); + } + if (card.details.tokens) { + card.details.tokens.forEach((element) => { + token_details = carddb.cardFromId(element.tokenId); + element['token'] = { + tags: [], + status: 'Not Owned', + colors: token_details.color_identity, + cmc: token_details.cmc, + cardID: token_details._id, + type_line: token_details.type, + addedTmsp: new Date(), + imgUrl: undefined, + finish: 'Non-foil', + details: { ...token_details }, + }; + }); + } + }); + GetPrices(pids, function(price_dict) { + cube.cards.forEach(function(card, index) { + if (card.details.tcgplayer_id) { + if (price_dict[card.details.tcgplayer_id]) { + card.details.price = price_dict[card.details.tcgplayer_id]; + } + if (price_dict[card.details.tcgplayer_id + '_foil']) { + card.details.price_foil = price_dict[card.details.tcgplayer_id + '_foil']; + } + } }); - } + if (err) { + res.render('cube/cube_analysis', { + cube: cube, + cube_id: req.params.id, + owner: user.username, + activeLink: 'analysis', + title: `${abbreviate(cube.name)} - Analysis`, + metadata: generateMeta( + `Cube Cobra Analysis: ${cube.name}`, + cube.type ? `${cube.card_count} Card ${cube.type} Cube` : `${cube.card_count} Card Cube`, + cube.image_uri, + `https://cubecobra.com/cube/analysis/${req.params.id}`, + ), + loginCallback: '/cube/analysis/' + req.params.id, + }); + } else { + res.render('cube/cube_analysis', { + cube: cube, + cube_id: req.params.id, + owner: user.username, + activeLink: 'analysis', + title: `${abbreviate(cube.name)} - Analysis`, + metadata: generateMeta( + `Cube Cobra Analysis: ${cube.name}`, + cube.type ? `${cube.card_count} Card ${cube.type} Cube` : `${cube.card_count} Card Cube`, + cube.image_uri, + `https://cubecobra.com/cube/analysis/${req.params.id}`, + ), + loginCallback: '/cube/analysis/' + req.params.id, + }); + } + }); }); } }); diff --git a/serverjs/analytics.js b/serverjs/analytics.js deleted file mode 100644 index 38a05bc9c..000000000 --- a/serverjs/analytics.js +++ /dev/null @@ -1,546 +0,0 @@ -/* used in the now abandoned GetTokens attempt to get token counts from rules text. If -var Small = { - 'zero': 0, - 'one': 1, - 'two': 2, - 'three': 3, - 'four': 4, - 'five': 5, - 'six': 6, - 'seven': 7, - 'eight': 8, - 'nine': 9, - 'ten': 10, - 'eleven': 11, - 'twelve': 12, - 'thirteen': 13, - 'fourteen': 14, - 'fifteen': 15, - 'sixteen': 16, - 'seventeen': 17, - 'eighteen': 18, - 'nineteen': 19, - 'twenty': 20, -}; - -var a, n, g; - -function text2num(a) { - return Small[a]; -} -*/ -function CheckContentsEqualityOfArray(target, candidate) { - var isValid = candidate.length == target.length; - if (!isValid) return false; - - for (idx = 0; idx < target.length; idx++) { - if (!candidate.includes(target[idx])) { - isValid = false; - break; - } - } - return isValid; -} - -function GetColorCat(type, colors) { - if (type.toLowerCase().includes('land')) { - return 'l'; - } else if (colors.length == 0) { - return 'c'; - } else if (colors.length > 1) { - return 'm'; - } else if (colors.length == 1) { - switch (colors[0]) { - case 'W': - return 'w'; - break; - case 'U': - return 'u'; - break; - case 'B': - return 'b'; - break; - case 'R': - return 'r'; - break; - case 'G': - return 'g'; - break; - case 'C': - return 'c'; - break; - } - } -} - -function GetColorIdentity(colors) { - if (colors.length == 0) { - return 'Colorless'; - } else if (colors.length > 1) { - return 'Multicolored'; - } else if (colors.length == 1) { - switch (colors[0]) { - case 'W': - return 'White'; - break; - case 'U': - return 'Blue'; - break; - case 'B': - return 'Black'; - break; - case 'R': - return 'Red'; - break; - case 'G': - return 'Green'; - break; - case 'C': - return 'Colorless'; - break; - } - } -} - -var methods = { - GetColorCat: GetColorCat, - GetColorIdentity: GetColorIdentity, - GetTypeByColorIdentity: function(cards, carddb) { - var TypeByColor = { - Creatures: { - White: 0, - Blue: 0, - Black: 0, - Red: 0, - Green: 0, - Colorless: 0, - Multi: 0, - Total: 0, - }, - Enchantments: { - White: 0, - Blue: 0, - Black: 0, - Red: 0, - Green: 0, - Colorless: 0, - Multi: 0, - Total: 0, - }, - Lands: { - White: 0, - Blue: 0, - Black: 0, - Red: 0, - Green: 0, - Colorless: 0, - Multi: 0, - Total: 0, - }, - Planeswalkers: { - White: 0, - Blue: 0, - Black: 0, - Red: 0, - Green: 0, - Colorless: 0, - Multi: 0, - Total: 0, - }, - Instants: { - White: 0, - Blue: 0, - Black: 0, - Red: 0, - Green: 0, - Colorless: 0, - Multi: 0, - Total: 0, - }, - Sorceries: { - White: 0, - Blue: 0, - Black: 0, - Red: 0, - Green: 0, - Colorless: 0, - Multi: 0, - Total: 0, - }, - Artifacts: { - White: 0, - Blue: 0, - Black: 0, - Red: 0, - Green: 0, - Colorless: 0, - Multi: 0, - Total: 0, - }, - Total: { - White: 0, - Blue: 0, - Black: 0, - Red: 0, - Green: 0, - Colorless: 0, - Multi: 0, - Total: 0, - }, - }; - cards.forEach(function(card, index) { - card.details = carddb.cardFromId(card.cardID); - }); - cards.forEach(function(card, index) { - var type = {}; - if (card.details.type.toLowerCase().includes('creature')) { - type = TypeByColor['Creatures']; - } else if (card.details.type.toLowerCase().includes('enchantment')) { - type = TypeByColor['Enchantments']; - } else if (card.details.type.toLowerCase().includes('land')) { - type = TypeByColor['Lands']; - } else if (card.details.type.toLowerCase().includes('planeswalker')) { - type = TypeByColor['Planeswalkers']; - } else if (card.details.type.toLowerCase().includes('instant')) { - type = TypeByColor['Instants']; - } else if (card.details.type.toLowerCase().includes('sorcery')) { - type = TypeByColor['Sorceries']; - } else if (card.details.type.toLowerCase().includes('artifact')) { - type = TypeByColor['Artifacts']; - } - - var colorCategory = GetColorCat(card.details.type, card.colors); - - // special case for land - if (colorCategory == 'l') { - if (card.colors.length == 0) { - colorCategory = 'c'; - } else if (card.colors.length > 1) { - colorCategory = 'm'; - } else { - colorCategory = card.colors[0].toLowerCase(); - } - } - - switch (colorCategory) { - case 'w': - type['White'] += 1; - type['Total'] += 1; - TypeByColor['Total']['White'] += 1; - TypeByColor['Total']['Total'] += 1; - break; - case 'u': - type['Blue'] += 1; - type['Total'] += 1; - TypeByColor['Total']['Blue'] += 1; - TypeByColor['Total']['Total'] += 1; - break; - case 'b': - type['Black'] += 1; - type['Total'] += 1; - TypeByColor['Total']['Black'] += 1; - TypeByColor['Total']['Total'] += 1; - break; - case 'r': - type['Red'] += 1; - type['Total'] += 1; - TypeByColor['Total']['Red'] += 1; - TypeByColor['Total']['Total'] += 1; - break; - case 'g': - type['Green'] += 1; - type['Total'] += 1; - TypeByColor['Total']['Green'] += 1; - TypeByColor['Total']['Total'] += 1; - break; - case 'm': - type['Multi'] += 1; - type['Total'] += 1; - TypeByColor['Total']['Multi'] += 1; - TypeByColor['Total']['Total'] += 1; - break; - case 'c': - type['Colorless'] += 1; - type['Total'] += 1; - TypeByColor['Total']['Colorless'] += 1; - TypeByColor['Total']['Total'] += 1; - break; - default: - } - }); - return TypeByColor; - }, - GetColorIdentityCounts: function(cards, carddb) { - var ColorCounts = { - Colorless: 0, - White: 0, - Blue: 0, - Black: 0, - Red: 0, - Green: 0, - Azorius: 0, - Dimir: 0, - Rakdos: 0, - Gruul: 0, - Selesnya: 0, - Orzhov: 0, - Izzet: 0, - Golgari: 0, - Boros: 0, - Simic: 0, - Jund: 0, - Bant: 0, - Grixis: 0, - Naya: 0, - Esper: 0, - Jeskai: 0, - Mardu: 0, - Sultai: 0, - Temur: 0, - Abzan: 0, - NonWhite: 0, - NonBlue: 0, - NonBlack: 0, - NonRed: 0, - NonGreen: 0, - FiveColor: 0, - }; - cards.forEach(function(card, index) { - card.details = carddb.cardFromId(card.cardID); - }); - var cardColors; - cards.forEach(function(card, index) { - cardColors = card.colors || []; - if (cardColors.length === 0) { - ColorCounts.Colorless += 1; - } else if (cardColors.length === 1) { - if (cardColors[0] === 'W') { - ColorCounts.White += 1; - } else if (cardColors[0] === 'U') { - ColorCounts.Blue += 1; - } else if (cardColors[0] === 'B') { - ColorCounts.Black += 1; - } else if (cardColors[0] === 'R') { - ColorCounts.Red += 1; - } else if (cardColors[0] === 'G') { - ColorCounts.Green += 1; - } - } else if (cardColors.length === 2) { - if (cardColors.includes('W') && cardColors.includes('U')) { - ColorCounts.Azorius += 1; - ColorCounts.White += 1; - ColorCounts.Blue += 1; - } else if (cardColors.includes('B') && cardColors.includes('U')) { - ColorCounts.Dimir += 1; - ColorCounts.Black += 1; - ColorCounts.Blue += 1; - } else if (cardColors.includes('B') && cardColors.includes('R')) { - ColorCounts.Rakdos += 1; - ColorCounts.Black += 1; - ColorCounts.Red += 1; - } else if (cardColors.includes('G') && cardColors.includes('R')) { - ColorCounts.Gruul += 1; - ColorCounts.Green += 1; - ColorCounts.Red += 1; - } else if (cardColors.includes('W') && cardColors.includes('G')) { - ColorCounts.Selesnya += 1; - ColorCounts.Green += 1; - ColorCounts.White += 1; - } else if (cardColors.includes('W') && cardColors.includes('B')) { - ColorCounts.Orzhov += 1; - ColorCounts.White += 1; - ColorCounts.Black += 1; - } else if (cardColors.includes('R') && cardColors.includes('U')) { - ColorCounts.Izzet += 1; - ColorCounts.Red += 1; - ColorCounts.Blue += 1; - } else if (cardColors.includes('G') && cardColors.includes('B')) { - ColorCounts.Golgari += 1; - ColorCounts.Green += 1; - ColorCounts.Black += 1; - } else if (cardColors.includes('W') && cardColors.includes('R')) { - ColorCounts.Boros += 1; - ColorCounts.White += 1; - ColorCounts.Red += 1; - } else if (cardColors.includes('G') && cardColors.includes('U')) { - ColorCounts.Simic += 1; - ColorCounts.Green += 1; - ColorCounts.Blue += 1; - } - } else if (cardColors.length == 3) { - if (cardColors.includes('G') && cardColors.includes('B') && cardColors.includes('R')) { - ColorCounts.Jund += 1; - ColorCounts.Green += 1; - ColorCounts.Black += 1; - ColorCounts.Red += 1; - } else if (cardColors.includes('G') && cardColors.includes('U') && cardColors.includes('W')) { - ColorCounts.Bant += 1; - ColorCounts.Green += 1; - ColorCounts.White += 1; - ColorCounts.Blue += 1; - } else if (cardColors.includes('U') && cardColors.includes('B') && cardColors.includes('R')) { - ColorCounts.Grixis += 1; - ColorCounts.Blue += 1; - ColorCounts.Black += 1; - ColorCounts.Red += 1; - } else if (cardColors.includes('G') && cardColors.includes('W') && cardColors.includes('R')) { - ColorCounts.Naya += 1; - ColorCounts.Green += 1; - ColorCounts.White += 1; - ColorCounts.Red += 1; - } else if (cardColors.includes('U') && cardColors.includes('B') && cardColors.includes('W')) { - ColorCounts.Esper += 1; - ColorCounts.Blue += 1; - ColorCounts.Black += 1; - ColorCounts.White += 1; - } else if (cardColors.includes('W') && cardColors.includes('U') && cardColors.includes('R')) { - ColorCounts.Jeskai += 1; - ColorCounts.Blue += 1; - ColorCounts.White += 1; - ColorCounts.Red += 1; - } else if (cardColors.includes('W') && cardColors.includes('B') && cardColors.includes('R')) { - ColorCounts.Mardu += 1; - ColorCounts.White += 1; - ColorCounts.Black += 1; - ColorCounts.Red += 1; - } else if (cardColors.includes('G') && cardColors.includes('B') && cardColors.includes('U')) { - ColorCounts.Sultai += 1; - ColorCounts.Green += 1; - ColorCounts.Black += 1; - ColorCounts.Blue += 1; - } else if (cardColors.includes('G') && cardColors.includes('U') && cardColors.includes('R')) { - ColorCounts.Temur += 1; - ColorCounts.Green += 1; - ColorCounts.Blue += 1; - ColorCounts.Red += 1; - } else if (cardColors.includes('G') && cardColors.includes('B') && cardColors.includes('W')) { - ColorCounts.Abzan += 1; - ColorCounts.Green += 1; - ColorCounts.Black += 1; - ColorCounts.White += 1; - } - } else if (cardColors.length == 4) { - if (!cardColors.includes('W')) { - ColorCounts.NonWhite += 1; - ColorCounts.Green += 1; - ColorCounts.Black += 1; - ColorCounts.Blue += 1; - ColorCounts.Red += 1; - } else if (!cardColors.includes('U')) { - ColorCounts.NonBlue += 1; - ColorCounts.Green += 1; - ColorCounts.Black += 1; - ColorCounts.White += 1; - ColorCounts.Red += 1; - } else if (!cardColors.includes('B')) { - ColorCounts.NonBlack += 1; - ColorCounts.Green += 1; - ColorCounts.White += 1; - ColorCounts.Blue += 1; - ColorCounts.Red += 1; - } else if (!cardColors.includes('R')) { - ColorCounts.NonRed += 1; - ColorCounts.Green += 1; - ColorCounts.Black += 1; - ColorCounts.White += 1; - ColorCounts.Blue += 1; - } else if (!cardColors.includes('G')) { - ColorCounts.NonGreen += 1; - ColorCounts.Black += 1; - ColorCounts.White += 1; - ColorCounts.Blue += 1; - ColorCounts.Red += 1; - } - } else if (cardColors.length == 5) { - ColorCounts.FiveColor += 1; - ColorCounts.Green += 1; - ColorCounts.Black += 1; - ColorCounts.White += 1; - ColorCounts.Blue += 1; - ColorCounts.Red += 1; - } - }); - return ColorCounts; - }, - GetTokens: function(cards, carddb) { - var mentionedTokens = []; - - for (var card of cards) { - card.details = carddb.cardFromId(card.cardID); - - if (card.details.tokens) { - card.details.tokens.forEach((element) => { - mentionedTokens.push(element); - }); - } - } - - let resultingTokens = []; - var tokenIndexArray = []; - mentionedTokens.forEach((element) => { - var relevantIndex = tokenIndexArray.indexOf(element.tokenId); - if (relevantIndex >= 0) { - resultingTokens[relevantIndex][1].push(carddb.cardFromId(element.sourceCardId)); - } else { - var cardId = [carddb.cardFromId(element.tokenId), [carddb.cardFromId(element.sourceCardId)]]; - resultingTokens.push(cardId); - tokenIndexArray.push(element.tokenId); - } - }); - return resultingTokens; - }, - GetCurve: function(cards, carddb) { - var curve = { - white: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - blue: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - black: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - red: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - green: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - colorless: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - multi: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - total: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - }; - - cards.forEach(function(card, index) { - card.details = carddb.cardFromId(card.cardID); - }); - cards.forEach(function(card, index) { - var category; - switch (GetColorCat(card.details.type, card.colors)) { - case 'w': - category = curve.white; - break; - case 'u': - category = curve.blue; - break; - case 'b': - category = curve.black; - break; - case 'r': - category = curve.red; - break; - case 'g': - category = curve.green; - break; - case 'c': - category = curve.colorless; - break; - case 'm': - category = curve.multi; - break; - } - if (category) { - if (card.cmc >= 9) { - category[9] += 1; - curve.total[9] += 1; - } else { - category[Math.floor(card.cmc)] += 1; - curve.total[Math.floor(card.cmc)] += 1; - } - } - }); - return curve; - }, -}; - -module.exports = methods; diff --git a/src/components/AnalyticsBarChart.js b/src/components/AnalyticsBarChart.js new file mode 100644 index 000000000..8a2e60488 --- /dev/null +++ b/src/components/AnalyticsBarChart.js @@ -0,0 +1,34 @@ +import React, { Component } from 'react'; +import ChartComponent from 'react-chartjs-2'; +import { Col, Row } from 'reactstrap'; + +// Data should be: +// { +// type: 'chart', +// description: str, +// chartType: 'doughnut'|'pie'|'line'|'bar'|'horizontalBar'|'radar'|'polarArea'|'bubble'|'scatter' +// datasets: [], data field of Chart +// options: [], options field of Chart +// } +// See https://github.com/jerairrest/react-chartjs-2 for more information. +class AnalyticsBarChart extends Component { + render() { + const { data } = this.props; + return ( + <> + + + (this.chartInstance = ref && ref.chartInstance)} + options={data['options']} + data={data['datasets']} + type={data['chartType']} + /> + + + + ); + } +} + +export default AnalyticsBarChart; diff --git a/src/components/AnalyticsCardGrid.js b/src/components/AnalyticsCardGrid.js new file mode 100644 index 000000000..183519fee --- /dev/null +++ b/src/components/AnalyticsCardGrid.js @@ -0,0 +1,52 @@ +import React, { Component } from 'react'; +import { Row, Col, Card, CardBody } from 'reactstrap'; + +import Affiliate from '../util/Affiliate'; + +import MagicMarkdown from './MagicMarkdown'; +import MassBuyButton from './MassBuyButton'; +import withAutocard from './WithAutocard'; + +const AutocardLink = withAutocard('a'); + +// Data should be: +// { +// type: 'cardGrid', +// description: str, +// massBuyLabel: str, +// cards: [ +// { +// card: Card, +// cardDescription: str, +// } +// ], +// } +const AnalyticsCardGrid = ({ data, title, cube }) => ( + <> + + + card)}> + + + + + + {data['cards'].map(({ card, cardDescription }) => ( + + + + + + +

+ +

+
+
+ + ))} +
+ +); + +export default AnalyticsCardGrid; diff --git a/src/components/AnalyticsCloud.js b/src/components/AnalyticsCloud.js new file mode 100644 index 000000000..00c3e7192 --- /dev/null +++ b/src/components/AnalyticsCloud.js @@ -0,0 +1,30 @@ +import { TagCloud } from 'react-tagcloud'; +import { Col, Row } from 'reactstrap'; + +// Data should be: +// { +// type: 'cloud', +// description: str, +// colorOptions: {}, see https://github.com/davidmerfield/randomColor#options +// words: [ +// { +// value: str, +// count: float, +// key: str, defaults to value +// color: str, defaults to random +// } +// ], +// } +// See https://www.npmjs.com/package/react-tagcloud for more information. +const AnalyticsCloud = ({ data, title }) => { + const colorOptions = { luminosity: 'dark' }; + return ( + + + + + + ); +}; + +export default AnalyticsCloud; diff --git a/src/components/AnalyticsTable.js b/src/components/AnalyticsTable.js new file mode 100644 index 000000000..7b4e59d79 --- /dev/null +++ b/src/components/AnalyticsTable.js @@ -0,0 +1,62 @@ +import React from 'react'; + +import { Col, Row, Table } from 'reactstrap'; + +import MagicMarkdown from './MagicMarkdown'; + +// Data should be: +// { +// type: 'table', +// description: str, +// headers: [ +// { +// header: str, label for the column +// key: str, key for values from this column in data +// rowHeader: bool, whether this column is a header for the rows +// } +// ], +// rows: [ +// { +// [key]: str|float, value to show for column with key [key] +// } +// ], +// } +const AnalyticsTable = ({ data, ...props }) => ( + + + + + + {data.columns.map(({ header, key }) => ( + + ))} + + + + {data.data.map((datapoint, position) => ( + + {data.columns.map(({ key, rowHeader }, columnPosition) => { + var Cell = ({ children, ...props }) => ; + if (rowHeader) + Cell = ({ children, ...props }) => ( + + ); + return ( + + + + ); + })} + + ))} + +
+ +
{children} + {children} +
+ +
+); + +export default AnalyticsTable; diff --git a/src/components/CurveAnalysis.js b/src/components/CurveAnalysis.js deleted file mode 100644 index 0bf797dce..000000000 --- a/src/components/CurveAnalysis.js +++ /dev/null @@ -1,76 +0,0 @@ -import React from 'react'; -import { Bar } from 'react-chartjs-2'; - -import { Col, Row } from 'reactstrap'; - -const CurveAnalysis = ({ curve, ...props }) => { - const data = { - labels: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9+'], - datasets: [ - ['White', curve.white, '#D8CEAB'], - ['Blue', curve.blue, '#67A6D3'], - ['Black', curve.black, '#8C7A91'], - ['Red', curve.red, '#D85F69'], - ['Green', curve.green, '#6AB572'], - ['Colorless', curve.colorless, '#ADADAD'], - ['Multicolored', curve.multi, '#DBC467'], - ['Total', curve.total, '#000000'], - ].map((color) => ({ - label: color[0], - data: color[1], - fill: false, - backgroundColor: color[2], - borderColor: color[2], - })), - }; - - const options = { - responsive: true, - tooltips: { - mode: 'index', - intersect: false, - }, - hover: { - mode: 'nearest', - intersect: true, - }, - scales: { - xAxes: [ - { - display: true, - scaleLabel: { - display: true, - labelString: 'CMC', - }, - }, - ], - yAxes: [ - { - display: true, - scaleLabel: { - display: true, - labelString: 'Count', - }, - }, - ], - }, - }; - - return ( -
- - -

Curve

- Click the labels to filter the datasets. Lands are omitted for the curve chart. - -
- - - - - -
- ); -}; - -export default CurveAnalysis; diff --git a/src/components/MagicMarkdown.js b/src/components/MagicMarkdown.js new file mode 100644 index 000000000..63f6a089c --- /dev/null +++ b/src/components/MagicMarkdown.js @@ -0,0 +1,53 @@ +import React from 'react'; + +import Affiliate from '../util/Affiliate'; + +import MassBuyButton from './MassBuyButton'; +import withAutocard from './WithAutocard'; + +const AutocardLink = withAutocard('a'); + +const MagicMarkdown = ({ markdown, cube }) => { + if (markdown === undefined) { + return ''; + } + const markdownStr = markdown.toString(); + const split = markdownStr.split(/({[wubrgcmWUBRGCM\d\-]+}|\[\[!?\d+\]\]|%%\d+%%|\n\n)/gm); + return split.map((section, position) => { + if (section.startsWith('{')) { + const symbol = section.substring(1, section.length - 1); + return {symbol}; + } else if (section.startsWith('[[!')) { + const cardIndex = parseInt(section.substring(3, section.length - 2), 10); + const card = cube.cards[cardIndex]; + return ( + + + + ); + return cardName; + } else if (section.startsWith('[[')) { + const cardIndex = parseInt(section.substring(2, section.length - 2), 10); + const card = cube.cards[cardIndex]; + return ( + + {card.details.name} + + ); + return cardName; + } else if (section.startsWith('%%')) { + const percentage = section.substring(2, section.length - 2); + return ( + + {percentage}% + + ); + } else if (section.startsWith('\n')) { + return
; + } else { + return section; + } + }); +}; + +export default MagicMarkdown; diff --git a/src/components/MulticoloredAnalysis.js b/src/components/MulticoloredAnalysis.js deleted file mode 100644 index a3a061b42..000000000 --- a/src/components/MulticoloredAnalysis.js +++ /dev/null @@ -1,90 +0,0 @@ -import React from 'react'; - -import { Col, Row, Table } from 'reactstrap'; - -const white = ['White', 'w']; -const blue = ['Blue', 'u']; -const black = ['Black', 'b']; -const red = ['Red', 'r']; -const green = ['Green', 'g']; - -const gold = [ - [ - '2 Color', - [ - ['Azorius', [white, blue]], - ['Dimir', [blue, black]], - ['Rakdos', [black, red]], - ['Gruul', [red, green]], - ['Selesnya', [green, white]], - ['Orzhov', [white, black]], - ['Izzet', [red, blue]], - ['Golgari', [green, black]], - ['Boros', [red, white]], - ['Simic', [green, blue]], - ], - ], - [ - '3 Color', - [ - ['Abzan', [white, black, green]], - ['Bant', [white, blue, green]], - ['Esper', [white, blue, black]], - ['Grixis', [blue, black, red]], - ['Jeskai', [white, blue, red]], - ['Jund', [black, red, green]], - ['Mardu', [white, black, red]], - ['Naya', [white, red, green]], - ['Sultai', [blue, black, green]], - ['Temur', [blue, red, green]], - ], - ], - [ - '4-5 Color', - [ - ['NonWhite', [blue, black, red, green]], - ['NonBlue', [blue, red, green, white]], - ['NonBlack', [red, green, white, blue]], - ['NonRed', [green, white, blue, black]], - ['NonGreen', [white, blue, black, red]], - ['FiveColor', [white, blue, black, red, green]], - ], - ], -]; - -const MulticoloredAnalysis = ({ multicoloredCounts, ...props }) => ( - - {gold.map(([numColors, combos]) => ( - - - - - - - - - - {combos.map(([name, colors]) => ( - - - - - ))} - -
{numColors}Count
- {colors.map((color) => ( - {color[0]} - ))} - {multicoloredCounts[name]}
- - ))} -
-); - -export default MulticoloredAnalysis; diff --git a/src/components/TokenAnalysis.js b/src/components/TokenAnalysis.js deleted file mode 100644 index e3e9c6eb0..000000000 --- a/src/components/TokenAnalysis.js +++ /dev/null @@ -1,67 +0,0 @@ -import React, { Component } from 'react'; -import { Row, Col, Card, CardBody } from 'reactstrap'; - -import Affiliate from '../util/Affiliate'; - -import MassBuyButton from './MassBuyButton'; -import withAutocard from './WithAutocard'; - -const AutocardLink = withAutocard('a'); - -const compareCards = (x, y) => { - if (x.name === y.name) { - return 0; - } else { - return x.name < y.name ? -1 : 1; - } -}; - -const compareTokens = (x, y) => compareCards(x[0], y[0]); - -const sortTokens = (tokens) => [...tokens].sort(compareTokens); -const sortCards = (cards) => [...cards].sort(compareCards); - -const dedupeCards = (cards) => { - const map = new Map(); - for (const card of [...cards].reverse()) { - map.set(card.name, card); - } - return [...map.values()]; -}; - -const TokenAnalysis = ({ tokens }) => ( - <> - - - ({ details: token }))}> - Buy all tokens - - - - - {sortTokens(tokens).map(([token, tokenCards]) => ( - - - - - - -

- {dedupeCards(sortCards(tokenCards)).map((card) => ( - <> - - {card.name} - -
- - ))} -

-
-
- - ))} -
- -); - -export default TokenAnalysis; diff --git a/src/components/TypeAnalysis.js b/src/components/TypeAnalysis.js deleted file mode 100644 index a7e38212d..000000000 --- a/src/components/TypeAnalysis.js +++ /dev/null @@ -1,71 +0,0 @@ -import React from 'react'; - -import { Col, Row, Table } from 'reactstrap'; - -const types = ['Creatures', 'Instants', 'Sorceries', 'Enchantments', 'Artifacts', 'Planeswalkers', 'Lands', 'Total']; - -const colors = [ - ['White', 'White', 'w'], - ['Blue', 'Blue', 'u'], - ['Black', 'Black', 'b'], - ['Red', 'Red', 'r'], - ['Green', 'Green', 'g'], - ['Colorless', 'Colorless', 'c'], - ['Multicolored', 'Multi', 'm'], - ['Total', 'Total', 'Total'], -]; - -const TypeAnalysis = ({ typeByColor, ...props }) => ( - - -

Type Breakdown

- - - - - ))} - - - - {types.map((type) => ( - - - {colors.map(([name, path, _]) => { - const reactKey = type + path; - const count = typeByColor[type][path]; - const colorTotal = typeByColor['Total'][path]; - if (name !== 'Total' && path !== 'Total' && count > 1 && colorTotal > count) { - const percent = Math.round((count / colorTotal) * 100.0); - return ( - - ); - } else { - return ; - } - })} - - ))} - -
- {colors.map(([name, _, short]) => ( - - {name === 'Total' ? ( - 'Total' - ) : ( - {name} - )} -
{type} - {count} - {percent}% - {count}
- -
-); - -export default TypeAnalysis; diff --git a/src/cube_analysis.js b/src/cube_analysis.js index 54b9df9df..ce892231a 100644 --- a/src/cube_analysis.js +++ b/src/cube_analysis.js @@ -1,16 +1,20 @@ import React, { Component } from 'react'; import ReactDOM from 'react-dom'; -import { Col, Nav, NavLink, Row } from 'reactstrap'; +import { Col, Container, Dropdown, DropdownMenu, DropdownToggle, DropdownItem, Nav, NavLink, Row } from 'reactstrap'; +import { getDraftFormat, calculateAsfans } from './util/draftutil'; +import Filter from './util/Filter'; import Hash from './util/Hash'; -import CurveAnalysis from './components/CurveAnalysis'; +import AnalyticsBarChart from './components/AnalyticsBarChart'; +import AnalyticsCardGrid from './components/AnalyticsCardGrid'; +import AnalyticsCloud from './components/AnalyticsCloud'; +import AnalyticsTable from './components/AnalyticsTable'; import DynamicFlash from './components/DynamicFlash'; import ErrorBoundary from './components/ErrorBoundary'; -import MulticoloredAnalysis from './components/MulticoloredAnalysis'; -import TypeAnalysis from './components/TypeAnalysis'; -import TokenAnalysis from './components/TokenAnalysis'; +import FilterCollapse from './components/FilterCollapse'; +import MagicMarkdown from './components/MagicMarkdown'; class CubeAnalysis extends Component { constructor(props) { @@ -18,49 +22,167 @@ class CubeAnalysis extends Component { this.state = { nav: Hash.get('nav', 'curve'), + data: { type: 'none' }, + workers: {}, + analytics: { + curve: { url: '/js/analytics/colorCurve.js', title: 'Curve' }, + typeBreakdown: { url: '/js/analytics/typeBreakdown.js', title: 'Type Breakdown' }, + colorCount: { url: '/js/analytics/colorCount.js', title: 'Color Counts' }, + tokenGrid: { url: '/js/analytics/tokenGrid.js', title: 'Tokens' }, + tagCloud: { url: '/js/analytics/tagCloud.js', title: 'Tag Cloud' }, + cumulativeColorCount: { url: '/js/analytics/cumulativeColorCount.js', title: 'Cumulative Color Counts' }, + }, + analytics_order: ['curve', 'typeBreakdown', 'colorCount', 'tokenGrid', 'tagCloud', 'cumulativeColorCount'], + filter: [], + cardsWithAsfan: null, + filteredWithAsfan: null, + formatId: Hash.get('formatId', -1), + formatDropdownOpen: false, }; + + this.updateAsfan = this.updateAsfan.bind(this); + this.updateFilter = this.updateFilter.bind(this); + this.updateData = this.updateData.bind(this); + this.setFilter = this.setFilter.bind(this); + this.toggleFormatDropdownOpen = this.toggleFormatDropdownOpen.bind(this); + this.setFormat = this.setFormat.bind(this); + } + + componentDidMount() { + this.updateAsfan(); } select(nav) { - if (nav === 'curve') { - Hash.del('nav'); - } else { - Hash.set('nav', nav); + Hash.set('nav', nav); + this.setState({ nav }, this.updateData); + } + + async updateAsfan() { + const { formatId } = this.state; + const { cube } = this.props; + const cardsWithAsfan = cube.cards.map((card) => Object.assign({}, card)); + const format = getDraftFormat({ id: formatId }, cube); + calculateAsfans(format, cardsWithAsfan); + this.setState({ cardsWithAsfan }, this.updateFilter); + } + + async updateFilter() { + const { filter, cardsWithAsfan } = this.state; + if (cardsWithAsfan == null) { + this.updateAsfan(); + return; + } + const filteredWithAsfan = + filter.length > 0 ? cardsWithAsfan.filter((card) => Filter.filterCard(card, filter)) : cardsWithAsfan; + this.setState({ filteredWithAsfan }, this.updateData); + } + + async updateData() { + const { nav, workers, analytics, analytics_order, filteredWithAsfan } = this.state; + if (filteredWithAsfan == null) { + this.updateFilter(); + return; + } + if (!workers[nav]) { + if (analytics[nav]) { + workers[nav] = new Worker(analytics[nav].url); + workers[nav].addEventListener('message', (e) => { + this.setState({ data: e.data }); + }); + } else { + this.select(analytics_order[0]); + return; + } } - this.setState({ nav }); + workers[nav].postMessage(filteredWithAsfan); + } + + setFilter(filter) { + this.setState({ filter }, this.updateFilter); + } + + setFormat(formatId) { + Hash.set('formatId', formatId); + this.setState({ formatId }, this.updateAsfan); + } + + toggleFormatDropdownOpen() { + this.setState((prevState, props) => { + return { formatDropdownOpen: !prevState.formatDropdownOpen }; + }); } render() { - const { curve, typeByColor, multicoloredCounts, tokens } = this.props; - const active = this.state.nav; - let navItem = (nav, text) => ( - + const { cube } = this.props; + const { analytics, analytics_order, data, filter, formatDropdownOpen, formatId, nav } = this.state; + const cards = cube.cards; + const filteredCards = + (filter && filter.length) > 0 ? cards.filter((card) => Filter.filterCard(card, filter)) : cards; + let navItem = (active, text) => ( + {text} ); + let visualization =

Loading Data

; + if (data) { + // Formats for data are documented in their respective components + if (data.type == 'table') visualization = ; + else if (data.type == 'chart') visualization = ; + else if (data.type == 'cloud') visualization = ; + else if (data.type == 'cardGrid') visualization = ; + } + + let dropdownElement =
Default Draft Format
; + if (cube.draft_formats) { + dropdownElement = ( + + +
{formatId >= 0 ? cube.draft_formats[formatId].title : 'Default Draft Format'}
+ + + + Change Draft Format + + this.setFormat(-1)}> + Default Draft Format + + + Custom Formats + + {cube.draft_formats + ? cube.draft_formats.map((format, formatIndex) => ( + this.setFormat(formatIndex)}> + {format.title} + + )) + : ''} + + + +
+ ); + } return ( <> + + {dropdownElement} - - { - { - curve: , - type: , - multi: , - tokens: , - }[active] - } - + + +

{analytics[nav].title}

+

+ +

+ +
+ {visualization}
@@ -68,13 +190,10 @@ class CubeAnalysis extends Component { } } -const curve = JSON.parse(document.getElementById('curveData').value); -const typeByColor = JSON.parse(document.getElementById('typeData').value); -const multicoloredCounts = JSON.parse(document.getElementById('multicolorData').value); -const tokens = JSON.parse(document.getElementById('generatedTokensData').value); - +const cube = JSON.parse(document.getElementById('cube').value); +cube.cards.forEach((card, index) => { + card.index = index; +}); const wrapper = document.getElementById('react-root'); -const element = ( - -); +const element = ; wrapper ? ReactDOM.render(element, wrapper) : false; diff --git a/views/cube/cube_analysis.pug b/views/cube/cube_analysis.pug index dfafda5d4..3503d2298 100644 --- a/views/cube/cube_analysis.pug +++ b/views/cube/cube_analysis.pug @@ -3,11 +3,9 @@ extends cube_layout block cube_content include ../dynamic_flash #react-root - input#curveData(type='hidden', value=curve) - input#typeData(type='hidden', value=TypeByColor) - input#multicolorData(type='hidden', value=MulticoloredCounts) - input#generatedTokensData(type='hidden',value=GeneratedTokensCounts) + input#cube(type='hidden', name='cube', value=cube) block scripts + script(src='/js/autocard.js', async) include ../react script(src='/js/cube_analysis.bundle.js') diff --git a/views/layout.pug b/views/layout.pug index 82176fa5c..161231d74 100644 --- a/views/layout.pug +++ b/views/layout.pug @@ -143,10 +143,10 @@ html(lang='en') .modal-body .form-group label.col-form-label Username or Email Address: - input#username.form-control(name='username', type='text') + input#username.form-control(name='username', type='text', autocomplete='username') .form-group label.col-form-label Password: - input#password.form-control(name='password', type='password') + input#password.form-control(name='password', type='password', autocomplete='current-password') a(href='/user/lostpassword') Forgot password? if loginCallback input(type='hidden', name='loginCallback', value=loginCallback)