summaryrefslogtreecommitdiff
path: root/src/LineGraph.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/LineGraph.java')
-rw-r--r--src/LineGraph.java197
1 files changed, 192 insertions, 5 deletions
diff --git a/src/LineGraph.java b/src/LineGraph.java
index 129bb1c..3097be4 100644
--- a/src/LineGraph.java
+++ b/src/LineGraph.java
@@ -1,10 +1,24 @@
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-08
@@ -13,20 +27,193 @@ import javax.swing.*;
* Draw a line graph.
*/
-public class LineGraph {
- private int[][] coordinates; // A line graph has-a set of coordinates
+// A LineGraph is-a JPanel
+public class LineGraph extends JPanel {
+ private ArrayList<FontFamily> 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() {
- coordinates = null;
+ public LineGraph(ArrayList<FontFamily> 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<String,Long> views = font.getViews(); // Keep the views hashmap handy.
+
+ /* Map of coordinates to draw. */
+ HashMap<Integer,Long> coordinates = new HashMap<Integer,Long>();
+
+ /* 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<FontFamily>() {
+ /* 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;
+ }
+ });
+ }
}