summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorArghKevin <kagheli@student.sdccd.edu>2024-05-20 05:42:37 -0700
committerArghKevin <kagheli@student.sdccd.edu>2024-05-20 05:42:37 -0700
commit583ecf2df556c0bb3ae5468a63d9231b3ef56f58 (patch)
tree32aadcfbd21d79e34eb7d228f8b6e76dce9c0158 /src
parentae99f9afc615bfd6f4dd0b6d6f7847928956a318 (diff)
Weeks 5 and 6.
Diffstat (limited to 'src')
-rw-r--r--src/CSVReader.java35
-rw-r--r--src/ComparisonView.java107
-rw-r--r--src/FontFamily.java93
-rw-r--r--src/JSONReader.java96
-rw-r--r--src/LineGraph.java197
-rw-r--r--src/Reader.java2
6 files changed, 452 insertions, 78 deletions
diff --git a/src/CSVReader.java b/src/CSVReader.java
index fa588ef..3666531 100644
--- a/src/CSVReader.java
+++ b/src/CSVReader.java
@@ -17,14 +17,15 @@ import java.util.Arrays;
*/
public class CSVReader extends Reader {
- String[] header; // A CSV file has-a header. One line, multiple fields.
- /* Array of Hash map from header to value for each
- line past the header. */
- ArrayList<HashMap<String,String>> body; // A CSV file has-a body. Multiple lines, multiple fields.
+ private String[] header; // A CSV file has-a header. One line, multiple fields.
+ // Array of HashMaps. Headers are keys, values are on each line of the CSV following the header.
+ private ArrayList<HashMap<String,String>> body; // A CSV file has-a body. Multiple lines, multiple fields.
public CSVReader(File file) {
super(file); // Call parent constructor
+ /* Construct the body to be filled. */
body = new ArrayList<HashMap<String,String>>();
+ /* Parse the contents of the file, store in body. */
parse();
}
@@ -32,26 +33,46 @@ public class CSVReader extends Reader {
* Parse CSV file.
*/
public void parse() {
+ /* Split the input on newlines. */
String[] lines = this.getContents().split("\n");
+ /* Split the first line by commas, assign to header array. */
this.header = lines[0].split(",");
- /* Iterate over all lines of the body. */
+ /* Iterate over all lines of the body.
+ First line is header, count from 1 instead of 0. */
for (int i = 1; i < lines.length; i++) {
+ /* Some lines have commas within parentheses.
+ Angkor,"/South East Asian (Thai, Khmer, Lao)/Looped",100
+ Remove everything within parentheses. We need to escape
+ not on a language level, but on a function call level.
+ Double-backslash for a literal backslash, which is then
+ used to escape the parentheses in the regex library. */
+ lines[i] = lines[i].replaceAll("\\(.*\\)", "");
+
+ /* CSV, Comma-Separated Values. */
String[] fields = lines[i].split(",");
+ /* Allocate a new HashMap for the given line of the CSV. */
HashMap<String,String> map = new HashMap<String,String>();
- /* For each of the columns, add the associated
- value in this row. */
+
+ /* Associate each of the line's fields with its header. */
for (int j = 0; j < header.length; j++) {
map.put(header[j], fields[j]);
}
+ /* Add the HashMap to the list. */
body.add(map);
}
}
+ /**
+ * @return the length of the body.
+ */
public int getLength() {
return body.size();
}
+ /**
+ * @return the value associated with the given key on the given line.
+ */
public String get(int i, String key) {
return body.get(i).get(key);
}
diff --git a/src/ComparisonView.java b/src/ComparisonView.java
index 8e20cf3..78897e9 100644
--- a/src/ComparisonView.java
+++ b/src/ComparisonView.java
@@ -10,6 +10,10 @@ import java.awt.*;
* References:
* https://zetcode.com/java/listdirectory/
* https://stackoverflow.com/questions/4871051/how-to-get-the-current-working-directory-in-java
+ * https://stackoverflow.com/questions/2501861/how-can-i-remove-a-jpanel-from-a-jframe
+ * https://stackoverflow.com/questions/5652344/how-can-i-use-a-custom-font-in-java
+ * https://www.javaprogramto.com/2019/03/java-uimanager.html
+ *
*
* Date:
* 2024-05-08
@@ -18,25 +22,29 @@ import java.awt.*;
* Provide a view for comparing font families.
*/
+// ComparisonView is a JFrame
public class ComparisonView extends JFrame {
- private final int WINDOW_MIN_WIDTH = 960;
- private final int WINDOW_MIN_HEIGHT = 540;
- private ArrayList<FontFamily> fonts;
+ private final int WINDOW_MIN_WIDTH = 960; // A ComparisonView has a minimum height
+ private final int WINDOW_MIN_HEIGHT = 540; // A ComparisonView has a minimum width
+ private ArrayList<FontFamily> fonts; // A ComparisonView has a list of fonts
+ private Font textFont; // A ComparisonView has a preferred text font
+ private final int TEXT_SIZE = 20; // A ComparisonView has a constant text size.
/**
* Walk the file tree.
*/
static void walk(File dir, ArrayList<File> list) {
- File[] dirContents = dir.listFiles();
+ File[] dirContents = dir.listFiles(); // List the files in the directory.
+ // For each of the items in the directory:
for (int i = 0; i < dirContents.length; i++) {
- if (dirContents[i].isFile()) {
- if (dirContents[i] != null) {
- list.add(dirContents[i]);
- }
+ // If it's a file, and its name ends with '.pb', add it to the list.
+ if (dirContents[i].isFile() && dirContents[i].getName().endsWith(".pb")) {
+ list.add(dirContents[i]);
+ // If it's a directory, recurse through it.
} else if (dirContents[i].isDirectory()) {
walk(dirContents[i], list);
}
- /* Otherwise, an irregular file. Ignore it. */
+ // Otherwise, it's an irregular file. Ignore it.
}
}
@@ -44,66 +52,89 @@ public class ComparisonView extends JFrame {
* Initialize GUI and comparison.
*/
public ComparisonView() {
+ /* Set text font. Register the included ttf file as a font
+ in the current environment. This is akin to adding it to the map
+ of font family names and file names. */
+ try {
+ GraphicsEnvironment environment = GraphicsEnvironment.getLocalGraphicsEnvironment();
+ environment.registerFont(Font.createFont(Font.TRUETYPE_FONT,
+ new File("./DejaVuSansCondensed.ttf")));
+ /* If either IOException or FontFormatException are thrown, error out. */
+ } catch (IOException|FontFormatException e) {
+ System.out.println(e.getMessage());
+ System.exit(-1);
+ }
+
+ /* Set default text font to included DejaVu Sans Condensed. */
+ textFont = new Font("DejaVu Sans Condensed", Font.PLAIN, TEXT_SIZE);
+ getContentPane().setFont(textFont);
+ UIManager.put("Label.font", textFont);
+ UIManager.put("Panel.font", textFont);
+
+
setTitle("Google Fonts Style vs. Popularity"); // Window title
- setMinimumSize(new Dimension(WINDOW_MIN_WIDTH,
- WINDOW_MIN_HEIGHT)); // Minimum window size
+ setMinimumSize(new Dimension(WINDOW_MIN_WIDTH, WINDOW_MIN_HEIGHT)); // Minimum window size
/* On window close, kill the program. */
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
pack(); // Pack the GUI
setVisible(true); // Make the window visible
- /* In $(find . | grep METADATA.pb), each
- family is described in JSON. Human name is provided on first line.
- For each METADATA.pb, instantiate a FontFamily object, keep
- in an ArrayList. */
- //System.out.println(System.getProperty("user.dir"));
- ArrayList<File> fileList = new ArrayList<File>();
+ /* In metadata/*.pb, each font is described.
+ Add each of the files in that directory to a list. */
+ ArrayList<File> metadataList = new ArrayList<File>();
try {
- walk(new File("."), fileList);
+ walk(new File("."), metadataList);
} catch (Exception e) {
System.out.println(e.getMessage()); // Print error message
- System.exit(1); // Exit with error
- }
-
- ArrayList<File> metadataList = new ArrayList<File>();
- /* Enhanced for loop. */
- for (File file : fileList) {
- if (file.getPath().endsWith(".pb")) {
- metadataList.add(file);
- }
+ System.exit(-1); // Exit with error
}
+ /* Open families.csv and popularity.json. If either are missing,
+ the program cannot run. */
File styleFile = new File("families.csv");
if (!styleFile.exists()) {
+ System.out.println("families.csv doesn't exist.");
System.exit(-1);
}
-
File popularityFile = new File("popularity.json");
if (!popularityFile.exists()) {
System.exit(-1);
}
+ /* Panel to show while waiting for FontFamily data structures to populate. */
+ JPanel waiting = new JPanel();
+ JLabel waitingLabel = new JLabel("Memory structures take 10-15 seconds to set up. My apologies.");
+ waiting.add(waitingLabel);
+ this.add(waiting, BorderLayout.CENTER);
+ setVisible(true);
+ /* Construct Readers from each of the metadata files. */
CSVReader style = new CSVReader(styleFile);
JSONReader popularity = new JSONReader(popularityFile);
- /* Create FontFamily objects from each METADATA.pb file. */
+ // Create FontFamily objects from each METADATA.pb file.
fonts = new ArrayList<FontFamily>();
+ /* For each metadata file, parse a FontFamily. */
for (File file : metadataList) {
JSONReader metadata = new JSONReader(file);
- metadata.setPb();
+ metadata.setMetadata();
fonts.add(new FontFamily(metadata, popularity, style));
}
-
- for (FontFamily font : fonts) {
- System.out.println(font.getFamilyName());
- for (String subset : font.getSubsets()) {
- System.out.println(subset);
- }
- }
+
+ waiting.remove(waitingLabel); // Remove waiting message.
+ JPanel author = new JPanel();
+ JLabel authorLabel = new JLabel("Written by Kian Agheli");
+ author.add(authorLabel);
+ add(author, BorderLayout.SOUTH); // Add author panel.
+
+ LineGraph graph = new LineGraph(fonts); // Draw a line graph of the font views metadata.
+ add(graph, BorderLayout.NORTH); // Add the line graph.
+
+ pack(); // Pack the window.
+ setVisible(true); // Re-draw the window.
}
public static void main(String[] argv) {
- new ComparisonView();
+ new ComparisonView(); // Call the constructor.
}
}
diff --git a/src/FontFamily.java b/src/FontFamily.java
index cff4f87..59a458d 100644
--- a/src/FontFamily.java
+++ b/src/FontFamily.java
@@ -28,9 +28,10 @@ public class FontFamily {
private String designer;
/* total views, 7day views, 30day views, 90day views, year views.
Taken from popularity.json. */
+ final String[] popularityMetrics = {"7day", "30day", "90day", "year"};
private HashMap<String,Long> views;
- private String license;
- private String category;
+ private String license; // Distribution license.
+ private String category; // Vague category.
/* popularity.json and families.csv are shared among all families.
Have their associated objects passed from outside. */
@@ -38,16 +39,21 @@ public class FontFamily {
* Constructor.
*/
public FontFamily(JSONReader metadata, JSONReader popularity, CSVReader styles) {
- this.metadata = metadata;
- parseMetadata(metadata);
- parsePopularity(popularity);
- parseStyles(styles);
+ this.metadata = metadata; // Set the instance's metadata to passed value. */
+ parseMetadata(metadata); // Parse the metadata.
+
+ views = new HashMap<String,Long>(); // Instantiate views hash map.
+ parsePopularity(popularity); // Parse the popularity data
+
+ this.styles = new HashMap<String,Integer>(); // Instantiate styles hash map
+ parseStyles(styles); // Parse the styles data
}
/**
* Parse metadata.
*/
private void parseMetadata(JSONReader metadata) {
+ /* Parse the given fields. See their values described near the top of the class definition. */
familyName = metadata.get("name");
dateAdded = metadata.get("date_added");
subsets = metadata.getAll("subsets");
@@ -60,18 +66,91 @@ public class FontFamily {
* Parse popularity.
*/
private void parsePopularity(JSONReader popularity) {
+ String popularityJson = popularity.getFamily(familyName); // Block of JSON for this family
+ if (popularityJson == null) {
+ /* Not all families have popularity metadata, just most. */
+ return;
+ }
+
+ /* For each timeframe provided in popularity.json, parse as long and add to hash map. */
+ for (String timeframe : popularityMetrics) {
+ String timeframeKey = "\"" + timeframe + "\":"; // JSON key
+ int timeframeIndex = popularityJson.indexOf(timeframeKey); // Index of JSON key
+
+ String viewsKey = "\"views\": "; // JSON key for views following timeframe key
+ int viewsIndex = popularityJson.indexOf(viewsKey, timeframeIndex);
+
+ int start = viewsIndex + viewsKey.length(); // Start of data after key
+ int end = popularityJson.indexOf(",", start); // End of data before next comma
+
+ long value = 0;
+ /* Parse views as long. */
+ try {
+ value = Long.parseLong(popularityJson.substring(start, end));
+ /* If fail to parse as long, something is wrong with popularity.json.
+ 2^63 is a big number to overflow. */
+ } catch (NumberFormatException e) {
+ System.out.println("Failed to parse " + e.getMessage());
+ System.exit(-1);
+ }
+
+ views.put(timeframe, value); // Add the parsed long to the hash map
+ }
+
+ /* Parse total views for given family. */
+ try {
+ views.put("total", Long.parseLong(JSONReader.get(popularityJson, "totalViews")));
+ /* If fail to parse as long, something is wrong with popularity.json.
+ 2^63 is a big number to overflow. */
+ } catch (NumberFormatException e) {
+ System.out.println("Failed to parse " + e.getMessage());
+ System.exit(-1);
+ }
+
}
/**
* Parse styles.
*/
- private void parseStyles(CSVReader styles) {
+ private void parseStyles(CSVReader styleCsv) {
+ int len = styleCsv.getLength(); // Lines of body in CSV
+
+ /* Styles CSV has three header fields: Family, Group/Tag, and Weight. */
+ for (int i = 0; i < len; i++) {
+ /* If the current row contains data for the given family, add that data. */
+ if (familyName.equals(styleCsv.get(i, "Family"))) {
+ try {
+ /* Add the Group/Tag and the associated weight. */
+ styles.put(styleCsv.get(i, "Group/Tag"),
+ Integer.parseInt(styleCsv.get(i, "Weight")));
+ /* If unable to parse as integer, something is wrong with families.csv. */
+ } catch (NumberFormatException e) {
+ System.out.println("Unable to parse " + e.getMessage());
+ System.exit(-1);
+ }
+ }
+
+ }
}
+ /**
+ * @return family name
+ */
public String getFamilyName() {
return familyName;
}
+
+ /**
+ * @return unicode subsets.
+ */
public String[] getSubsets() {
return subsets;
}
+
+ /**
+ * @return views hash map
+ */
+ public HashMap<String,Long> getViews() {
+ return views;
+ }
}
diff --git a/src/JSONReader.java b/src/JSONReader.java
index 45f6bd5..44831c9 100644
--- a/src/JSONReader.java
+++ b/src/JSONReader.java
@@ -1,5 +1,6 @@
import java.util.HashMap;
import java.util.ArrayList;
+
import java.io.*;
/*
@@ -9,6 +10,8 @@ import java.io.*;
* References:
* https://googlefonts.github.io/gf-guide/metadata.html
* https://stackoverflow.com/questions/3880274/how-to-convert-the-object-to-string-in-java
+ * https://stackoverflow.com/questions/8938498/get-the-index-of-a-pattern-in-a-string-using-regex
+ * https://howtodoinjava.com/java/regex/start-end-of-string/
*
* Date:
* 2024-05-08
@@ -21,15 +24,23 @@ public class JSONReader extends Reader {
/* metadata.pb is not exactly JSON, but so close to it
that it's practically a subset of JSON with fewer quotation
marks. If set, this is a pb file. */
- private boolean pb;
+ private boolean isMetadata;
public JSONReader(File file) {
- super(file);
- pb = false;
+ super(file); // Call constructor.
+ isMetadata = false; // By default, not metadata.
}
private String composeKey(String key) {
- if (!pb) {
+ /* Call static version. This has the benefit of reducing code size.
+ Rather than write two separate static and instantiated versions, the
+ instantiated version may call the static version. */
+ return composeKey(key, this.isMetadata);
+ }
+
+ private static String composeKey(String key, boolean isMetadata) {
+ /* JSON usually has quotes surrounding keys. Google's metadata doesn't. */
+ if (!isMetadata) {
key = "\"" + key + "\"";
}
key += ": ";
@@ -42,20 +53,32 @@ public class JSONReader extends Reader {
* match.
*/
public String get(String key) {
- key = composeKey(key);
-
- int start = this.getContents().indexOf(key);
+ /* Call static version. */
+ return JSONReader.get(this.getContents(), key, isMetadata);
+ }
+
+ public static String get(String json, String key) {
+ /* No isMetadata boolean passed, presumably irrelevant. */
+ return JSONReader.get(json, key, false);
+ }
+
+ /**
+ * Get a value given a key and a block of JSON to search.
+ */
+ public static String get(String json, String key, boolean isMetadata) {
+ key = composeKey(key, isMetadata); // Compose a key
+ int start = json.indexOf(key); // Index of key
if (start == -1) {
/* No match. */
return null;
}
- start += key.length();
+ start += key.length(); // Start of data past key
/* This JSON is pretty-printed. */
- int nl = this.getContents().indexOf("\n", start);
+ int nl = json.indexOf("\n", start);
/* Extract substring. Remove any quotation marks and commas. */
- String substring = this.getContents().substring(start, nl);
+ String substring = json.substring(start, nl);
return substring.replaceAll("[\",]", "");
}
@@ -63,12 +86,13 @@ public class JSONReader extends Reader {
* Search for and return all matches.
*/
public String[] getAll(String key) {
- key = composeKey(key);
+ key = composeKey(key); // Compose key
- ArrayList<String> list = new ArrayList<String>();
- int lastMatch = 0;
+ ArrayList<String> list = new ArrayList<String>(); // ArrayList to compose output array
+ int lastMatch = 0; // Previous match's index. Necessary for iteration through file contents.
+ /* While matches exist, continue. */
while ((lastMatch = this.getContents().indexOf(key, lastMatch)) != -1) {
- lastMatch += key.length();
+ lastMatch += key.length(); // Data starts after key.
int start = lastMatch;
/* This JSON is pretty-printed. */
int nl = this.getContents().indexOf("\n", start);
@@ -90,16 +114,48 @@ public class JSONReader extends Reader {
}
/**
- * Search for and return a table of fields.
+ * @return a block of JSON associated with a font family.
+ * Heavily reliant on the format of popularity.json. Not
+ * suitable for generic JSON parsing.
*/
- public String getTable(HashMap<String,String> name) {
- return null;
+ public String getFamily(String family) {
+ /* Taking advantage of the specific structure
+ of popularity.json. Find the index of the family
+ name declaration. Then, all items until the next line
+ starting with ' }' are included. */
+ String contents = this.getContents();
+ int start = contents.indexOf(" \"family\": \"" + family + "\",");
+ if (start == -1) {
+ /* No match. */
+ return null;
+ }
+
+ /* Restring input starting from family name declaration. */
+ contents = contents.substring(start);
+
+ String[] lines = contents.split("\n");
+ int i = 0;
+ /* In this format, an end block is guaranteed.
+ Iterate through lines until the desired level of end
+ block is found. */
+ while (!lines[i].startsWith(" }")) {
+ i++;
+ }
+
+ /* For each of the lines in the block preceding the end block,
+ add to output. */
+ String result = "";
+ for (int j = 0; j < i; j++) {
+ result += lines[j] + "\n";
+ }
+
+ return result;
}
/**
- * Specify that this is the special PB format.
+ * Specify that this is the special pb metadata format. Almost JSON.
* See https://googlefonts.github.io/gf-guide/metadata.html. */
- public void setPb() {
- this.pb = true;
+ public void setMetadata() {
+ this.isMetadata = true;
}
}
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;
+ }
+ });
+ }
}
diff --git a/src/Reader.java b/src/Reader.java
index cdad0cc..05dd03e 100644
--- a/src/Reader.java
+++ b/src/Reader.java
@@ -33,7 +33,7 @@ class Reader {
/* On exception, exit. */
} catch (Exception e) {
System.out.println(e.getMessage());
- System.exit(1);
+ System.exit(-1);
}
}