Visitar URL original
redesign the help statistics plot by Neil-Tomar · Pull Request #560 · Java-Discord/JavaBot · GitHub
Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
fixed requested changes
  • Loading branch information
Neil-Tomar committed Sep 26, 2026
commit f5a6ff888df1eb9ebe5f738f4bdedd425c8281f3
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.time.LocalDate;
import java.time.format.TextStyle;
import java.util.*;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please avoid the wildcard import.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: The wildcard import is still present.

import java.util.Map.Entry;

Expand Down Expand Up @@ -45,7 +46,7 @@ public class HelpStatisticsSubcommand extends SlashCommand.Subcommand {
public HelpStatisticsSubcommand(HelpTransactionRepository transactionRepository) {
this.transactionRepository = transactionRepository;
setCommandData(new SubcommandData("stats", "Shows an general plot about help activity in this server")
.addOption(OptionType.BOOLEAN, "darkmode", "generate a plot in dark mode.", false)
.addOption(OptionType.BOOLEAN, "darkmode", "Generates the plot with a dark background.", false)
);

}
Expand Down Expand Up @@ -78,7 +79,7 @@ public void execute(SlashCommandInteractionEvent event) {
correctMonth = false;
}
}
plotData.add(new Pair<>(position.getMonth() + " " + position.getYear(), new Plotter.Bar(entriesForThisMonth)));
plotData.add(new Pair<>(position.getMonth().getDisplayName(TextStyle.SHORT_STANDALONE, Locale.ROOT) + " '" + String.valueOf(position.getYear()).substring(2), new Plotter.Bar(entriesForThisMonth)));
}

BufferedImage plot = new Plotter(plotData, "Help Statistics","Monthly assistance provided to community members",darkMode).plot();
Expand Down
298 changes: 158 additions & 140 deletions src/main/java/net/discordjug/javabot/util/Plotter.java

Large diffs are not rendered by default.

227 changes: 171 additions & 56 deletions src/test/java/net/discordjug/javabot/util/PlotterTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,86 +5,201 @@
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.time.YearMonth;
import java.time.format.TextStyle;
import java.util.ArrayList;
import java.net.URL;
import java.util.*;
import java.util.List;
import java.util.Locale;
import java.util.Random;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;


public class PlotterTest {
private static final Color[] PALETTE = {
new Color(0xF9C74F), new Color(0xF9506E), new Color(0x6C7280),
new Color(0x5B8CFF), new Color(0x43D9AD), new Color(0xC084FC),
new Color(0x22D3EE), new Color(0xFB923C)
};
@Test
public void ImageDifferenceTest() throws IOException{
List<Pair<String, Plotter.Bar>> testData = testData0();
BufferedImage generatedImage = new Plotter(testData, "General helper statistics","subtitle").plot();
BufferedImage expectedImage = readImage("PlotterTestImages/PlotterTest-Light.png");

assertTrue(assertImagesAreEqual(generatedImage,expectedImage));
}

@Test
public void ImageDifferenceTest(){
List<Pair<String, Plotter.Bar>> testData = testData();
BufferedImage img1 = new Plotter(testData, "General helper statistics","subtitle").plot();
BufferedImage img2 = readImage("src/test/resources/PlotterTest.png");
public void ImageDifferenceTestDarkMode() throws IOException{
List<Pair<String, Plotter.Bar>> testData = testData0();
BufferedImage generatedImage = new Plotter(testData, "General helper statistics","subtitle",true).plot();
BufferedImage expectedImage = readImage("PlotterTestImages/PlotterTest-Dark.png");

assertTrue(compareImage(img1,img2));
assertTrue(assertImagesAreEqual(generatedImage,expectedImage));
}

public static BufferedImage readImage(String path){
try {
File inputFile = new File(path);
BufferedImage image = ImageIO.read(inputFile);
if (image == null) {
throw new IOException("Image is null.");
}
return image;
} catch (IOException e) {
System.err.println("Error reading the image file: " + e.getMessage());
return null;
}
@Test
public void ImageDifferenceTestLessData() throws IOException{
List<Pair<String, Plotter.Bar>> testData = testData1();
BufferedImage generatedImage = new Plotter(testData, "General helper statistics","subtitle").plot();
BufferedImage expectedImage = readImage("PlotterTestImages/PlotterTest-LessData.png");

assertTrue(assertImagesAreEqual(generatedImage,expectedImage));
}

public static boolean compareImage(BufferedImage img1, BufferedImage img2) {
if (img1 == null || img2 == null) {
return false;
}
@Test
public void ImageDifferenceTestNoData() throws IOException{
List<Pair<String, Plotter.Bar>> testData = testData2();
BufferedImage generatedImage = new Plotter(testData, "General helper statistics","subtitle").plot();
BufferedImage expectedImage = readImage("PlotterTestImages/PlotterTest-NoData.png");

if (img1.getWidth() != img2.getWidth() ||
img1.getHeight() != img2.getHeight()) {
return false;
}
assertTrue(assertImagesAreEqual(generatedImage,expectedImage));
}

private BufferedImage readImage(String fileName) throws IOException{
URL resource = getClass().getClassLoader().getResource(fileName);
BufferedImage image = ImageIO.read(resource);
return image;
}

public static boolean assertImagesAreEqual(BufferedImage actualImage, BufferedImage expectedImage) throws IOException {
assertNotNull(actualImage, "Generated Image is Null.");
assertNotNull(expectedImage, "Expected Image is Null.");

String actualImageBase64 = convertToBase64(actualImage);

for (int y = 0; y < img1.getHeight(); y++) {
for (int x = 0; x < img1.getWidth(); x++) {
if (img1.getRGB(x, y) != img2.getRGB(x, y)) {
return false;
}
assertEquals(expectedImage.getWidth(), actualImage.getWidth(), "Image width does not match.\nActual image:"+actualImageBase64);
assertEquals(expectedImage.getHeight(), actualImage.getHeight(), "Image height does not match.\nActual image:"+actualImageBase64);

for (int y = 0; y < actualImage.getHeight(); y++) {
for (int x = 0; x < actualImage.getWidth(); x++) {
assertEquals(actualImage.getRGB(x, y),expectedImage.getRGB(x, y),() -> "Image does not match.\nActual image:"+actualImageBase64);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be useful to include the coordinates in the message here.

}
}
return true;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there's a reason to return anything here. Just make it void.

}

private static List<Pair<String, Plotter.Bar>> testData() {
Random random = new Random(42);
List<Pair<String, Plotter.Bar>> entries = new ArrayList<>();
YearMonth month = YearMonth.of(2026,9);
private static String convertToBase64(BufferedImage image) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ImageIO.write(image, "png", out);
return Base64.getEncoder().encodeToString(out.toByteArray());
}

for (int i = 0; i < 13; i++) {
int segments = random.nextInt(3,5);
List<Pair<Color, Double>> parts = new ArrayList<>();
private static List<Pair<String, Plotter.Bar>> testData0() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think having descriptive names for the test data methods would be useful. I guess you can just call this testData (or createTestData) and then use names like testDataSmall and createEmptyTestData or similar.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think it would be good to have a bar with no segments in one of the test data methods.

Color yellow = new Color(249,199,79);
Color red = new Color(249,80,110);
Color grey = new Color(108,114,128);
Color blue = new Color(91,140,255);

for (int s = 0; s < segments; s++) {
double value = random.nextDouble(50,1800);
parts.add(new Pair<>(PALETTE[s % PALETTE.length], value));
}
List<Pair<String, Plotter.Bar>> entries = Arrays.asList(
new Pair<>("SEP '26", new Plotter.Bar(Arrays.asList(
new Pair<>(yellow, 145.6),
new Pair<>(red, 133.8),
new Pair<>(grey, 1698.6),
new Pair<>(blue, 1288.5)
))),
new Pair<>("OCT '26", new Plotter.Bar(Arrays.asList(
new Pair<>(yellow, 1630.9),
new Pair<>(red, 695.3),
new Pair<>(grey, 532.5)
))),
new Pair<>("NOV '26", new Plotter.Bar(Arrays.asList(
new Pair<>(yellow, 1383.6),
new Pair<>(red, 1796.8),
new Pair<>(grey, 315.9),
new Pair<>(blue, 819.6)
))),
new Pair<>("DEC '2026", new Plotter.Bar(Arrays.asList(
new Pair<>(yellow, 726.5),
new Pair<>(red, 360.4),
new Pair<>(grey, 1090.1)
))),
new Pair<>("JAN '27", new Plotter.Bar(Arrays.asList(
new Pair<>(yellow, 488.6),
new Pair<>(red, 689.0),
new Pair<>(grey, 327.6),
new Pair<>(blue, 529.8)
))),
new Pair<>("FEB '27", new Plotter.Bar(Arrays.asList(
new Pair<>(yellow, 1049.3),
new Pair<>(red, 1065.0),
new Pair<>(grey, 1366.8)
))),
new Pair<>("MAR '27", new Plotter.Bar(Arrays.asList(
new Pair<>(yellow, 603.9),
new Pair<>(red, 398.4),
new Pair<>(grey, 69.3),
new Pair<>(blue, 1396.6)
))),
new Pair<>("APR '27", new Plotter.Bar(Arrays.asList(
new Pair<>(yellow, 1298.4),
new Pair<>(red, 891.0),
new Pair<>(grey, 560.4)
))),
new Pair<>("MAY '27", new Plotter.Bar(Arrays.asList(
new Pair<>(yellow, 1092.8),
new Pair<>(red, 380.3),
new Pair<>(grey, 712.2)
))),
new Pair<>("JUN '27", new Plotter.Bar(Arrays.asList(
new Pair<>(yellow, 680.4),
new Pair<>(red, 810.6),
new Pair<>(grey, 850.3),
new Pair<>(blue, 877.2)
))),
new Pair<>("JUL '27", new Plotter.Bar(Arrays.asList(
new Pair<>(yellow, 1010.6),
new Pair<>(red, 115.4),
new Pair<>(grey, 142.0),
new Pair<>(blue, 1430.0)
))),
new Pair<>("AUG '27", new Plotter.Bar(Arrays.asList(
new Pair<>(yellow, 541.0),
new Pair<>(red, 392.9),
new Pair<>(grey, 363.7),
new Pair<>(blue, 1564.9)
))),
new Pair<>("SEP '27", new Plotter.Bar(Arrays.asList(
new Pair<>(yellow, 1009.2),
new Pair<>(red, 64.2),
new Pair<>(grey, 1375.8),
new Pair<>(blue, 1154.3)
)))
);
return entries;
}

String monthLabel = month.getMonth().getDisplayName(TextStyle.FULL, Locale.ENGLISH).toUpperCase(Locale.ROOT) + " " + month.getYear();
entries.add(new Pair<>(monthLabel, new Plotter.Bar(parts)));
month = month.plusMonths(1);
}
private static List<Pair<String, Plotter.Bar>> testData1() {
Color yellow = new Color(249,199,79);
Color red = new Color(249,80,110);
Color grey = new Color(108,114,128);
Color blue = new Color(91,140,255);

List<Pair<String, Plotter.Bar>> entries = Arrays.asList(
new Pair<>("SEP '26", new Plotter.Bar(Arrays.asList(
new Pair<>(yellow, 145.6),
new Pair<>(red, 133.8),
new Pair<>(grey, 1698.6),
new Pair<>(blue, 1288.5)
))),
new Pair<>("OCT '26", new Plotter.Bar(Arrays.asList(
new Pair<>(yellow, 1630.9),
new Pair<>(red, 695.3),
new Pair<>(grey, 532.5)
))),
new Pair<>("NOV '26", new Plotter.Bar(Arrays.asList(
new Pair<>(yellow, 1383.6),
new Pair<>(red, 1796.8),
new Pair<>(grey, 315.9),
new Pair<>(blue, 819.6)
))),
new Pair<>("DEC '2026", new Plotter.Bar(Arrays.asList(
new Pair<>(yellow, 726.5),
new Pair<>(red, 360.4),
new Pair<>(grey, 1090.1)
)))
);
return entries;
}

private static List<Pair<String, Plotter.Bar>> testData2() {
return new ArrayList<Pair<String, Plotter.Bar>>();
}
}
Binary file removed src/test/resources/PlotterTest.png
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading