import javax.swing.*; import java.awt.*; import java.util.ArrayList; import java.util.HashMap; import java.util.Collections; import java.util.Comparator; import java.util.Calendar; import java.util.Date; /* * @author * Kian Agheli * * References: * https://stackoverflow.com/questions/8693342/drawing-a-simple-line-graph-in-java * https://www.geeksforgeeks.org/sorting-a-hashmap-according-to-values/ * https://stackoverflow.com/questions/16252269/how-to-sort-a-list-arraylist * https://stackoverflow.com/questions/2839508/java2d-increase-the-line-width * https://stackoverflow.com/questions/4285464/java2d-graphics-anti-aliased * https://stackoverflow.com/questions/7182996/java-get-month-integer-from-date * https://stackoverflow.com/questions/5799140/java-get-month-string-from-integer * * Date: * 2024-05-20 * * Purpose of class: * Draw a line graph. */ // A LineGraph is-a JPanel public class LineGraph extends JPanel { private ArrayList fonts; // A line graph has-a set of fonts private final int WIDTH = 640; // A line graph has a preferred width private final int HEIGHT = 480; // A line graph has a preferred height private final int LINES = 0x10; // A line graph has a maximum lines graphed at a time private final int LINE_WIDTH = 3; // A line graph has lines with a given pixel width private final int TICK_HEIGHT = 10; // A line graph has ticks with a given pixel height /* A line graph has an array of abbreviated month names. */ private final String[] ABBREVIATED_MONTH = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; /* A line graph has an array of unique color values to use for each of the lines. */ private final int[] COLORS = {0xff0000, 0x00ff00, 0x0000ff, 0xffff00, 0xff00ff, 0x00ffff, 0xff8040, 0xff4080, 0x80ff40, 0x40ff80, 0x4080ff, 0x8040ff, 0xff8080, 0x80ff80, 0x8080ff, 0xff4040, 0x40ff40, 0x4040ff}; /** * Constructor. */ public LineGraph(ArrayList fonts) { this.fonts = fonts; // Set instance's list of fonts to the list passed. this.setPreferredSize(new Dimension(WIDTH, HEIGHT)); // Set the preferred size of the panel. } /** * Set a point at a pair of coordinates. */ void setCoordinates(int y, int x) { } /* Automatically called at construction time. */ @Override public void paintComponent(Graphics g) { /* Cast to Graphics2D. Permits use of Graphics2D methods. */ Graphics2D graphics = (Graphics2D) g; /* White background. */ graphics.setBackground(Color.WHITE); /* Fill entire space allocated. No margins or padding. */ graphics.clearRect(0, 0, getWidth(), getHeight()); /* Enable anti-aliasing. */ graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); /* Set line width. */ graphics.setStroke(new BasicStroke(LINE_WIDTH)); /* Draw in black. */ graphics.setColor(Color.BLACK); /* Store font metrics so as to calculate width and height of each tick's text. */ FontMetrics metrics = graphics.getFontMetrics(); /* Store enough space on the left for 12 digits. Assuming tabular figures. */ int xOffset = metrics.stringWidth("012345678901"); /* Keep enough space at bottom for text with descenders and ticks. */ int yMax = getHeight() - (metrics.getHeight() << 1) - TICK_HEIGHT; /* Keep enough space at right for the right half of the text "MMM". */ int xMax = getWidth() - (metrics.stringWidth("MMM") >> 1) - TICK_HEIGHT - xOffset; /* Draw x axis. */ graphics.drawLine(TICK_HEIGHT + xOffset, yMax, getWidth(), yMax); /* Store calendar, so as to get month. */ Date date = new Date(); Calendar calendar = Calendar.getInstance(); calendar.setTime(date); // Set calendar to current date /* For each month in the year, draw a tick on the x axis. */ int x = xMax; int xTicks = 12; double xSpace = (float) x / xTicks; for (int i = 0; i <= xTicks; i++) { graphics.drawLine(x + TICK_HEIGHT + xOffset, yMax, x + TICK_HEIGHT + xOffset, getHeight() - metrics.getHeight()); String month = ABBREVIATED_MONTH[calendar.get(Calendar.MONTH)]; /* Half the width for centering offset. */ int textOffset = metrics.stringWidth(month) >> 1; graphics.drawString(month, x + TICK_HEIGHT + xOffset - textOffset, getHeight() - metrics.getHeight() / 5 * 2); x = (int) (x - xSpace); // Decrement x offset. calendar.roll(Calendar.MONTH, false); // Roll back by one month } /* Draw y axis. Subtract one third of stroke width to account for width of first tick. */ graphics.drawLine(TICK_HEIGHT + xOffset - LINE_WIDTH / 3, 0, TICK_HEIGHT + xOffset - LINE_WIDTH / 3, yMax); sortFonts("30day"); // Sort the list of fonts by their 30day metrics. int y = 0 + metrics.getHeight(); /* Cast to double within parentheses for floating-point division, cast back to int to remove fractional part. 5/4 spacing for legibility. */ int yTicks = (int) (yMax / ((double) metrics.getHeight() * 5 / 4)); /* Sorted values by monthly views in descending order. The greatest value is at the top. */ long maxViews = fonts.get(0).getViews().get("30day"); float ySpace = (float) yMax / yTicks; for (int i = yTicks; i >= 0; i--) { String text = String.format("%d", maxViews * i / yTicks); /* Difference between maximum string width and actual string width is offset. */ int textOff = xOffset - metrics.stringWidth(text); graphics.drawString(text, textOff, y); y += ySpace; } /* Draw lines. */ for (int i = 0; i < LINES; i++) { FontFamily font = fonts.get(i); // Keep the current font handy. HashMap views = font.getViews(); // Keep the views hashmap handy. /* Map of coordinates to draw. */ HashMap coordinates = new HashMap(); /* Use the given metrics to guesstimate the values of each month. The last month's views are known. */ coordinates.put(12, views.get("30day")); /* Using 90day views, we may subtract 30day to get a closer approximation of 60day views. */ coordinates.put(11, (views.get("90day") - views.get("30day")) / 2); /* Account for changes in views by deriving views 3 months ago from 90day views / 3. */ coordinates.put(10, (views.get("90day") / 3)); /* Guesstimate the rest of the months by yearly views / 12. */ for (int j = 1; j <= 9; j++) { coordinates.put(j, (views.get("year") / 12)); } graphics.setColor(new Color(COLORS[i])); // Set the drawing color. /* Previous x and y values. Necessary for drawing lines between points. */ int xPrevious = 0; int yPrevious = 0; for (int xCoordinate = 1; xCoordinate <= 12; xCoordinate++) { /* x is ratio of month value / 12. Because y starts at the top, invert with subtraction from maximum pixel value. Ratio of given views over maximum views, multiplied by maximum pixel value. Circle with radius of LINE_WIDTH * 2. */ int xCurrent = xOffset + TICK_HEIGHT + xMax * xCoordinate / 12; int yCurrent = yMax - (int) ((float) coordinates.get(xCoordinate) / maxViews * yMax); graphics.fillOval(xCurrent, yCurrent, LINE_WIDTH << 1, LINE_WIDTH << 1); /* Line to previous point, if set. */ if (xPrevious == 0) { /* If no previous point, use current y value. */ graphics.drawLine(xOffset + TICK_HEIGHT + xMax * (xCoordinate - 1) / 12, yCurrent, xCurrent, yCurrent); } else { /* Line from previous x and y to current x and y. */ graphics.drawLine(xPrevious, yPrevious, xCurrent, yCurrent); } /* Set previous values for next iteration. */ yPrevious = yCurrent; xPrevious = xCurrent; } } } /* Sort by views, in descending order. */ private void sortFonts(String metric) { Collections.sort(fonts, new Comparator() { /* If a is lesser, return 1. If b is lesser, return -1. Otherwise, they are equal, return 0. */ public int compare (FontFamily a, FontFamily b) { /* If equal, return 0. */ if (a.getViews().get(metric) == b.getViews().get(metric)) { return 0; } /* If either is null, it is lesser. */ if (a.getViews().get(metric) == null) { return 1; } if (b.getViews().get(metric) == null) { return -1; } /* Neither are null, compare. */ if (a.getViews().get(metric) > b.getViews().get(metric)) { return -1; } /* aViews must be lesser. */ return 1; } }); } }