From e5d85eced4a780a17d0ac65a8619ec3d33c511b0 Mon Sep 17 00:00:00 2001 From: Sendou Date: Sat, 12 Oct 2019 14:24:32 +0300 Subject: [PATCH] x search and x trends --- react-ui/package-lock.json | 196 ++++++++++ react-ui/package.json | 1 + .../src/components/common/WeaponDropdown.js | 2 +- react-ui/src/components/root/Footer.js | 24 +- react-ui/src/components/root/MainMenu.js | 12 +- react-ui/src/components/root/RollSim.js | 32 +- react-ui/src/components/root/Routes.js | 8 + react-ui/src/components/rotation/Rotations.js | 2 +- .../xleaderboard/FlexLeaderboard.js | 4 +- .../xleaderboard/WeaponLeaderboard.js | 2 +- .../src/components/xsearch/MonthsTable.js | 61 +++ .../components/xsearch/PlayerXRankStats.js | 146 ++++++++ .../components/xsearch/TopPlacementsTable.js | 118 ++++++ .../src/components/xsearch/WpnPlayedTable.js | 63 ++++ react-ui/src/components/xtrends/XTrends.js | 350 ++++++++++++++++++ react-ui/src/index.css | 2 +- react-ui/src/utils/lists.js | 187 +++++++++- react-ui/src/utils/useTrends.js | 278 ++++++++++++++ 18 files changed, 1448 insertions(+), 40 deletions(-) create mode 100644 react-ui/src/components/xsearch/MonthsTable.js create mode 100644 react-ui/src/components/xsearch/PlayerXRankStats.js create mode 100644 react-ui/src/components/xsearch/TopPlacementsTable.js create mode 100644 react-ui/src/components/xsearch/WpnPlayedTable.js create mode 100644 react-ui/src/components/xtrends/XTrends.js create mode 100644 react-ui/src/utils/useTrends.js diff --git a/react-ui/package-lock.json b/react-ui/package-lock.json index b45093d67..a31343d07 100644 --- a/react-ui/package-lock.json +++ b/react-ui/package-lock.json @@ -4113,6 +4113,73 @@ "type": "^1.0.1" } }, + "d3-array": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", + "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==" + }, + "d3-collection": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", + "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==" + }, + "d3-color": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.0.tgz", + "integrity": "sha512-TzNPeJy2+iEepfiL92LAAB7fvnp/dV2YwANPVHdDWmYMm23qIJBYww3qT8I8C1wXrmrg4UWs7BKc2tKIgyjzHg==" + }, + "d3-format": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.1.tgz", + "integrity": "sha512-TUswGe6hfguUX1CtKxyG2nymO+1lyThbkS1ifLX0Sr+dOQtAD5gkrffpHnx+yHNKUZ0Bmg5T4AjUQwugPDrm0g==" + }, + "d3-interpolate": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.3.2.tgz", + "integrity": "sha512-NlNKGopqaz9qM1PXh9gBF1KSCVh+jSFErrSlD/4hybwoNX/gt1d8CDbDW+3i+5UOHhjC6s6nMvRxcuoMVNgL2w==", + "requires": { + "d3-color": "1" + } + }, + "d3-path": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.8.tgz", + "integrity": "sha512-J6EfUNwcMQ+aM5YPOB8ZbgAZu6wc82f/0WFxrxwV6Ll8wBwLaHLKCqQ5Imub02JriCVVdPjgI+6P3a4EWJCxAg==" + }, + "d3-scale": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz", + "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==", + "requires": { + "d3-array": "^1.2.0", + "d3-collection": "1", + "d3-format": "1", + "d3-interpolate": "1", + "d3-time": "1", + "d3-time-format": "2" + } + }, + "d3-shape": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.5.tgz", + "integrity": "sha512-VKazVR3phgD+MUCldapHD7P9kcrvPcexeX/PkMJmkUov4JM8IxsSg1DvbYoYich9AtdTsa5nNk2++ImPiDiSxg==", + "requires": { + "d3-path": "1" + } + }, + "d3-time": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz", + "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==" + }, + "d3-time-format": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.1.3.tgz", + "integrity": "sha512-6k0a2rZryzGm5Ihx+aFMuO1GgelgIz+7HhB4PH4OEndD5q2zGn1mDfRdNrulspOfR6JXkb2sThhDK41CSK85QA==", + "requires": { + "d3-time": "1" + } + }, "damerau-levenshtein": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.5.tgz", @@ -4166,6 +4233,11 @@ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, + "decimal.js-light": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.0.tgz", + "integrity": "sha512-b3VJCbd2hwUpeRGG3Toob+CRo8W22xplipNhP3tN7TSVB/cyMX71P1vM2Xjc9H74uV6dS2hDDmo/rHq8L87Upg==" + }, "decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", @@ -4402,6 +4474,14 @@ "utila": "~0.4" } }, + "dom-helpers": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", + "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", + "requires": { + "@babel/runtime": "^7.1.2" + } + }, "dom-serializer": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.1.tgz", @@ -8137,6 +8217,11 @@ "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=" }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" + }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -8164,6 +8249,11 @@ "lodash._reinterpolate": "^3.0.0" } }, + "lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=" + }, "lodash.unescape": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz", @@ -8260,6 +8350,11 @@ "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==" }, + "math-expression-evaluator": { + "version": "1.2.17", + "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.17.tgz", + "integrity": "sha1-3oGf282E3M2PrlnGrreWFbnSZqw=" + }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -10618,6 +10713,11 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.10.1.tgz", "integrity": "sha512-BXUMf9sIOPXXZWqr7+c5SeOKJykyVr2u0UDzEf4LNGc6taGkQe1A9DFD07umCIXz45RLr9oAAwZbAJ0Pkknfaw==" }, + "react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, "react-popper": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.4.tgz", @@ -10631,6 +10731,17 @@ "warning": "^4.0.2" } }, + "react-resize-detector": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-2.3.0.tgz", + "integrity": "sha512-oCAddEWWeFWYH5FAcHdBYcZjAw9fMzRUK9sWSx6WvSSOPVRxcHd5zTIGy/mOus+AhN/u6T4TMiWxvq79PywnJQ==", + "requires": { + "lodash.debounce": "^4.0.8", + "lodash.throttle": "^4.1.1", + "prop-types": "^15.6.0", + "resize-observer-polyfill": "^1.5.0" + } + }, "react-router": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.1.2.tgz", @@ -10748,6 +10859,28 @@ "react-dom": "^16.6.1" } }, + "react-smooth": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-1.0.2.tgz", + "integrity": "sha512-pIGzL1g9VGAsRsdZQokIK0vrCkcdKtnOnS1gyB2rrowdLy69lNSWoIjCTWAfgbiYvria8tm5hEZqj+jwXMkV4A==", + "requires": { + "lodash": "~4.17.4", + "prop-types": "^15.6.0", + "raf": "^3.4.0", + "react-transition-group": "^2.5.0" + } + }, + "react-transition-group": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz", + "integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==", + "requires": { + "dom-helpers": "^3.4.0", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2", + "react-lifecycles-compat": "^3.0.4" + } + }, "reactcss": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", @@ -10827,6 +10960,39 @@ "util.promisify": "^1.0.0" } }, + "recharts": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-1.7.1.tgz", + "integrity": "sha512-i4vK/ZSICr+dXGmaijuNybc+xhctiX0464xnqauY+OvE6WvU5v+0GYciQvD/HJSObkKG4wY8aRtiuUL9YtXnHQ==", + "requires": { + "classnames": "^2.2.5", + "core-js": "^2.5.1", + "d3-interpolate": "^1.3.0", + "d3-scale": "^2.1.0", + "d3-shape": "^1.2.0", + "lodash": "^4.17.5", + "prop-types": "^15.6.0", + "react-resize-detector": "^2.3.0", + "react-smooth": "^1.0.0", + "recharts-scale": "^0.4.2", + "reduce-css-calc": "^1.3.0" + }, + "dependencies": { + "core-js": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz", + "integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==" + } + } + }, + "recharts-scale": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.2.tgz", + "integrity": "sha512-p/cKt7j17D1CImLgX2f5+6IXLbRHGUQkogIp06VUoci/XkhOQiGSzUrsD1uRmiI7jha4u8XNFOjkHkzzBPivMg==", + "requires": { + "decimal.js-light": "^2.4.1" + } + }, "recursive-readdir": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz", @@ -10835,6 +11001,31 @@ "minimatch": "3.0.4" } }, + "reduce-css-calc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz", + "integrity": "sha1-dHyRTgSWFKTJz7umKYca0dKSdxY=", + "requires": { + "balanced-match": "^0.4.2", + "math-expression-evaluator": "^1.2.14", + "reduce-function-call": "^1.0.1" + }, + "dependencies": { + "balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=" + } + } + }, + "reduce-function-call": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.3.tgz", + "integrity": "sha512-Hl/tuV2VDgWgCSEeWMLwxLZqX7OK59eU1guxXsRKTAyeYimivsKdtcV4fu3r710tpG5GmDKDhQ0HSZLExnNmyQ==", + "requires": { + "balanced-match": "^1.0.0" + } + }, "regenerate": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", @@ -11064,6 +11255,11 @@ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" }, + "resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" + }, "resolve": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", diff --git a/react-ui/package.json b/react-ui/package.json index 8e8b8767c..a38f63f52 100644 --- a/react-ui/package.json +++ b/react-ui/package.json @@ -13,6 +13,7 @@ "react-router-dom": "^5.1.2", "react-scripts": "3.1.2", "react-sketch": "^0.5.1", + "recharts": "^1.7.1", "semantic-ui-css": "^2.4.1", "semantic-ui-react": "^0.88.1" }, diff --git a/react-ui/src/components/common/WeaponDropdown.js b/react-ui/src/components/common/WeaponDropdown.js index 74c02269f..884dd1d0a 100644 --- a/react-ui/src/components/common/WeaponDropdown.js +++ b/react-ui/src/components/common/WeaponDropdown.js @@ -5,7 +5,7 @@ import { weapons } from "../../utils/lists" import { wpnSmall } from "../../assets/imageImports" import weaponDict from "../../utils/english_internal.json" -const WeaponDropdown = ({ onChange, value, showImages = true }) => { +const WeaponDropdown = ({ value, onChange, showImages = true }) => { return ( {
- Twitter - Twitch - YouTube - Discord + + Twitter + + + Twitch + + + YouTube + + + Discord +
- Splatoonwiki - Some random + + Inkipedia + + + stat.ink + Links More diff --git a/react-ui/src/components/root/MainMenu.js b/react-ui/src/components/root/MainMenu.js index 834b6446d..01a0e7adf 100644 --- a/react-ui/src/components/root/MainMenu.js +++ b/react-ui/src/components/root/MainMenu.js @@ -1,6 +1,6 @@ import React from "react" -import { Menu, Container, Image, Dropdown, Button } from "semantic-ui-react" -import { NavLink } from "react-router-dom" +import { Menu, Container, Image, Dropdown, Icon } from "semantic-ui-react" +import { Link, NavLink } from "react-router-dom" import sink_logo from "../../assets/sink_logo.png" const dropdownStyle = { @@ -11,7 +11,7 @@ const dropdownStyle = { const MainMenu = () => { return ( - + @@ -53,8 +53,10 @@ const MainMenu = () => { - - + + + + Log in via Discord diff --git a/react-ui/src/components/root/RollSim.js b/react-ui/src/components/root/RollSim.js index 9146e807d..507add723 100644 --- a/react-ui/src/components/root/RollSim.js +++ b/react-ui/src/components/root/RollSim.js @@ -1,10 +1,8 @@ import React, { useState } from "react" -import { Comment } from "semantic-ui-react" import { clothingGear, shoesGear, headGear } from "../../utils/lists" import { choose } from "../../utils/helperFunctions" import { abilityIcons } from "../../assets/imageImports" -import murchpfp from "../../assets/murchpfp.png" import head from "../../utils/head.json" import clothes from "../../utils/clothes.json" import shoes from "../../utils/shoes.json" @@ -37,6 +35,8 @@ const subAbilityStyle = { } const gearStyle = { maxWidth: "50px", height: "auto" } +const paddingStyle = { paddingLeft: "0.7em" } + const subAbilities = [ abilityIcons.ISM, abilityIcons.ISS, @@ -167,7 +167,6 @@ const RollSim = () => { choose(subAbilities) ]) const [rolling, setRolling] = useState(false) - const [rollCount, setRollCount] = useState(0) const setSubs = (json, gear) => { let gearName = gear.split("_")[1] @@ -239,7 +238,6 @@ const RollSim = () => { } setSubsState(setSubs(json, gear)) //only using this method in the last roll that matters for optimization purposes setRolling(false) - setRollCount(rollCount + 1) } return ( @@ -250,7 +248,9 @@ const RollSim = () => { style={gearStyle} alt="" /> - + + + { style={gearStyle} alt="" /> - + + + { style={gearStyle} alt="" /> - + + + { onClick={() => roll("SHOES")} /> - {rollCount > 0 && ( - - - - - Murch - - You have rolled {rollCount} {rollCount === 1 ? "time" : "times"} - , chum. - - - - - )} ) } diff --git a/react-ui/src/components/root/Routes.js b/react-ui/src/components/root/Routes.js index d0a3b434e..33533347a 100644 --- a/react-ui/src/components/root/Routes.js +++ b/react-ui/src/components/root/Routes.js @@ -7,6 +7,8 @@ const Rotations = lazy(() => import("../rotation/Rotations")) const MapPlanner = lazy(() => import("../plans/MapPlanner")) const Calendar = lazy(() => import("../calendar/Calendar")) const XLeaderboard = lazy(() => import("../xleaderboard/XLeaderboard")) +const PlayerXRankStats = lazy(() => import("../xsearch/PlayerXRankStats")) +const XTrends = lazy(() => import("../xtrends/XTrends")) const Routes = () => { return ( @@ -27,6 +29,12 @@ const Routes = () => { + + + + + + ) diff --git a/react-ui/src/components/rotation/Rotations.js b/react-ui/src/components/rotation/Rotations.js index f10f29023..2535f6f81 100644 --- a/react-ui/src/components/rotation/Rotations.js +++ b/react-ui/src/components/rotation/Rotations.js @@ -39,7 +39,7 @@ const Rotations = ({ setMenuSelection }) => { }, [data, loading, monthly.loading]) if (loading || monthly.loading || rotation.length === 0) return - if (error || monthly.error) return + if (error || monthly.error) return const monthlyMaps = monthly.data.maplists[0] diff --git a/react-ui/src/components/xleaderboard/FlexLeaderboard.js b/react-ui/src/components/xleaderboard/FlexLeaderboard.js index 8ec7b1746..0eaa4fdd3 100644 --- a/react-ui/src/components/xleaderboard/FlexLeaderboard.js +++ b/react-ui/src/components/xleaderboard/FlexLeaderboard.js @@ -1,5 +1,5 @@ import React, { useEffect } from "react" -import { Table, Icon, Popup, Segment } from "semantic-ui-react" +import { Table, Icon, Popup } from "semantic-ui-react" import { useQuery } from "@apollo/react-hooks" import { Link } from "react-router-dom" @@ -21,7 +21,7 @@ const FlexLeaderboard = () => { ) } if (result.error) { - return + return } const leaderboard = result.data["topFlex"] diff --git a/react-ui/src/components/xleaderboard/WeaponLeaderboard.js b/react-ui/src/components/xleaderboard/WeaponLeaderboard.js index 4e25cd6ca..ec435dd86 100644 --- a/react-ui/src/components/xleaderboard/WeaponLeaderboard.js +++ b/react-ui/src/components/xleaderboard/WeaponLeaderboard.js @@ -22,7 +22,7 @@ const WeaponLeaderboard = ({ query, queryName, scoreField, weaponsField }) => { ) } if (result.error) { - return + return } const leaderboard = result.data[queryName] diff --git a/react-ui/src/components/xsearch/MonthsTable.js b/react-ui/src/components/xsearch/MonthsTable.js new file mode 100644 index 000000000..b803d5f21 --- /dev/null +++ b/react-ui/src/components/xsearch/MonthsTable.js @@ -0,0 +1,61 @@ +import React from "react" +import { Table, Header, Image } from "semantic-ui-react" + +import { wpnSmall } from "../../assets/imageImports" +import szIcon from "../../assets/sz.png" +import tcIcon from "../../assets/tc.png" +import rmIcon from "../../assets/rm.png" +import cbIcon from "../../assets/cb.png" +import { months } from "../../utils/lists" +import weaponDict from "../../utils/english_internal.json" + +const MonthsTable = ({ placements }) => { + //data received is ordered chronologically and sz->tc->rm->cb + const modeIcons = [null, szIcon, tcIcon, rmIcon, cbIcon] + let lastMonth = placements[0].month + let toggle = false + + return ( + + + + + Name + X Power + Placement + Weapon + + + + + {placements.map(p => { + if (lastMonth !== p.month) { + lastMonth = p.month + toggle = !toggle + } + return ( + + +
+ + + {months[p.month]} + {p.year} + +
+
+ {p.name} + {p.x_power} + {p.rank} + + {p.weapon} + +
+ ) + })} +
+
+ ) +} + +export default MonthsTable diff --git a/react-ui/src/components/xsearch/PlayerXRankStats.js b/react-ui/src/components/xsearch/PlayerXRankStats.js new file mode 100644 index 000000000..ec7940b0a --- /dev/null +++ b/react-ui/src/components/xsearch/PlayerXRankStats.js @@ -0,0 +1,146 @@ +import React, { useEffect, useState } from "react" +import { useQuery } from "@apollo/react-hooks" +import { Header, Image, Icon, List, Segment } from "semantic-ui-react" +import { Link, Redirect, useParams } from "react-router-dom" + +import { playerInfo } from "../../graphql/queries/playerInfo" +import TopPlacementsTable from "./TopPlacementsTable" +import WpnPlayedTable from "./WpnPlayedTable" +import MonthsTable from "./MonthsTable" +import Loading from "../common/Loading" +import Error from "../common/Error" + +const addXRankHelp = ( +
+ This user has no X Rank profile set up. If you are the owner of this user + page here is how you can set it up: +
+ + + Finish an X rank season in the Top 500 in at least one mode. + + Send your Twitter handle to Sendou via DM. + + Add your Twitter account to your profile on Discord, verify it and + make sure it's set to appear publicly. + + Log in to sendou.ink + +
+
+) + +const PlayerXRankStats = ({ twitter, tabMode = false }) => { + let searchVariables = {} + const { uid } = useParams() + if (uid) searchVariables = { uid } + if (twitter) searchVariables = { twitter } + const { data, error, loading } = useQuery(playerInfo, { + variables: searchVariables + }) + const [top, setTop] = useState([]) + + useEffect(() => { + if (loading) { + return + } + if (data && data.playerInfo) { + document.title = `${data.playerInfo.player.name} - X Rank - sendou.ink` + const placements = data.playerInfo.placements + + //reducing placements to top sz, tc etc. rank and x power + const tops = ["", "szTop", "tcTop", "rmTop", "cbTop"] + const exs = ["", "szX", "tcX", "rmX", "cbX"] + setTop( + placements.reduce( + (acc, cur) => { + const topKey = tops[cur.mode] + const xKey = exs[cur.mode] + if (!acc[xKey]) { + acc[xKey] = cur + acc[topKey] = cur + return acc + } + if (acc[xKey].x_power < cur.x_power) { + acc[xKey] = cur + } + if (acc[topKey].rank > cur.rank) { + acc[topKey] = cur + } + + return acc + }, + { + szX: null, + szTop: null, + tcX: null, + tcTop: null, + rmX: null, + rmTop: null, + cbX: null, + cbTop: null + } + ) + ) + } + }, [data, loading]) + + if (!uid && !twitter) return addXRankHelp + if (loading) return + if (error) { + if (error.message === "GraphQL error: player not found") { + if (tabMode) return addXRankHelp + else return + } + return + } + const playerData = data.playerInfo.player + return ( + <> + {!tabMode ? ( +
+ {playerData.twitter ? ( + + ) : null}{" "} + {playerData.alias ? playerData.alias : playerData.name} + {playerData.twitter ? ( + + + + ) : null} +
+ ) : null} + {tabMode && playerData.discord_id ? ( + <> + User page +
+ + ) : null} + +
+ +
+
+ +
+
All Top 500 placements
+ +
+
+ +
+
Weapons reached Top 500 with
+ +
+
+ + ) +} +export default PlayerXRankStats diff --git a/react-ui/src/components/xsearch/TopPlacementsTable.js b/react-ui/src/components/xsearch/TopPlacementsTable.js new file mode 100644 index 000000000..248e545ff --- /dev/null +++ b/react-ui/src/components/xsearch/TopPlacementsTable.js @@ -0,0 +1,118 @@ +import React from "react" +import { Header, Image, Table } from "semantic-ui-react" + +import { wpnSmall } from "../../assets/imageImports" +import weaponDict from "../../utils/english_internal.json" +import { months } from "../../utils/lists" +import szIcon from "../../assets/sz.png" +import tcIcon from "../../assets/tc.png" +import rmIcon from "../../assets/rm.png" +import cbIcon from "../../assets/cb.png" + +const TopPlacementsTable = ({ top }) => { + const returnRow = (x, placement, mode) => { + if (!x) { + return null + } + + const modeIcon = ["", szIcon, tcIcon, rmIcon, cbIcon][mode] + const modeName = [ + "", + "Splat Zones", + "Tower Control", + "Rainmaker", + "Clam Blitz" + ][mode] + + if (x === placement) { + return ( + + + +
+ + + {modeName} + + Highest X Power & Placement + + +
+
+ {x.x_power} + {x.rank} + + {x.weapon} + + {months[x.month]} + {x.year} +
+
+ ) + } + + return ( + + + +
+ + + {modeName} + Highest X Power + +
+
+ {x.x_power} + {x.rank} + + {x.weapon} + + {months[x.month]} + {x.year} +
+ + +
+ + + {modeName} + Highest Placement + +
+
+ {placement.x_power} + {placement.rank} + + {placement.weapon} + + {months[placement.month]} + {placement.year} +
+
+ ) + } + return ( + + + + + X Power + Placement + Weapon + Month + Year + + + {returnRow(top.szX, top.szTop, 1)} + {returnRow(top.tcX, top.tcTop, 2)} + {returnRow(top.rmX, top.rmTop, 3)} + {returnRow(top.cbX, top.cbTop, 4)} +
+ ) +} + +export default TopPlacementsTable diff --git a/react-ui/src/components/xsearch/WpnPlayedTable.js b/react-ui/src/components/xsearch/WpnPlayedTable.js new file mode 100644 index 000000000..8887a9877 --- /dev/null +++ b/react-ui/src/components/xsearch/WpnPlayedTable.js @@ -0,0 +1,63 @@ +import React from "react" +import { weaponsByCategory } from "../../utils/lists" +import { categoryKeys } from "../../utils/lists" +import weaponDict from "../../utils/english_internal.json" +import { wpnSmall } from "../../assets/imageImports" +import { Table, Header, Popup } from "semantic-ui-react" + +const WpnPlayedTable = ({ weapons }) => { + const weaponStyle = wpnName => { + const activeStyle = {} + const inactiveStyle = { filter: "grayscale(1)", opacity: "0.3" } + + return weapons.includes(wpnName) ? activeStyle : inactiveStyle + } + return ( + + {categoryKeys.map(c => { + return ( + + + +
+ {c} +
+
+ + {weaponsByCategory[c].map(w => { + return ( + + } + content={w} + /> + ) + })} + +
+
+ ) + })} + + + + +
+ Total +
+
+ {weapons.length} / 129 +
+
+
+ ) +} + +export default WpnPlayedTable diff --git a/react-ui/src/components/xtrends/XTrends.js b/react-ui/src/components/xtrends/XTrends.js new file mode 100644 index 000000000..e3bd31484 --- /dev/null +++ b/react-ui/src/components/xtrends/XTrends.js @@ -0,0 +1,350 @@ +import React, { useState, useEffect } from "react" +import { + Button, + Form, + Radio, + List, + Image, + Grid, + Dropdown, + Message +} from "semantic-ui-react" +import { wpnSmall } from "../../assets/imageImports" +import useTrends from "../../utils/useTrends" +import useWindowDimensions from "../../utils/useWindowDimensions" +import english_internal from "../../utils/english_internal.json" +import { months } from "../../utils/lists" +import WeaponDropdown from "../common/WeaponDropdown" +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, + Brush +} from "recharts" + +const customToolTipStyle = { + width: "200px", + margin: "0", + lineHeight: "24px", + border: "1px solid #f5f5f5", + backgroundColor: "hsla(0,0%,100%,.8)", + padding: "10px" +} + +const labelStyle = { + margin: "0", + color: "#666", + fontWeight: "700" +} + +const introStyle = { + borderTop: "1px solid #f5f5f5", + margin: "0" +} + +const descStyle = { + margin: 0, + color: "#999" +} + +const XTrends = () => { + const [weaponForm, setWeaponForm] = useState(null) + const [combineFormLeft, setCombineFormLeft] = useState(null) + const [combineFormRight, setCombineFormRight] = useState(null) + const [weaponForDispatch, setWeaponForDispatch] = useState(null) + const [mode, setMode] = useState("ALL") + const [modeForDispatch, setModeForDispatch] = useState(null) + const { loading, error, plotData, dispatch } = useTrends( + weaponForDispatch, + modeForDispatch + ) + const { containerWidth } = useWindowDimensions() + + useEffect(() => { + document.title = "X Rank Trends - sendou.ink" + }, []) + + const CustomTooltip = ({ active, payload }) => { + if (active) { + if (payload.length === 0) return null + const monthNumber = payload[0].payload.name + const yearNumber = payload[0].payload.year + let patchDescription = null + if (payload[0].payload.hasOwnProperty("patch")) { + if (monthNumber === 4 && yearNumber === 2019) { + patchDescription = "Patches 4.6 and 4.7 were released." + } else { + patchDescription = `Patch ${payload[0].payload.patch} was released.` + } + } + return ( +
+

{`${months[monthNumber]} (${yearNumber})`}

+ {payload.map(p => { + return ( +

+ {p.payload[p.dataKey]} - {p.dataKey} +

+ ) + })} + {patchDescription &&

{patchDescription}

} +
+ ) + } + + return null + } + + if (error) { + return
{error.message}
+ } + + return ( + <> + setWeaponForm(value)} + /> +
+
+ + setMode("ALL")} + /> + + + setMode("SZ")} + /> + + + setMode("TC")} + /> + + + setMode("RM")} + /> + + + setMode("CB")} + /> + +
+
+
+ + {plotData.keys.length >= 2 && ( +
+ + + {" "} + { + return { text: k.weapon, value: k.weapon } + }) + .filter(k => k.text !== combineFormRight)} + onChange={(event, { value }) => { + setCombineFormLeft(value) + }} + value={combineFormLeft} + />{" "} + with{" "} + { + return { text: k.weapon, value: k.weapon } + }) + .filter(k => k.text !== combineFormLeft)} + onChange={(event, { value }) => { + setCombineFormRight(value) + }} + value={combineFormRight} + /> + +
+ )} +
+ {plotData.keys.length > 0 && ( +
+ + + + + + } /> + + + {plotData.keys.map(keyObj => { + const w = keyObj.weapon + const color = keyObj.color + return + })} + + + + + {plotData.keys.map(keyObj => { + const w = keyObj.weapon + const color = keyObj.color + return ( + + +
+ )} + + + + You can check out all the patch notes{" "} + + here + + + + For alternative take on X Rank trends check out{" "} + Splat Meta + + + + + ) +} + +export default XTrends diff --git a/react-ui/src/index.css b/react-ui/src/index.css index 81e613d27..c57360d16 100644 --- a/react-ui/src/index.css +++ b/react-ui/src/index.css @@ -6,7 +6,7 @@ body { height: 100%; margin: 0; background: #79f1a4; - background-image: linear-gradient(135deg, #79f1a4 10%, #0e5cad 100%); + background: linear-gradient(to right, #0f2027, #203a43, #2c5364); background-repeat: no-repeat; background-attachment: fixed; } diff --git a/react-ui/src/utils/lists.js b/react-ui/src/utils/lists.js index 9ed6e434d..dbb8e5d04 100644 --- a/react-ui/src/utils/lists.js +++ b/react-ui/src/utils/lists.js @@ -154,6 +154,191 @@ export const weapons = [ "Kensa Undercover Brella" ] +export const shooters = [ + "Sploosh-o-matic", + "Neo Sploosh-o-matic", + "Sploosh-o-matic 7", + "Splattershot Jr.", + "Custom Splattershot Jr.", + "Kensa Splattershot Jr.", + "Splash-o-matic", + "Neo Splash-o-matic", + "Aerospray MG", + "Aerospray RG", + "Aerospray PG", + "Splattershot", + "Tentatek Splattershot", + "Kensa Splattershot", + ".52 Gal", + ".52 Gal Deco", + "Kensa .52 Gal", + "N-ZAP '85", + "N-ZAP '89", + "N-ZAP '83", + "Splattershot Pro", + "Forge Splattershot Pro", + "Kensa Splattershot Pro", + ".96 Gal", + ".96 Gal Deco", + "Jet Squelcher", + "Custom Jet Squelcher" +] + +export const semiauto = [ + "L-3 Nozzlenose", + "L-3 Nozzlenose D", + "Kensa L-3 Nozzlenose", + "H-3 Nozzlenose", + "H-3 Nozzlenose D", + "Cherry H-3 Nozzlenose", + "Squeezer", + "Foil Squeezer" +] + +export const blasters = [ + "Luna Blaster", + "Luna Blaster Neo", + "Kensa Luna Blaster", + "Blaster", + "Custom Blaster", + "Range Blaster", + "Custom Range Blaster", + "Grim Range Blaster", + "Rapid Blaster", + "Rapid Blaster Deco", + "Kensa Rapid Blaster", + "Rapid Blaster Pro", + "Rapid Blaster Pro Deco", + "Clash Blaster", + "Clash Blaster Neo" +] + +export const rollers = [ + "Carbon Roller", + "Carbon Roller Deco", + "Splat Roller", + "Krak-On Splat Roller", + "Kensa Splat Roller", + "Dynamo Roller", + "Gold Dynamo Roller", + "Kensa Dynamo Roller", + "Flingza Roller", + "Foil Flingza Roller" +] + +export const brushes = [ + "Inkbrush", + "Inkbrush Nouveau", + "Permanent Inkbrush", + "Octobrush", + "Octobrush Nouveau", + "Kensa Octobrush" +] + +export const chargers = [ + "Classic Squiffer", + "New Squiffer", + "Fresh Squiffer", + "Splat Charger", + "Firefin Splat Charger", + "Kensa Charger", + "Splatterscope", + "Firefin Splatterscope", + "Kensa Splatterscope", + "E-liter 4K", + "Custom E-liter 4K", + "E-liter 4K Scope", + "Custom E-liter 4K Scope", + "Bamboozler 14 Mk I", + "Bamboozler 14 Mk II", + "Bamboozler 14 Mk III", + "Goo Tuber", + "Custom Goo Tuber" +] + +export const sloshers = [ + "Slosher", + "Slosher Deco", + "Soda Slosher", + "Tri-Slosher", + "Tri-Slosher Nouveau", + "Sloshing Machine", + "Sloshing Machine Neo", + "Kensa Sloshing Machine", + "Bloblobber", + "Bloblobber Deco", + "Explosher", + "Custom Explosher" +] + +export const splatlings = [ + "Mini Splatling", + "Zink Mini Splatling", + "Kensa Mini Splatling", + "Heavy Splatling", + "Heavy Splatling Deco", + "Heavy Splatling Remix", + "Hydra Splatling", + "Custom Hydra Splatling", + "Ballpoint Splatling", + "Ballpoint Splatling Nouveau", + "Nautilus 47", + "Nautilus 79" +] + +export const dualies = [ + "Dapple Dualies", + "Dapple Dualies Nouveau", + "Clear Dapple Dualies", + "Splat Dualies", + "Enperry Splat Dualies", + "Kensa Splat Dualies", + "Glooga Dualies", + "Glooga Dualies Deco", + "Kensa Glooga Dualies", + "Dualie Squelchers", + "Custom Dualie Squelchers", + "Dark Tetra Dualies", + "Light Tetra Dualies" +] + +export const brellas = [ + "Splat Brella", + "Sorella Brella", + "Tenta Brella", + "Tenta Sorella Brella", + "Tenta Camo Brella", + "Undercover Brella", + "Undercover Sorella Brella", + "Kensa Undercover Brella" +] + +export const weaponsByCategory = { + Shooters: shooters, + "Semi-automatic Shooters": semiauto, + Blasters: blasters, + Rollers: rollers, + Brushes: brushes, + Chargers: chargers, + Sloshers: sloshers, + Splatlings: splatlings, + Dualies: dualies, + Brellas: brellas +} + +export const categoryKeys = [ + "Shooters", + "Semi-automatic Shooters", + "Blasters", + "Rollers", + "Brushes", + "Chargers", + "Sloshers", + "Splatlings", + "Dualies", + "Brellas" +] + export const clothingGear = [ "Clt_AMB000", "Clt_AMB001", @@ -442,7 +627,6 @@ export const headGear = [ "Hed_CAP011", "Hed_CAP012", "Hed_CAP014", - "Hed_CAP015", "Hed_CAP018", "Hed_CAP019", "Hed_CAP020", @@ -569,7 +753,6 @@ export const headGear = [ "Hed_NCP004", "Hed_NCP005", "Hed_NCP006", - "Hed_NCP007", "Hed_NCP008", "Hed_NCP009", "Hed_NCP010", diff --git a/react-ui/src/utils/useTrends.js b/react-ui/src/utils/useTrends.js new file mode 100644 index 000000000..7a3328ca5 --- /dev/null +++ b/react-ui/src/utils/useTrends.js @@ -0,0 +1,278 @@ +import { useReducer, useEffect } from "react" +import { useQuery } from "@apollo/react-hooks" +import { searchForTrend } from "../graphql/queries/seachForTrend" + +const month = [] +month[0] = null +month[1] = "Jan" +month[2] = "Feb" +month[3] = "Mar" +month[4] = "Apr" +month[5] = "May" +month[6] = "Jun" +month[7] = "Jul" +month[8] = "Aug" +month[9] = "Sep" +month[10] = "Oct" +month[11] = "Nov" +month[12] = "Dec" + +const patches = { + 5: { + 2018: { + name: "3.0", + link: "https://splatoonwiki.org/wiki/Version_3.0.0_(Splatoon_2)" + }, + 2019: { + name: "4.8", + link: "https://splatoonwiki.org/wiki/Version_4.8.0_(Splatoon_2)" + } + }, + 6: { + 2018: { + name: "3.1", + link: "https://splatoonwiki.org/wiki/Version_3.1.0_(Splatoon_2)" + }, + 2019: { + name: "4.9", + link: "https://splatoonwiki.org/wiki/Version_4.9.0_(Splatoon_2)" + } + }, + 7: { + 2018: { + name: "3.2", + link: "https://splatoonwiki.org/wiki/Version_3.2.0_(Splatoon_2)" + }, + 2019: { + name: "5.0", + link: "https://splatoonwiki.org/wiki/Version_5.0.0_(Splatoon_2)" + } + }, + 9: { + 2018: { + name: "4.0", + link: "https://splatoonwiki.org/wiki/Version_4.0.0_(Splatoon_2)" + } + }, + 10: { + 2018: { + name: "4.1", + link: "https://splatoonwiki.org/wiki/Version_4.1.0_(Splatoon_2)" + } + }, + 11: { + 2018: { + name: "4.2", + link: "https://splatoonwiki.org/wiki/Version_4.2.0_(Splatoon_2)" + } + }, + 12: { + 2018: { + name: "4.3", + link: "https://splatoonwiki.org/wiki/Version_4.3.0_(Splatoon_2)" + } + }, + 1: { + 2019: { + name: "4.4", + link: "https://splatoonwiki.org/wiki/Version_4.4.0_(Splatoon_2)" + } + }, + 3: { + 2019: { + name: "4.5", + link: "https://splatoonwiki.org/wiki/Version_4.5.0_(Splatoon_2)" + } + }, + 4: { + 2019: { + name: "4.6+4.7", + link: "https://splatoonwiki.org/wiki/List_of_updates_in_Splatoon_2" + } + } +} + +const presetColors = [ + "#FF00FF", + "#008000", + "#FF0000", + "#0000FF", + "#FFA500", + "#800080", + "#A52A2A", + "#1BC5CD", + "#000080", + "#5BCCA0" +] + +const setPlotDataInitial = () => { + const arr_to_return = [] + for (let i = 5; i < 13; i++) { + if (patches.hasOwnProperty(i) && patches[i].hasOwnProperty(2018)) { + arr_to_return.push({ + name: i, + year: 2018, + xLabel: month[i], + patch: patches[i][2018].name + }) + } else { + arr_to_return.push({ name: i, year: 2018, xLabel: month[i] }) + } + } + const d = new Date() + const year = d.getFullYear() + const currentMonth = d.getMonth() + 1 + for (let i = 2019; i < year + 1; i++) { + for (let j = 1; j < 13; j++) { + // break the loop when we reach the future + if (i === year && j === currentMonth) break + const xLabel = j === 1 ? `Jan (${year})` : month[j] + if (patches.hasOwnProperty(j) && patches[j].hasOwnProperty(i)) { + arr_to_return.push({ + name: j, + year: i, + xLabel, + patch: patches[j][i].name + }) + } else { + arr_to_return.push({ name: j, year: i, xLabel }) + } + } + } + + return arr_to_return +} + +const mergeModeArrays = countObj => { + const sz_arr = countObj["SZ"] + const tc_arr = countObj["TC"] + const rm_arr = countObj["RM"] + const cb_arr = countObj["CB"] + const arr_to_return = new Array(12).fill(0) + for (let i = 1; i < 13; i++) { + arr_to_return[i] = sz_arr[i] + tc_arr[i] + rm_arr[i] + cb_arr[i] + } + return arr_to_return +} + +// monthIndex is between 1 and 12 (inclusive) +const resolveStartIndex = (monthIndex, year) => { + // resolves start index for an array where index 0 is always May 2018 + if (year === 2018) { + return monthIndex - 5 + } else if (year === 2019) { + return 7 + monthIndex + } else { + return 7 + monthIndex + (year - 2019) * 12 + } +} + +const getColor = state => { + if (state.keys.length < 9) { + return presetColors[state.keys.length] + } + + return "#000000".replace(/0/g, function() { + return (~~(Math.random() * 16)).toString(16) + }) +} + +const reducer = (state, action) => { + switch (action.type) { + case "add": + const trend = action.trendDocument + const mode = action.mode + const modeLabel = mode === "ALL" ? "" : ` (${mode})` + const weapon = `${action.trendDocument.weapon}${modeLabel}` + // don't add duplicate plots + if (state.keys.indexOf(weapon) !== -1) return state + const toPlotData = [...state.data] + for (let index = 0; index < trend.counts.length; index++) { + let year = trend.counts[index].year + let year_arr = null + if (mode === "ALL") { + year_arr = mergeModeArrays(trend.counts[index]) + } else { + year_arr = trend.counts[index][mode] + } + + for (let i = 1; i < 13; i++) { + // if year is 2018 skipping to the index where values are found + if (year === 2018 && i < 5) { + i = 4 + continue + } + const plotIndex = resolveStartIndex(i, year) + if (plotIndex === toPlotData.length) break + toPlotData[plotIndex][weapon] = year_arr[i] + } + } + + const keyObj = { + weapon, + color: getColor(state) + } + return { data: toPlotData, keys: [...state.keys, keyObj] } + case "delete": + const weaponToDelete = action.weapon + const newKeys = [...state.keys].filter(k => k.weapon !== weaponToDelete) + return { data: state.data, keys: newKeys } + case "combine": + const weaponLeft = action.left + const weaponRight = action.right + const newKey = `${weaponLeft} + ${weaponRight}` + const newKeysWithCombined = [ + ...state.keys, + { weapon: newKey, color: getColor(state) } + ].filter(k => k.weapon !== weaponLeft && k.weapon !== weaponRight) + const newDataWithCombined = [...state.data].map(d => { + const dataObj = { ...d } + let weaponLeftCount = 0 + let weaponRightCount = 0 + if (dataObj.hasOwnProperty(weaponLeft)) + weaponLeftCount = dataObj[weaponLeft] + if (dataObj.hasOwnProperty(weaponRight)) + weaponRightCount = dataObj[weaponRight] + dataObj[newKey] = weaponLeftCount + weaponRightCount + delete dataObj[weaponLeft] + delete dataObj[weaponRight] + return dataObj + }) + + return { data: newDataWithCombined, keys: newKeysWithCombined } + case "randomizeColor": + const keysWithNewColor = [...state.keys].map(k => { + if (k.weapon !== action.weapon) { + return k + } + return { + ...k, + color: "#000000".replace(/0/g, function() { + return (~~(Math.random() * 16)).toString(16) + }) + } + }) + return { data: state.data, keys: keysWithNewColor } + default: + throw new Error() + } +} + +export default function useTrends(weapon, mode) { + const [plotData, dispatch] = useReducer(reducer, { + data: setPlotDataInitial(), + keys: [] + }) + // Skip query if there is no weapon provided + const { data, loading, error } = useQuery(searchForTrend, { + skip: !weapon, + variables: { weapon } + }) + + useEffect(() => { + if (loading || !data) return + dispatch({ type: "add", trendDocument: data.searchForTrend, mode }) + }, [loading, data, mode]) + + return { loading, error, plotData, dispatch } +}