commit 692f6a15ed2ac26513906c17135875b742b47735
parent 0636ea27d94a93636d7b824d072ceb9daae44853
Author: ashermorgan <59518073+ashermorgan@users.noreply.github.com>
Date: Tue, 29 Jun 2021 14:49:13 -0700
Implement heatmap command
Diffstat:
4 files changed, 193 insertions(+), 39 deletions(-)
diff --git a/src/analyticsCog.py b/src/analyticsCog.py
@@ -4,12 +4,13 @@ import discord
from discord.ext import commands
from matplotlib import pyplot as plt
from matplotlib.ticker import PercentFormatter
+import numpy as np
import os
import re
import tempfile
# Import modules
-from src.botUtilities import COLORS, getContextCountdown, getNickname, getUsername
+from src.botUtilities import COLORS, getContextCountdown, getUsername, getContributor, ContributorNotFound
from src.models import POINT_RULES
@@ -42,6 +43,7 @@ class Analytics(commands.Cog):
await self.contributors(ctx, "")
await self.contributors(ctx, "history")
if (len(countdown.messages) >= 2): await self.eta(ctx) # Countdown must have 2 messages to run eta command
+ await self.heatmap(ctx)
await self.leaderboard(ctx)
await self.progress(ctx)
await self.speed(ctx)
@@ -238,6 +240,75 @@ class Analytics(commands.Cog):
+ @commands.command()
+ async def heatmap(self, ctx, user=None):
+ """
+ Shows a heatmap of when countdown messages are sent
+ """
+
+ with self.databaseSessionMaker() as session:
+ # Get countdown channel
+ countdown = getContextCountdown(session, ctx)
+
+ # Create temp file
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
+ tmp.close()
+
+ # Create embed
+ embed=discord.Embed(title=":calendar_spiral: Countdown Heatmap", color=COLORS["embed"])
+
+ # Get user
+ try:
+ if (user == None): userID = None
+ else: userID = await getContributor(self.bot, countdown, user)
+ except ContributorNotFound:
+ embed.color = COLORS["error"]
+ embed.description = f"Contributor not found: `{user}`"
+ await ctx.send(embed=embed)
+ return
+
+ # Get heatmap matrix
+ heatmapMatrix = np.ma.masked_equal(np.array(countdown.heatmap(userID)), 0)
+
+ # Create figure
+ fig, ax = plt.subplots()
+ ax.set_xlabel("Hour of Day")
+ ax.set_xticks([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23])
+ ax.set_xticklabels(["12 AM", "1 AM", "2 AM", "3 AM", "4 AM", "5 AM", "6 AM", "7 AM", "8 AM", "9 AM", "10 AM", "11 AM", "12 PM", "1 PM", "2 PM", "3 PM", "4 PM", "5 PM", "6 PM", "7 PM", "8 PM", "9 PM", "10 PM", "11 PM"])
+ ax.set_ylabel("Day of Week")
+ ax.set_yticks([0, 1, 2, 3, 4, 5, 6])
+ ax.set_yticklabels(["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"])
+
+ # Add data to graph
+ cmap = plt.get_cmap("jet").copy()
+ cmap.set_bad("gray")
+ cax = ax.matshow(heatmapMatrix, cmap=cmap, aspect="auto")
+ fig.colorbar(cax)
+
+ # Save graph
+ fig.savefig(tmp.name, bbox_inches="tight", pad_inches=0.2)
+ file = discord.File(tmp.name, filename="image.png")
+
+ # Add content to embed
+ embed.description = f"**Countdown Channel:** <#{countdown.id}>\n"
+ if (userID): embed.description += f"**User:** <@{userID}>\n"
+ embed.description += f"**Total Contributions:** {np.sum(heatmapMatrix):,}\n"
+ embed.set_image(url="attachment://image.png")
+
+ # Send embed
+ try:
+ await ctx.send(file=file, embed=embed)
+ except:
+ await ctx.send(embed=embed)
+
+ # Remove temp file
+ try:
+ os.remove(tmp.name)
+ except:
+ print(f"Unable to delete temp file: {tmp.name}.")
+
+
+
@commands.command(aliases=["l"])
async def leaderboard(self, ctx, user=None):
"""
@@ -284,35 +355,16 @@ class Analytics(commands.Cog):
embed.add_field(name="Numbers", value=rules, inline=True)
embed.add_field(name="Points", value=values, inline=True)
else:
- rank = None
- if (re.match("^\d+$", user) and int(user) > 0 and int(user) <= len(leaderboard)):
- # Get user from rank
- rank = int(user) - 1
- elif (re.match("^<@!\d+>$", user) and int(user[3:-1]) in [x["author"] for x in leaderboard]):
- # Get user from mention
- rank = [x["author"] for x in leaderboard].index(int(user[3:-1]))
- else:
- # Get user from username
- for contributor in leaderboard:
- try: username = await getUsername(self.bot, contributor["author"])
- except: pass
- if (username.lower().startswith(user.lower())):
- rank = leaderboard.index(contributor)
-
- if (rank == None):
- # Get user from nickname
- for contributor in leaderboard:
- try: nickname = await getNickname(self.bot, countdown.server_id, contributor["author"])
- except: pass
- if (nickname.lower().startswith(user.lower())):
- rank = leaderboard.index(contributor)
-
- if (rank == None):
- # User not found
- embed.color = COLORS["error"]
- embed.description = f"User not found: `{user}`"
- await ctx.send(embed=embed)
- return
+ try:
+ if (re.match("^\d+$", user) and int(user) > 0 and int(user) <= len(leaderboard)):
+ rank = int(user) - 1
+ else:
+ rank = [x["author"] for x in leaderboard].index(await getContributor(self.bot, countdown, user))
+ except ContributorNotFound:
+ embed.color = COLORS["error"]
+ embed.description = f"Contributor not found: `{user}`"
+ await ctx.send(embed=embed)
+ return
# Add description
embed.description = f"**Countdown Channel:** <#{countdown.id}>\n\n"
diff --git a/src/botUtilities.py b/src/botUtilities.py
@@ -14,6 +14,13 @@ COLORS = {
+# Error classes
+class ContributorNotFound(Exception):
+ """Raised when a matching countdown contributor cannot be found"""
+ pass
+
+
+
async def getUsername(bot, id):
"""
Get a username from a user ID.
@@ -59,6 +66,53 @@ async def getNickname(bot, server, id):
+async def getContributor(bot, countdown, text):
+ """
+ Get the ID of the countdown contributor refered to by a string
+
+ Parameters
+ ----------
+ bot : commands.Bot
+ The bot
+ countdown : Countdown
+ The countdown
+ text : str
+ The string
+
+ Returns
+ -------
+ int
+ The ID of the contributor
+
+ Raises
+ ------
+ ContributorNotFound
+ If a matching contributor cannot be found
+ """
+
+ # Get countdown contributors
+ contributors = [x["author"] for x in countdown.contributors()]
+
+ # Get user from mention
+ if (re.match("^<@!\d+>$", text) and int(text[3:-1]) in contributors): return int(text[3:-1])
+ elif (re.match("^<@!\d+>$", text)): raise ContributorNotFound(text)
+
+ # Get user from username
+ for contributor in contributors:
+ try: username = await getUsername(bot, contributor)
+ except: continue
+ if (username.lower().startswith(text.lower())): return contributor
+
+ # Get user from nickname
+ for contributor in contributors:
+ try: nickname = await getNickname(bot, countdown.server_id, contributor)
+ except: continue
+ if (nickname.lower().startswith(text.lower())): return contributor
+
+ raise ContributorNotFound(text)
+
+
+
def getCountdown(session, id):
"""
Get a countdown object
diff --git a/src/models.py b/src/models.py
@@ -123,7 +123,7 @@ class Countdown(Base):
# Remove ".0" from the end
if (self.timezone % 1 == 0): result = result[:-2]
-
+
# Return timezone string
return result
@@ -208,6 +208,43 @@ class Countdown(Base):
# Return eta data
return data
+ def heatmap(self, user=None):
+ """
+ Get a heatmap of when countdown messages are sent
+
+ Parameters
+ ----------
+ user : int
+ The ID of the specific user to generate the heatmap for (the default is None)
+
+ Returns
+ -------
+ list
+ A 7x24 2D array containing the heatmap
+ """
+
+ # Initialize result matrix
+ result = [[0 for i in range(24)] for j in range(7)]
+
+ for message in self.messages:
+ if (user != None and message.author_id != user): continue
+
+ # Apply timezone offset
+ timestamp = message.timestamp + timedelta(hours=self.timezone)
+
+ # Get time and weekday
+ dayOfWeek = timestamp.weekday() # 0-6, 0=Monday
+ timeOfDay = timestamp.hour # 0-23
+
+ # Make Sunday the first day of the week
+ dayOfWeek = (dayOfWeek + 1) % 7
+
+ # Add data to result matrix
+ result[dayOfWeek][timeOfDay] += 1
+
+ # Return result matrix
+ return result
+
def leaderboard(self):
"""
Get countdown leaderboard.
@@ -376,7 +413,7 @@ class Prefix(Base):
value : string
The command prefix
"""
-
+
__tablename__ = "prefix"
id = Column(Integer, primary_key=True)
@@ -403,7 +440,7 @@ class Reaction(Base):
value : string
The reaction
"""
-
+
__tablename__ = "reaction"
id = Column(Integer, primary_key=True)
diff --git a/src/utilitiesCog.py b/src/utilitiesCog.py
@@ -50,7 +50,7 @@ class Utilities(commands.Cog):
messages = [],
)
- # Send initial responce
+ # Send initial response
print(f"Activated {self.bot.get_channel(ctx.channel.id)} as a countdown")
embed = discord.Embed(title=":clock3: Loading Countdown", description="@here This channel is now a countdown\nPlease wait to start counting", color=COLORS["embed"])
msg = await ctx.send(embed=embed)
@@ -60,7 +60,7 @@ class Utilities(commands.Cog):
session.add(countdown)
session.commit()
- # Send final responce
+ # Send final response
embed = discord.Embed(title=":white_check_mark: Countdown Activated", description="@here This channel is now a countdown\nYou may start counting!", color=COLORS["embed"])
await msg.edit(embed=embed)
@@ -164,7 +164,7 @@ class Utilities(commands.Cog):
session.delete(countdown)
session.commit()
- # Send responce
+ # Send response
print(f"Deactivated {self.bot.get_channel(ctx.channel.id)} as a countdown")
embed = discord.Embed(title=":octagonal_sign: Countdown Deactivated", description="@here This channel is no longer a countdown", color=COLORS["embed"])
await ctx.send(embed=embed)
@@ -193,6 +193,7 @@ class Utilities(commands.Cog):
"**-** `analytics`, `a`: Shows all countdown analytics\n" \
"**-** `contributors`, `c`: Shows information about countdown contributors\n" \
"**-** `eta`, `e`: Shows information about the estimated completion date\n" \
+ "**-** `heatmap`: Shows a heatmap of when messages are sent\n" \
"**-** `leaderboard`, `l`: Shows the countdown leaderboard\n" \
"**-** `progress`, `p`: Shows information about countdown progress\n" \
"**-** `speed`, `s`: Shows information about countdown speed\n",
@@ -250,6 +251,14 @@ class Utilities(commands.Cog):
"**Arguments:**\n" \
"**-** `<period>`: The size of the period in hours. The default is 24 hours.\n" \
"**Notes:** none\n",
+ "heatmap":
+ "**Name:** heatmap\n" \
+ "**Description:** Shows a heatmap of when countdown messages are sent\n" \
+ f"**Usage:** `{prefixes[0]}heatmap [<user>]`\n" \
+ "**Aliases:** none\n" \
+ "**Arguments:**\n" \
+ "**-** `<user>`: The username or nickname of the user to view heatmap information about. If no value is supplied, the general heatmap will be shown.\n" \
+ "**Notes:** none\n",
"help":
"**Name:** help\n" \
"**Description:** Shows help information\n" \
@@ -264,7 +273,7 @@ class Utilities(commands.Cog):
f"**Usage:** `{prefixes[0]}leaderboard|l [<user>]`\n" \
"**Aliases:** `l`\n" \
"**Arguments:**\n" \
- "**-** `<user>`: The rank, username, or nickname of the user to viewleaderboard information about. If no value is supplied, the whole leaderboard will be shown.\n" \
+ "**-** `<user>`: The rank, username, or nickname of the user to view leaderboard information about. If no value is supplied, the whole leaderboard will be shown.\n" \
"**Notes:** The leaderboard embed will only show the top 20 contributors\n",
"ping":
"**Name:** ping\n" \
@@ -317,6 +326,8 @@ class Utilities(commands.Cog):
embed.description = help_text["deactivate"]
elif (command.lower() in ["e", "eta"]):
embed.description = help_text["eta"]
+ elif (command.lower() in ["heatmap"]):
+ embed.description = help_text["heatmap"]
elif (command.lower() in ["h", "help"]):
embed.description = help_text["help"]
elif (command.lower() in ["l", "leaderboard"]):
@@ -360,7 +371,7 @@ class Utilities(commands.Cog):
with self.databaseSessionMaker() as session:
countdown = getCountdown(session, ctx.channel.id)
if (countdown):
- # Send inital responce
+ # Send initial response
embed = discord.Embed(title=":clock3: Reloading Countdown Cache", description="Please wait to continue counting.", color=COLORS["embed"])
msg = await ctx.channel.send(embed=embed)
@@ -368,7 +379,7 @@ class Utilities(commands.Cog):
await loadCountdown(self.bot, countdown)
session.commit()
- # Send final responce
+ # Send final response
print(f"Reloaded messages from {self.bot.get_channel(ctx.channel.id)}")
embed = discord.Embed(title=":white_check_mark: Countdown Cache Reloaded", description="Done! You may continue counting!", color=COLORS["embed"])
await msg.edit(embed=embed)