summaryrefslogtreecommitdiff
path: root/src/LineGraph.java
blob: 7c4b8acbf7dc491cb2ee717dd33a41f86670012d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
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<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(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;
			}
		});
	}
}