Add download counters to the GAE site

This commit is contained in:
mchr3k 2011-11-13 22:33:51 +00:00
parent 59960565ee
commit b3ea5ba6bc
24 changed files with 753 additions and 7 deletions

4
.gitignore vendored
View File

@ -8,4 +8,6 @@ asm3.2_*
asm_patch*
libsrc*
*private*
*genfile*
*genfile*
classes/
appengine-generated/

View File

@ -3,5 +3,8 @@
<classpathentry kind="src" path="src"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
<classpathentry kind="con" path="com.google.appengine.eclipse.core.GAE_CONTAINER"/>
<classpathentry kind="lib" path="lib/junit-4.10.jar"/>
<classpathentry kind="lib" path="war/WEB-INF/lib/joda-time-2.0.jar"/>
<classpathentry kind="src" path="testsrc"/>
<classpathentry kind="output" path="war/WEB-INF/classes"/>
</classpath>

View File

@ -1,3 +1,4 @@
#Mon Nov 07 21:54:15 GMT 2011
#Sun Nov 13 21:28:21 GMT 2011
eclipse.preferences.version=1
filesCopiedToWebInfLib=
filesCopiedToWebInfLib=appengine-api-1.0-sdk-1.5.5.jar|appengine-api-labs-1.5.5.jar|appengine-jsr107cache-1.5.5.jar|jsr107cache-1.1.jar|datanucleus-appengine-1.0.9.final.jar|datanucleus-core-1.1.5.jar|datanucleus-jpa-1.1.5.jar|geronimo-jpa_3.0_spec-1.1.1.jar|geronimo-jta_1.1_spec-1.1.1.jar|jdo2-api-2.3-eb.jar
gaeDeployDialogSettings=

Binary file not shown.

View File

@ -0,0 +1,76 @@
package org.gaecounter;
import java.io.IOException;
import javax.jdo.PersistenceManager;
import javax.jdo.Query;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.gaecounter.data.Counter;
import org.gaecounter.data.PMF;
public class ActionServlet extends HttpServlet
{
private static final long serialVersionUID = 1L;
@Override
protected void doGet(HttpServletRequest xiReq,
HttpServletResponse xiResp)
throws ServletException,
IOException
{
String lAction = xiReq.getParameter("action");
String lRedir = xiReq.getParameter("redir");
if ("clear".equals(lAction) ||
"clearfile".equals(lAction))
{
PersistenceManager pm = PMF.get().getPersistenceManager();
try
{
Query lDeletion = pm.newQuery(Counter.class);
if ("clearfile".equals(lAction))
{
String lFile = Utils.dec(xiReq.getParameter("file"));
lDeletion.setFilter("mFile == mFilenameParam");
lDeletion.declareParameters("String mFilenameParam");
lDeletion.deletePersistentAll(lFile);
}
else
{
lDeletion.deletePersistentAll();
}
}
finally
{
pm.close();
}
}
else if ("testdata".equals(lAction))
{
CountDownloadsFilter.countDownload("test_0.1.jar", 2010, 1, 1);
CountDownloadsFilter.countDownload("test_0.1.jar", 2010, 1, 1);
CountDownloadsFilter.countDownload("test_0.1.jar", 2010, 1, 2);
CountDownloadsFilter.countDownload("test_0.1.jar", 2010, 2, 2);
CountDownloadsFilter.countDownload("test_0.1.jar", 2010, 3, 2);
CountDownloadsFilter.countDownload("test_0.2.jar", 2010, 4, 2);
CountDownloadsFilter.countDownload("test_0.2.jar", 2010, 5, 2);
CountDownloadsFilter.countDownload("test_0.2.jar", 2010, 5, 2);
CountDownloadsFilter.countDownload("test_0.2.jar", 2010, 6, 2);
CountDownloadsFilter.countDownload("test_0.3.jar", 2011, 1, 1);
CountDownloadsFilter.countDownload("test_0.3.jar", 2011, 1, 1);
CountDownloadsFilter.countDownload("test_0.3.jar", 2011, 1, 2);
CountDownloadsFilter.countDownload("test_0.3.jar", 2011, 2, 2);
CountDownloadsFilter.countDownload("test_0.3.jar", 2011, 3, 2);
CountDownloadsFilter.countDownload("test_0.4.jar", 2011, 4, 2);
CountDownloadsFilter.countDownload("test_0.4.jar", 2011, 5, 2);
CountDownloadsFilter.countDownload("test_0.4.jar", 2011, 5, 2);
CountDownloadsFilter.countDownload("test_0.4.jar", 2011, 6, 2);
}
xiResp.sendRedirect(lRedir);
}
}

View File

@ -0,0 +1,128 @@
package org.gaecounter;
import java.io.IOException;
import javax.jdo.JDOCanRetryException;
import javax.jdo.JDOObjectNotFoundException;
import javax.jdo.PersistenceManager;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.gaecounter.data.Counter;
import org.gaecounter.data.Counter.Type;
import org.gaecounter.data.PMF;
import org.joda.time.DateTime;
public class CountDownloadsFilter implements Filter
{
@Override
public void init(FilterConfig xiConfig) throws ServletException
{
// Do nothing
}
@Override
public void doFilter(ServletRequest xiReq,
ServletResponse xiResp,
FilterChain xiChain)
throws IOException,
ServletException
{
if (xiReq instanceof HttpServletRequest)
{
HttpServletRequest lHttpReq = (HttpServletRequest)xiReq;
String lReqURI = lHttpReq.getRequestURI();
DateTime lNow = new DateTime();
countDownload(lReqURI, lNow.getYear(), lNow.getMonthOfYear(), lNow.getDayOfMonth());
if (lReqURI.endsWith(".so"))
{
xiResp.setContentType("application/octet-stream");
}
}
xiChain.doFilter(xiReq, xiResp);
}
public static void countDownload(String xiReqURI, int y, int m, int d)
{
String lYStr = Type.YEAR.getPartialStr(y, m, d);
String lYStrKey = Counter.getKey(xiReqURI, lYStr);
String lYMStr = Type.MONTH.getPartialStr(y, m, d);
String lYMStrKey = Counter.getKey(xiReqURI, lYMStr);
String lYMDStr = Type.DAY.getPartialStr(y, m, d);
String lYMDStrKey = Counter.getKey(xiReqURI, lYMDStr);
PersistenceManager pm = PMF.get().getPersistenceManager();
try
{
incrementCounter(pm, lYStrKey, Type.YEAR, lYStr, xiReqURI);
incrementCounter(pm, lYMStrKey, Type.MONTH, lYMStr, xiReqURI);
incrementCounter(pm, lYMDStrKey, Type.DAY, lYMDStr, xiReqURI);
}
finally
{
pm.close();
}
}
private static void incrementCounter(PersistenceManager xiPm,
String xiKey,
Type xiType,
String xiDateStr,
String xiReqURI)
{
try
{
int retries = 0;
int NUM_RETRIES = 10;
while (true) // break out below
{
retries++;
xiPm.currentTransaction().begin();
Counter lCounter = null;
try
{
lCounter = xiPm.getObjectById(Counter.class, xiKey);
lCounter.incrementCount();
}
catch (JDOObjectNotFoundException ex)
{
lCounter = new Counter(xiType, xiDateStr, xiReqURI);
}
xiPm.makePersistent(lCounter);
try
{
xiPm.currentTransaction().commit();
break;
}
catch (JDOCanRetryException ex)
{
if (retries == NUM_RETRIES)
{
throw ex;
}
}
}
}
finally
{
if (xiPm.currentTransaction().isActive())
{
xiPm.currentTransaction().commit();
}
}
}
@Override
public void destroy()
{
// Do nothing
}
}

View File

@ -0,0 +1,34 @@
package org.gaecounter;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
public class Utils
{
public static String enc(String xiVal)
{
try
{
return URLEncoder.encode(xiVal, "UTF-8");
}
catch (UnsupportedEncodingException e)
{
// We will never hit this
return null;
}
}
public static String dec(String xiVal)
{
try
{
return URLDecoder.decode(xiVal, "UTF-8");
}
catch (UnsupportedEncodingException e)
{
// We will never hit this
return null;
}
}
}

View File

@ -0,0 +1,174 @@
package org.gaecounter.data;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.jdo.PersistenceManager;
import javax.jdo.Query;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;
import org.joda.time.DateTimeFieldType;
import org.joda.time.Partial;
import org.joda.time.Period;
@PersistenceCapable
public class Counter
{
public Counter(Type mType, String mDate, String mFile)
{
this.mKey = getKey(mFile, mDate);
this.mType = mType;
this.mDate = mDate;
this.mFile = mFile;
this.mCount = 1;
}
public static String getKey(String xiFile, String xiDate)
{
return xiFile + "_" + xiDate;
}
@PrimaryKey
@Persistent
public String mKey;
public enum Type
{
YEAR("yyyy", Period.years(1))
{
@Override
public Partial getPartial(int y, int m, int d)
{
return new Partial(new DateTimeFieldType[]
{DateTimeFieldType.year()},
new int[] {y});
}
},
MONTH("yyyy/MM", Period.months(1))
{
@Override
public Partial getPartial(int y, int m, int d)
{
return new Partial(new DateTimeFieldType[]
{DateTimeFieldType.year(),
DateTimeFieldType.monthOfYear()},
new int[] {y, m});
}
},
DAY("yyyy/MM/dd", Period.days(1))
{
@Override
public Partial getPartial(int y, int m, int d)
{
return new Partial(new DateTimeFieldType[]
{DateTimeFieldType.year(),
DateTimeFieldType.monthOfYear(),
DateTimeFieldType.dayOfMonth()},
new int[] {y, m, d});
}
};
private final String mFormat;
public final Period mPeriod;
private Type (String xiF, Period xiP) {mFormat = xiF; mPeriod = xiP;}
public Partial getPartial(int y, int m, int d) {return null;}
public String getPartialStr(int y, int m, int d)
{
return getPartial(y, m, d).toString(mFormat);
}
}
@Persistent
public Type mType;
@Persistent
public String mDate;
@Persistent
public String mFile;
@Persistent
public Integer mCount;
public void incrementCount()
{
mCount++;
}
@SuppressWarnings("unchecked")
public static List<Counter> getAllByType(Type xiType)
{
List<Counter> detachedList = null, list = null;
PersistenceManager pm = PMF.get().getPersistenceManager();
try
{
Query lQuery = pm.newQuery(Counter.class);;
lQuery.setFilter("mType == mTypeParam");
lQuery.declareParameters("String mTypeParam");
list = (List<Counter>)lQuery.execute(xiType);
detachedList = new ArrayList<Counter>();
for (Counter obj : list)
{
detachedList.add(pm.detachCopy(obj));
}
}
finally
{
pm.close();
}
return detachedList;
}
public static Map<String,Map<String,Integer>> getPerFilePerDateMap(List<Counter> xiList)
{
Map<String,Map<String,Integer>> lFileMap = new HashMap<String, Map<String,Integer>>();
for (Counter lCounter : xiList)
{
String lFile = lCounter.mFile;
String lDateStr = lCounter.mDate;
Integer lCount = lCounter.mCount;
Map<String, Integer> lDateMap = lFileMap.get(lFile);
if (lDateMap == null)
{
lDateMap = new HashMap<String, Integer>();
lFileMap.put(lFile, lDateMap);
}
lDateMap.put(lDateStr, lCount);
}
return lFileMap;
}
public static List<String> getDateStrs(Type xiType,
Partial xiStart,
Set<String> xiDataDateStrs)
{
List<String> lDateStrs = new ArrayList<String>();
if (xiDataDateStrs.size() > 0)
{
List<String> lInDataStrsList = new ArrayList<String>(xiDataDateStrs);
Collections.sort(lInDataStrsList);
String lOldestDate = lInDataStrsList.get(0);
Partial lDate = xiStart;
String lDateStr = lDate.toString(xiType.mFormat);
while (lDateStr.compareTo(lOldestDate) >= 0)
{
lDateStrs.add(lDateStr);
lDate = lDate.minus(xiType.mPeriod);
lDateStr = lDate.toString(xiType.mFormat);
}
lDateStrs.add(lDateStr);
Collections.reverse(lDateStrs);
}
return lDateStrs;
}
}

View File

@ -0,0 +1,17 @@
package org.gaecounter.data;
import javax.jdo.JDOHelper;
import javax.jdo.PersistenceManagerFactory;
public final class PMF
{
private static final PersistenceManagerFactory pmfInstance = JDOHelper
.getPersistenceManagerFactory("transactions-optional");
private PMF() { }
public static PersistenceManagerFactory get()
{
return pmfInstance;
}
}

View File

@ -0,0 +1,58 @@
package org.gaecounter;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.gaecounter.data.Counter;
import org.gaecounter.data.Counter.Type;
import org.junit.Test;
public class CounterTest
{
@Test
public void testMapping()
{
List<Counter> lTestData = new ArrayList<Counter>();
lTestData.add(new Counter(Type.DAY,
Type.DAY.getPartialStr(2010, 1, 1),
"test.wav"));
lTestData.add(new Counter(Type.DAY,
Type.DAY.getPartialStr(2011, 2, 2),
"test.wav"));
lTestData.add(new Counter(Type.DAY,
Type.DAY.getPartialStr(2011, 2, 2),
"foo.wav"));
Map<String, Map<String, Integer>> map = Counter.getPerFilePerDateMap(lTestData);
assertEquals(2, map.keySet().size());
assertTrue(map.keySet().toString(), map.keySet().contains("test.wav"));
assertTrue(map.keySet().toString(), map.keySet().contains("foo.wav"));
Map<String, Integer> lData = map.get("test.wav");
assertEquals(2, lData.keySet().size());
assertTrue(lData.keySet().toString(), lData.keySet().contains("2010/01/01"));
assertTrue(lData.keySet().toString(), lData.keySet().contains("2011/02/02"));
}
@Test
public void testDateStrs()
{
Type t = Type.DAY;
Set<String> lDateStrs = new HashSet<String>();
lDateStrs.add(t.getPartialStr(2010, 12, 18));
lDateStrs.add(t.getPartialStr(2010, 12, 16));
List<String> dateStrs = Counter.getDateStrs(Type.DAY, Type.DAY.getPartial(2010, 12, 20), lDateStrs);
assertEquals(6, dateStrs.size());
assertEquals("2010/12/15", dateStrs.get(0));
assertEquals("2010/12/16", dateStrs.get(1));
assertEquals("2010/12/17", dateStrs.get(2));
assertEquals("2010/12/18", dateStrs.get(3));
assertEquals("2010/12/19", dateStrs.get(4));
assertEquals("2010/12/20", dateStrs.get(5));
}
}

View File

@ -9,9 +9,14 @@
</system-properties>
<static-files>
<include path="/**.html" />
<include path="/**.jar" />
<include path="/**.xml" />
</static-files>
<include path="/**.html" />
<include path="/**.xml" />
<exclude path="/plugins/**" />
<exclude path="/files/**" />
</static-files>
<admin-console>
<page name="Downloads" url="/downloads.jsp" />
</admin-console>
</appengine-web-app>

Binary file not shown.

View File

@ -4,6 +4,28 @@ xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
<filter>
<filter-name>CountDownloadsFilter</filter-name>
<filter-class>org.gaecounter.CountDownloadsFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CountDownloadsFilter</filter-name>
<url-pattern>/plugins/*</url-pattern>
<url-pattern>/files/*</url-pattern>
</filter-mapping>
<servlet>
<servlet-name>Action</servlet-name>
<servlet-class>org.gaecounter.ActionServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>Action</servlet-name>
<url-pattern>/action</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>

View File

@ -0,0 +1,161 @@
<%@ page import="javax.jdo.PersistenceManager" %>
<%@ page import="javax.jdo.Query" %>
<%@ page import="org.gaecounter.data.Counter" %>
<%@ page import="org.gaecounter.data.Counter.Type" %>
<%@ page import="org.gaecounter.data.PMF" %>
<%@ page import="org.gaecounter.Utils" %>
<%@ page import="java.util.List" %>
<%@ page import="java.util.ArrayList" %>
<%@ page import="java.util.Iterator" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.util.Map.Entry" %>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.util.Collections" %>
<%@ page import="java.util.Set" %>
<%@ page import="java.util.HashSet" %>
<%@ page import="org.joda.time.DateTime" %>
<!DOCTYPE html>
<html>
<head>
<title>Downloads</title>
<script type="text/javascript" src="http://www.google.com/jsapi"></script>
<script type="text/javascript">
google.load('visualization', '1', {packages: ['corechart']});
</script>
</head>
<body>
<%
// Set type
Type lType = Type.DAY;
// Check type
String lModeStr = request.getParameter("mode");
if ("month".equals(lModeStr))
{
lType = Type.MONTH;
}
else if ("year".equals(lModeStr))
{
lType = Type.YEAR;
}
// Fetch data
List<Counter> lRecords = Counter.getAllByType(lType);
// Filename -> DateStr -> Count
Map<String,Map<String,Integer>> lPerDateStrDownloads = Counter.getPerFilePerDateMap(lRecords);
// Date strings
Set<String> dateStrsSet = new HashSet<String>();
for (String file : lPerDateStrDownloads.keySet())
{
dateStrsSet.addAll(lPerDateStrDownloads.get(file).keySet());
}
DateTime lNow = new DateTime();
List<String> dateStrs = Counter.getDateStrs(lType,
lType.getPartial(lNow.getYear(), lNow.getMonthOfYear(), lNow.getDayOfMonth()),
dateStrsSet);
%>
<script type="text/javascript">
function drawVisualization() {
// Raw data
var filenames =
[<%Iterator<String> lFiles = lPerDateStrDownloads.keySet().iterator(); while (lFiles.hasNext()) {%>
'<%=lFiles.next()%>'<%=(lFiles.hasNext() ? "," : "")%>
<%}%>];
var datestrs =
[<%Iterator<String> dateStrsIter = dateStrs.iterator(); while (dateStrsIter.hasNext()) {%>
'<%=dateStrsIter.next()%>'<%=(dateStrsIter.hasNext() ? "," : "")%>
<%}%>];
var downloadsByFile =
[
<%Iterator<Entry<String,Map<String,Integer>>> iter = lPerDateStrDownloads.entrySet().iterator();
while (iter.hasNext()) {
Entry<String,Map<String,Integer>> entry = iter.next();
Map<String,Integer> perDateCount = entry.getValue();
dateStrsIter = dateStrs.iterator();%>
[
<%while(dateStrsIter.hasNext()) {
String dateStr = dateStrsIter.next();
Integer lVal = perDateCount.get(dateStr);
if (lVal == null) lVal = 0;%>
<%=lVal%><%=(dateStrsIter.hasNext() ? "," : "")%>
<%}%>
]<%=(iter.hasNext() ? "," : "")%> // <%=entry.getKey()%>
<%}%>
];
// Create and populate the data table.
var data = new google.visualization.DataTable();
// Prepare columns
data.addColumn('string', 'Date');
for (var i = 0; i < filenames.length; ++i) {
data.addColumn('number', filenames[i]);
}
// Fill in datestr strings
data.addRows(datestrs.length);
for (var i = 0; i < datestrs.length; ++i) {
data.setCell(i, 0, datestrs[i]);
}
// Fill in download data
for (var i = 0; i < filenames.length; ++i) {
var downloads = downloadsByFile[i];
for (var datestr = 0; datestr < datestrs.length; ++datestr) {
data.setCell(datestr, i + 1, downloads[datestr]);
}
}
// Create and draw the visualization.
var ac = new google.visualization.AreaChart(document.getElementById('visualization'));
ac.draw(data, {
title : 'Downloads by Date',
isStacked: true,
//width: 600,
height: 400,
vAxis: {title: "Downloads"},
hAxis: {title: "Date"}
});
}
google.setOnLoadCallback(drawVisualization);
</script>
<script>
function doClearFile(fileName)
{ window.location.href = "action?action=clearfile&file=" + fileName + "&redir=downloads.jsp"; }
function doClear()
{ window.location.href = "action?action=clear&redir=downloads.jsp"; }
function doTestData()
{ window.location.href = "action?action=testdata&redir=downloads.jsp"; }
function setMode(mode)
{ window.location.href = "downloads.jsp?mode=" + mode; }
</script>
<input type="submit" value="Year" onclick="setMode('year')" />
<input type="submit" value="Month" onclick="setMode('month')" />
<input type="submit" value="Day" onclick="setMode('day')" /><br>
<div style="width: 100%;" id="visualization"></div>
<table>
<tr>
<td>Filename</td>
<td></td>
</tr>
<%
for (Entry<String,Map<String,Integer>> entry : lPerDateStrDownloads.entrySet()) {
%>
<tr>
<td><%=entry.getKey()%></td>
<td><input type="submit" value="Clear File" onclick="doClearFile('<%=Utils.enc(entry.getKey())%>')" /></td>
</tr>
<% } %>
</table><br>
<input type="submit" value="Clear All Data (<%=lPerDateStrDownloads.keySet().size()%> Files)" onclick="doClear()" />
<br><br><br>
<input type="submit" value="Create Test Data" onclick="doTestData()" />
</body>
</html>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,65 @@
<!--
You are free to copy and use this sample in accordance with the terms of the
Apache license (http://www.apache.org/licenses/LICENSE-2.0.html)
-->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
<title>
Google Visualization API Sample
</title>
<script type="text/javascript" src="http://www.google.com/jsapi"></script>
<script type="text/javascript">
google.load('visualization', '1', {packages: ['corechart']});
</script>
<script type="text/javascript">
function drawVisualization() {
// Some raw data (not necessarily accurate)
var countries =
['Bolivia', 'Ecuador',
'Madagascar', 'Papua Guinea', 'Rwanda'];
var months = ['2004/05', '2005/06', '2006/07', '2007/08', '2008/09'];
var productionByCountry = [[165, 135, 157, 139, 136],
[938, 1120, 1167, 1110, 691],
[522, 599, 587, 615, 629],
[998, 1268, 807, 968, 1026],
[450, 288, 397, 215, 366]];
// Create and populate the data table.
var data = new google.visualization.DataTable();
data.addColumn('string', 'Month');
for (var i = 0; i < countries.length; ++i) {
data.addColumn('number', countries[i]);
}
data.addRows(months.length);
for (var i = 0; i < months.length; ++i) {
data.setCell(i, 0, months[i]);
}
for (var i = 0; i < countries.length; ++i) {
var country = productionByCountry[i];
for (var month = 0; month < months.length; ++month) {
data.setCell(month, i + 1, country[month]);
}
}
// Create and draw the visualization.
var ac = new google.visualization.AreaChart(document.getElementById('visualization'));
ac.draw(data, {
title : 'Monthly Coffee Production by Country',
isStacked: true,
width: 600,
height: 400,
vAxis: {title: "Cups"},
hAxis: {title: "Month"}
});
}
google.setOnLoadCallback(drawVisualization);
</script>
</head>
<body style="font-family: Arial;border: 0 none;">
<div id="visualization" style="width: 600px; height: 400px;"></div>
</body>
</html>