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 (
+ <>
+
+
+
+
+
+
+ {data.columns.map(({ header, key }) => (
+
+
+
+ {data.data.map((datapoint, position) => (
+
+
+ ))}
+
+ {data.columns.map(({ key, rowHeader }, columnPosition) => {
+ var Cell = ({ children, ...props }) =>
+ ))}
+
+ {children} ;
+ if (rowHeader)
+ Cell = ({ children, ...props }) => (
+
+ {children}
+
+ );
+ return (
+
{numColors} | -Count | -
---|---|
- {colors.map((color) => ( - - ))} - | -{multicoloredCounts[name]} | -
- {dedupeCards(sortCards(tokenCards)).map((card) => (
- <>
-
- >
- ))}
-
- {colors.map(([name, _, short]) => ( - | - {name === 'Total' ? ( - 'Total' - ) : ( - - )} - | - ))} -|
---|---|---|
{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 ( -- {count} - {percent}% - | - ); - } else { - return{count} | ; - } - })} -
Loading Data
; + if (data) { + // Formats for data are documented in their respective components + if (data.type == 'table') visualization =
+