static final String SHOW_DATA_FILE = "defiance-ohio-past-shows-20110303-001933.tsv"; ArrayList showDates; HashMap showCountsByYear; int showCount; int yearMin = 3000; int yearMax = 0; int minMonthShowCount = 0; int maxMonthShowCount = 0; int minYearShowCount = 0; int maxYearShowCount = 0; float plotX1, plotY1; float plotX2, plotY2; float labelX, labelY; int showLabelInterval = 5; float barWidth; float labelTextZ; int labelTextSize; int viewCount = 2; static final int YEAR_VIEW = 0; static final int MONTH_VIEW = 1; int currentView = MONTH_VIEW; String[] viewTitles; float[] tabLeft, tabRight; float tabTop, tabBottom; float tabPad = 10; /** * Google Refine or OpenOffice Calc wraps strings in tab-separated value files in quotes. * We need to get rid of these for our processing. */ String unquote(String str) { if (str.length() > 2 && str.charAt(0) == '"' && str.charAt(str.length() - 1) == '"') { return str.substring(1, str.length() - 1); } else { return str; } } void setupShows() { String[] lines = loadStrings(SHOW_DATA_FILE); showCount = lines.length; showDates = new ArrayList(); // The date field, as output by Google Refine looks like // Sat Oct 30 00:00:00:00 CDT 2010 DateFormat refineDateFormat = new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy"); DateFormat dateOnlyFormat = new SimpleDateFormat("MM/dd/yyyy"); TimeZone tz = TimeZone.getTimeZone("America/Chicago"); Calendar cal = new GregorianCalendar(tz); // Calculate the minimum and maximum years for shows. for (int i = 1; i < lines.length; i++) { String[] pieces = split(lines[i], TAB); String dateStr = unquote(pieces[0]); int showYear; try { Date showDate = refineDateFormat.parse(dateStr); cal.setTime(showDate); showYear = cal.get(Calendar.YEAR); if (showYear > yearMax) { yearMax = showYear; } if (showYear < yearMin) { yearMin = showYear; } showDates.add(dateOnlyFormat.format(showDate)); } catch (ParseException e) { // Ignore lines without dates if (dateStr.equals("2002-12")) { // Special case for the first show when we don't remember the date. showYear = 2002; if (showYear > yearMax) { yearMax = showYear; } if (showYear < yearMin) { yearMin = showYear; } // Fake the day of the month since it doesn't matter. showDates.add("12/01/2002"); } else { print("Error parsing date \"" + dateStr + "\"\n"); } } } // Initialize the data structure to hold show counts. showCountsByYear = new HashMap(); int year; for (year = yearMin; year <= yearMax; year++) { HashMap showCountsByMonth = new HashMap(); int month; for (month = 1; month <= 12; month++) { showCountsByMonth.put(month, 0); } showCountsByMonth.put("total", 0); showCountsByYear.put(year, showCountsByMonth); } Iterator itr = showDates.iterator(); while (itr.hasNext()) { String dateStr = itr.next().toString(); try { HashMap showCountsByMonth; Date showDate = dateOnlyFormat.parse(dateStr); cal.setTime(showDate); int showYear = cal.get(Calendar.YEAR); int showMonth = cal.get(Calendar.MONTH) + 1; showCountsByMonth = (HashMap) showCountsByYear.get(showYear); Integer monthShowCount = (Integer) showCountsByMonth.get(showMonth); if (monthShowCount != null) { monthShowCount++; showCountsByMonth.put(showMonth, monthShowCount); if (monthShowCount > maxMonthShowCount) { maxMonthShowCount = monthShowCount; } } } catch (ParseException e) { // Just ignore unparseable lines } } // Sum the monthly totals to get yearly totals. for (year = yearMin; year <= yearMax; year++) { int yearTotal = 0; HashMap showCountsByMonth = (HashMap) showCountsByYear.get(year); int month; for (month = 1; month <= 12; month++) { Integer value = (Integer) showCountsByMonth.get(month); yearTotal += value; } showCountsByMonth.put("total", yearTotal); showCountsByYear.put(year, showCountsByMonth); if (yearTotal > maxYearShowCount) { maxYearShowCount = yearTotal; } } } void setup() { setupShows(); PFont font = loadFont("FreeSans-12.vlw"); textFont(font); size(800, 405); // Corners of the plotted time series plotX1 = 80; plotX2 = width - (plotX1/2); plotY1 = 60; plotY2 = height - plotY1; labelX = 30; labelY = height - 15; smooth(); // Configure some options for the draw stage barWidth = 4; labelTextZ = 0; // 0 = surface labelTextSize = 10; } /** * Is the point (x, y) in the rectangle defined by the corner points rectX1, rectY1, rectX2, rectY2 * * This assumes rectMode(CORNERS) which means rectX1, rectY1 is the upper left hand corner and * rectX2, rectY2 is the lower right hand corner. * */ boolean pointInRect(float x, float y, float rectX1, float rectY1, float rectX2, float rectY2) { float tolerance = 1; if (x + tolerance >= rectX1 && x <= rectX2 + tolerance && y >= rectY1 && y <= rectY2) { return true; } else { // if (abs(x - rectX1) <= 3) { // print("DEBUG: (" + x + ", " + y + ") not in rect(" + rectX1 + ", " + rectY1 + ", " + rectX2 + ", " + rectY2 + ")\n"); // } return false; } } // pointInRect void drawDataBarLabel(String s, float x, float y) { float labelRectWidth = textWidth(s); float labelRectHeight = labelTextSize; // I'm trying to draw the label background last, because shapes in processing are // layered based on the order that they're drawn (see // http://processing.org/discourse/yabb2/YaBB.pl?num=1148924643). But this doesn't // always seem to happen. fill(#ffffff); rect(x - labelRectWidth/2, y-8-labelRectHeight/2, x + labelRectWidth/2, y-8+labelRectHeight/2); fill(0); textSize(labelTextSize); textAlign(CENTER, CENTER); text(s, x, y-8, labelTextZ); } void drawMonthlyDataBars() { noStroke(); rectMode(CORNERS); float labelX = 0; float labelY = 0; String labelText = ""; //print("DEBUG: beginning one loop through data\n"); for (int year = yearMin; year <= yearMax; year++) { if (showCountsByYear.containsKey(year)) { HashMap showCountsByMonth = (HashMap) showCountsByYear.get(year); int month; for (month = 1; month <= 12; month++) { if (showCountsByMonth.containsKey(month)) { Integer monthShowCount = (Integer) showCountsByMonth.get(month); int maxVal = ((yearMax - yearMin) * 12) + 11; int minVal = 0; int curVal = ((year - yearMin) * 12) + month - 1; float x = map(curVal, minVal, maxVal, plotX1, plotX2); float y = map(monthShowCount, minMonthShowCount, maxMonthShowCount, plotY2, plotY1); if (monthShowCount != 0) { fill(#5679C1); rect(x-barWidth/2, y, x+barWidth/2, plotY2 - 2); } // Draw a label for the bar closest to the cursor if (pointInRect(mouseX, mouseY, x-barWidth/2, y, x+barWidth/2, plotY2 - 2)) { DateFormat monthYearFormat = new SimpleDateFormat("yyyy-MM"); DateFormat monthNameYear = new SimpleDateFormat("MMMM yyyy"); try { Date monthYear = monthYearFormat.parse(year + "-" + month); labelText = monthNameYear.format(monthYear) + ": " + monthShowCount + " shows"; labelX = x; labelY = y; } catch(ParseException e) { print(year + "-" + month); } } // if } // if } // for } // if } // for // Delay drawing the label so it appears over all the other rectangles. if (!labelText.equals("")) { drawDataBarLabel(labelText, labelX, labelY); } //print("DEBUG: Ending one loop through data\n"); } // drawMonthlyDataBars void drawYearlyDataBars() { noStroke(); rectMode(CORNERS); for (int year = yearMin; year <= yearMax; year++) { if (showCountsByYear.containsKey(year)) { HashMap showCountsByMonth = (HashMap) showCountsByYear.get(year); Integer yearShowCount = (Integer) showCountsByMonth.get("total"); float x = map(year, yearMin, yearMax, plotX1, plotX2); float y = map(yearShowCount, minYearShowCount, maxYearShowCount, plotY2, plotY1); fill(#5679C1); rect(x-barWidth/2, y, x+barWidth/2, plotY2); // Draw a label for the point closest to the cursor if (pointInRect(mouseX, mouseY, x-barWidth/2, y, x+barWidth/2, plotY2 - 2)) { drawDataBarLabel(year + ": " + yearShowCount + " shows", x, y); } } } } // drawYearlyDataBars void drawMonthLabels( ) { fill(0); textSize(10); textAlign(CENTER, TOP); // Use thin, gray lines to draw the grid. stroke(224); strokeWeight(1); for (int year = yearMin; year <= yearMax; year++) { int maxVal = ((yearMax - yearMin) * 12) + 11; int minVal = 0; int curVal = (year - yearMin) * 12; float x = map(curVal, minVal, maxVal, plotX1, plotX2); text(year, x, plotY2 + 10); //line(x, plotY1, x, plotY2); } } void drawYearLabels( ) { fill(0); textSize(10); textAlign(CENTER, TOP); // Use thin, gray lines to draw the grid. //stroke(224); //strokeWeight(1); for (int year = yearMin; year <= yearMax; year++) { float x = map(year, yearMin, yearMax, plotX1, plotX2); text(year, x, plotY2 + 10); //line(x, plotY1, x, plotY2); } } void drawShowCountLabels(int minCount, int maxCount) { fill(0); textSize(10); for (float s = minCount; s <= maxCount; s += showLabelInterval) { float y = map(s, minCount, maxCount, plotY2, plotY1); if (s == minCount) { textAlign(RIGHT); // Align by the bottom } else if (s == maxCount) { textAlign(RIGHT, TOP); // Align by the top } else { textAlign(RIGHT, CENTER); // Center vertically } text(floor(s), plotX1 - 10, y); } } void drawAxisLabels( ) { fill(0); textSize(13); textLeading(15); textAlign(CENTER, CENTER); // Use \n (aka enter or linefeed) to break the text into separate lines. text("Shows", labelX, (plotY1+plotY2)/2); textAlign(CENTER); text("Year", (plotX1+plotX2)/2, labelY); } void drawTitleTabs() { rectMode(CORNERS); noStroke( ); textSize(20); textAlign(LEFT); // On first use of this method, allocate space for an array // to store the values for the left and right edges of the tabs. if (tabLeft == null) { tabLeft = new float[viewCount]; tabRight = new float[viewCount]; } if (viewTitles == null) { viewTitles = new String[2]; viewTitles[YEAR_VIEW] = "Year"; viewTitles[MONTH_VIEW] = "Month"; } float runningX = plotX1; tabTop = plotY1 - textAscent( ) - 15; tabBottom = plotY1; for (int view = 0; view < viewCount; view++) { String title = viewTitles[view]; tabLeft[view] = runningX; float titleWidth = textWidth(title); tabRight[view] = tabLeft[view] + tabPad + titleWidth + tabPad; // If the current tab, set its background white; otherwise use pale gray. fill(view == currentView ? 255 : 224); rect(tabLeft[view], tabTop, tabRight[view], tabBottom); // If the current tab, use black for the text; otherwise use dark gray. fill(view == currentView ? 0 : 64); text(title, runningX + tabPad, plotY1 - 10); runningX = tabRight[view]; } } void mousePressed( ) { if (mouseY > tabTop && mouseY < tabBottom) { for (int view = 0; view < viewCount; view++) { if (mouseX > tabLeft[view] && mouseX < tabRight[view]) { setView(view); } } } } void setView(int view) { if (view != currentView) { currentView = view; } } void draw() { background(224); // Show the plot area as a white box. fill(255); rectMode(CORNERS); noStroke( ); rect(plotX1, plotY1, plotX2, plotY2); drawTitleTabs(); if (currentView == MONTH_VIEW) { drawMonthLabels(); drawShowCountLabels(minMonthShowCount, maxMonthShowCount); //drawMonthlyDataPoints(); drawMonthlyDataBars(); } else { drawYearLabels(); drawShowCountLabels(minYearShowCount, maxYearShowCount); // currentView == YEAR_VIEW drawYearlyDataBars(); } drawAxisLabels(); }