├── .gitignore ├── config.ini ├── main.py ├── readme.md └── ServerInfo.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | mee6-leaderboard.csv 3 | .vscode -------------------------------------------------------------------------------- /config.ini: -------------------------------------------------------------------------------- 1 | 234696507352154112 2 | 100 3 | 10,20,30,50 4 | na 5 | na 6 | y -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # main.py 2 | # A program and scrapes the mee6 leaderboard site api and generates graphs 3 | 4 | import matplotlib.pyplot as plt 5 | from ServerInfo import ServerInfo 6 | 7 | # -------- 8 | # config.ini file reader 9 | # -------- 10 | with open('config.ini', 'r') as configfile: 11 | configs = configfile.readlines() 12 | # checks that config file has 6 lines 13 | if not len(configs) == 6: 14 | print("Invalid config file: incorrect number of lines!") 15 | else: 16 | try: 17 | # completes json link with server id 18 | url = 'https://mee6.xyz/api/plugins/levels/leaderboard/' + str(configs[0]) + '?limit=999&page=0' 19 | # checks for 'na' and sets default 20 | if not configs[1] == "na": 21 | top = int(configs[1]) 22 | else: 23 | top = 100 24 | # converts str to int before being used in functions 25 | statuslevels = list(configs[2].strip(" ").split(",")) 26 | for i in range(len(statuslevels)): 27 | statuslevels[i] = int(statuslevels[i]) 28 | # setting statusnames 29 | statusnames = list(configs[3].strip("\n").strip(" ").split(",")) 30 | yvar = configs[4].strip("\n") 31 | # output csv? 32 | printcsv = configs[5] 33 | csv = False 34 | if printcsv == 'y': 35 | csv = True 36 | except: 37 | print("An error ocurred, please recheck config.ini") 38 | 39 | # sets server variable 40 | server = ServerInfo(url, top) 41 | # csv 42 | if csv: 43 | server.csv() 44 | # plots figure 45 | plt.figure() 46 | plt.suptitle(server.servername) 47 | plt.subplot(221) 48 | server.statuspie(statuslevels, statusnames) 49 | plt.subplot(222) 50 | server.rankbar(yvar) 51 | plt.subplot(223) 52 | server.lvldistribution() 53 | plt.subplot(224) 54 | server.randomstats() 55 | plt.subplots_adjust(hspace=0.3) 56 | plt.show() 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Mee6 Leaderboard Graphs 2 | Here you go! Spent a couple days working on this little project. I always thought that the Mee6 leaderboard site had great data for demographic charts, especially with its use in popular servers. This should work with all Discord servers that use the Mee6 bot. Let me know if there are any bugs. 3 | 4 | ## What it does 5 | This program creates graphs to visualize up to the top 1000 members of your Discord server. The graphs include a pie chart showing a breakdown of level ranges of members, a leaderboard the top members and their levels/experience represented in a bar graph, a histogram showing the frequency distribution of levels in the sample, and a few more general analytics statements. 6 | 7 | ## How to use 8 | 9 | ### Getting started 10 | To use this program, you will need a couple things: 11 | - Python 3 12 | - Make sure that the server you want to graph has the Mee6 bot 13 | - Your Mee6 Server ID (more on this below) 14 | 15 | Getting your Mee6 Server ID: 16 | 1. Go on the server you want to use and type `!levels` in the chat 17 | 2. Visit the link that Mee6 replies with, it should look like this: https://mee6.xyz/leaderboard/123456789123456789 or https://mee6.xyz/levels/123456789123456789 18 | 3. The server ID is the numbers at the end of the url, in this case "123456789123456789" 19 | 20 | ### Configurations 21 | The config.ini file you will need to edit to run the program with the server and settings you want. Here's how to use it. To use the default settings (if available) in each line, write `na` 22 | 23 | Each line of config.ini explained: 24 | 1. Mee6 Server ID: the number from the Mee6 leaderboard url (see above). REQUIRED FIELD 25 | 2. Number of members (3 - 1000): How many server top members the program should take into account, sample size. Default is `100` 26 | 3. Level cutoffs for pie chart: You decide the level ranges for the pie chart slices, separate with commas. Default is `10,20,30,40,50` 27 | 4. Names for each level range: For the pie chart legend. Useful if your server has roles when members reach certain levels and you want to show them (eg "Bronze, Silver, Gold"). Write `na` to auto generate descriptions for the legend 28 | 5. Dependent var for leaderboard: Plot the leaderboard bar graph with level or xp on the y-axis. `lvl` for levels, `exp` for xp. Default is `lvl` 29 | 6. Specify whether to output a csv file with data for the number of members graphed. `y` for yes. Default is `n` 30 | 31 | Example of a valid config.ini file: 32 | ``` 33 | 123456789123456789 34 | 500 35 | 5,10,20,30,50 36 | na 37 | exp 38 | y 39 | ``` 40 | 41 | ### Running the program 42 | After filling in the config file, save all your changes. Run main.py with Python 3 to use the program 43 | -------------------------------------------------------------------------------- /ServerInfo.py: -------------------------------------------------------------------------------- 1 | # ServerInfo.py 2 | # Attempt 1 using the json link to get the API 3 | # Object contains info of up to top 1000 members in server 4 | # Contains methods for csv and data visualization 5 | 6 | import requests 7 | import csv 8 | import matplotlib.pyplot as plt 9 | import numpy as np 10 | 11 | class ServerInfo: 12 | def __init__(self, URL, top=100): 13 | res = requests.get(URL) 14 | # res.json() is now the entire webpage API as a dictionary 15 | print("Communicating with the Mee6 servers...") 16 | self.servername = res.json()["guild"]["name"] 17 | print("server name: " + str(self.servername)) 18 | # catches errors associated with inputs for top n members 19 | maxtop = False 20 | if top < 3: 21 | top = 3 22 | print("top: cannot be less than 3, changed to 3") 23 | elif top > 1000: 24 | top = 1000 25 | print("top: cannot be greater than 1000, changed to 1000") 26 | # special case to handle if top == 1000 27 | if top == 1000: 28 | maxtop = True 29 | top = 999 30 | # the members information is contained in a list of 100(default) dictionaries, one dictionary per member 31 | username, discordtag, exp, level, messages = [], [], [], [], [] 32 | print("number of members in server:", len(res.json()["players"])) 33 | # makes sure the server has enough members to graph the top 100/1000 of, else, cap it at an amount 34 | if top > len(res.json()["players"]): 35 | top = len(res.json()["players"]) 36 | print("top: cannot be greater than number of members in server, changed to", len(res.json()["players"])) 37 | for user in res.json()['players'][0:top]: 38 | username.append(user['username']) 39 | discordtag.append("#" + str(user['discriminator'])) 40 | exp.append(user['xp']) 41 | level.append(user['level']) 42 | messages.append(user['message_count']) 43 | # Somehow the json link only contains the top 999 entries 44 | # To make graph titles nicer to read, this makes it 1000 by duplicating the last element 45 | if maxtop: 46 | self.username, self.discordtag, self.exp, self.level, self.messages = username + [username[-1]], discordtag + [discordtag[-1]], exp + [exp[-1]], level + [level[-1]], messages + [messages[-1]] 47 | top = 1000 48 | else: 49 | self.username, self.discordtag, self.exp, self.level, self.messages = username, discordtag, exp, level, messages 50 | self.top = top 51 | print("number of members to graph: " + str(top)) 52 | print("----------") 53 | 54 | def csv(self): 55 | ''' 56 | Writes csv of top n members 57 | ''' 58 | with open("mee6-leaderboard.csv", "w", newline="", encoding = "utf-8") as outfile: 59 | fileWriter = csv.writer(outfile) 60 | header = ["username", "discord tag", "exp", "level", "messages sent"] 61 | fileWriter.writerow(header) 62 | for i in range(len(self.username)): 63 | fileWriter.writerow([self.username[i], self.discordtag[i], self.exp[i], self.level[i], self.messages[i]]) 64 | print("mee6-leaderboard.csv created") 65 | 66 | def statuspie(self, statuslevels=[10, 20, 30, 40, 50], statusnames=""): 67 | """ 68 | Pie chart showing the percentage of members in each level range 69 | --- 70 | statuslevels: list of int specifying the cutoffs for each range. eg [10, 20, 30, 40, 50] 71 | statusnames: list of str specifying custom name for each range, leave blank to autogenerate 72 | """ 73 | # checks if user wanted defaults 74 | if statuslevels == "na": 75 | statuslevels = [10, 20, 30, 40, 50] 76 | # checks if input level cutoffs start at 0, if not add 0 to start of list 77 | if not int(statuslevels[0]) == 0: 78 | statuslevels = [0] + statuslevels 79 | if statusnames: 80 | if statuslevels[0] == 0 and (len(statusnames) + 1) == len(statuslevels): 81 | statusnames = ["below " + statusnames[0]] + statusnames 82 | else: 83 | statusnames = "" 84 | # creates array to tally number of members in each rank status 85 | tally = np.zeros(len(statuslevels), dtype=int) 86 | # tallies up number of members in each level category 87 | #print(self.level) 88 | for i in range(len(self.level)): 89 | userlevel = self.level[i] 90 | for n in reversed(range(len(statuslevels))): 91 | if userlevel >= statuslevels[n]: 92 | tally[n] += 1 93 | break 94 | # generates default category names if not specified 95 | if statusnames and not statusnames == "na": 96 | labels = statusnames 97 | else: 98 | labels = [] 99 | for i in range(1, len(statuslevels)): 100 | labels.append("lvl" + str(statuslevels[i - 1]) + " to lvl" + str(statuslevels[i])) 101 | labels.append("above lvl" + str(statuslevels[-1])) 102 | # prints inputs to terminal 103 | print("level cutoffs: " + str(statuslevels)) 104 | print("level range names: " + str(labels)) 105 | # generates pie chart 106 | self.tally = tally 107 | self.statusnames = labels 108 | sizes = self.tally 109 | plt.pie(sizes) 110 | plt.legend(labels, loc=1) 111 | plt.title("Breakdown of Top " + str(self.top) + " Members") 112 | plt.axis("equal") 113 | 114 | def rankbar(self, yvar="lvl"): 115 | """ 116 | yvar: str, determines the dependent variable. 'lvl' for levels, 'exp' for experience points 117 | """ 118 | # leaderboard visualized 119 | if yvar == "exp": 120 | bars = self.exp[0:self.top] 121 | yvarname = "Experience" 122 | print("yvar: exp") 123 | else: 124 | bars = self.level[0:self.top] 125 | yvarname = "Level" 126 | print("yvar: lvl") 127 | labels = list(range(1, self.top + 1)) 128 | plt.bar(labels, bars) 129 | plt.title("Leaderboard") 130 | plt.xlabel('Rank') 131 | plt.ylabel(yvarname) 132 | plt.grid() 133 | 134 | def lvldistribution(self): 135 | """ 136 | Plots frequency of each level in a histogram 137 | """ 138 | levels = self.level[0:self.top] 139 | # determines number of bars needed, one for each level 140 | bins = levels[0] - levels[-1] 141 | plt.hist(levels, bins=bins, align="right") 142 | plt.title("Level Distribution of Top " + str(self.top) + " Members") 143 | plt.ylabel("Frequency") 144 | plt.xlabel("Level") 145 | plt.grid() 146 | 147 | def randomstats(self): 148 | """ 149 | analysis statements about the data collected 150 | """ 151 | textlist = [] 152 | # statements about pie chart data 153 | piestats = [] 154 | for n in range(len(self.tally)): 155 | percent = (self.tally[n] / self.top) * 100 156 | piestats.append(str(round(percent, 2)) + "% are " + str(self.statusnames[n])) 157 | # statement about first place 158 | leader = str(self.username[0]) + " is first place at level " + str(self.level[0]) + " and " + str(self.exp[0]) + " XP" 159 | # central tendencies of data 160 | levels = self.level[0:self.top] 161 | mode = "The modal level is " + str(max(set(levels), key=(levels).count)) 162 | median = "The median level is " + str(self.level[(self.top + 1) // 2]) 163 | sumlevels = sum(levels) 164 | avglevel = sumlevels / self.top 165 | aboveavg = 0 166 | for level in levels: 167 | if level > avglevel: 168 | aboveavg += 1 169 | mean = "The mean level is " + str(avglevel) 170 | percentabove = str(round(aboveavg * 100 / self.top, 2)) + "% of the top " + str(self.top) + " is above the mean level" 171 | # gathers all text into a list 172 | textlist = piestats + [leader, mode, median, mean, percentabove] 173 | # formats the text with line breaks and plots it 174 | textdata = "" 175 | for text in textlist: 176 | textdata += text + "\n" 177 | plt.text(0, 0, textdata) 178 | plt.title("For the Top " + str(self.top) + " Members") 179 | plt.axis("off") 180 | 181 | 182 | 183 | 184 | 185 | 186 | --------------------------------------------------------------------------------