[tor-commits] [metrics-web/master] Move website parts to new website/ subdirectory.

karsten at torproject.org karsten at torproject.org
Wed Jan 22 16:09:29 UTC 2014


commit c4f76b8096c16a689a94b503f1ce7085a14b1ea4
Author: Karsten Loesing <karsten.loesing at gmx.net>
Date:   Tue Jan 21 17:16:43 2014 +0100

    Move website parts to new website/ subdirectory.
---
 build.xml                                          |   38 +-
 etc/context.xml.template                           |   13 -
 etc/logging.properties                             |    6 -
 etc/web.xml                                        |  320 -------
 lib/REngine.jar                                    |  Bin 30637 -> 0 bytes
 lib/RserveEngine.jar                               |  Bin 27827 -> 0 bytes
 lib/jstl.jar                                       |  Bin 16905 -> 0 bytes
 lib/standard.jar                                   |  Bin 293750 -> 0 bytes
 rserve/Rserv.conf                                  |    2 -
 rserve/csv.R                                       |  213 -----
 rserve/graphs.R                                    |  929 ------------------
 rserve/rserve-init.R                               |   14 -
 rserve/shutdown.sh                                 |    5 -
 rserve/start.sh                                    |    3 -
 rserve/tables.R                                    |   55 --
 .../status/doctor/ConsensusHealthServlet.java      |   57 --
 .../ernie/status/exonerator/ExoneraTorServlet.java |   24 -
 .../status/relaysearch/RelaySearchServlet.java     |  535 -----------
 src/org/torproject/ernie/web/GraphsServlet.java    |   24 -
 src/org/torproject/ernie/web/IndexServlet.java     |   24 -
 src/org/torproject/ernie/web/ResearchServlet.java  |   24 -
 src/org/torproject/ernie/web/StatusServlet.java    |   24 -
 .../ernie/web/graphs/BubblesServlet.java           |   24 -
 src/org/torproject/ernie/web/graphs/Countries.java |  285 ------
 .../torproject/ernie/web/graphs/CsvServlet.java    |   97 --
 .../ernie/web/graphs/GraphImageServlet.java        |   76 --
 .../ernie/web/graphs/GraphParameterChecker.java    |  280 ------
 .../ernie/web/graphs/GraphsSubpagesServlet.java    |  162 ----
 src/org/torproject/ernie/web/graphs/RObject.java   |   23 -
 .../ernie/web/graphs/RObjectGenerator.java         |  398 --------
 .../ernie/web/graphs/TableParameterChecker.java    |  120 ---
 .../ernie/web/research/ResearchDataServlet.java    |  260 ------
 .../ernie/web/research/ResearchFormatsServlet.java |   24 -
 .../ernie/web/research/ResearchPapersServlet.java  |   24 -
 .../ernie/web/research/ResearchStatsServlet.java   |  132 ---
 .../ernie/web/research/ResearchToolsServlet.java   |   24 -
 web/WEB-INF/banner.jsp                             |   81 --
 web/WEB-INF/bubbles.jsp                            |   40 -
 web/WEB-INF/data.jsp                               |  286 ------
 web/WEB-INF/dir.jsp                                |   25 -
 web/WEB-INF/error.jsp                              |   77 --
 web/WEB-INF/exonerator.jsp                         |   45 -
 web/WEB-INF/fast-exits.jsp                         |   81 --
 web/WEB-INF/footer.jsp                             |   13 -
 web/WEB-INF/formats.jsp                            |  986 --------------------
 web/WEB-INF/graphs.jsp                             |   34 -
 web/WEB-INF/index.jsp                              |   40 -
 web/WEB-INF/network.jsp                            |  305 ------
 web/WEB-INF/papers.jsp                             |   81 --
 web/WEB-INF/performance.jsp                        |  150 ---
 web/WEB-INF/relay-search.jsp                       |   85 --
 web/WEB-INF/research.jsp                           |   32 -
 web/WEB-INF/stats.jsp                              |  313 -------
 web/WEB-INF/status.jsp                             |   30 -
 web/WEB-INF/tools.jsp                              |  110 ---
 web/WEB-INF/users.jsp                              |  390 --------
 web/css/stylesheet-ltr.css                         |  161 ----
 web/images/favicon.ico                             |  Bin 1150 -> 0 bytes
 web/images/top-left.png                            |  Bin 11137 -> 0 bytes
 web/images/top-middle.png                          |  Bin 240 -> 0 bytes
 web/images/top-right.png                           |  Bin 607 -> 0 bytes
 web/images/tor-metrics-overview.png                |  Bin 309306 -> 0 bytes
 web/js/bubbles.js                                  |  288 ------
 web/js/d3.min.js                                   |    5 -
 web/robots.txt                                     |    5 -
 website/build.xml                                  |   64 ++
 website/etc/context.xml                            |    3 +
 website/etc/logging.properties                     |    6 +
 website/etc/web.xml                                |  320 +++++++
 website/lib/REngine.jar                            |  Bin 0 -> 30637 bytes
 website/lib/RserveEngine.jar                       |  Bin 0 -> 27827 bytes
 website/lib/jstl.jar                               |  Bin 0 -> 16905 bytes
 website/lib/standard.jar                           |  Bin 0 -> 293750 bytes
 website/rserve/Rserv.conf                          |    2 +
 website/rserve/csv.R                               |  213 +++++
 website/rserve/graphs.R                            |  929 ++++++++++++++++++
 website/rserve/rserve-init.R                       |   14 +
 website/rserve/shutdown.sh                         |    5 +
 website/rserve/start.sh                            |    3 +
 website/rserve/tables.R                            |   55 ++
 .../status/doctor/ConsensusHealthServlet.java      |   57 ++
 .../ernie/status/exonerator/ExoneraTorServlet.java |   24 +
 .../status/relaysearch/RelaySearchServlet.java     |  535 +++++++++++
 .../org/torproject/ernie/web/GraphsServlet.java    |   24 +
 .../src/org/torproject/ernie/web/IndexServlet.java |   24 +
 .../org/torproject/ernie/web/ResearchServlet.java  |   24 +
 .../org/torproject/ernie/web/StatusServlet.java    |   24 +
 .../ernie/web/graphs/BubblesServlet.java           |   24 +
 .../org/torproject/ernie/web/graphs/Countries.java |  285 ++++++
 .../torproject/ernie/web/graphs/CsvServlet.java    |   97 ++
 .../ernie/web/graphs/GraphImageServlet.java        |   76 ++
 .../ernie/web/graphs/GraphParameterChecker.java    |  280 ++++++
 .../ernie/web/graphs/GraphsSubpagesServlet.java    |  162 ++++
 .../org/torproject/ernie/web/graphs/RObject.java   |   23 +
 .../ernie/web/graphs/RObjectGenerator.java         |  398 ++++++++
 .../ernie/web/graphs/TableParameterChecker.java    |  120 +++
 .../ernie/web/research/ResearchDataServlet.java    |  260 ++++++
 .../ernie/web/research/ResearchFormatsServlet.java |   24 +
 .../ernie/web/research/ResearchPapersServlet.java  |   24 +
 .../ernie/web/research/ResearchStatsServlet.java   |  132 +++
 .../ernie/web/research/ResearchToolsServlet.java   |   24 +
 website/web/WEB-INF/banner.jsp                     |   81 ++
 website/web/WEB-INF/bubbles.jsp                    |   40 +
 website/web/WEB-INF/data.jsp                       |  286 ++++++
 website/web/WEB-INF/dir.jsp                        |   25 +
 website/web/WEB-INF/error.jsp                      |   77 ++
 website/web/WEB-INF/exonerator.jsp                 |   45 +
 website/web/WEB-INF/fast-exits.jsp                 |   81 ++
 website/web/WEB-INF/footer.jsp                     |   13 +
 website/web/WEB-INF/formats.jsp                    |  986 ++++++++++++++++++++
 website/web/WEB-INF/graphs.jsp                     |   34 +
 website/web/WEB-INF/index.jsp                      |   40 +
 website/web/WEB-INF/network.jsp                    |  305 ++++++
 website/web/WEB-INF/papers.jsp                     |   81 ++
 website/web/WEB-INF/performance.jsp                |  150 +++
 website/web/WEB-INF/relay-search.jsp               |   85 ++
 website/web/WEB-INF/research.jsp                   |   32 +
 website/web/WEB-INF/stats.jsp                      |  313 +++++++
 website/web/WEB-INF/status.jsp                     |   30 +
 website/web/WEB-INF/tools.jsp                      |  110 +++
 website/web/WEB-INF/users.jsp                      |  390 ++++++++
 website/web/css/stylesheet-ltr.css                 |  161 ++++
 website/web/images/favicon.ico                     |  Bin 0 -> 1150 bytes
 website/web/images/top-left.png                    |  Bin 0 -> 11137 bytes
 website/web/images/top-middle.png                  |  Bin 0 -> 240 bytes
 website/web/images/top-right.png                   |  Bin 0 -> 607 bytes
 website/web/images/tor-metrics-overview.png        |  Bin 0 -> 309306 bytes
 website/web/js/bubbles.js                          |  288 ++++++
 website/web/js/d3.min.js                           |    5 +
 website/web/robots.txt                             |    5 +
 130 files changed, 7919 insertions(+), 7901 deletions(-)

diff --git a/build.xml b/build.xml
index cfcfdac..393e2d8 100644
--- a/build.xml
+++ b/build.xml
@@ -4,10 +4,6 @@
   <property name="sources" value="src"/>
   <property name="classes" value="classes"/>
   <property name="config" value="etc"/>
-  <property name="webxmlfile" value="${config}/web.xml"/>
-  <property name="contextxmltemplate"
-            value="${config}/context.xml.template"/>
-  <property name="contextxml" value="${config}/context.xml"/>
   <property name="warfile" value="ernie.war"/>
   <path id="classpath">
     <pathelement path="${classes}"/>
@@ -29,7 +25,6 @@
   </path>
 
   <target name="init">
-    <copy file="${contextxmltemplate}" tofile="${contextxml}"/>
     <copy file="config.template" tofile="config"/>
     <mkdir dir="${classes}"/>
   </target>
@@ -37,7 +32,7 @@
     <ant dir="deps/metrics-lib"/>
   </target>
 
-  <!-- Compile all servlets and plain Java classes. -->
+  <!-- Compile all plain Java classes. -->
   <target name="compile" depends="metrics-lib,init">
     <javac destdir="${classes}"
            srcdir="${sources}"
@@ -72,36 +67,5 @@
       </batchtest>
     </junit>
   </target>
-
-  <!-- Create a .war file for deployment. -->
-  <target name="war"
-          depends="compile">
-    <war destfile="${warfile}"
-         webxml="${webxmlfile}">
-      <fileset dir="web"/>
-      <lib dir="lib">
-          <include name="jstl.jar"/>
-          <include name="REngine.jar"/>
-          <include name="RserveEngine.jar"/>
-          <include name="standard.jar"/>
-      </lib>
-      <lib dir="/usr/share/java">
-          <include name="commons-codec.jar"/>
-          <include name="commons-compress.jar"/>
-          <include name="postgresql-jdbc3.jar"/>
-          <include name="junit4.jar"/>
-          <include name="commons-lang.jar"/>
-      </lib>
-      <lib dir="deps/metrics-lib">
-          <include name="descriptor.jar"/>
-      </lib>
-      <classes dir="${classes}"/>
-      <zipfileset dir="${config}"
-                  prefix="WEB-INF/classes"
-                  includes="logging.properties"/>
-      <metainf dir="${config}"
-               includes="context.xml"/>
-    </war>
-  </target>
 </project>
 
diff --git a/etc/context.xml.template b/etc/context.xml.template
deleted file mode 100644
index 00f14fe..0000000
--- a/etc/context.xml.template
+++ /dev/null
@@ -1,13 +0,0 @@
-<Context cookies="false">
-  <Resource name="jdbc/tordir"
-            type="javax.sql.DataSource"
-            auth="Container"
-            username="metrics"
-            password="password"
-            driverClassName="org.postgresql.Driver"
-            url="jdbc:postgresql://localhost/tordir"
-            maxActive="8"
-            maxIdle="4"
-            maxWait="15000"/>
-</Context>
-
diff --git a/etc/logging.properties b/etc/logging.properties
deleted file mode 100644
index 6f12902..0000000
--- a/etc/logging.properties
+++ /dev/null
@@ -1,6 +0,0 @@
-handlers = org.apache.juli.FileHandler
-
-org.apache.juli.FileHandler.level = FINE
-org.apache.juli.FileHandler.directory = ${catalina.base}/logs
-org.apache.juli.FileHandler.prefix = ernie.
-
diff --git a/etc/web.xml b/etc/web.xml
deleted file mode 100644
index 992de93..0000000
--- a/etc/web.xml
+++ /dev/null
@@ -1,320 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-
-<web-app version="2.4"
-  xmlns="http://java.sun.com/xml/ns/j2ee"
-  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-  xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee 
-  http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" >
-
-  <servlet>
-    <servlet-name>Index</servlet-name>
-    <servlet-class>
-      org.torproject.ernie.web.IndexServlet
-    </servlet-class>
-  </servlet>
-  <servlet-mapping>
-    <servlet-name>Index</servlet-name>
-    <url-pattern>/index.html</url-pattern>
-  </servlet-mapping>
-
-  <servlet>
-    <servlet-name>Graphs</servlet-name>
-    <servlet-class>
-      org.torproject.ernie.web.GraphsServlet
-    </servlet-class>
-  </servlet>
-  <servlet-mapping>
-    <servlet-name>Graphs</servlet-name>
-    <url-pattern>/graphs.html</url-pattern>
-  </servlet-mapping>
-
-  <servlet>
-    <servlet-name>GraphsSubpages</servlet-name>
-    <servlet-class>
-      org.torproject.ernie.web.graphs.GraphsSubpagesServlet
-    </servlet-class>
-  </servlet>
-  <servlet-mapping>
-    <servlet-name>GraphsSubpages</servlet-name>
-    <url-pattern>/network.html</url-pattern>
-  </servlet-mapping>
-  <servlet-mapping>
-    <servlet-name>GraphsSubpages</servlet-name>
-    <url-pattern>/fast-exits.html</url-pattern>
-  </servlet-mapping>
-  <servlet-mapping>
-    <servlet-name>GraphsSubpages</servlet-name>
-    <url-pattern>/users.html</url-pattern>
-  </servlet-mapping>
-  <servlet-mapping>
-    <servlet-name>GraphsSubpages</servlet-name>
-    <url-pattern>/performance.html</url-pattern>
-  </servlet-mapping>
-
-  <servlet>
-    <servlet-name>Bubbles</servlet-name>
-    <servlet-class>
-      org.torproject.ernie.web.graphs.BubblesServlet
-    </servlet-class>
-  </servlet>
-  <servlet-mapping>
-    <servlet-name>Bubbles</servlet-name>
-    <url-pattern>/bubbles.html</url-pattern>
-  </servlet-mapping>
-
-  <servlet>
-    <servlet-name>Research</servlet-name>
-    <servlet-class>
-      org.torproject.ernie.web.ResearchServlet
-    </servlet-class>
-  </servlet>
-  <servlet-mapping>
-    <servlet-name>Research</servlet-name>
-    <url-pattern>/research.html</url-pattern>
-  </servlet-mapping>
-
-  <servlet>
-    <servlet-name>ResearchData</servlet-name>
-    <servlet-class>
-      org.torproject.ernie.web.research.ResearchDataServlet
-    </servlet-class>
-    <init-param>
-      <param-name>localDataDir</param-name>
-      <param-value>
-        /srv/metrics.torproject.org/db/data/
-      </param-value>
-    </init-param>
-  </servlet>
-  <servlet-mapping>
-    <servlet-name>ResearchData</servlet-name>
-    <url-pattern>/data.html</url-pattern>
-  </servlet-mapping>
-
-  <servlet>
-    <servlet-name>ResearchPapers</servlet-name>
-    <servlet-class>
-      org.torproject.ernie.web.research.ResearchPapersServlet
-    </servlet-class>
-  </servlet>
-  <servlet-mapping>
-    <servlet-name>ResearchPapers</servlet-name>
-    <url-pattern>/papers.html</url-pattern>
-  </servlet-mapping>
-
-  <servlet>
-    <servlet-name>ResearchTools</servlet-name>
-    <servlet-class>
-      org.torproject.ernie.web.research.ResearchToolsServlet
-    </servlet-class>
-  </servlet>
-  <servlet-mapping>
-    <servlet-name>ResearchTools</servlet-name>
-    <url-pattern>/tools.html</url-pattern>
-  </servlet-mapping>
-
-  <servlet>
-    <servlet-name>ResearchFormats</servlet-name>
-    <servlet-class>
-      org.torproject.ernie.web.research.ResearchFormatsServlet
-    </servlet-class>
-  </servlet>
-  <servlet-mapping>
-    <servlet-name>ResearchFormats</servlet-name>
-    <url-pattern>/formats.html</url-pattern>
-  </servlet-mapping>
-
-  <servlet>
-    <servlet-name>ResearchStats</servlet-name>
-    <servlet-class>
-      org.torproject.ernie.web.research.ResearchStatsServlet
-    </servlet-class>
-    <init-param>
-      <param-name>statsDir</param-name>
-      <param-value>
-        /srv/metrics.torproject.org/web/stats/
-      </param-value>
-    </init-param>
-  </servlet>
-  <servlet-mapping>
-    <servlet-name>ResearchStats</servlet-name>
-    <url-pattern>/stats/*</url-pattern>
-    <url-pattern>/stats.html</url-pattern>
-  </servlet-mapping>
-
-  <servlet>
-    <servlet-name>Status</servlet-name>
-    <servlet-class>
-      org.torproject.ernie.web.StatusServlet
-    </servlet-class>
-  </servlet>
-  <servlet-mapping>
-    <servlet-name>Status</servlet-name>
-    <url-pattern>/status.html</url-pattern>
-  </servlet-mapping>
-
-  <servlet>
-    <servlet-name>RelaySearch</servlet-name>
-    <servlet-class>
-      org.torproject.ernie.status.relaysearch.RelaySearchServlet
-    </servlet-class>
-  </servlet>
-  <servlet-mapping>
-    <servlet-name>RelaySearch</servlet-name>
-    <url-pattern>/relay-search.html</url-pattern>
-  </servlet-mapping>
-
-  <servlet>
-    <servlet-name>GraphImage</servlet-name>
-    <servlet-class>
-      org.torproject.ernie.web.graphs.GraphImageServlet
-    </servlet-class>
-  </servlet>
-  <servlet-mapping>
-    <servlet-name>GraphImage</servlet-name>
-    <url-pattern>/networksize.png</url-pattern>
-    <url-pattern>/networksize.pdf</url-pattern>
-    <url-pattern>/networksize.svg</url-pattern>
-    <url-pattern>/cloudbridges.png</url-pattern>
-    <url-pattern>/cloudbridges.pdf</url-pattern>
-    <url-pattern>/cloudbridges.svg</url-pattern>
-    <url-pattern>/relaycountries.png</url-pattern>
-    <url-pattern>/relaycountries.pdf</url-pattern>
-    <url-pattern>/relaycountries.svg</url-pattern>
-    <url-pattern>/relayflags.png</url-pattern>
-    <url-pattern>/relayflags.pdf</url-pattern>
-    <url-pattern>/relayflags.svg</url-pattern>
-    <url-pattern>/versions.png</url-pattern>
-    <url-pattern>/versions.pdf</url-pattern>
-    <url-pattern>/versions.svg</url-pattern>
-    <url-pattern>/platforms.png</url-pattern>
-    <url-pattern>/platforms.pdf</url-pattern>
-    <url-pattern>/platforms.svg</url-pattern>
-    <url-pattern>/bandwidth.png</url-pattern>
-    <url-pattern>/bandwidth.pdf</url-pattern>
-    <url-pattern>/bandwidth.svg</url-pattern>
-    <url-pattern>/bwhist-flags.png</url-pattern>
-    <url-pattern>/bwhist-flags.pdf</url-pattern>
-    <url-pattern>/bwhist-flags.svg</url-pattern>
-    <url-pattern>/bandwidth-flags.png</url-pattern>
-    <url-pattern>/bandwidth-flags.pdf</url-pattern>
-    <url-pattern>/bandwidth-flags.svg</url-pattern>
-    <url-pattern>/dirbytes.png</url-pattern>
-    <url-pattern>/dirbytes.pdf</url-pattern>
-    <url-pattern>/dirbytes.svg</url-pattern>
-    <url-pattern>/torperf.png</url-pattern>
-    <url-pattern>/torperf.pdf</url-pattern>
-    <url-pattern>/torperf.svg</url-pattern>
-    <url-pattern>/torperf-failures.png</url-pattern>
-    <url-pattern>/torperf-failures.pdf</url-pattern>
-    <url-pattern>/torperf-failures.svg</url-pattern>
-    <url-pattern>/connbidirect.png</url-pattern>
-    <url-pattern>/connbidirect.pdf</url-pattern>
-    <url-pattern>/connbidirect.svg</url-pattern>
-    <url-pattern>/fast-exits.png</url-pattern>
-    <url-pattern>/fast-exits.pdf</url-pattern>
-    <url-pattern>/fast-exits.svg</url-pattern>
-    <url-pattern>/almost-fast-exits.png</url-pattern>
-    <url-pattern>/almost-fast-exits.pdf</url-pattern>
-    <url-pattern>/almost-fast-exits.svg</url-pattern>
-    <url-pattern>/userstats-relay-country.png</url-pattern>
-    <url-pattern>/userstats-relay-country.pdf</url-pattern>
-    <url-pattern>/userstats-relay-country.svg</url-pattern>
-    <url-pattern>/userstats-bridge-country.png</url-pattern>
-    <url-pattern>/userstats-bridge-country.pdf</url-pattern>
-    <url-pattern>/userstats-bridge-country.svg</url-pattern>
-    <url-pattern>/userstats-bridge-transport.png</url-pattern>
-    <url-pattern>/userstats-bridge-transport.pdf</url-pattern>
-    <url-pattern>/userstats-bridge-transport.svg</url-pattern>
-    <url-pattern>/userstats-bridge-version.png</url-pattern>
-    <url-pattern>/userstats-bridge-version.pdf</url-pattern>
-    <url-pattern>/userstats-bridge-version.svg</url-pattern>
-  </servlet-mapping>
-
-  <servlet>
-    <servlet-name>Csv</servlet-name>
-    <servlet-class>
-      org.torproject.ernie.web.graphs.CsvServlet
-    </servlet-class>
-  </servlet>
-  <servlet-mapping>
-    <servlet-name>Csv</servlet-name>
-    <url-pattern>/csv/*</url-pattern>
-  </servlet-mapping>
-
-  <servlet>
-    <servlet-name>ExoneraTor</servlet-name>
-    <servlet-class>
-      org.torproject.ernie.status.exonerator.ExoneraTorServlet
-    </servlet-class>
-  </servlet>
-  <servlet-mapping>
-    <servlet-name>ExoneraTor</servlet-name>
-    <url-pattern>/exonerator.html</url-pattern>
-  </servlet-mapping>
-
-  <servlet>
-    <servlet-name>ConsensusHealthServlet</servlet-name>
-    <servlet-class>
-      org.torproject.ernie.status.doctor.ConsensusHealthServlet
-    </servlet-class>
-  </servlet>
-  <servlet-mapping>
-    <servlet-name>ConsensusHealthServlet</servlet-name>
-    <url-pattern>/consensus-health.html</url-pattern>
-  </servlet-mapping>
-
-  <welcome-file-list>
-    <welcome-file>index.html</welcome-file>
-  </welcome-file-list>
-
-  <error-page>
-    <error-code>400</error-code>
-    <location>/WEB-INF/error.jsp</location>
-  </error-page>
-  <error-page>
-    <error-code>404</error-code>
-    <location>/WEB-INF/error.jsp</location>
-  </error-page>
-  <error-page>
-    <error-code>500</error-code>
-    <location>/WEB-INF/error.jsp</location>
-  </error-page>
-  <error-page>
-    <exception-type>java.lang.Throwable</exception-type>
-    <location>/WEB-INF/error.jsp</location>
-  </error-page>
-
-  <resource-ref>
-    <description>Tor Metrics Database</description>
-    <res-ref-name>jdbc/tordir</res-ref-name>
-    <res-type>javax.sql.DataSource</res-type>
-    <res-auth>Container</res-auth>
-  </resource-ref>
-
-  <context-param>
-    <param-name>rserveHost</param-name>
-    <param-value>localhost</param-value>
-  </context-param>
-  <context-param>
-    <param-name>rservePort</param-name>
-    <param-value>6311</param-value>
-  </context-param>
-  <context-param>
-    <param-name>maxCacheAge</param-name>
-    <param-value>21600</param-value>
-  </context-param>
-  <context-param>
-    <param-name>cachedGraphsDir</param-name>
-    <param-value>
-      /srv/metrics.torproject.org/web/rserve/graphs/
-    </param-value>
-  </context-param>
-
-  <listener>
-    <listener-class>
-      org.torproject.ernie.web.graphs.RObjectGenerator
-    </listener-class>
-  </listener>
-
-</web-app>
-
diff --git a/lib/REngine.jar b/lib/REngine.jar
deleted file mode 100644
index ddf4059..0000000
Binary files a/lib/REngine.jar and /dev/null differ
diff --git a/lib/RserveEngine.jar b/lib/RserveEngine.jar
deleted file mode 100644
index 77b22ce..0000000
Binary files a/lib/RserveEngine.jar and /dev/null differ
diff --git a/lib/jstl.jar b/lib/jstl.jar
deleted file mode 100644
index 6b41358..0000000
Binary files a/lib/jstl.jar and /dev/null differ
diff --git a/lib/standard.jar b/lib/standard.jar
deleted file mode 100644
index 258daae..0000000
Binary files a/lib/standard.jar and /dev/null differ
diff --git a/rserve/Rserv.conf b/rserve/Rserv.conf
deleted file mode 100644
index a23af49..0000000
--- a/rserve/Rserv.conf
+++ /dev/null
@@ -1,2 +0,0 @@
-workdir /srv/metrics.torproject.org/web/rserve/workdir
-source rserve-init.R
diff --git a/rserve/csv.R b/rserve/csv.R
deleted file mode 100644
index 2ec6e7b..0000000
--- a/rserve/csv.R
+++ /dev/null
@@ -1,213 +0,0 @@
-options(scipen = 15)
-
-export_networksize <- function(path) {
-  s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv",
-    stringsAsFactors = FALSE)
-  s <- s[s$flag == '' & s$country == '' & s$version == '' &
-         s$platform == '' & s$ec2bridge == '',
-         c("date", "relays", "bridges")]
-  write.csv(s, path, quote = FALSE, row.names = FALSE)
-}
-
-export_cloudbridges <- function(path) {
-  s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv",
-    stringsAsFactors = FALSE)
-  s <- s[s$flag == '' & s$country == '' & s$version == '' &
-         s$platform == '' & s$ec2bridge == 't', ]
-  cloudbridges <- data.frame(date = s$date, cloudbridges = s$bridges)
-  write.csv(cloudbridges, path, quote = FALSE, row.names = FALSE)
-}
-
-export_relaycountries <- function(path) {
-  s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv",
-    stringsAsFactors = FALSE)
-  s <- s[s$flag == '' & s$country != '' & s$version == '' &
-         s$platform == '' & s$ec2bridge == '',
-         c("date", "country", "relays")]
-  write.csv(s, path, quote = FALSE, row.names = FALSE)
-}
-
-export_versions <- function(path) {
-  s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv",
-    stringsAsFactors = FALSE)
-  s <- s[s$flag == '' & s$country == '' & s$version != '' &
-         s$platform == '' & s$ec2bridge == '',
-         c("date", "version", "relays")]
-  versions <- cast(s, date ~ version, value = "relays")
-  versions <- versions[order(versions$date), ]
-  write.csv(versions, path, quote = FALSE, row.names = FALSE)
-}
-
-export_platforms <- function(path) {
-  s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv",
-    stringsAsFactors = FALSE)
-  s <- s[s$flag == '' & s$country == '' & s$version == '' &
-         s$platform != '' & s$ec2bridge == '',
-         c("date", "platform", "relays")]
-  s <- data.frame(date = s$date,
-                  platform = ifelse(s$platform == 'FreeBSD', 'bsd',
-                  tolower(s$platform)), relays = s$relays)
-  s <- cast(s, date ~ platform, value = "relays")
-  platforms <- s[order(s$date), ]
-  write.csv(platforms, path, quote = FALSE, row.names = FALSE)
-}
-
-export_bandwidth <- function(path) {
-  b <- read.csv("/srv/metrics.torproject.org/web/stats/bandwidth.csv",
-    stringsAsFactors = FALSE)
-  b <- b[b$isexit == '' & b$isguard == '', ]
-  b <- data.frame(date = as.Date(b$date, "%Y-%m-%d"),
-                  bwadv = b$advbw,
-                  bwhist = floor((b$bwread + b$bwwrite) / 2))
-  b <- b[order(b$date), ]
-  write.csv(b, path, quote = FALSE, row.names = FALSE)
-}
-
-export_bwhist_flags <- function(path) {
-  b <- read.csv("/srv/metrics.torproject.org/web/stats/bandwidth.csv",
-    stringsAsFactors = FALSE)
-  b <- b[b$isexit != '' & b$isguard != '' & !is.na(b$bwread) &
-         !is.na(b$bwwrite), ]
-  b <- data.frame(date = as.Date(b$date, "%Y-%m-%d"),
-                  isexit = b$isexit == 't', isguard = b$isguard == 't',
-                  read = b$bwread, written = b$bwwrite)
-  write.csv(b, path, quote = FALSE, row.names = FALSE)
-}
-
-export_dirbytes <- function(path) {
-  b <- read.csv("/srv/metrics.torproject.org/web/stats/bandwidth.csv",
-    stringsAsFactors = FALSE)
-  b <- b[b$isexit == '' & b$isguard == '' & !is.na(b$dirread) &
-         !is.na(b$dirwrite), ]
-  b <- data.frame(date = as.Date(b$date, "%Y-%m-%d"),
-                  dirread = b$dirread, dirwrite = b$dirwrite)
-  b <- b[order(b$date), ]
-  write.csv(b, path, quote = FALSE, row.names = FALSE)
-}
-
-export_relayflags <- function(path) {
-  s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv",
-    stringsAsFactors = FALSE)
-  s <- s[s$country == '' & s$version == '' & s$platform == '' &
-         s$ec2bridge == '', ]
-  s <- data.frame(date = as.Date(s$date, "%Y-%m-%d"),
-                  flag = ifelse(s$flag == '', 'running', tolower(s$flag)),
-                  relays = s$relays)
-  s <- cast(s, date ~ flag, value = "relays")
-  relayflags <- s[order(s$date), ]
-  write.csv(relayflags, path, quote = FALSE, row.names = FALSE)
-}
-
-export_torperf <- function(path) {
-  t <- read.csv("/srv/metrics.torproject.org/web/stats/torperf.csv",
-    stringsAsFactors = FALSE)
-  t <- data.frame(
-     source = paste(ifelse(t$source == '', 'all', t$source),
-                    ifelse(t$size == 50 * 1024, '50kb',
-                           ifelse(t$size == 1024 * 1024, '1mb', '5mb')),
-                    sep = '-'),
-     date = as.Date(t$date, "%Y-%m-%d"),
-     q1 = t$q1, md = t$md, q3 = t$q3)
-  torperf <- t[order(t$source, t$date), ]
-  write.csv(torperf, path, quote = FALSE, row.names = FALSE)
-}
-
-export_torperf_failures <- function(path) {
-  t <- read.csv("/srv/metrics.torproject.org/web/stats/torperf.csv",
-    stringsAsFactors = FALSE)
-  t <- data.frame(
-     source = paste(ifelse(t$source == '', 'all', t$source),
-                    ifelse(t$size == 50 * 1024, '50kb',
-                           ifelse(t$size == 1024 * 1024, '1mb', '5mb')),
-                    sep = '-'),
-     date = as.Date(t$date, "%Y-%m-%d"),
-     timeouts = t$timeouts, failures = t$failures, requests = t$requests)
-  torperf <- t[order(t$source, t$date), ]
-  write.csv(torperf, path, quote = FALSE, row.names = FALSE)
-}
-
-export_connbidirect <- function(path) {
-  c <- read.csv("/srv/metrics.torproject.org/web/stats/connbidirect.csv",
-    stringsAsFactors = FALSE)
-  write.csv(format(c, trim = TRUE, scientific = FALSE), path,
-      quote = FALSE, row.names = FALSE)
-}
-
-export_bandwidth_flags <- function(path) {
-  b <- read.csv("/srv/metrics.torproject.org/web/stats/bandwidth.csv",
-    stringsAsFactors = FALSE)
-  b <- b[b$isexit != '' & b$isguard != '', ]
-  b <- data.frame(date = as.Date(b$date, "%Y-%m-%d"),
-                  isexit = b$isexit == 't', isguard = b$isguard == 't',
-                  advbw = b$advbw,
-                  bwhist = floor((b$bwread + b$bwwrite) / 2))
-  b <- rbind(
-    data.frame(b[b$isguard == TRUE, ], flag = "guard"),
-    data.frame(b[b$isexit == TRUE, ], flag = "exit"))
-  b <- data.frame(date = b$date, advbw = b$advbw, bwhist = b$bwhist,
-                  flag = b$flag)
-  b <- aggregate(list(advbw = b$advbw, bwhist = b$bwhist),
-                 by = list(date = b$date, flag = b$flag), FUN = sum,
-                 na.rm = TRUE, na.action = NULL)
-  b <- melt(b, id.vars = c("date", "flag"))
-  b <- data.frame(date = b$date, type = b$variable, flag = b$flag,
-                  value = b$value)
-  b <- b[b$value > 0, ]
-  write.csv(b, path, quote = FALSE, row.names = FALSE)
-}
-
-export_userstats <- function(path) {
-  c <- read.csv("/srv/metrics.torproject.org/web/stats/clients.csv",
-    stringsAsFactors = FALSE)
-  c <- data.frame(date = c$date, node = c$node, country = c$country,
-                  transport = c$transport, version = c$version,
-                  frac = c$frac, users = c$clients)
-  write.csv(format(c, trim = TRUE, scientific = FALSE), path,
-      quote = FALSE, row.names = FALSE)
-}
-
-help_export_monthly_userstats <- function(path, aggr_fun) {
-  c <- read.csv("/srv/metrics.torproject.org/web/stats/clients.csv",
-    stringsAsFactors = FALSE)
-  c <- c[c$country != '' & c$transport == '' & c$version == '', ]
-  u <- data.frame(date = c$date, country = c$country, users = c$clients,
-                  stringsAsFactors = FALSE)
-  u <- aggregate(list(users = u$users),
-                      by = list(date = u$date, country = u$country), sum)
-  u <- aggregate(list(users = u$users),
-                      by = list(country = u$country,
-                                month = substr(u$date, 1, 7)), aggr_fun)
-  u <- rbind(u, data.frame(country = "zy",
-                aggregate(list(users = u$users),
-                          by = list(month = u$month), sum)))
-  u <- cast(u, country ~ month, value = "users")
-  u[u$country == "zy", "country"] <- "all"
-  u[, 2:length(u)] <- floor(u[, 2:length(u)])
-  write.csv(u, path, quote = FALSE, row.names = FALSE)
-}
-
-export_monthly_userstats_peak <- function(path) {
-  help_export_monthly_userstats(path, max)
-}
-
-export_monthly_userstats_average <- function(path) {
-  help_export_monthly_userstats(path, mean)
-}
-
-export_userstats_detector <- function(path) {
-  c <- read.csv("/srv/metrics.torproject.org/web/stats/clients.csv",
-    stringsAsFactors = FALSE)
-  c <- c[c$country != '' & c$transport == '' & c$version == '' &
-         c$node == 'relay', ]
-  u <- data.frame(country = c$country, date = c$date, users = c$clients,
-                  stringsAsFactors = FALSE)
-  u <- rbind(u, data.frame(country = "zy",
-                aggregate(list(users = u$users),
-                          by = list(date = u$date), sum)))
-  u <- data.frame(date = u$date, country = u$country,
-                  users = floor(u$users))
-  u <- cast(u, date ~ country, value = "users")
-  names(u)[names(u) == "zy"] <- "all"
-  write.csv(u, path, quote = FALSE, row.names = FALSE)
-}
-
diff --git a/rserve/graphs.R b/rserve/graphs.R
deleted file mode 100644
index c4170e7..0000000
--- a/rserve/graphs.R
+++ /dev/null
@@ -1,929 +0,0 @@
-countrylist <- list(
-  "ad" = "Andorra",
-  "ae" = "the United Arab Emirates",
-  "af" = "Afghanistan",
-  "ag" = "Antigua and Barbuda",
-  "ai" = "Anguilla",
-  "al" = "Albania",
-  "am" = "Armenia",
-  "an" = "the Netherlands Antilles",
-  "ao" = "Angola",
-  "aq" = "Antarctica",
-  "ar" = "Argentina",
-  "as" = "American Samoa",
-  "at" = "Austria",
-  "au" = "Australia",
-  "aw" = "Aruba",
-  "ax" = "the Aland Islands",
-  "az" = "Azerbaijan",
-  "ba" = "Bosnia and Herzegovina",
-  "bb" = "Barbados",
-  "bd" = "Bangladesh",
-  "be" = "Belgium",
-  "bf" = "Burkina Faso",
-  "bg" = "Bulgaria",
-  "bh" = "Bahrain",
-  "bi" = "Burundi",
-  "bj" = "Benin",
-  "bl" = "Saint Bartelemey",
-  "bm" = "Bermuda",
-  "bn" = "Brunei",
-  "bo" = "Bolivia",
-  "br" = "Brazil",
-  "bs" = "the Bahamas",
-  "bt" = "Bhutan",
-  "bv" = "the Bouvet Island",
-  "bw" = "Botswana",
-  "by" = "Belarus",
-  "bz" = "Belize",
-  "ca" = "Canada",
-  "cc" = "the Cocos (Keeling) Islands",
-  "cd" = "the Democratic Republic of the Congo",
-  "cf" = "Central African Republic",
-  "cg" = "Congo",
-  "ch" = "Switzerland",
-  "ci" = "Côte d'Ivoire",
-  "ck" = "the Cook Islands",
-  "cl" = "Chile",
-  "cm" = "Cameroon",
-  "cn" = "China",
-  "co" = "Colombia",
-  "cr" = "Costa Rica",
-  "cu" = "Cuba",
-  "cv" = "Cape Verde",
-  "cx" = "the Christmas Island",
-  "cy" = "Cyprus",
-  "cz" = "the Czech Republic",
-  "de" = "Germany",
-  "dj" = "Djibouti",
-  "dk" = "Denmark",
-  "dm" = "Dominica",
-  "do" = "the Dominican Republic",
-  "dz" = "Algeria",
-  "ec" = "Ecuador",
-  "ee" = "Estonia",
-  "eg" = "Egypt",
-  "eh" = "the Western Sahara",
-  "er" = "Eritrea",
-  "es" = "Spain",
-  "et" = "Ethiopia",
-  "fi" = "Finland",
-  "fj" = "Fiji",
-  "fk" = "the Falkland Islands (Malvinas)",
-  "fm" = "the Federated States of Micronesia",
-  "fo" = "the Faroe Islands",
-  "fr" = "France",
-  "fx" = "Metropolitan France",
-  "ga" = "Gabon",
-  "gb" = "the United Kingdom",
-  "gd" = "Grenada",
-  "ge" = "Georgia",
-  "gf" = "French Guiana",
-  "gg" = "Guernsey",
-  "gh" = "Ghana",
-  "gi" = "Gibraltar",
-  "gl" = "Greenland",
-  "gm" = "Gambia",
-  "gn" = "Guinea",
-  "gp" = "Guadeloupe",
-  "gq" = "Equatorial Guinea",
-  "gr" = "Greece",
-  "gs" = "South Georgia and the South Sandwich Islands",
-  "gt" = "Guatemala",
-  "gu" = "Guam",
-  "gw" = "Guinea-Bissau",
-  "gy" = "Guyana",
-  "hk" = "Hong Kong",
-  "hm" = "Heard Island and McDonald Islands",
-  "hn" = "Honduras",
-  "hr" = "Croatia",
-  "ht" = "Haiti",
-  "hu" = "Hungary",
-  "id" = "Indonesia",
-  "ie" = "Ireland",
-  "il" = "Israel",
-  "im" = "the Isle of Man",
-  "in" = "India",
-  "io" = "the British Indian Ocean Territory",
-  "iq" = "Iraq",
-  "ir" = "Iran",
-  "is" = "Iceland",
-  "it" = "Italy",
-  "je" = "Jersey",
-  "jm" = "Jamaica",
-  "jo" = "Jordan",
-  "jp" = "Japan",
-  "ke" = "Kenya",
-  "kg" = "Kyrgyzstan",
-  "kh" = "Cambodia",
-  "ki" = "Kiribati",
-  "km" = "Comoros",
-  "kn" = "Saint Kitts and Nevis",
-  "kp" = "North Korea",
-  "kr" = "the Republic of Korea",
-  "kw" = "Kuwait",
-  "ky" = "the Cayman Islands",
-  "kz" = "Kazakhstan",
-  "la" = "Laos",
-  "lb" = "Lebanon",
-  "lc" = "Saint Lucia",
-  "li" = "Liechtenstein",
-  "lk" = "Sri Lanka",
-  "lr" = "Liberia",
-  "ls" = "Lesotho",
-  "lt" = "Lithuania",
-  "lu" = "Luxembourg",
-  "lv" = "Latvia",
-  "ly" = "Libya",
-  "ma" = "Morocco",
-  "mc" = "Monaco",
-  "md" = "the Republic of Moldova",
-  "me" = "Montenegro",
-  "mf" = "Saint Martin",
-  "mg" = "Madagascar",
-  "mh" = "the Marshall Islands",
-  "mk" = "Macedonia",
-  "ml" = "Mali",
-  "mm" = "Burma",
-  "mn" = "Mongolia",
-  "mo" = "Macau",
-  "mp" = "the Northern Mariana Islands",
-  "mq" = "Martinique",
-  "mr" = "Mauritania",
-  "ms" = "Montserrat",
-  "mt" = "Malta",
-  "mu" = "Mauritius",
-  "mv" = "the Maldives",
-  "mw" = "Malawi",
-  "mx" = "Mexico",
-  "my" = "Malaysia",
-  "mz" = "Mozambique",
-  "na" = "Namibia",
-  "nc" = "New Caledonia",
-  "ne" = "Niger",
-  "nf" = "Norfolk Island",
-  "ng" = "Nigeria",
-  "ni" = "Nicaragua",
-  "nl" = "the Netherlands",
-  "no" = "Norway",
-  "np" = "Nepal",
-  "nr" = "Nauru",
-  "nu" = "Niue",
-  "nz" = "New Zealand",
-  "om" = "Oman",
-  "pa" = "Panama",
-  "pe" = "Peru",
-  "pf" = "French Polynesia",
-  "pg" = "Papua New Guinea",
-  "ph" = "the Philippines",
-  "pk" = "Pakistan",
-  "pl" = "Poland",
-  "pm" = "Saint Pierre and Miquelon",
-  "pn" = "the Pitcairn Islands",
-  "pr" = "Puerto Rico",
-  "ps" = "the Palestinian Territory",
-  "pt" = "Portugal",
-  "pw" = "Palau",
-  "py" = "Paraguay",
-  "qa" = "Qatar",
-  "re" = "Reunion",
-  "ro" = "Romania",
-  "rs" = "Serbia",
-  "ru" = "Russia",
-  "rw" = "Rwanda",
-  "sa" = "Saudi Arabia",
-  "sb" = "the Solomon Islands",
-  "sc" = "the Seychelles",
-  "sd" = "Sudan",
-  "se" = "Sweden",
-  "sg" = "Singapore",
-  "sh" = "Saint Helena",
-  "si" = "Slovenia",
-  "sj" = "Svalbard and Jan Mayen",
-  "sk" = "Slovakia",
-  "sl" = "Sierra Leone",
-  "sm" = "San Marino",
-  "sn" = "Senegal",
-  "so" = "Somalia",
-  "sr" = "Suriname",
-  "ss" = "South Sudan",
-  "st" = "São Tomé and Príncipe",
-  "sv" = "El Salvador",
-  "sy" = "the Syrian Arab Republic",
-  "sz" = "Swaziland",
-  "tc" = "Turks and Caicos Islands",
-  "td" = "Chad",
-  "tf" = "the French Southern Territories",
-  "tg" = "Togo",
-  "th" = "Thailand",
-  "tj" = "Tajikistan",
-  "tk" = "Tokelau",
-  "tl" = "East Timor",
-  "tm" = "Turkmenistan",
-  "tn" = "Tunisia",
-  "to" = "Tonga",
-  "tr" = "Turkey",
-  "tt" = "Trinidad and Tobago",
-  "tv" = "Tuvalu",
-  "tw" = "Taiwan",
-  "tz" = "the United Republic of Tanzania",
-  "ua" = "Ukraine",
-  "ug" = "Uganda",
-  "um" = "the United States Minor Outlying Islands",
-  "us" = "the United States",
-  "uy" = "Uruguay",
-  "uz" = "Uzbekistan",
-  "va" = "Vatican City",
-  "vc" = "Saint Vincent and the Grenadines",
-  "ve" = "Venezuela",
-  "vg" = "the British Virgin Islands",
-  "vi" = "the United States Virgin Islands",
-  "vn" = "Vietnam",
-  "vu" = "Vanuatu",
-  "wf" = "Wallis and Futuna",
-  "ws" = "Samoa",
-  "ye" = "Yemen",
-  "yt" = "Mayotte",
-  "za" = "South Africa",
-  "zm" = "Zambia",
-  "zw" = "Zimbabwe")
-
-countryname <- function(country) {
-  res <- countrylist[[country]]
-  if (is.null(res))
-    res <- "no-man's-land"
-  res
-}
-
-date_breaks <- function(days) {
-  length <- cut(days, c(0, 7, 12, 56, 180, 600, 5000, Inf), labels=FALSE)
-  major <- c("days", "2 days", "weeks", "months", "3 months", "years",
-    "5 years")[length]
-  minor <- c("10 years", "days", "days", "weeks", "months", "months",
-    "years")[length]
-  format <- c("%d-%b", "%d-%b", "%d-%b", "%b-%Y", "%b-%Y", "%Y",
-    "%Y")[length]
-  list(major = major, minor = minor, format = format)
-}
-
-plot_networksize <- function(start, end, path) {
-  end <- min(end, as.character(Sys.Date() - 2))
-  s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv",
-    stringsAsFactors = FALSE)
-  s <- s[s$date >= start & s$date <= end & s$flag == '' &
-         s$country == '' & s$version == '' & s$platform == '' &
-         s$ec2bridge == '', ]
-  s <- data.frame(date = as.Date(s$date, "%Y-%m-%d"), relays = s$relays,
-                  bridges = s$bridges)
-  dates <- seq(from = as.Date(start, "%Y-%m-%d"),
-      to = as.Date(end, "%Y-%m-%d"), by="1 day")
-  missing <- setdiff(dates, as.Date(s$date, origin = "1970-01-01"))
-  if (length(missing) > 0)
-    s <- rbind(s,
-        data.frame(date = as.Date(missing, origin = "1970-01-01"),
-        relays = NA, bridges = NA))
-  networksize <- melt(s, id = "date")
-  date_breaks <- date_breaks(
-    as.numeric(max(as.Date(networksize$date, "%Y-%m-%d")) -
-    min(as.Date(networksize$date, "%Y-%m-%d"))))
-  ggplot(networksize, aes(x = as.Date(date, "%Y-%m-%d"), y = value,
-    colour = variable)) + geom_line(size = 1) +
-    scale_x_date(name = paste("\nThe Tor Project - ",
-        "https://metrics.torproject.org/", sep = ""),
-        format = date_breaks$format, major = date_breaks$major,
-        minor = date_breaks$minor) +
-    scale_y_continuous(name = "", limits = c(0, max(networksize$value,
-        na.rm = TRUE))) +
-    scale_colour_hue("", breaks = c("relays", "bridges"),
-        labels = c("Relays", "Bridges")) +
-    opts(title = "Number of relays\n")
-  ggsave(filename = path, width = 8, height = 5, dpi = 72)
-}
-
-plot_cloudbridges <- function(start, end, path) {
-  end <- min(end, as.character(Sys.Date() - 2))
-  s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv",
-    stringsAsFactors = FALSE)
-  s <- s[s$date >= start & s$date <= end & s$flag == '' &
-         s$country == '' & s$version == '' & s$platform == '' &
-         s$ec2bridge == 't', ]
-  s <- data.frame(date = as.Date(s$date, "%Y-%m-%d"), bridges = s$bridges)
-  dates <- seq(from = as.Date(start, "%Y-%m-%d"),
-      to = as.Date(end, "%Y-%m-%d"), by="1 day")
-  missing <- setdiff(dates, s$date)
-  if (length(missing) > 0)
-    s <- rbind(s,
-        data.frame(date = as.Date(missing, origin = "1970-01-01"),
-        bridges = NA))
-  date_breaks <- date_breaks(
-    as.numeric(max(as.Date(s$date, "%Y-%m-%d")) -
-    min(as.Date(s$date, "%Y-%m-%d"))))
-  ggplot(s, aes(x = as.Date(date, "%Y-%m-%d"), y = bridges)) +
-    geom_line(size = 1, colour = "green3") +
-    scale_x_date(name = paste("\nThe Tor Project - ",
-        "https://metrics.torproject.org/", sep = ""),
-        format = date_breaks$format, major = date_breaks$major,
-        minor = date_breaks$minor) +
-    scale_y_continuous(name = "", limits = c(0,
-        max(s$bridges, na.rm = TRUE))) +
-    opts(title = "Number of Tor Cloud bridges\n")
-  ggsave(filename = path, width = 8, height = 5, dpi = 72)
-}
-
-plot_relaycountries <- function(start, end, country, path) {
-  end <- min(end, as.character(Sys.Date() - 2))
-  s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv",
-    stringsAsFactors = FALSE)
-  s <- s[s$date >= start & s$date <= end & s$flag == '' &
-         s$country == ifelse(country == "all", '', country) &
-         s$version == '' & s$platform == '' & s$ec2bridge == '', ]
-  s <- data.frame(date = as.Date(s$date, "%Y-%m-%d"), relays = s$relays)
-  dates <- seq(from = as.Date(start, "%Y-%m-%d"),
-      to = as.Date(end, "%Y-%m-%d"), by="1 day")
-  missing <- setdiff(dates, s$date)
-  if (length(missing) > 0)
-    s <- rbind(s,
-        data.frame(date = as.Date(missing, origin = "1970-01-01"),
-        relays = NA))
-  title <- ifelse(country == "all",
-    "Number of relays in all countries\n",
-    paste("Number of relays in ", countryname(country), "\n", sep = ""))
-  formatter <- function(x, ...) { format(x, scientific = FALSE, ...) }
-  date_breaks <- date_breaks(
-    as.numeric(max(as.Date(s$date, "%Y-%m-%d")) -
-    min(as.Date(s$date, "%Y-%m-%d"))))
-  ggplot(s, aes(x = as.Date(date, "%Y-%m-%d"), y = relays)) +
-    geom_line(size = 1) +
-    scale_x_date(name = paste("\nThe Tor Project - ",
-        "https://metrics.torproject.org/", sep = ""),
-        format = date_breaks$format, major = date_breaks$major,
-        minor = date_breaks$minor) +
-    scale_y_continuous(name = "", limits = c(0, max(s$relays,
-        na.rm = TRUE)), formatter = formatter) +
-    opts(title = title)
-  ggsave(filename = path, width = 8, height = 5, dpi = 72)
-}
-
-plot_versions <- function(start, end, path) {
-  end <- min(end, as.character(Sys.Date() - 2))
-  s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv",
-    stringsAsFactors = FALSE)
-  s <- s[s$date >= start & s$date <= end & s$flag == '' &
-         s$country == '' & s$version != '' & s$platform == '' &
-         s$ec2bridge == '', ]
-  s <- data.frame(date = as.Date(s$date, "%Y-%m-%d"), version = s$version,
-                  relays = s$relays)
-  known_versions <- c("0.1.0", "0.1.1", "0.1.2", "0.2.0", "0.2.1",
-        "0.2.2", "0.2.3", "0.2.4", "0.2.5")
-  colours <- data.frame(breaks = known_versions,
-    values = brewer.pal(length(known_versions), "Paired"),
-    stringsAsFactors = FALSE)
-  versions <- s[s$version %in% known_versions, ]
-  visible_versions <- sort(unique(versions$version))
-  date_breaks <- date_breaks(
-    as.numeric(max(as.Date(versions$date, "%Y-%m-%d")) -
-    min(as.Date(versions$date, "%Y-%m-%d"))))
-  ggplot(versions, aes(x = as.Date(date, "%Y-%m-%d"), y = relays,
-      colour = version)) +
-    geom_line(size = 1) +
-    scale_x_date(name = paste("\nThe Tor Project - ",
-        "https://metrics.torproject.org/", sep = ""),
-        format = date_breaks$format, major = date_breaks$major,
-        minor = date_breaks$minor) +
-    scale_y_continuous(name = "",
-      limits = c(0, max(versions$relays, na.rm = TRUE))) +
-    scale_colour_manual(name = "Tor version",
-      values = colours[colours$breaks %in% visible_versions, 2],
-      breaks = visible_versions) +
-    opts(title = "Relay versions\n")
-  ggsave(filename = path, width = 8, height = 5, dpi = 72)
-}
-
-plot_platforms <- function(start, end, path) {
-  end <- min(end, as.character(Sys.Date() - 2))
-  s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv",
-    stringsAsFactors = FALSE)
-  s <- s[s$date >= start & s$date <= end & s$flag == '' &
-         s$country == '' & s$version == '' & s$platform != '' &
-         s$ec2bridge == '', ]
-  platforms <- data.frame(date = as.Date(s$date, "%Y-%m-%d"),
-                  variable = s$platform, value = s$relays)
-  date_breaks <- date_breaks(
-    as.numeric(max(as.Date(platforms$date, "%Y-%m-%d")) -
-    min(as.Date(platforms$date, "%Y-%m-%d"))))
-  ggplot(platforms, aes(x = as.Date(date, "%Y-%m-%d"), y = value,
-      colour = variable)) +
-    geom_line(size = 1) +
-    scale_x_date(name = paste("\nThe Tor Project - ",
-        "https://metrics.torproject.org/", sep = ""),
-        format = date_breaks$format, major = date_breaks$major,
-        minor = date_breaks$minor) +
-    scale_y_continuous(name = "",
-      limits = c(0, max(platforms$value, na.rm = TRUE))) +
-    scale_colour_manual(name = "Platform",
-      breaks = c("Linux", "Darwin", "FreeBSD", "Windows", "Other"),
-      values = c("#E69F00", "#56B4E9", "#009E73", "#0072B2", "#333333")) +
-    opts(title = "Relay platforms\n")
-  ggsave(filename = path, width = 8, height = 5, dpi = 72)
-}
-
-plot_bandwidth <- function(start, end, path) {
-  end <- min(end, as.character(Sys.Date() - 4))
-  b <- read.csv("/srv/metrics.torproject.org/web/stats/bandwidth.csv",
-    stringsAsFactors = FALSE)
-  b <- b[b$date >= start & b$date <= end & b$isexit == '' &
-         b$isguard == '', ]
-  b <- data.frame(date = as.Date(b$date, "%Y-%m-%d"),
-                  bwadv = b$advbw,
-                  bwhist = (b$bwread + b$bwwrite) / 2)
-  bandwidth <- melt(b, id = "date")
-  date_breaks <- date_breaks(
-    as.numeric(max(as.Date(bandwidth$date, "%Y-%m-%d")) -
-    min(as.Date(bandwidth$date, "%Y-%m-%d"))))
-  ggplot(bandwidth, aes(x = as.Date(date, "%Y-%m-%d"), y = value / 2^20,
-      colour = variable)) +
-    geom_line(size = 1) +
-    scale_x_date(name = paste("\nThe Tor Project - ",
-        "https://metrics.torproject.org/", sep = ""),
-        format = date_breaks$format, major = date_breaks$major,
-        minor = date_breaks$minor) +
-    scale_y_continuous(name="Bandwidth (MiB/s)",
-        limits = c(0, max(bandwidth$value, na.rm = TRUE) / 2^20)) +
-    scale_colour_hue(name = "", h.start = 90,
-        breaks = c("bwadv", "bwhist"),
-        labels = c("Advertised bandwidth", "Bandwidth history")) +
-    opts(title = "Total relay bandwidth", legend.position = "top")
-  ggsave(filename = path, width = 8, height = 5, dpi = 72)
-}
-
-plot_bwhist_flags <- function(start, end, path) {
-  end <- min(end, as.character(Sys.Date() - 4))
-  b <- read.csv("/srv/metrics.torproject.org/web/stats/bandwidth.csv",
-    stringsAsFactors = FALSE)
-  b <- b[b$date >= start & b$date <= end & b$isexit != '' &
-         b$isguard != '', ]
-  bw <- data.frame(date = as.Date(b$date, "%Y-%m-%d"),
-                  isexit = b$isexit == 't', isguard = b$isguard == 't',
-                  read = b$bwread, written = b$bwwrite)
-  dates <- seq(from = as.Date(start, "%Y-%m-%d"),
-      to = as.Date(end, "%Y-%m-%d"), by = "1 day")
-  missing <- setdiff(dates, as.Date(bw$date, origin = "1970-01-01"))
-  if (length(missing) > 0)
-    bw <- rbind(bw,
-        data.frame(date = as.Date(missing, origin = "1970-01-01"),
-        isexit = FALSE, isguard = FALSE, read = NA, written = NA),
-        data.frame(date = as.Date(missing, origin = "1970-01-01"),
-        isexit = FALSE, isguard = TRUE, read = NA, written = NA),
-        data.frame(date = as.Date(missing, origin = "1970-01-01"),
-        isexit = TRUE, isguard = FALSE, read = NA, written = NA),
-        data.frame(date = as.Date(missing, origin = "1970-01-01"),
-        isexit = TRUE, isguard = TRUE, read = NA, written = NA))
-  bw <- data.frame(date = bw$date, variable = ifelse(bw$isexit,
-        ifelse(bw$isguard, "Guard & Exit", "Exit only"),
-        ifelse(bw$isguard, "Guard only", "Middle only")),
-        value = (bw$read + bw$written) / 2)
-  date_breaks <- date_breaks(
-    as.numeric(max(as.Date(bw$date, "%Y-%m-%d")) -
-    min(as.Date(bw$date, "%Y-%m-%d"))))
-  ggplot(bw, aes(x = as.Date(date, "%Y-%m-%d"), y = value / 2^20,
-      colour = variable)) +
-    geom_line(size = 1) +
-    scale_x_date(name = paste("\nThe Tor Project - ",
-        "https://metrics.torproject.org/", sep = ""),
-        format = date_breaks$format, major = date_breaks$major,
-        minor = date_breaks$minor) +
-    scale_y_continuous(name="Bandwidth (MiB/s)",
-        limits = c(0, max(bw$value, na.rm = TRUE) / 2^20)) +
-    scale_colour_manual(name = "",
-        values = c("#E69F00", "#56B4E9", "#009E73", "#0072B2")) +
-    opts(title = "Bandwidth history by relay flags",
-        legend.position = "top")
-  ggsave(filename = path, width = 8, height = 5, dpi = 72)
-}
-
-plot_dirbytes <- function(start, end, path) {
-  end <- min(end, as.character(Sys.Date() - 4))
-  b <- read.csv("/srv/metrics.torproject.org/web/stats/bandwidth.csv",
-    stringsAsFactors = FALSE)
-  b <- b[b$date >= start & b$date <= end & b$isexit == '' &
-         b$isguard == '', ]
-  b <- data.frame(date = as.Date(b$date, "%Y-%m-%d"),
-                  dirread = b$dirread, dirwrite = b$dirwrite)
-  dir <- melt(b, id = "date")
-  date_breaks <- date_breaks(
-    as.numeric(max(as.Date(dir$date, "%Y-%m-%d")) -
-    min(as.Date(dir$date, "%Y-%m-%d"))))
-  ggplot(dir, aes(x = as.Date(date, "%Y-%m-%d"), y = value / 2^20,
-      colour = variable)) +
-    geom_line(size = 1) +
-    scale_x_date(name = paste("\nThe Tor Project - ",
-        "https://metrics.torproject.org/", sep = ""),
-        format = date_breaks$format, major = date_breaks$major,
-        minor = date_breaks$minor) +
-    scale_y_continuous(name="Bandwidth (MiB/s)",
-        limits = c(0, max(dir$value, na.rm = TRUE) / 2^20)) +
-    scale_colour_hue(name = "",
-        breaks = c("dirwrite", "dirread"),
-        labels = c("Written dir bytes", "Read dir bytes")) +
-    opts(title = "Number of bytes spent on answering directory requests",
-        legend.position = "top")
-  ggsave(filename = path, width = 8, height = 5, dpi = 72)
-}
-
-plot_relayflags <- function(start, end, flags, path) {
-  end <- min(end, as.character(Sys.Date() - 2))
-  s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv",
-    stringsAsFactors = FALSE)
-  s <- s[s$date >= start & s$date <= end & s$country == '' &
-         s$version == '' & s$platform == '' & s$ec2bridge == '', ]
-  s <- data.frame(date = as.Date(s$date, "%Y-%m-%d"),
-                  variable = ifelse(s$flag == '', 'Running', s$flag),
-                  value = s$relays)
-  networksize <- s[s$variable %in% flags, ]
-  networksize <- rbind(data.frame(
-    date = as.Date(end) + 1,
-    variable = c("Running", "Exit", "Guard", "Fast", "Stable", "HSDir"),
-    value = NA), networksize)
-  dates <- seq(from = as.Date(start, "%Y-%m-%d"),
-      to = as.Date(end, "%Y-%m-%d"), by="1 day")
-  missing <- setdiff(dates, networksize$date)
-  if (length(missing) > 0)
-    networksize <- rbind(data.frame(
-      date = as.Date(rep(missing, 6), origin = "1970-01-01"),
-      variable = c("Running", "Exit", "Guard", "Fast", "Stable", "HSDir"),
-      value = rep(NA, length(missing) * 6)), networksize)
-  date_breaks <- date_breaks(
-    as.numeric(max(as.Date(end, "%Y-%m-%d")) -
-    min(as.Date(networksize$date, "%Y-%m-%d"))))
-  ggplot(networksize, aes(x = as.Date(date, "%Y-%m-%d"), y = value,
-    colour = as.factor(variable))) + geom_line(size = 1) +
-    scale_x_date(name = paste("\nThe Tor Project - ",
-        "https://metrics.torproject.org/", sep = ""),
-          format = date_breaks$format, major = date_breaks$major,
-        minor = date_breaks$minor, limits = as.Date(c(start, end))) +
-    scale_y_continuous(name = "", limits = c(0, max(networksize$value,
-        na.rm = TRUE))) +
-    scale_colour_manual(name = "Relay flags", values = c("#E69F00",
-        "#56B4E9", "#009E73", "#EE6A50", "#000000", "#0072B2"),
-        breaks = flags, labels = flags) +
-    opts(title = "Number of relays with relay flags assigned\n")
-  ggsave(filename = path, width = 8, height = 5, dpi = 72)
-}
-
-plot_torperf <- function(start, end, source, filesize, path) {
-  end <- min(end, as.character(Sys.Date() - 2))
-  size <- ifelse(filesize == '50kb', 50 * 1024,
-          ifelse(filesize == '1mb', 1024 * 1024, 5 * 1024 * 1024))
-  t <- read.csv("/srv/metrics.torproject.org/web/stats/torperf.csv",
-    stringsAsFactors = FALSE)
-  t <- t[t$date >= start & t$date <= end & t$size == size &
-         t$source == ifelse(source == 'all', '', source), ]
-  torperf <- data.frame(date = as.Date(t$date, "%Y-%m-%d"),
-                        q1 = t$q1, md = t$md, q3 = t$q3)
-  dates <- seq(from = as.Date(start, "%Y-%m-%d"),
-      to = as.Date(end, "%Y-%m-%d"), by="1 day")
-  missing <- setdiff(dates, torperf$date)
-  if (length(missing) > 0)
-    torperf <- rbind(torperf,
-        data.frame(date = as.Date(missing, origin = "1970-01-01"),
-        q1 = NA, md = NA, q3 = NA))
-  colours <- data.frame(source = c("all", "siv", "moria", "torperf"),
-      colour = c("#FF8C00", "#0000EE", "#EE0000", "#00CD00"),
-      stringsAsFactors = FALSE)
-  colour <- colours[colours$source == source, "colour"]
-  filesizes <- data.frame(filesizes = c("5mb", "1mb", "50kb"),
-      label = c("5 MiB", "1 MiB", "50 KiB"), stringsAsFactors = FALSE)
-  filesizeStr <- filesizes[filesizes$filesize == filesize, "label"]
-  maxY <- max(torperf$q3, na.rm = TRUE)
-  date_breaks <- date_breaks(
-    as.numeric(max(as.Date(torperf$date, "%Y-%m-%d")) -
-    min(as.Date(torperf$date, "%Y-%m-%d"))))
-  ggplot(torperf, aes(x = as.Date(date, "%Y-%m-%d"), y = md/1e3,
-      fill = "line")) +
-    geom_line(colour = colour, size = 0.75) +
-    geom_ribbon(data = torperf, aes(x = date, ymin = q1/1e3,
-      ymax = q3/1e3, fill = "ribbon")) +
-    scale_x_date(name = paste("\nThe Tor Project - ",
-        "https://metrics.torproject.org/", sep = ""),
-        format = date_breaks$format, major = date_breaks$major,
-        minor = date_breaks$minor) +
-    scale_y_continuous(name = "", limits = c(0, maxY) / 1e3) +
-    scale_fill_manual(name = paste("Measured times on",
-        ifelse(source == "all", "all sources", source), "per day"),
-      breaks = c("line", "ribbon"),
-      labels = c("Median", "1st to 3rd quartile"),
-      values = paste(colour, c("", "66"), sep = "")) +
-    opts(title = paste("Time in seconds to complete", filesizeStr,
-        "request"), legend.position = "top")
-  ggsave(filename = path, width = 8, height = 5, dpi = 72)
-}
-
-plot_torperf_failures <- function(start, end, source, filesize, path) {
-  end <- min(end, as.character(Sys.Date() - 2))
-  size <- ifelse(filesize == '50kb', 50 * 1024,
-          ifelse(filesize == '1mb', 1024 * 1024, 5 * 1024 * 1024))
-  t <- read.csv("/srv/metrics.torproject.org/web/stats/torperf.csv",
-    stringsAsFactors = FALSE)
-  t <- t[t$date >= start & t$date <= end & t$size == size &
-         t$source == ifelse(source == 'all', '', source), ]
-  torperf <- data.frame(date = as.Date(t$date, "%Y-%m-%d"),
-                        timeouts = t$timeouts, failures = t$failures,
-                        requests = t$requests)
-  dates <- seq(from = as.Date(start, "%Y-%m-%d"),
-      to = as.Date(end, "%Y-%m-%d"), by="1 day")
-  missing <- setdiff(dates, torperf$date)
-  if (length(missing) > 0)
-    torperf <- rbind(torperf,
-        data.frame(date = as.Date(missing, origin = "1970-01-01"),
-        timeouts = NA, failures = NA, requests = NA))
-  colours <- data.frame(source = c("all", "siv", "moria", "torperf"),
-      colour = c("#FF8C00", "#0000EE", "#EE0000", "#00CD00"),
-      stringsAsFactors = FALSE)
-  colour <- colours[colours$source == source, "colour"]
-  filesizes <- data.frame(filesizes = c("5mb", "1mb", "50kb"),
-      label = c("5 MiB", "1 MiB", "50 KiB"), stringsAsFactors = FALSE)
-  filesizeStr <- filesizes[filesizes$filesize == filesize, "label"]
-  torperf <- rbind(data.frame(date = torperf$date,
-      value = ifelse(torperf$requests > 0,
-                     torperf$timeouts / torperf$requests, 0),
-      variable = "timeouts"),
-    data.frame(date = torperf$date,
-      value = ifelse(torperf$requests > 0,
-                     torperf$failures / torperf$requests, 0),
-      variable = "failures"))
-  date_breaks <- date_breaks(
-    as.numeric(max(as.Date(torperf$date, "%Y-%m-%d")) -
-    min(as.Date(torperf$date, "%Y-%m-%d"))))
-  ggplot(torperf, aes(x = as.Date(date, "%Y-%m-%d"), y = value,
-    colour = variable)) +
-    geom_point(size = 2) +
-    scale_x_date(name = paste("\nThe Tor Project - ",
-        "https://metrics.torproject.org/", sep = ""),
-        format = date_breaks$format, major = date_breaks$major,
-        minor = date_breaks$minor) +
-    scale_y_continuous(name = "", formatter = "percent") +
-    scale_colour_hue(name = paste("Problems encountered on",
-        ifelse(source == "all", "all sources", source)),
-        h.start = 45, breaks = c("timeouts", "failures"),
-        labels = c("Timeouts", "Failures")) +
-    opts(title = paste("Timeouts and failures of", filesizeStr,
-        "requests"), legend.position = "top")
-  ggsave(filename = path, width = 8, height = 5, dpi = 72)
-}
-
-plot_connbidirect <- function(start, end, path) {
-  end <- min(end, as.character(Sys.Date() - 2))
-  c <- read.csv("/srv/metrics.torproject.org/web/stats/connbidirect.csv",
-    stringsAsFactors = FALSE)
-  c <- c[c$date >= start & c$date <= end &
-         c$read + c$write + c$both > 0, ]
-  c <- data.frame(date = as.Date(c$date, "%Y-%m-%d"),
-                  both = c$both / (c$read + c$write + c$both),
-                  read = c$read / (c$read + c$write + c$both),
-                  write = c$write / (c$read + c$write + c$both))
-  c <- aggregate(list(both = c$both, read = c$read, write = c$write),
-                 by = list(date = c$date), quantile,
-                 probs = c(0.25, 0.5, 0.75))
-  c <- rbind(
-    data.frame(date = as.Date(c$date), data.frame(c$both),
-               variable = "both"),
-    data.frame(date = as.Date(c$date), data.frame(c$write),
-               variable = "write"),
-    data.frame(date = as.Date(c$date), data.frame(c$read),
-               variable = "read"))
-  date_breaks <- date_breaks(
-    as.numeric(max(as.Date(c$date, "%Y-%m-%d")) -
-    min(as.Date(c$date, "%Y-%m-%d"))))
-  ggplot(c, aes(x = date, y = X50., colour = variable)) +
-    geom_line(size = 0.75) +
-    geom_ribbon(aes(x = date, ymin = X25., ymax = X75., fill = variable),
-                alpha = 0.5, legend = FALSE) +
-    scale_x_date(name = paste("\nThe Tor Project - ",
-        "https://metrics.torproject.org/", sep = ""),
-        format = date_breaks$format, major = date_breaks$major,
-        minor = date_breaks$minor) +
-    scale_y_continuous(name = "", formatter = "percent") +
-    scale_colour_hue(name = "Medians and interquartile ranges",
-                     breaks = c("both", "write", "read"),
-        labels = c("Both reading and writing", "Mostly writing",
-                   "Mostly reading")) +
-    scale_fill_hue(name = "Medians and interquartile ranges",
-                   breaks = c("both", "write", "read"),
-        labels = c("Both reading and writing", "Mostly writing",
-                   "Mostly reading")) +
-    opts(title = "Fraction of connections used uni-/bidirectionally\n",
-      legend.position = "top")
-  ggsave(filename = path, width = 8, height = 5, dpi = 72)
-}
-
-plot_fast_exits <- function(start, end, path) {
-  f <- read.csv("/srv/metrics.torproject.org/web/stats/fast-exits.csv",
-    stringsAsFactors = FALSE)
-  f <- f[f$date >= start & f$date <= end, ]
-  f <- data.frame(date = as.Date(f$date, "%Y-%m-%d"),
-                  relays = f$fastnum, P_exit = f$fastprob)
-  r <- melt(f, id.vars = c("date"))
-  r <- data.frame(r, type = ifelse(r$variable == "P_exit",
-    "Total exit probability (in %)", "Number of relays"))
-  ggplot(r, aes(x = date, y = value)) +
-  geom_line(colour = "purple", size = 0.75) +
-  facet_grid(type ~ ., scales = "free_y") +
-  scale_x_date(name = "") +
-  scale_y_continuous(name = "") +
-  scale_colour_manual(values = c("purple", "orange")) +
-  opts(title = paste("Fast exits (95+ Mbit/s configured bandwidth ",
-    "rate,\n5000+ KB/s advertised bandwidth capacity,\n",
-    "exit to ports 80, 443, 554, and 1755,\n",
-    "at most 2 relays per /24 network)\n", sep = ""))
-  ggsave(filename = path, width = 8, height = 6, dpi = 72)
-}
-
-plot_almost_fast_exits <- function(start, end, path) {
-  f <- read.csv("/srv/metrics.torproject.org/web/stats/fast-exits.csv",
-    stringsAsFactors = FALSE)
-  f <- f[f$date >= start & f$date <= end, ]
-  f <- melt(f, id.vars = c("date"))
-  t <- data.frame(date = as.Date(f$date, "%Y-%m-%d"),
-       var = ifelse(f$variable == 'fastnum' | f$variable == 'almostnum',
-             "Number of relays", "Total exit probability (in %)"),
-       variable = ifelse(f$variable == 'fastnum' |
-                  f$variable == 'fastprob', "fast", "almost fast"),
-       value = floor(f$value))
-  t <- data.frame(t, type = ifelse(t$variable == "fast",
-    "fast exits (95+ Mbit/s, 5000+ KB/s, 80/443/554/1755, 2- per /24",
-    paste("almost fast exits (80+ Mbit/s, 2000+ KB/s, 80/443,",
-    "not in set of fast exits)")))
-  ggplot(t, aes(x = date, y = value, colour = type)) +
-  geom_line(size = 0.75) +
-  facet_grid(var ~ ., scales = "free_y") +
-  scale_x_date(name = "") +
-  scale_y_continuous(name = "") +
-  scale_colour_manual(name = "", values = c("orange", "purple")) +
-  opts(title = "Relays almost meeting the fast-exit requirements",
-    legend.position = "top")
-  ggsave(filename = path, width = 8, height = 6, dpi = 72)
-}
-
-plot_bandwidth_flags <- function(start, end, path) {
-  end <- min(end, as.character(Sys.Date() - 4))
-  b <- read.csv("/srv/metrics.torproject.org/web/stats/bandwidth.csv",
-    stringsAsFactors = FALSE)
-  b <- b[b$date >= start & b$date <= end & b$isexit != '' &
-         b$isguard != '', ]
-  b <- data.frame(date = as.Date(b$date, "%Y-%m-%d"),
-                  isexit = b$isexit == 't', isguard = b$isguard == 't',
-                  advbw = b$advbw,
-                  bwhist = floor((b$bwread + b$bwwrite) / 2))
-  b <- rbind(
-    data.frame(b[b$isguard == TRUE, ], flag = "Guard"),
-    data.frame(b[b$isexit == TRUE, ], flag = "Exit"))
-  b <- data.frame(date = b$date, advbw = b$advbw, bwhist = b$bwhist,
-                  flag = b$flag)
-  b <- aggregate(list(advbw = b$advbw, bwhist = b$bwhist),
-                 by = list(date = b$date, flag = b$flag), FUN = sum,
-                 na.rm = TRUE, na.action = NULL)
-  b <- melt(b, id.vars = c("date", "flag"))
-  b <- data.frame(date = b$date,
-      type = ifelse(b$variable == 'advbw', 'advertised bandwidth',
-                    'bandwidth history'),
-      flag = b$flag, value = b$value)
-  bandwidth <- b[b$value > 0, ]
-  date_breaks <- date_breaks(
-    as.numeric(max(as.Date(bandwidth$date, "%Y-%m-%d")) -
-    min(as.Date(bandwidth$date, "%Y-%m-%d"))))
-  dates <- seq(from = as.Date(start, "%Y-%m-%d"),
-      to = as.Date(end, "%Y-%m-%d"), by = "1 day")
-  missing <- setdiff(dates, as.Date(bandwidth$date,
-    origin = "1970-01-01"))
-  if (length(missing) > 0) {
-    bandwidth <- rbind(bandwidth,
-        data.frame(date = as.Date(missing, origin = "1970-01-01"),
-        type = "advertised bandwidth", flag = "Exit", value = NA),
-        data.frame(date = as.Date(missing, origin = "1970-01-01"),
-        type = "bandwidth history", flag = "Exit", value = NA),
-        data.frame(date = as.Date(missing, origin = "1970-01-01"),
-        type = "advertised bandwidth", flag = "Guard", value = NA),
-        data.frame(date = as.Date(missing, origin = "1970-01-01"),
-        type = "bandwidth history", flag = "Guard", value = NA))
-  }
-  bandwidth <- data.frame(date = bandwidth$date,
-    variable = as.factor(paste(bandwidth$flag, ", ", bandwidth$type,
-    sep = "")), value = bandwidth$value)
-  bandwidth$variable <- factor(bandwidth$variable,
-    levels = levels(bandwidth$variable)[c(3, 4, 1, 2)])
-  ggplot(bandwidth, aes(x = as.Date(date, "%Y-%m-%d"), y = value / 2^20,
-      colour = variable)) +
-    geom_line(size = 1) +
-    scale_x_date(name = paste("\nThe Tor Project - ",
-        "https://metrics.torproject.org/", sep = ""),
-        format = date_breaks$format, major = date_breaks$major,
-        minor = date_breaks$minor) +
-    scale_y_continuous(name="Bandwidth (MiB/s)",
-        limits = c(0, max(bandwidth$value, na.rm = TRUE) / 2^20)) +
-    scale_colour_manual(name = "",
-        values = c("#E69F00", "#D6C827", "#009E73", "#00C34F")) +
-    opts(title = paste("Advertised bandwidth and bandwidth history by",
-        "relay flags"), legend.position = "top")
-  ggsave(filename = path, width = 8, height = 5, dpi = 72)
-}
-
-plot_userstats <- function(start, end, node, variable, value, events,
-                           path) {
-  end <- min(end, as.character(Sys.Date() - 2))
-  c <- read.csv("/srv/metrics.torproject.org/web/stats/clients.csv",
-    stringsAsFactors = FALSE)
-  u <- c[c$date >= start & c$date <= end, ]
-  if (node == 'relay') {
-    if (value != 'all') {
-      u <- u[u$country == value & u$node == 'relay', ]
-      title <- paste("Directly connecting users from ",
-                     countryname(value), "\n", sep = "")
-    } else {
-      u <- u[u$country == '' & u$transport == '' & u$version == '' &
-             u$node == 'relay', ]
-      title <- "Directly connecting users\n"
-    }
-  } else if (variable == 'transport') {
-    u <- u[u$transport == value & u$node == 'bridge', ]
-    title <- paste("Bridge users using transport ", value, "\n",
-                   sep = "")
-  } else if (variable == 'version') {
-    u <- u[u$version== value & u$node == 'bridge', ]
-    title <- paste("Bridge users using IP", value, "\n", sep = "")
-  } else {
-    if (value != 'all') {
-      u <- u[u$country == value & u$node == 'bridge', ]
-      title <- paste("Bridge users from ", countryname(value),
-                     "\n", sep = "")
-    } else {
-      u <- u[u$country == '' & u$transport == '' & u$version == '' &
-             u$node == 'bridge', ]
-      title <- "Bridge users\n"
-    }
-  }
-  u <- data.frame(date = as.Date(u$date, "%Y-%m-%d"), users = u$clients,
-                  lower = u$lower, upper = u$upper)
-  dates <- seq(from = as.Date(start, "%Y-%m-%d"),
-      to = as.Date(end, "%Y-%m-%d"), by="1 day")
-  missing <- setdiff(dates, u$date)
-  if (length(missing) > 0) {
-    u <- rbind(u,
-        data.frame(date = as.Date(missing, origin = "1970-01-01"),
-        users = NA, lower = NA, upper = NA))
-  }
-  formatter <- function(x, ...) { format(x, scientific = FALSE, ...) }
-  date_breaks <- date_breaks(
-    as.numeric(max(u$date) - min(u$date)))
-  max_y <- ifelse(length(na.omit(u$users)) == 0, 0,
-      max(u$users, na.rm = TRUE))
-  plot <- ggplot(u, aes(x = date, y = users))
-  if (length(na.omit(u$users)) > 0 & events != "off" &
-      variable == 'country' & value != "all") {
-    upturns <- u[u$users > u$upper, c("date", "users")]
-    downturns <- u[u$users <= u$lower, c("date", "users")]
-    if (events == "on") {
-      if (length(u$upper) > 0)
-        max_y <- max(max_y, max(u$upper, na.rm = TRUE))
-      u[!is.na(u$lower) & u$lower < 0, "lower"] <- 0
-      plot <- plot +
-        geom_ribbon(aes(ymin = lower, ymax = upper), fill = "gray")
-    }
-    if (length(upturns$date) > 0)
-      plot <- plot +
-          geom_point(data = upturns, aes(x = date, y = users), size = 5,
-          colour = "dodgerblue2")
-    if (length(downturns$date) > 0)
-      plot <- plot +
-          geom_point(data = downturns, aes(x = date, y = users), size = 5,
-          colour = "firebrick2")
-  }
-  plot <- plot +
-    geom_line(size = 1) +
-    scale_x_date(name = paste("\nThe Tor Project - ",
-        "https://metrics.torproject.org/", sep = ""),
-        format = date_breaks$format, major = date_breaks$major,
-        minor = date_breaks$minor) +
-    scale_y_continuous(name = "", limits = c(0, max_y),
-        formatter = formatter) +
-    opts(title = title)
-  ggsave(filename = path, width = 8, height = 5, dpi = 72)
-}
-
-plot_userstats_relay_country <- function(start, end, country, events,
-    path) {
-  plot_userstats(start, end, 'relay', 'country', country, events, path)
-}
-
-plot_userstats_bridge_country <- function(start, end, country, path) {
-  plot_userstats(start, end, 'bridge', 'country', country, 'off', path)
-}
-
-plot_userstats_bridge_transport <- function(start, end, transport, path) {
-  plot_userstats(start, end, 'bridge', 'transport', transport, 'off',
-    path)
-}
-
-plot_userstats_bridge_version <- function(start, end, version, path) {
-  plot_userstats(start, end, 'bridge', 'version', version, 'off', path)
-}
-
diff --git a/rserve/rserve-init.R b/rserve/rserve-init.R
deleted file mode 100644
index 7a87b16..0000000
--- a/rserve/rserve-init.R
+++ /dev/null
@@ -1,14 +0,0 @@
-##Pre-loaded libraries and graphing functions to speed things up
-
-library("ggplot2")
-library("proto")
-library("grid")
-library("reshape")
-library("plyr")
-library("digest")
-library("RColorBrewer")
-
-source('graphs.R')
-source('csv.R')
-source('tables.R')
-
diff --git a/rserve/shutdown.sh b/rserve/shutdown.sh
deleted file mode 100755
index dd5ecd8..0000000
--- a/rserve/shutdown.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/bin/sh
-
-echo "library(Rserve)
-c <- RSconnect()
-RSshutdown(c)" | R --slave
diff --git a/rserve/start.sh b/rserve/start.sh
deleted file mode 100755
index 0979817..0000000
--- a/rserve/start.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/sh
-dir=`pwd`
-R CMD /home/metrics/R/x86_64-pc-linux-gnu-library/2.11/Rserve/libs/Rserve-bin.so --no-save --RS-conf $dir/Rserv.conf >> rserve.log 2>&1
diff --git a/rserve/tables.R b/rserve/tables.R
deleted file mode 100644
index 091a4de..0000000
--- a/rserve/tables.R
+++ /dev/null
@@ -1,55 +0,0 @@
-countrynames <- function(countries) {
-  sapply(countries, countryname)
-}
-
-write_userstats <- function(start, end, node, path) {
-  end <- min(end, as.character(Sys.Date()))
-  c <- read.csv("/srv/metrics.torproject.org/web/stats/clients.csv",
-    stringsAsFactors = FALSE)
-  c <- c[c$date >= start & c$date <= end & c$country != '' &
-         c$transport == '' & c$version == '' & c$node == node, ]
-  u <- data.frame(country = c$country, users = c$clients,
-                  stringsAsFactors = FALSE)
-  u <- aggregate(list(users = u$users), by = list(country = u$country),
-                 mean)
-  total <- sum(u$users)
-  u <- u[!(u$country %in% c("zy", "??", "a1", "a2", "o1", "ap", "eu")), ]
-  u <- u[order(u$users, decreasing = TRUE), ]
-  u <- u[1:10, ]
-  u <- data.frame(
-    cc = as.character(u$country),
-    country = sub('the ', '', countrynames(as.character(u$country))),
-    abs = round(u$users),
-    rel = round(100 * u$users / total, 2))
-  write.csv(u, path, quote = FALSE, row.names = FALSE)
-}
-
-write_userstats_relay <- function(start, end, path) {
-  write_userstats(start, end, 'relay', path)
-}
-
-write_userstats_bridge <- function(start, end, path) {
-  write_userstats(start, end, 'bridge', path)
-}
-
-write_userstats_censorship_events <- function(start, end, path) {
-  end <- min(end, as.character(Sys.Date()))
-  c <- read.csv("/srv/metrics.torproject.org/web/stats/clients.csv",
-    stringsAsFactors = FALSE)
-  c <- c[c$date >= start & c$date <= end & c$country != '' &
-         c$transport == '' & c$version == '' & c$node == 'relay', ]
-  r <- data.frame(date = c$date, country = c$country,
-                  upturn = ifelse(c$clients > c$upper, 1, 0),
-                  downturn = ifelse(c$clients <= c$lower, 1, 0))
-  r <- aggregate(r[, c("upturn", "downturn")],
-    by = list(country = r$country), sum)
-  r <- r[!(r$country %in% c("zy", "??", "a1", "a2", "o1", "ap", "eu")), ]
-  r <- r[order(r$downturn, r$upturn, decreasing = TRUE), ]
-  r <- r[1:10, ]
-  r <- data.frame(cc = r$country,
-    country = sub('the ', '', countrynames(as.character(r$country))),
-    downturns = r$downturn,
-    upturns = r$upturn)
-  write.csv(r, path, quote = FALSE, row.names = FALSE)
-}
-
diff --git a/src/org/torproject/ernie/status/doctor/ConsensusHealthServlet.java b/src/org/torproject/ernie/status/doctor/ConsensusHealthServlet.java
deleted file mode 100644
index 330708f..0000000
--- a/src/org/torproject/ernie/status/doctor/ConsensusHealthServlet.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/* Copyright 2011, 2012 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.ernie.status.doctor;
-
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-public class ConsensusHealthServlet extends HttpServlet {
-
-  private static final long serialVersionUID = -5230032733057814869L;
-
-  public void doGet(HttpServletRequest request,
-      HttpServletResponse response) throws IOException,
-      ServletException {
-
-    /* Read file from disk and write it to response. */
-    BufferedInputStream input = null;
-    BufferedOutputStream output = null;
-    try {
-      File f = new File("/srv/metrics.torproject.org/ernie/website/"
-          + "consensus-health.html");
-      if (!f.exists()) {
-        response.sendError(HttpServletResponse.SC_NOT_FOUND);
-        return;
-      }
-      response.setContentType(this.getServletContext().getMimeType(f.getName()));
-      response.setHeader("Content-Length", String.valueOf(
-          f.length()));
-      response.setHeader("Content-Disposition",
-          "inline; filename=\"" + f.getName() + "\"");
-      input = new BufferedInputStream(new FileInputStream(f),
-          1024);
-      output = new BufferedOutputStream(response.getOutputStream(), 1024);
-      byte[] buffer = new byte[1024];
-      int length;
-      while ((length = input.read(buffer)) > 0) {
-          output.write(buffer, 0, length);
-      }
-    } finally {
-      if (output != null) {
-        output.close();
-      }
-      if (input != null) {
-        input.close();
-      }
-    }
-  }
-}
-
diff --git a/src/org/torproject/ernie/status/exonerator/ExoneraTorServlet.java b/src/org/torproject/ernie/status/exonerator/ExoneraTorServlet.java
deleted file mode 100644
index d37b9a8..0000000
--- a/src/org/torproject/ernie/status/exonerator/ExoneraTorServlet.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/* Copyright 2011, 2012 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.ernie.status.exonerator;
-
-import java.io.IOException;
-
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-public class ExoneraTorServlet extends HttpServlet {
-
-  private static final long serialVersionUID = -6227541092325776626L;
-
-  public void doGet(HttpServletRequest request,
-      HttpServletResponse response) throws IOException, ServletException {
-
-    /* Forward the request to the JSP that does all the hard work. */
-    request.getRequestDispatcher("WEB-INF/exonerator.jsp").forward(
-        request, response);
-  }
-}
-
diff --git a/src/org/torproject/ernie/status/relaysearch/RelaySearchServlet.java b/src/org/torproject/ernie/status/relaysearch/RelaySearchServlet.java
deleted file mode 100644
index b7c8291..0000000
--- a/src/org/torproject/ernie/status/relaysearch/RelaySearchServlet.java
+++ /dev/null
@@ -1,535 +0,0 @@
-/* Copyright 2011, 2012 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.ernie.status.relaysearch;
-
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.math.BigInteger;
-import java.sql.Connection;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.SortedMap;
-import java.util.SortedSet;
-import java.util.TimeZone;
-import java.util.TreeMap;
-import java.util.TreeSet;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import java.util.regex.Pattern;
-
-import javax.naming.Context;
-import javax.naming.InitialContext;
-import javax.naming.NamingException;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.sql.DataSource;
-
-import org.apache.commons.codec.binary.Base64;
-
-/**
- * Web page that allows users to search for relays in the descriptor
- * archives.
- *
- * Possible search terms for testing:
- * - gabelmoo
- * - gabelmoo 2010-09
- * - gabelmoo 2010-09-18
- * - gabelmoo $F2044413DAC2E02E3D6BCF4735A19BCA1DE97281
- * - gabelmoo 80.190.246
- * - gabelmoo $F2044413DAC2E02E3D6BCF4735A19BCA1DE97281 80.190.246
- * - 5898549205 dc737cc9dca16af6 79.212.74.45
- * - 5898549205 dc737cc9dca16af6
- * - 80.190.246.100
- * - $F2044413DAC2E02E3D6BCF4735A19BCA1DE97281
- * - $F2044413DAC2E02E3D6BCF4735A19BCA1DE97281 80.190.246
- * - 58985492
- * - 58985492 79.212.74.45
- */
-public class RelaySearchServlet extends HttpServlet {
-
-  private static final long serialVersionUID = -1772662230310611805L;
-
-  private Pattern alphaNumDotDashDollarSpacePattern =
-      Pattern.compile("[A-Za-z0-9\\.\\-$ ]+");
-
-  private Pattern numPattern = Pattern.compile("[0-9]+");
-
-  private Pattern hexPattern = Pattern.compile("[A-Fa-f0-9]+");
-
-  private Pattern alphaNumPattern = Pattern.compile("[A-Za-z0-9]+");
-
-  private SimpleDateFormat dayFormat = new SimpleDateFormat("yyyy-MM-dd");
-
-  private SimpleDateFormat monthFormat = new SimpleDateFormat("yyyy-MM");
-
-  private SimpleDateFormat dateTimeFormat =
-      new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
-
-  private long minValidAfterMillis;
-
-  private DataSource ds;
-
-  private Logger logger;
-
-  public void init() {
-
-    /* Initialize logger. */
-    this.logger = Logger.getLogger(RelaySearchServlet.class.toString());
-
-    /* Initialize date format parsers. */
-    dayFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
-    monthFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
-    dateTimeFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
-
-    /* Look up data source. */
-    try {
-      Context cxt = new InitialContext();
-      this.ds = (DataSource) cxt.lookup("java:comp/env/jdbc/tordir");
-      this.logger.info("Successfully looked up data source.");
-    } catch (NamingException e) {
-      this.logger.log(Level.WARNING, "Could not look up data source", e);
-    }
-
-    /* Look up first consensus in the database. */
-    try {
-      long requestedConnection = System.currentTimeMillis();
-      Connection conn = this.ds.getConnection();
-      String query = "SELECT MIN(validafter) AS first FROM consensus";
-      Statement statement = conn.createStatement();
-      ResultSet rs = statement.executeQuery(query);
-      if (rs.next()) {
-        this.minValidAfterMillis = rs.getTimestamp(1).getTime();
-      }
-      rs.close();
-      statement.close();
-      conn.close();
-      this.logger.info("Returned a database connection to the pool "
-          + "after " + (System.currentTimeMillis()
-          - requestedConnection) + " millis.");
-    } catch (SQLException e) {
-      this.logger.log(Level.WARNING, "Could not look up first consensus "
-          + "valid-after time in the database.", e);
-    }
-  }
-
-  public void doGet(HttpServletRequest request,
-      HttpServletResponse response) throws IOException,
-      ServletException {
-
-    /* Read search parameter. If we don't have a search parameter, we're
-     * done here. */
-    String searchParameter = request.getParameter("search");
-    if (searchParameter == null || searchParameter.length() == 0) {
-      request.getRequestDispatcher("WEB-INF/relay-search.jsp").forward(
-          request, response);
-      return;
-    }
-
-    /* Parse search parameter to identify what nickname, fingerprint,
-     * and/or IP address to search for. A valid query contains no more
-     * than one identifier for each of the fields. As a special case,
-     * there are search terms consisting of 8 to 19 hex characters that
-     * can be either a nickname or a fingerprint. */
-    String searchNickname = "";
-    String searchFingerprint = "";
-    String searchIPAddress = "";
-    SortedSet<String> searchDays = new TreeSet<String>();
-    SortedSet<String> searchMonths = new TreeSet<String>();
-    SortedSet<Long> searchDayTimestamps = new TreeSet<Long>();
-    SortedSet<Long> searchMonthTimestamps = new TreeSet<Long>();
-    boolean validQuery = false;
-
-    /* Only parse search parameter if it contains nothing else than
-     * alphanumeric characters, dots, and spaces. */
-    if (alphaNumDotDashDollarSpacePattern.matcher(searchParameter).
-        matches()) {
-      SortedSet<String> searchTerms = new TreeSet<String>();
-      if (searchParameter.trim().contains(" ")) {
-        String[] split = searchParameter.trim().split(" ");
-        for (int i = 0; i < split.length; i++) {
-          if (split[i].length() > 0) {
-            searchTerms.add(split[i]);
-          }
-        }
-      } else {
-        searchTerms.add(searchParameter.trim());
-      }
-
-      /* Parse each search term separately. */
-      for (String searchTerm : searchTerms) {
-
-        /* If the search term contains a dot, it can only be an IP
-         * address. */
-        if (searchTerm.contains(".") && !searchTerm.startsWith(".")) {
-          String[] octets = searchTerm.split("\\.");
-          if (searchIPAddress.length() > 0 || octets.length < 2 ||
-              octets.length > 4) {
-            validQuery = false;
-            break;
-          }
-          boolean invalidOctet = false;
-          StringBuilder sb = new StringBuilder();
-          for (int i = 0; i < octets.length; i++) {
-            if (!numPattern.matcher(octets[i]).matches() ||
-                octets[i].length() > 3 ||
-                Integer.parseInt(octets[i]) > 255) {
-              invalidOctet = true;
-              break;
-            } else {
-              sb.append("." + Integer.parseInt(octets[i]));
-            }
-          }
-          if (invalidOctet) {
-            validQuery = false;
-            break;
-          }
-          if (octets.length < 4) {
-            sb.append(".");
-          }
-          searchIPAddress = sb.toString().substring(1);
-          validQuery = true;
-        }
-
-        /* If the search term contains hyphens, it must be a month or a
-         * day. */
-        else if (searchTerm.contains("-") &&
-            searchTerm.startsWith("20")) {
-          try {
-            if (searchTerm.length() == 10) {
-              searchDayTimestamps.add(dayFormat.parse(searchTerm).
-                  getTime());
-              searchDays.add(searchTerm);
-            } else if (searchTerm.length() == 7) {
-              searchMonthTimestamps.add(monthFormat.parse(searchTerm).
-                  getTime());
-              searchMonths.add(searchTerm);
-            } else {
-              validQuery = false;
-              break;
-            }
-          } catch (ParseException e) {
-            validQuery = false;
-            break;
-          }
-        }
-
-        /* If the search term starts with a $ followed by 8 to 40 hex
-         * characters, it must be a fingerprint. */
-        else if ((searchTerm.length() >= 9 && searchTerm.length() <= 41 &&
-            searchTerm.startsWith("$") &&
-            hexPattern.matcher(searchTerm.substring(1)).matches()) ||
-            (searchTerm.length() > 19 && searchTerm.length() <= 40 &&
-            !searchTerm.startsWith("$") &&
-            hexPattern.matcher(searchTerm).matches())) {
-          if (searchFingerprint.length() > 0) {
-            validQuery = false;
-            break;
-          }
-          searchFingerprint = searchTerm.substring(
-              (searchTerm.startsWith("$") ? 1 : 0));
-          validQuery = true;
-        }
-
-        /* If the search term contains up to 19 alphanumerical characters,
-         * it must be a nickname. */
-        else if (searchTerm.length() <= 19 &&
-            alphaNumPattern.matcher(searchTerm).matches()) {
-          if (searchNickname.length() > 0) {
-            validQuery = false;
-            break;
-          }
-          searchNickname = searchTerm;
-          validQuery = true;
-        }
-
-        /* We didn't recognize this search term. */
-        else {
-          validQuery = false;
-          break;
-        }
-      }
-    }
-
-    /* We only accept at most one month or three days, but not both, or
-     * people could accidentally keep the database busy. */
-    if (searchDays.size() > 3 || searchMonths.size() > 1 ||
-        (searchMonths.size() == 1 && searchDays.size() > 0)) {
-      validQuery = false;
-    }
-
-    /* If the query is invalid, stop here. */
-    if (!validQuery) {
-      request.setAttribute("invalidQuery", "Query is invalid.");
-      request.getRequestDispatcher("WEB-INF/relay-search.jsp").
-          forward(request, response);
-      return;
-    }
-
-    /* Look up last consensus in the database. */
-    long maxValidAfterMillis = -1L;
-    try {
-      long requestedConnection = System.currentTimeMillis();
-      Connection conn = this.ds.getConnection();
-      String query = "SELECT MAX(validafter) AS last FROM consensus";
-      Statement statement = conn.createStatement();
-      ResultSet rs = statement.executeQuery(query);
-      if (rs.next()) {
-        maxValidAfterMillis = rs.getTimestamp(1).getTime();
-      }
-      rs.close();
-      statement.close();
-      conn.close();
-      this.logger.info("Returned a database connection to the pool "
-          + "after " + (System.currentTimeMillis()
-          - requestedConnection) + " millis.");
-    } catch (SQLException e) {
-      this.logger.log(Level.WARNING, "Could not look up last consensus "
-          + "valid-after time in the database.", e);
-    }
-
-    /* Prepare a string that says what we're searching for. */
-    List<String> recognizedSearchTerms = new ArrayList<String>();
-    if (searchNickname.length() > 0) {
-      recognizedSearchTerms.add("nickname <b>" + searchNickname + "</b>");
-    }
-    if (searchFingerprint.length() > 0) {
-      recognizedSearchTerms.add("fingerprint <b>" + searchFingerprint
-          + "</b>");
-    }
-    if (searchIPAddress.length() > 0) {
-      recognizedSearchTerms.add("IP address <b>" + searchIPAddress
-          + "</b>");
-    }
-    List<String> recognizedIntervals = new ArrayList<String>();
-    for (String searchTerm : searchMonths) {
-      recognizedIntervals.add("in <b>" + searchTerm + "</b>");
-    }
-    for (String searchTerm : searchDays) {
-      recognizedIntervals.add("on <b>" + searchTerm + "</b>");
-    }
-    StringBuilder searchNoticeBuilder = new StringBuilder();
-    if (maxValidAfterMillis > 0L) {
-      searchNoticeBuilder.append("Most recent consensus in database is "
-          + "from " + dateTimeFormat.format(maxValidAfterMillis)
-          + ".</p><p>");
-    }
-    searchNoticeBuilder.append("Searching for relays with ");
-    if (recognizedSearchTerms.size() == 1) {
-      searchNoticeBuilder.append(recognizedSearchTerms.get(0));
-    } else if (recognizedSearchTerms.size() == 2) {
-      searchNoticeBuilder.append(recognizedSearchTerms.get(0) + " and "
-          + recognizedSearchTerms.get(1));
-    } else {
-      for (int i = 0; i < recognizedSearchTerms.size() - 1; i++) {
-        searchNoticeBuilder.append(recognizedSearchTerms.get(i) + ", ");
-      }
-      searchNoticeBuilder.append("and " + recognizedSearchTerms.get(
-          recognizedSearchTerms.size() - 1));
-    }
-    if (recognizedIntervals.size() == 1) {
-      searchNoticeBuilder.append(" running "
-          + recognizedIntervals.get(0));
-    } else if (recognizedIntervals.size() == 2) {
-      searchNoticeBuilder.append(" running " + recognizedIntervals.get(0)
-          + " and/or " + recognizedIntervals.get(1));
-    } else if (recognizedIntervals.size() > 2) {
-      searchNoticeBuilder.append(" running ");
-      for (int i = 0; i < recognizedIntervals.size() - 1; i++) {
-      searchNoticeBuilder.append(recognizedIntervals.get(i) + ", ");
-      }
-      searchNoticeBuilder.append("and/or " + recognizedIntervals.get(
-          recognizedIntervals.size() - 1));
-    }
-    searchNoticeBuilder.append(" ...");
-    String searchNotice = searchNoticeBuilder.toString();
-    request.setAttribute("searchNotice", searchNotice);
-
-    /* Prepare the query string. */
-    StringBuilder conditionBuilder = new StringBuilder();
-    boolean addAnd = false;
-    if (searchNickname.length() > 0) {
-      conditionBuilder.append((addAnd ? "AND " : "")
-          + "LOWER(nickname) LIKE '" + searchNickname.toLowerCase()
-          + "%' ");
-      addAnd = true;
-    }
-    if (searchFingerprint.length() > 0) {
-      conditionBuilder.append((addAnd ? "AND " : "")
-          + "fingerprint LIKE '" + searchFingerprint.toLowerCase()
-          + "%' ");
-      addAnd = true;
-    }
-    if (searchIPAddress.length() > 0) {
-      conditionBuilder.append((addAnd ? "AND " : "")
-          + "address LIKE '" + searchIPAddress + "%' ");
-      addAnd = true;
-    }
-    List<String> timeIntervals = new ArrayList<String>();
-    if (searchDayTimestamps.size() > 0 ||
-        searchMonthTimestamps.size() > 0) {
-      StringBuilder timeIntervalBuilder = new StringBuilder();
-      boolean addOr = false;
-      timeIntervalBuilder.append("AND (");
-      for (long searchTimestamp : searchDayTimestamps) {
-        if (searchTimestamp < this.minValidAfterMillis) {
-          request.setAttribute("outsideInterval", "Returned search "
-              + "results may be incomplete, as our data only dates back "
-              + "to " + dateTimeFormat.format(this.minValidAfterMillis)
-              + ".  Older archives are not available.");
-        }
-        timeIntervalBuilder.append((addOr ? "OR " : "")
-            + "(validafter >= '"
-            + dateTimeFormat.format(searchTimestamp) + "' AND "
-            + "validafter < '" + dateTimeFormat.format(searchTimestamp
-            + 24L * 60L * 60L * 1000L) + "') ");
-        addOr = true;
-      }
-      for (long searchTimestamp : searchMonthTimestamps) {
-        if (searchTimestamp < this.minValidAfterMillis) {
-          request.setAttribute("outsideInterval", "Returned search "
-              + "results may be incomplete, as our data only dates back "
-              + "to " + dateTimeFormat.format(this.minValidAfterMillis)
-              + ".  Older archives are not available.");
-        }
-        Calendar firstOfNextMonth = Calendar.getInstance(
-            TimeZone.getTimeZone("UTC"));
-        firstOfNextMonth.setTimeInMillis(searchTimestamp);
-        firstOfNextMonth.add(Calendar.MONTH, 1);
-        timeIntervalBuilder.append((addOr ? "OR " : "")
-            + "(validafter >= '"
-            + dateTimeFormat.format(searchTimestamp) + "' AND "
-            + "validafter < '" + dateTimeFormat.format(
-            firstOfNextMonth.getTimeInMillis()) + "') ");
-        addOr = true;
-      }
-      timeIntervalBuilder.append(") ");
-      timeIntervals.add(timeIntervalBuilder.toString());
-    } else {
-      timeIntervals.add("AND validafter >= '"
-          + dateTimeFormat.format(System.currentTimeMillis()
-          - 4L * 24L * 60L * 60L * 1000L) + "' ");
-      timeIntervals.add("AND validafter >= '"
-          + dateTimeFormat.format(System.currentTimeMillis()
-          - 30L * 24L * 60L * 60L * 1000L) + "' ");
-    }
-    List<String> queries = new ArrayList<String>();
-    for (String timeInterval : timeIntervals) {
-      StringBuilder queryBuilder = new StringBuilder();
-      queryBuilder.append("SELECT validafter, fingerprint, descriptor, "
-          + "rawdesc FROM statusentry WHERE validafter IN (SELECT "
-          + "validafter FROM statusentry WHERE ");
-      queryBuilder.append(conditionBuilder.toString());
-      queryBuilder.append(timeInterval);
-      queryBuilder.append("ORDER BY validafter DESC LIMIT 31) AND ");
-      queryBuilder.append(conditionBuilder.toString());
-      queryBuilder.append(timeInterval);
-      queries.add(queryBuilder.toString());
-    }
-
-    /* Actually execute the query. */
-    long startedQuery = System.currentTimeMillis();
-    SortedMap<String, SortedSet<String>> foundDescriptors =
-        new TreeMap<String, SortedSet<String>>(
-        Collections.reverseOrder());
-    Map<String, String> rawValidAfterLines =
-        new HashMap<String, String>();
-    Map<String, String> rawStatusEntries = new HashMap<String, String>();
-    String query = null;
-    int matches = 0;
-    try {
-      long requestedConnection = System.currentTimeMillis();
-      Connection conn = this.ds.getConnection();
-      while (!queries.isEmpty()) {
-        query = queries.remove(0);
-        this.logger.info("Running query '" + query + "'.");
-        Statement statement = conn.createStatement();
-        ResultSet rs = statement.executeQuery(query);
-        while (rs.next()) {
-          matches++;
-          String validAfter = rs.getTimestamp(1).toString().
-              substring(0, 19);
-          String fingerprint = rs.getString(2);
-          if (!foundDescriptors.containsKey(validAfter)) {
-            foundDescriptors.put(validAfter, new TreeSet<String>());
-          }
-          foundDescriptors.get(validAfter).add(validAfter + " "
-              + fingerprint);
-          if (!rawValidAfterLines.containsKey(validAfter)) {
-            rawValidAfterLines.put(validAfter, "<tt>valid-after "
-                + "<a href=\"https://exonerator.torproject.org/"
-                + "consensus?valid-after="
-                + validAfter.replaceAll(":", "-").replaceAll(" ", "-")
-                + "\" target=\"_blank\">" + validAfter + "</a></tt><br>");
-          }
-          byte[] rawStatusEntry = rs.getBytes(4);
-          String statusEntryLines = null;
-          try {
-            statusEntryLines = new String(rawStatusEntry, "US-ASCII");
-          } catch (UnsupportedEncodingException e) {
-            /* This shouldn't happen, because we know that ASCII is
-             * supported. */
-          }
-          StringBuilder rawStatusEntryBuilder = new StringBuilder();
-          String[] lines = statusEntryLines.split("\n");
-          for (String line : lines) {
-            if (line.startsWith("r ")) {
-              String[] parts = line.split(" ");
-              String descriptorBase64 = String.format("%040x",
-                  new BigInteger(1, Base64.decodeBase64(parts[3]
-                  + "==")));
-              rawStatusEntryBuilder.append("<tt>r " + parts[1] + " "
-                  + parts[2] + " <a href=\""
-                  + "https://exonerator.torproject.org/"
-                  + "serverdesc?desc-id="
-                  + descriptorBase64 + "\" target=\"_blank\">" + parts[3]
-                  + "</a> " + parts[4] + " " + parts[5] + " " + parts[6]
-                  + " " + parts[7] + " " + parts[8] + "</tt><br>");
-            } else {
-              rawStatusEntryBuilder.append("<tt>" + line + "</tt><br>");
-            }
-          }
-          rawStatusEntries.put(validAfter + " " + fingerprint,
-                rawStatusEntryBuilder.toString());
-        }
-        rs.close();
-        statement.close();
-        if (matches >= 31) {
-          queries.clear();
-        }
-      }
-      conn.close();
-      this.logger.info("Returned a database connection to the pool "
-          + "after " + (System.currentTimeMillis()
-          - requestedConnection) + " millis.");
-    } catch (SQLException e) {
-
-      /* Tell the user we have a database problem. */
-      response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
-          "Database problem");
-      return;
-    }
-    request.setAttribute("query", query);
-    request.setAttribute("queryTime", System.currentTimeMillis()
-        - startedQuery);
-    request.setAttribute("foundDescriptors", foundDescriptors);
-    request.setAttribute("rawValidAfterLines", rawValidAfterLines);
-    request.setAttribute("rawStatusEntries", rawStatusEntries);
-    request.setAttribute("matches", matches);
-
-    /* We're done. Let the JSP do the rest. */
-    request.getRequestDispatcher("WEB-INF/relay-search.jsp").forward(
-        request, response);
-  }
-}
-
diff --git a/src/org/torproject/ernie/web/GraphsServlet.java b/src/org/torproject/ernie/web/GraphsServlet.java
deleted file mode 100644
index 111aa0f..0000000
--- a/src/org/torproject/ernie/web/GraphsServlet.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/* Copyright 2011, 2012 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.ernie.web;
-
-import java.io.IOException;
-
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-public class GraphsServlet extends HttpServlet {
-
-  private static final long serialVersionUID = 7615715032362498151L;
-
-  public void doGet(HttpServletRequest request,
-      HttpServletResponse response) throws IOException, ServletException {
-
-    /* Forward the request to the JSP that does all the hard work. */
-    request.getRequestDispatcher("WEB-INF/graphs.jsp").forward(request,
-        response);
-  }
-}
-
diff --git a/src/org/torproject/ernie/web/IndexServlet.java b/src/org/torproject/ernie/web/IndexServlet.java
deleted file mode 100644
index 11aff7c..0000000
--- a/src/org/torproject/ernie/web/IndexServlet.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/* Copyright 2011, 2012 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.ernie.web;
-
-import java.io.IOException;
-
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-public class IndexServlet extends HttpServlet {
-
-  private static final long serialVersionUID = 7871368999788994664L;
-
-  public void doGet(HttpServletRequest request,
-      HttpServletResponse response) throws IOException, ServletException {
-
-    /* Forward the request to the JSP that does all the hard work. */
-    request.getRequestDispatcher("WEB-INF/index.jsp").forward(request,
-        response);
-  }
-}
-
diff --git a/src/org/torproject/ernie/web/ResearchServlet.java b/src/org/torproject/ernie/web/ResearchServlet.java
deleted file mode 100644
index c78be69..0000000
--- a/src/org/torproject/ernie/web/ResearchServlet.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/* Copyright 2011, 2012 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.ernie.web;
-
-import java.io.IOException;
-
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-public class ResearchServlet extends HttpServlet {
-
-  private static final long serialVersionUID = -9151727188925700665L;
-
-  public void doGet(HttpServletRequest request,
-      HttpServletResponse response) throws IOException, ServletException {
-
-    /* Forward the request to the JSP that does all the hard work. */
-    request.getRequestDispatcher("WEB-INF/research.jsp").forward(request,
-        response);
-  }
-}
-
diff --git a/src/org/torproject/ernie/web/StatusServlet.java b/src/org/torproject/ernie/web/StatusServlet.java
deleted file mode 100644
index 07790ec..0000000
--- a/src/org/torproject/ernie/web/StatusServlet.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/* Copyright 2011, 2012 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.ernie.web;
-
-import java.io.IOException;
-
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-public class StatusServlet extends HttpServlet {
-
-  private static final long serialVersionUID = -7249872082399236981L;
-
-  public void doGet(HttpServletRequest request,
-      HttpServletResponse response) throws IOException, ServletException {
-
-    /* Forward the request to the JSP that does all the hard work. */
-    request.getRequestDispatcher("WEB-INF/status.jsp").forward(request,
-        response);
-  }
-}
-
diff --git a/src/org/torproject/ernie/web/graphs/BubblesServlet.java b/src/org/torproject/ernie/web/graphs/BubblesServlet.java
deleted file mode 100644
index 6f66413..0000000
--- a/src/org/torproject/ernie/web/graphs/BubblesServlet.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/* Copyright 2013 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.ernie.web.graphs;
-
-import java.io.IOException;
-
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-public class BubblesServlet extends HttpServlet {
-
-  private static final long serialVersionUID = -6011833075497881033L;
-
-  public void doGet(HttpServletRequest request,
-      HttpServletResponse response) throws IOException, ServletException {
-
-    /* Forward the request to the JSP that does all the hard work. */
-    request.getRequestDispatcher("WEB-INF/bubbles.jsp").forward(request,
-        response);
-  }
-}
-
diff --git a/src/org/torproject/ernie/web/graphs/Countries.java b/src/org/torproject/ernie/web/graphs/Countries.java
deleted file mode 100644
index 93dac4c..0000000
--- a/src/org/torproject/ernie/web/graphs/Countries.java
+++ /dev/null
@@ -1,285 +0,0 @@
-/* Copyright 2011, 2012 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.ernie.web.graphs;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class Countries {
-
-  private static Countries instance = new Countries();
-
-  public static Countries getInstance() {
-    return Countries.instance;
-  }
-
-  /* List of arrays of length 2, containing country codes at [0] and
-   * country names at [1], alphabetically ordered by country names. */
-  private List<String[]> knownCountries;
-
-  private Countries() {
-    this.knownCountries = new ArrayList<String[]>();
-    this.knownCountries.add("af;Afghanistan".split(";"));
-    this.knownCountries.add("ax;Aland Islands".split(";"));
-    this.knownCountries.add("al;Albania".split(";"));
-    this.knownCountries.add("dz;Algeria".split(";"));
-    this.knownCountries.add("as;American Samoa".split(";"));
-    this.knownCountries.add("ad;Andorra".split(";"));
-    this.knownCountries.add("ao;Angola".split(";"));
-    this.knownCountries.add("ai;Anguilla".split(";"));
-    this.knownCountries.add("aq;Antarctica".split(";"));
-    this.knownCountries.add("ag;Antigua and Barbuda".split(";"));
-    this.knownCountries.add("ar;Argentina".split(";"));
-    this.knownCountries.add("am;Armenia".split(";"));
-    this.knownCountries.add("aw;Aruba".split(";"));
-    this.knownCountries.add("au;Australia".split(";"));
-    this.knownCountries.add("at;Austria".split(";"));
-    this.knownCountries.add("az;Azerbaijan".split(";"));
-    this.knownCountries.add("bs;Bahamas".split(";"));
-    this.knownCountries.add("bh;Bahrain".split(";"));
-    this.knownCountries.add("bd;Bangladesh".split(";"));
-    this.knownCountries.add("bb;Barbados".split(";"));
-    this.knownCountries.add("by;Belarus".split(";"));
-    this.knownCountries.add("be;Belgium".split(";"));
-    this.knownCountries.add("bz;Belize".split(";"));
-    this.knownCountries.add("bj;Benin".split(";"));
-    this.knownCountries.add("bm;Bermuda".split(";"));
-    this.knownCountries.add("bt;Bhutan".split(";"));
-    this.knownCountries.add("bo;Bolivia".split(";"));
-    this.knownCountries.add("ba;Bosnia and Herzegovina".split(";"));
-    this.knownCountries.add("bw;Botswana".split(";"));
-    this.knownCountries.add("bv;Bouvet Island".split(";"));
-    this.knownCountries.add("br;Brazil".split(";"));
-    this.knownCountries.add("io;British Indian Ocean Territory".
-        split(";"));
-    this.knownCountries.add("bn;Brunei".split(";"));
-    this.knownCountries.add("bg;Bulgaria".split(";"));
-    this.knownCountries.add("bf;Burkina Faso".split(";"));
-    this.knownCountries.add("mm;Burma".split(";"));
-    this.knownCountries.add("bi;Burundi".split(";"));
-    this.knownCountries.add("kh;Cambodia".split(";"));
-    this.knownCountries.add("cm;Cameroon".split(";"));
-    this.knownCountries.add("ca;Canada".split(";"));
-    this.knownCountries.add("cv;Cape Verde".split(";"));
-    this.knownCountries.add("ky;Cayman Islands".split(";"));
-    this.knownCountries.add("cf;Central African Republic".split(";"));
-    this.knownCountries.add("td;Chad".split(";"));
-    this.knownCountries.add("cl;Chile".split(";"));
-    this.knownCountries.add("cn;China".split(";"));
-    this.knownCountries.add("cx;Christmas Island".split(";"));
-    this.knownCountries.add("cc;Cocos (Keeling) Islands".split(";"));
-    this.knownCountries.add("co;Colombia".split(";"));
-    this.knownCountries.add("km;Comoros".split(";"));
-    this.knownCountries.add("cd;Congo, The Democratic Republic of the".
-        split(";"));
-    this.knownCountries.add("cg;Congo".split(";"));
-    this.knownCountries.add("ck;Cook Islands".split(";"));
-    this.knownCountries.add("cr;Costa Rica".split(";"));
-    this.knownCountries.add("ci:Côte d'Ivoire".split(":"));
-    this.knownCountries.add("hr;Croatia".split(";"));
-    this.knownCountries.add("cu;Cuba".split(";"));
-    this.knownCountries.add("cy;Cyprus".split(";"));
-    this.knownCountries.add("cz;Czech Republic".split(";"));
-    this.knownCountries.add("dk;Denmark".split(";"));
-    this.knownCountries.add("dj;Djibouti".split(";"));
-    this.knownCountries.add("dm;Dominica".split(";"));
-    this.knownCountries.add("do;Dominican Republic".split(";"));
-    this.knownCountries.add("ec;Ecuador".split(";"));
-    this.knownCountries.add("eg;Egypt".split(";"));
-    this.knownCountries.add("sv;El Salvador".split(";"));
-    this.knownCountries.add("gq;Equatorial Guinea".split(";"));
-    this.knownCountries.add("er;Eritrea".split(";"));
-    this.knownCountries.add("ee;Estonia".split(";"));
-    this.knownCountries.add("et;Ethiopia".split(";"));
-    this.knownCountries.add("fk;Falkland Islands (Malvinas)".split(";"));
-    this.knownCountries.add("fo;Faroe Islands".split(";"));
-    this.knownCountries.add("fj;Fiji".split(";"));
-    this.knownCountries.add("fi;Finland".split(";"));
-    this.knownCountries.add("fx;France, Metropolitan".split(";"));
-    this.knownCountries.add("fr;France".split(";"));
-    this.knownCountries.add("gf;French Guiana".split(";"));
-    this.knownCountries.add("pf;French Polynesia".split(";"));
-    this.knownCountries.add("tf;French Southern Territories".split(";"));
-    this.knownCountries.add("ga;Gabon".split(";"));
-    this.knownCountries.add("gm;Gambia".split(";"));
-    this.knownCountries.add("ge;Georgia".split(";"));
-    this.knownCountries.add("de;Germany".split(";"));
-    this.knownCountries.add("gh;Ghana".split(";"));
-    this.knownCountries.add("gi;Gibraltar".split(";"));
-    this.knownCountries.add("gr;Greece".split(";"));
-    this.knownCountries.add("gl;Greenland".split(";"));
-    this.knownCountries.add("gd;Grenada".split(";"));
-    this.knownCountries.add("gp;Guadeloupe".split(";"));
-    this.knownCountries.add("gu;Guam".split(";"));
-    this.knownCountries.add("gt;Guatemala".split(";"));
-    this.knownCountries.add("gg;Guernsey".split(";"));
-    this.knownCountries.add("gn;Guinea".split(";"));
-    this.knownCountries.add("gw;Guinea-Bissau".split(";"));
-    this.knownCountries.add("gy;Guyana".split(";"));
-    this.knownCountries.add("ht;Haiti".split(";"));
-    this.knownCountries.add("hm;Heard Island and McDonald Islands".
-        split(";"));
-    this.knownCountries.add("va;Vatican City".split(";"));
-    this.knownCountries.add("hn;Honduras".split(";"));
-    this.knownCountries.add("hk;Hong Kong".split(";"));
-    this.knownCountries.add("hu;Hungary".split(";"));
-    this.knownCountries.add("is;Iceland".split(";"));
-    this.knownCountries.add("in;India".split(";"));
-    this.knownCountries.add("id;Indonesia".split(";"));
-    this.knownCountries.add("ir;Iran".split(";"));
-    this.knownCountries.add("iq;Iraq".split(";"));
-    this.knownCountries.add("ie;Ireland".split(";"));
-    this.knownCountries.add("im;Isle of Man".split(";"));
-    this.knownCountries.add("il;Israel".split(";"));
-    this.knownCountries.add("it;Italy".split(";"));
-    this.knownCountries.add("jm;Jamaica".split(";"));
-    this.knownCountries.add("jp;Japan".split(";"));
-    this.knownCountries.add("je;Jersey".split(";"));
-    this.knownCountries.add("jo;Jordan".split(";"));
-    this.knownCountries.add("kz;Kazakhstan".split(";"));
-    this.knownCountries.add("ke;Kenya".split(";"));
-    this.knownCountries.add("ki;Kiribati".split(";"));
-    this.knownCountries.add("kp;North Korea".split(";"));
-    this.knownCountries.add("kr;Korea, Republic of".split(";"));
-    this.knownCountries.add("kw;Kuwait".split(";"));
-    this.knownCountries.add("kg;Kyrgyzstan".split(";"));
-    this.knownCountries.add("la;Laos".split(";"));
-    this.knownCountries.add("lv;Latvia".split(";"));
-    this.knownCountries.add("lb;Lebanon".split(";"));
-    this.knownCountries.add("ls;Lesotho".split(";"));
-    this.knownCountries.add("lr;Liberia".split(";"));
-    this.knownCountries.add("ly;Libya".split(";"));
-    this.knownCountries.add("li;Liechtenstein".split(";"));
-    this.knownCountries.add("lt;Lithuania".split(";"));
-    this.knownCountries.add("lu;Luxembourg".split(";"));
-    this.knownCountries.add("mo;Macau".split(";"));
-    this.knownCountries.add("mk;Macedonia".split(";"));
-    this.knownCountries.add("mg;Madagascar".split(";"));
-    this.knownCountries.add("mw;Malawi".split(";"));
-    this.knownCountries.add("my;Malaysia".split(";"));
-    this.knownCountries.add("mv;Maldives".split(";"));
-    this.knownCountries.add("ml;Mali".split(";"));
-    this.knownCountries.add("mt;Malta".split(";"));
-    this.knownCountries.add("mh;Marshall Islands".split(";"));
-    this.knownCountries.add("mq;Martinique".split(";"));
-    this.knownCountries.add("mr;Mauritania".split(";"));
-    this.knownCountries.add("mu;Mauritius".split(";"));
-    this.knownCountries.add("yt;Mayotte".split(";"));
-    this.knownCountries.add("mx;Mexico".split(";"));
-    this.knownCountries.add("fm;Micronesia, Federated States of".
-        split(";"));
-    this.knownCountries.add("md;Moldova, Republic of".split(";"));
-    this.knownCountries.add("mc;Monaco".split(";"));
-    this.knownCountries.add("mn;Mongolia".split(";"));
-    this.knownCountries.add("me;Montenegro".split(";"));
-    this.knownCountries.add("ms;Montserrat".split(";"));
-    this.knownCountries.add("ma;Morocco".split(";"));
-    this.knownCountries.add("mz;Mozambique".split(";"));
-    this.knownCountries.add("mm;Burma".split(";"));
-    this.knownCountries.add("na;Namibia".split(";"));
-    this.knownCountries.add("nr;Nauru".split(";"));
-    this.knownCountries.add("np;Nepal".split(";"));
-    this.knownCountries.add("an;Netherlands Antilles".split(";"));
-    this.knownCountries.add("nl;Netherlands".split(";"));
-    this.knownCountries.add("nc;New Caledonia".split(";"));
-    this.knownCountries.add("nz;New Zealand".split(";"));
-    this.knownCountries.add("ni;Nicaragua".split(";"));
-    this.knownCountries.add("ne;Niger".split(";"));
-    this.knownCountries.add("ng;Nigeria".split(";"));
-    this.knownCountries.add("nu;Niue".split(";"));
-    this.knownCountries.add("nf;Norfolk Island".split(";"));
-    this.knownCountries.add("mp;Northern Mariana Islands".split(";"));
-    this.knownCountries.add("no;Norway".split(";"));
-    this.knownCountries.add("om;Oman".split(";"));
-    this.knownCountries.add("pk;Pakistan".split(";"));
-    this.knownCountries.add("pw;Palau".split(";"));
-    this.knownCountries.add("ps;Palestinian Territory".split(";"));
-    this.knownCountries.add("pa;Panama".split(";"));
-    this.knownCountries.add("pg;Papua New Guinea".split(";"));
-    this.knownCountries.add("py;Paraguay".split(";"));
-    this.knownCountries.add("pe;Peru".split(";"));
-    this.knownCountries.add("ph;Philippines".split(";"));
-    this.knownCountries.add("pn;Pitcairn Islands".split(";"));
-    this.knownCountries.add("pl;Poland".split(";"));
-    this.knownCountries.add("pt;Portugal".split(";"));
-    this.knownCountries.add("pr;Puerto Rico".split(";"));
-    this.knownCountries.add("qa;Qatar".split(";"));
-    this.knownCountries.add("re;Reunion".split(";"));
-    this.knownCountries.add("ro;Romania".split(";"));
-    this.knownCountries.add("ru;Russia".split(";"));
-    this.knownCountries.add("rw;Rwanda".split(";"));
-    this.knownCountries.add("bl;Saint Bartelemey".split(";"));
-    this.knownCountries.add("sh;Saint Helena".split(";"));
-    this.knownCountries.add("kn;Saint Kitts and Nevis".split(";"));
-    this.knownCountries.add("lc;Saint Lucia".split(";"));
-    this.knownCountries.add("mf;Saint Martin".split(";"));
-    this.knownCountries.add("pm;Saint Pierre and Miquelon".split(";"));
-    this.knownCountries.add("vc;Saint Vincent and the Grenadines".
-        split(";"));
-    this.knownCountries.add("ws;Samoa".split(";"));
-    this.knownCountries.add("sm;San Marino".split(";"));
-    this.knownCountries.add("st:São Tomé and Príncipe".
-        split(":"));
-    this.knownCountries.add("sa;Saudi Arabia".split(";"));
-    this.knownCountries.add("sn;Senegal".split(";"));
-    this.knownCountries.add("rs;Serbia".split(";"));
-    this.knownCountries.add("sc;Seychelles".split(";"));
-    this.knownCountries.add("sl;Sierra Leone".split(";"));
-    this.knownCountries.add("sg;Singapore".split(";"));
-    this.knownCountries.add("sk;Slovakia".split(";"));
-    this.knownCountries.add("si;Slovenia".split(";"));
-    this.knownCountries.add("sb;Solomon Islands".split(";"));
-    this.knownCountries.add("so;Somalia".split(";"));
-    this.knownCountries.add("za;South Africa".split(";"));
-    this.knownCountries.add(("gs;South Georgia and the South Sandwich "
-        + "Islands").split(";"));
-    this.knownCountries.add("ss;South Sudan".split(";"));
-    this.knownCountries.add("es;Spain".split(";"));
-    this.knownCountries.add("lk;Sri Lanka".split(";"));
-    this.knownCountries.add("sd;Sudan".split(";"));
-    this.knownCountries.add("sr;Suriname".split(";"));
-    this.knownCountries.add("sj;Svalbard and Jan Mayen".split(";"));
-    this.knownCountries.add("sz;Swaziland".split(";"));
-    this.knownCountries.add("se;Sweden".split(";"));
-    this.knownCountries.add("ch;Switzerland".split(";"));
-    this.knownCountries.add("sy;Syrian Arab Republic".split(";"));
-    this.knownCountries.add("tw;Taiwan".split(";"));
-    this.knownCountries.add("tj;Tajikistan".split(";"));
-    this.knownCountries.add("tz;Tanzania, United Republic of".split(";"));
-    this.knownCountries.add("th;Thailand".split(";"));
-    this.knownCountries.add("tl;East Timor".split(";"));
-    this.knownCountries.add("tg;Togo".split(";"));
-    this.knownCountries.add("tk;Tokelau".split(";"));
-    this.knownCountries.add("to;Tonga".split(";"));
-    this.knownCountries.add("tt;Trinidad and Tobago".split(";"));
-    this.knownCountries.add("tn;Tunisia".split(";"));
-    this.knownCountries.add("tr;Turkey".split(";"));
-    this.knownCountries.add("tm;Turkmenistan".split(";"));
-    this.knownCountries.add("tc;Turks and Caicos Islands".split(";"));
-    this.knownCountries.add("tv;Tuvalu".split(";"));
-    this.knownCountries.add("ug;Uganda".split(";"));
-    this.knownCountries.add("ua;Ukraine".split(";"));
-    this.knownCountries.add("ae;United Arab Emirates".split(";"));
-    this.knownCountries.add("gb;United Kingdom".split(";"));
-    this.knownCountries.add("um;United States Minor Outlying Islands".
-        split(";"));
-    this.knownCountries.add("us;United States".split(";"));
-    this.knownCountries.add("uy;Uruguay".split(";"));
-    this.knownCountries.add("uz;Uzbekistan".split(";"));
-    this.knownCountries.add("vu;Vanuatu".split(";"));
-    this.knownCountries.add("ve;Venezuela".split(";"));
-    this.knownCountries.add("vn;Vietnam".split(";"));
-    this.knownCountries.add("vg;Virgin Islands, British".split(";"));
-    this.knownCountries.add("vi;Virgin Islands, U.S.".split(";"));
-    this.knownCountries.add("wf;Wallis and Futuna".split(";"));
-    this.knownCountries.add("eh;Western Sahara".split(";"));
-    this.knownCountries.add("ye;Yemen".split(";"));
-    this.knownCountries.add("zm;Zambia".split(";"));
-    this.knownCountries.add("zw;Zimbabwe".split(";"));
-  }
-
-  public List<String[]> getCountryList() {
-    return this.knownCountries;
-  }
-}
-
diff --git a/src/org/torproject/ernie/web/graphs/CsvServlet.java b/src/org/torproject/ernie/web/graphs/CsvServlet.java
deleted file mode 100644
index 40e3bea..0000000
--- a/src/org/torproject/ernie/web/graphs/CsvServlet.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/* Copyright 2011, 2012 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.ernie.web.graphs;
-
-import java.io.IOException;
-import java.util.SortedSet;
-import java.util.logging.Logger;
-
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-/**
- * Servlet that reads an HTTP request for a comma-separated value file,
- * asks the GraphGenerator to generate this file, and returns it to the
- * client.
- */
-public class CsvServlet extends HttpServlet {
-
-  private static final long serialVersionUID = 7501442926823719958L;
-
-  private RObjectGenerator rObjectGenerator;
-
-  /* Available CSV files. */
-  private SortedSet<String> availableCsvFiles;
-
-  private Logger logger;
-
-  public void init() {
-
-    /* Initialize logger. */
-    this.logger = Logger.getLogger(CsvServlet.class.toString());
-
-    /* Get a reference to the R object generator that we need to generate
-     * CSV files. */
-    this.rObjectGenerator = (RObjectGenerator) getServletContext().
-        getAttribute("RObjectGenerator");
-    this.availableCsvFiles = rObjectGenerator.getAvailableCsvFiles();
-  }
-
-  public void doGet(HttpServletRequest request,
-      HttpServletResponse response) throws IOException,
-      ServletException {
-
-    /* Check if the directory listing was requested. */
-    String requestURI = request.getRequestURI();
-    if (requestURI.equals("/ernie/csv/")) {
-      request.setAttribute("directory", "/csv");
-      request.setAttribute("extension", ".csv");
-      request.setAttribute("files", this.availableCsvFiles);
-      request.getRequestDispatcher("/WEB-INF/dir.jsp").forward(request,
-          response);
-      return;
-    }
-
-    /* Find out which CSV file was requested and make sure we know this
-     * CSV file type. */
-    String requestedCsvFile = requestURI;
-    if (requestedCsvFile.endsWith(".csv")) {
-      requestedCsvFile = requestedCsvFile.substring(0,
-          requestedCsvFile.length() - ".csv".length());
-    }
-    if (requestedCsvFile.contains("/")) {
-      requestedCsvFile = requestedCsvFile.substring(requestedCsvFile.
-          lastIndexOf("/") + 1);
-    }
-    if (!availableCsvFiles.contains(requestedCsvFile)) {
-      logger.info("Did not recognize requested .csv file from request "
-          + "URI: '" + requestURI + "'. Responding with 404 Not Found.");
-      response.sendError(HttpServletResponse.SC_NOT_FOUND);
-      return;
-    }
-    logger.fine("CSV file '" + requestedCsvFile + ".csv' requested.");
-
-    /* Request CSV file from R object generator, which asks Rserve to
-     * generate it. */
-    RObject csvFile = this.rObjectGenerator.generateCsv(
-        requestedCsvFile, true);
-
-    /* Make sure that we have a .csv file to return. */
-    if (csvFile == null) {
-      response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
-      return;
-    }
-
-    /* Write CSV file to response. */
-    String csvFileContent = new String(csvFile.getBytes());
-    response.setContentType("text/csv");
-    response.setHeader("Content-Length", String.valueOf(
-        csvFileContent.length()));
-    response.setHeader("Content-Disposition",
-        "inline; filename=\"" + requestedCsvFile + ".csv\"");
-    response.getWriter().print(csvFileContent);
-  }
-}
-
diff --git a/src/org/torproject/ernie/web/graphs/GraphImageServlet.java b/src/org/torproject/ernie/web/graphs/GraphImageServlet.java
deleted file mode 100644
index b7d0b17..0000000
--- a/src/org/torproject/ernie/web/graphs/GraphImageServlet.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/* Copyright 2011, 2012 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.ernie.web.graphs;
-
-import java.io.BufferedOutputStream;
-import java.io.IOException;
-
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-/**
- * Servlet that reads an HTTP request for a graph image, asks the
- * RObjectGenerator to generate this graph if it's not in the cache, and
- * returns the image bytes to the client.
- */
-public class GraphImageServlet extends HttpServlet {
-
-  private static final long serialVersionUID = -7356818641689744288L;
-
-  private RObjectGenerator rObjectGenerator;
-
-  public void init() {
-
-    /* Get a reference to the R object generator that we need to generate
-     * graph images. */
-    this.rObjectGenerator = (RObjectGenerator) getServletContext().
-        getAttribute("RObjectGenerator");
-  }
-
-  public void doGet(HttpServletRequest request,
-      HttpServletResponse response) throws IOException,
-      ServletException {
-
-    /* Find out which graph type was requested and make sure we know this
-     * graph type and file type. */
-    String requestedGraph = request.getRequestURI();
-    String fileType = null;
-    if (requestedGraph.endsWith(".png") ||
-        requestedGraph.endsWith(".pdf") ||
-        requestedGraph.endsWith(".svg")) {
-      fileType = requestedGraph.substring(requestedGraph.length() - 3);
-      requestedGraph = requestedGraph.substring(0, requestedGraph.length()
-          - 4);
-    }
-    if (requestedGraph.contains("/")) {
-      requestedGraph = requestedGraph.substring(requestedGraph.
-          lastIndexOf("/") + 1);
-    }
-
-    /* Request graph from R object generator, which either returns it from
-     * its cache or asks Rserve to generate it. */
-    RObject graph = rObjectGenerator.generateGraph(requestedGraph,
-        fileType, request.getParameterMap(), true);
-
-    /* Make sure that we have a graph to return. */
-    if (graph == null || graph.getBytes() == null || fileType == null) {
-      response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
-      return;
-    }
-
-    /* Write graph bytes to response. */
-    BufferedOutputStream output = null;
-    response.setContentType("image/" + fileType);
-    response.setHeader("Content-Length",
-        String.valueOf(graph.getBytes().length));
-    response.setHeader("Content-Disposition",
-        "inline; filename=\"" + graph.getFileName() + "\"");
-    output = new BufferedOutputStream(response.getOutputStream(), 1024);
-    output.write(graph.getBytes(), 0, graph.getBytes().length);
-    output.flush();
-    output.close();
-  }
-}
-
diff --git a/src/org/torproject/ernie/web/graphs/GraphParameterChecker.java b/src/org/torproject/ernie/web/graphs/GraphParameterChecker.java
deleted file mode 100644
index 74ca6f9..0000000
--- a/src/org/torproject/ernie/web/graphs/GraphParameterChecker.java
+++ /dev/null
@@ -1,280 +0,0 @@
-/* Copyright 2011, 2012 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.ernie.web.graphs;
-
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.TimeZone;
-import java.util.regex.Pattern;
-
-/**
- * Checks request parameters passed to graph-generating servlets.
- */
-public class GraphParameterChecker {
-
-  /**
-   * Singleton instance of this class.
-   */
-  private static GraphParameterChecker instance =
-      new GraphParameterChecker();
-
-  /**
-   * Returns the singleton instance of this class.
-   */
-  public static GraphParameterChecker getInstance() {
-    return instance;
-  }
-
-  /* Date format for parsing start and end dates. */
-  private SimpleDateFormat dateFormat;
-
-  /* Available graphs with corresponding parameter lists. */
-  private Map<String, String> availableGraphs;
-
-  /* Known parameters and parameter values. */
-  private Map<String, String> knownParameterValues;
-
-  /**
-   * Initializes map with valid parameters for each of the graphs.
-   */
-  public GraphParameterChecker() {
-    this.dateFormat = new SimpleDateFormat("yyyy-MM-dd");
-    this.dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
-
-    this.knownParameterValues = new HashMap<String, String>();
-    this.knownParameterValues.put("flag",
-        "Running,Exit,Guard,Fast,Stable,HSDir");
-    StringBuilder sb = new StringBuilder("all");
-    for (String[] country : Countries.getInstance().getCountryList()) {
-      sb.append("," + country[0]);
-    }
-    this.knownParameterValues.put("country", sb.toString());
-    this.knownParameterValues.put("events", "on,off,points");
-    this.knownParameterValues.put("source", "all,siv,moria,torperf");
-    this.knownParameterValues.put("filesize", "50kb,1mb,5mb");
-    this.knownParameterValues.put("transport",
-        "obfs2,obfs3,websocket,<OR>,<??>");
-    this.knownParameterValues.put("version", "v4,v6");
-  }
-
-  public void setAvailableGraphs(Map<String, String> availableGraphs) {
-    this.availableGraphs = availableGraphs;
-  }
-
-  /**
-   * Checks request parameters for the given graph type and returns a map
-   * of recognized parameters, or null if the graph type doesn't exist or
-   * the parameters are invalid.
-   */
-  public Map<String, String[]> checkParameters(String graphType,
-      Map requestParameters) {
-
-    /* Check if the graph type exists. */
-    if (graphType == null ||
-        !this.availableGraphs.containsKey(graphType)) {
-      return null;
-    }
-
-    /* Find out which other parameters are supported by this graph type
-     * and parse them if they are given. */
-    Set<String> supportedGraphParameters = new HashSet<String>(Arrays.
-        asList(this.availableGraphs.get(graphType).split(",")));
-    Map<String, String[]> recognizedGraphParameters =
-        new HashMap<String, String[]>();
-
-    /* Parse start and end dates if supported by the graph type. If no end
-     * date is provided, set it to today. If no start date is provided,
-     * set it to 90 days before the end date. Make sure that start date
-     * precedes end date. */
-    if (supportedGraphParameters.contains("start") ||
-        supportedGraphParameters.contains("end")) {
-      String[] startParameter = (String[]) requestParameters.get("start");
-      String[] endParameter = (String[]) requestParameters.get("end");
-      long endTimestamp = System.currentTimeMillis();
-      if (endParameter != null && endParameter.length > 0 &&
-          endParameter[0].length() > 0) {
-        try {
-          endTimestamp = dateFormat.parse(endParameter[0]).getTime();
-        } catch (ParseException e)  {
-          return null;
-        }
-        if (!endParameter[0].startsWith("20")) {
-          return null;
-        }
-      }
-      endParameter = new String[] { dateFormat.format(endTimestamp) };
-      long startTimestamp = endTimestamp - 90L * 24L * 60L * 60L * 1000L;
-      if (startParameter != null && startParameter.length > 0 &&
-          startParameter[0].length() > 0) {
-        try {
-          startTimestamp = dateFormat.parse(startParameter[0]).getTime();
-        } catch (ParseException e)  {
-          return null;
-        }
-        if (!startParameter[0].startsWith("20")) {
-          return null;
-        }
-      }
-      startParameter = new String[] { dateFormat.format(startTimestamp) };
-      if (startTimestamp > endTimestamp) {
-       return null;
-      }
-      recognizedGraphParameters.put("start", startParameter);
-      recognizedGraphParameters.put("end", endParameter);
-    }
-
-    /* Parse relay flags if supported by the graph type. If no relay flags
-     * are passed or none of them have been recognized, use the set of all
-     * known flags as default. */
-    if (supportedGraphParameters.contains("flag")) {
-      String[] flagParameters = (String[]) requestParameters.get("flag");
-      List<String> knownFlags = Arrays.asList(
-          this.knownParameterValues.get("flag").split(","));
-      if (flagParameters != null) {
-        for (String flag : flagParameters) {
-          if (flag == null || flag.length() == 0 ||
-              !knownFlags.contains(flag)) {
-            return null;
-          }
-        }
-      } else {
-        flagParameters = "Running,Exit,Guard,Fast,Stable".split(",");
-      }
-      recognizedGraphParameters.put("flag", flagParameters);
-    }
-
-    /* Parse country codes if supported by the graph type. If no countries
-     * are passed, use country code "all" (all countries) as default. */
-    if (supportedGraphParameters.contains("country")) {
-      String[] countryParameters = (String[]) requestParameters.get(
-          "country");
-      List<String> knownCountries = Arrays.asList(
-          this.knownParameterValues.get("country").split(","));
-      if (countryParameters != null) {
-        for (String country : countryParameters) {
-          if (country == null || country.length() == 0 ||
-              !knownCountries.contains(country)) {
-            return null;
-          }
-        }
-      } else {
-        countryParameters = new String[] { "all" };
-      }
-      recognizedGraphParameters.put("country", countryParameters);
-    }
-
-    /* Parse whether the estimated min/max range shall be displayed if
-     * supported by the graph type. This parameter can either be "on" or
-     * "off," where "off" is the default. */
-    if (supportedGraphParameters.contains("events")) {
-      String[] eventsParameter = (String[]) requestParameters.get(
-          "events");
-      List<String> knownRanges = Arrays.asList(
-          this.knownParameterValues.get("events").split(","));
-      if (eventsParameter != null) {
-        if (eventsParameter.length != 1 ||
-            eventsParameter[0].length() == 0 ||
-            !knownRanges.contains(eventsParameter[0])) {
-          return null;
-        }
-      } else {
-        eventsParameter = new String[] { "off" };
-      }
-      recognizedGraphParameters.put("events", eventsParameter);
-    }
-
-    /* Parse torperf data source if supported by the graph type. Only a
-     * single source can be passed. If no source is passed, use "torperf"
-     * as default. */
-    if (supportedGraphParameters.contains("source")) {
-      String[] sourceParameter = (String[]) requestParameters.get(
-          "source");
-      List<String> knownSources = Arrays.asList(
-          this.knownParameterValues.get("source").split(","));
-      if (sourceParameter != null) {
-        if (sourceParameter.length != 1) {
-          return null;
-        }
-        if (sourceParameter[0].length() == 0 ||
-            !knownSources.contains(sourceParameter[0])) {
-          return null;
-        }
-      } else {
-        sourceParameter = new String[] { "all" };
-      }
-      recognizedGraphParameters.put("source", sourceParameter);
-    }
-
-    /* Parse torperf file size if supported by the graph type. Only a
-     * single file size can be passed. If no file size is passed, use
-     * "50kb" as default. */
-    if (supportedGraphParameters.contains("filesize")) {
-      String[] filesizeParameter = (String[]) requestParameters.get(
-          "filesize");
-      List<String> knownFilesizes = Arrays.asList(
-          this.knownParameterValues.get("filesize").split(","));
-      if (filesizeParameter != null) {
-        if (filesizeParameter.length != 1) {
-          return null;
-        }
-        if (filesizeParameter[0].length() == 0 ||
-            !knownFilesizes.contains(filesizeParameter[0])) {
-          return null;
-        }
-      } else {
-        filesizeParameter = new String[] { "50kb" };
-      }
-      recognizedGraphParameters.put("filesize", filesizeParameter);
-    }
-
-    /* Parse transports if supported by the graph type. If no transports
-     * are passed, use "<OR>" as default. */
-    if (supportedGraphParameters.contains("transport")) {
-      String[] transportParameters = (String[]) requestParameters.get(
-          "transport");
-      List<String> knownTransports = Arrays.asList(
-          this.knownParameterValues.get("transport").split(","));
-      if (transportParameters != null) {
-        for (String transport : transportParameters) {
-          if (transport == null || transport.length() == 0 ||
-              !knownTransports.contains(transport)) {
-            return null;
-          }
-        }
-      } else {
-        transportParameters = new String[] { "<OR>" };
-      }
-      recognizedGraphParameters.put("transport", transportParameters);
-    }
-
-    /* Parse versions if supported by the graph type. If no versions
-     * are passed, use "v4" as default. */
-    if (supportedGraphParameters.contains("version")) {
-      String[] versionParameters = (String[]) requestParameters.get(
-          "version");
-      List<String> knownVersions = Arrays.asList(
-          this.knownParameterValues.get("version").split(","));
-      if (versionParameters != null) {
-        for (String version : versionParameters) {
-          if (version == null || version.length() == 0 ||
-              !knownVersions.contains(version)) {
-            return null;
-          }
-        }
-      } else {
-        versionParameters = new String[] { "v4" };
-      }
-      recognizedGraphParameters.put("version", versionParameters);
-    }
-
-    /* We now have a map with all required graph parameters. Return it. */
-    return recognizedGraphParameters;
-  }
-}
-
diff --git a/src/org/torproject/ernie/web/graphs/GraphsSubpagesServlet.java b/src/org/torproject/ernie/web/graphs/GraphsSubpagesServlet.java
deleted file mode 100644
index 94d7340..0000000
--- a/src/org/torproject/ernie/web/graphs/GraphsSubpagesServlet.java
+++ /dev/null
@@ -1,162 +0,0 @@
-/* Copyright 2011, 2012 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.ernie.web.graphs;
-
-import java.io.IOException;
-import java.text.SimpleDateFormat;
-import java.util.Arrays;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.TimeZone;
-
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-public class GraphsSubpagesServlet extends HttpServlet {
-
-  private static final long serialVersionUID = -5959829347747628403L;
-
-  /* Available graphs subpages with corresponding JSP to which requests
-   * are forwarded. */
-  private Map<String, String> availableGraphsSubpages;
-
-  /* Available tables on graphs subpages. */
-  private Map<String, Set<String>> availableGraphsSubpageTables;
-
-  /* Country codes and names for per-country graphs. */
-  private List<String[]> knownCountries;
-
-  /* R object generator for generating table data. */
-  private RObjectGenerator rObjectGenerator;
-
-  public GraphsSubpagesServlet() {
-    this.availableGraphsSubpages = new HashMap<String, String>();
-    this.availableGraphsSubpages.put("network.html",
-        "WEB-INF/network.jsp");
-    this.availableGraphsSubpages.put("fast-exits.html",
-        "WEB-INF/fast-exits.jsp");
-    this.availableGraphsSubpages.put("users.html", "WEB-INF/users.jsp");
-    this.availableGraphsSubpages.put("performance.html",
-        "WEB-INF/performance.jsp");
-
-    this.availableGraphsSubpageTables =
-        new HashMap<String, Set<String>>();
-    this.availableGraphsSubpageTables.put("users.html",
-        new HashSet<String>(Arrays.asList((
-        "direct-users,censorship-events,bridge-users,userstats-relay,"
-        + "userstats-censorship-events,userstats-bridge").split(","))));
-
-    this.knownCountries = Countries.getInstance().getCountryList();
-  }
-
-  public void init() {
-    /* Get a reference to the R object generator that we need to generate
-     * table data. */
-    this.rObjectGenerator = (RObjectGenerator) getServletContext().
-        getAttribute("RObjectGenerator");
-  }
-
-  public void doGet(HttpServletRequest request,
-      HttpServletResponse response) throws IOException, ServletException {
-
-    /* Find out which graph subpage was requested and look up which JSP
-     * handles this subpage. */
-    String requestedPage = request.getRequestURI();
-    if (requestedPage == null) {
-      response.sendError(HttpServletResponse.SC_BAD_REQUEST);
-      return;
-    }
-    if (requestedPage.contains("/")) {
-      requestedPage = requestedPage.substring(requestedPage.
-          lastIndexOf("/") + 1);
-    }
-    if (!availableGraphsSubpages.containsKey(requestedPage)) {
-      response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
-      return;
-    }
-    String jsp = availableGraphsSubpages.get(requestedPage);
-
-    /* Find out which graph or table type was requested, if any. */
-    String requestedGraph = request.getParameter("graph");
-    String requestedTable = request.getParameter("table");
-    if (requestedGraph != null) {
-
-      /* Check if the passed parameters are valid. */
-      Map<String, String[]> checkedParameters = GraphParameterChecker.
-          getInstance().checkParameters(requestedGraph,
-          request.getParameterMap());
-      if (checkedParameters != null) {
-
-        /* Set the graph's attributes to the appropriate values, so that
-         * we can display the correct graph and prepopulate the form. */
-        StringBuilder urlBuilder = new StringBuilder();
-        for (Map.Entry<String, String[]> param :
-            checkedParameters.entrySet()) {
-          request.setAttribute(requestedGraph.replaceAll("-", "_") + "_"
-              + param.getKey(), param.getValue());
-          for (String paramValue : param.getValue()) {
-            urlBuilder.append("&" + param.getKey() + "=" + paramValue);
-          }
-        }
-        String url = "?" + urlBuilder.toString().substring(1);
-        request.setAttribute(requestedGraph.replaceAll("-", "_") + "_url",
-            url);
-      }
-    }
-    if (requestedTable != null) {
-
-      /* Check if the passed parameters are valid. */
-      Map<String, String[]> checkedParameters = TableParameterChecker.
-          getInstance().checkParameters(requestedTable,
-          request.getParameterMap());
-      if (checkedParameters != null) {
-
-        /* Set the table's attributes to the appropriate values, so that
-         * we can prepopulate the form. */
-        for (Map.Entry<String, String[]> param :
-            checkedParameters.entrySet()) {
-          request.setAttribute(requestedTable.replaceAll("-", "_") + "_"
-              + param.getKey(), param.getValue());
-        }
-      }
-    }
-
-    /* Generate table data if the graphs subpage has any tables,
-     * regardless of whether a table update was requested, and add the
-     * table data as request attribute. */
-    if (this.availableGraphsSubpageTables.containsKey(requestedPage)) {
-      for (String tableName :
-          this.availableGraphsSubpageTables.get(requestedPage)) {
-        List<Map<String, String>> tableData = rObjectGenerator.
-            generateTable(tableName, requestedTable,
-            request.getParameterMap(), true);
-        request.setAttribute(tableName.replaceAll("-", "_")
-              + "_tabledata", tableData);
-      }
-    }
-
-    /* Pass list of known countries in case we want to display them. */
-    request.setAttribute("countries", this.knownCountries);
-
-    /* Pass the default start and end dates. */
-    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
-    dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
-    Date defaultEndDate = new Date();
-    Date defaultStartDate = new Date(defaultEndDate.getTime()
-        - 90L * 24L * 60L * 60L * 1000L);
-    request.setAttribute("default_start_date",
-        dateFormat.format(defaultStartDate));
-    request.setAttribute("default_end_date",
-        dateFormat.format(defaultEndDate));
-
-    /* Forward the request to the JSP that does all the hard work. */
-    request.getRequestDispatcher(jsp).forward(request, response);
-  }
-}
-
diff --git a/src/org/torproject/ernie/web/graphs/RObject.java b/src/org/torproject/ernie/web/graphs/RObject.java
deleted file mode 100644
index cfab819..0000000
--- a/src/org/torproject/ernie/web/graphs/RObject.java
+++ /dev/null
@@ -1,23 +0,0 @@
-/* Copyright 2011, 2012 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.ernie.web.graphs;
-
-public class RObject {
-  private byte[] bytes;
-  private String fileName;
-  private long lastModified;
-  public RObject(byte[] bytes, String fileName, long lastModified) {
-    this.bytes = bytes;
-    this.fileName = fileName;
-    this.lastModified = lastModified;
-  }
-  public String getFileName() {
-    return this.fileName;
-  }
-  public byte[] getBytes() {
-    return this.bytes;
-  }
-  public long getLastModified() {
-    return this.lastModified;
-  }
-}
diff --git a/src/org/torproject/ernie/web/graphs/RObjectGenerator.java b/src/org/torproject/ernie/web/graphs/RObjectGenerator.java
deleted file mode 100644
index 927b5c4..0000000
--- a/src/org/torproject/ernie/web/graphs/RObjectGenerator.java
+++ /dev/null
@@ -1,398 +0,0 @@
-/* Copyright 2011, 2012 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.ernie.web.graphs;
-
-import java.io.BufferedInputStream;
-import java.io.BufferedReader;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.SortedSet;
-import java.util.TreeSet;
-
-import javax.servlet.ServletContext;
-import javax.servlet.ServletContextEvent;
-import javax.servlet.ServletContextListener;
-
-import org.rosuda.REngine.Rserve.RConnection;
-import org.rosuda.REngine.Rserve.RserveException;
-
-public class RObjectGenerator implements ServletContextListener {
-
-  /* Host and port where Rserve is listening. */
-  private String rserveHost;
-  private int rservePort;
-
-  /* Some parameters for our cache of graph images. */
-  private String cachedGraphsDirectory;
-  private long maxCacheAge;
-
-  private SortedSet<String> availableCsvFiles;
-  private Map<String, String> availableTables;
-  private Map<String, String> availableGraphs;
-  private Set<String> availableGraphFileTypes;
-
-  public void contextInitialized(ServletContextEvent event) {
-
-    /* Initialize using context parameters. */
-    ServletContext servletContext = event.getServletContext();
-    this.rserveHost = servletContext.getInitParameter("rserveHost");
-    this.rservePort = Integer.parseInt(servletContext.getInitParameter(
-        "rservePort"));
-    this.maxCacheAge = Long.parseLong(servletContext.getInitParameter(
-        "maxCacheAge"));
-    this.cachedGraphsDirectory = servletContext.getInitParameter(
-        "cachedGraphsDir");
-
-    /* Initialize map of available CSV files. */
-    this.availableCsvFiles = new TreeSet<String>();
-    this.availableCsvFiles.add("bandwidth");
-    this.availableCsvFiles.add("bandwidth-flags");
-    this.availableCsvFiles.add("bwhist-flags");
-    this.availableCsvFiles.add("connbidirect");
-    this.availableCsvFiles.add("cloudbridges");
-    this.availableCsvFiles.add("dirbytes");
-    this.availableCsvFiles.add("monthly-userstats-average");
-    this.availableCsvFiles.add("monthly-userstats-peak");
-    this.availableCsvFiles.add("networksize");
-    this.availableCsvFiles.add("platforms");
-    this.availableCsvFiles.add("relaycountries");
-    this.availableCsvFiles.add("relayflags");
-    this.availableCsvFiles.add("torperf");
-    this.availableCsvFiles.add("torperf-failures");
-    this.availableCsvFiles.add("userstats");
-    this.availableCsvFiles.add("userstats-detector");
-    this.availableCsvFiles.add("versions");
-
-    this.availableTables = new HashMap<String, String>();
-    this.availableTables.put("userstats-relay", "start,end,filename");
-    this.availableTables.put("userstats-bridge", "start,end,filename");
-    this.availableTables.put("userstats-censorship-events",
-        "start,end,filename");
-    TableParameterChecker.getInstance().setAvailableTables(
-        availableTables);
-
-    this.availableGraphs = new HashMap<String, String>();
-    this.availableGraphs.put("networksize", "start,end,filename");
-    this.availableGraphs.put("cloudbridges", "start,end,filename");
-    this.availableGraphs.put("relaycountries",
-        "start,end,country,filename");
-    this.availableGraphs.put("relayflags", "start,end,flag,filename");
-    this.availableGraphs.put("versions", "start,end,filename");
-    this.availableGraphs.put("platforms", "start,end,filename");
-    this.availableGraphs.put("bandwidth", "start,end,filename");
-    this.availableGraphs.put("bandwidth-flags", "start,end,filename");
-    this.availableGraphs.put("bwhist-flags", "start,end,filename");
-    this.availableGraphs.put("dirbytes", "start,end,filename");
-    this.availableGraphs.put("torperf",
-         "start,end,source,filesize,filename");
-    this.availableGraphs.put("torperf-failures",
-         "start,end,source,filesize,filename");
-    this.availableGraphs.put("connbidirect", "start,end,filename");
-    this.availableGraphs.put("fast-exits", "start,end,filename");
-    this.availableGraphs.put("almost-fast-exits", "start,end,filename");
-    this.availableGraphs.put("userstats-relay-country",
-        "start,end,country,events,filename");
-    this.availableGraphs.put("userstats-bridge-country",
-        "start,end,country,filename");
-    this.availableGraphs.put("userstats-bridge-transport",
-        "start,end,transport,filename");
-    this.availableGraphs.put("userstats-bridge-version",
-        "start,end,version,filename");
-    this.availableGraphFileTypes = new HashSet<String>(Arrays.asList(
-        "png,pdf,svg".split(",")));
-    GraphParameterChecker.getInstance().setAvailableGraphs(
-        availableGraphs);
-
-    /* Register ourself, so that servlets can use us. */
-    servletContext.setAttribute("RObjectGenerator", this);
-
-    /* Periodically generate R objects with default parameters. */
-    new Thread() {
-      public void run() {
-        long lastUpdated = 0L, sleep;
-        while (true) {
-          while ((sleep = maxCacheAge * 1000L / 2L + lastUpdated
-              - System.currentTimeMillis()) > 0L) {
-            try {
-              Thread.sleep(sleep);
-            } catch (InterruptedException e) {
-            }
-          }
-          for (String csvFile : availableCsvFiles) {
-            generateCsv(csvFile, false);
-          }
-          for (String tableName : availableTables.keySet()) {
-            generateTable(tableName, tableName, new HashMap(), false);
-          }
-          for (String graphName : availableGraphs.keySet()) {
-            for (String fileType : availableGraphFileTypes) {
-              generateGraph(graphName, fileType, new HashMap(), false);
-            }
-          }
-          lastUpdated = System.currentTimeMillis();
-        }
-      };
-    }.start();
-  }
-
-  public void contextDestroyed(ServletContextEvent event) {
-    /* Nothing to do. */
-  }
-
-  public RObject generateGraph(String requestedGraph, String fileType,
-      Map parameterMap, boolean checkCache) {
-    Map<String, String[]> checkedParameters = GraphParameterChecker.
-        getInstance().checkParameters(requestedGraph, parameterMap);
-    if (checkedParameters == null) {
-      /* TODO We're going to take the blame by sending an internal server
-       * error to the client, but really the user is to blame. */
-      return null;
-    }
-    StringBuilder rQueryBuilder = new StringBuilder("plot_"
-        + requestedGraph.replaceAll("-", "_") + "("),
-        imageFilenameBuilder = new StringBuilder(requestedGraph);
-    for (Map.Entry<String, String[]> parameter :
-        checkedParameters.entrySet()) {
-      String parameterName = parameter.getKey();
-      String[] parameterValues = parameter.getValue();
-      for (String param : parameterValues) {
-        imageFilenameBuilder.append("-" + param);
-      }
-      if (parameterValues.length < 2) {
-        rQueryBuilder.append(parameterName + " = '" + parameterValues[0]
-            + "', ");
-      } else {
-        rQueryBuilder.append(parameterName + " = c(");
-        for (int i = 0; i < parameterValues.length - 1; i++) {
-          rQueryBuilder.append("'" + parameterValues[i] + "', ");
-        }
-        rQueryBuilder.append("'" + parameterValues[
-            parameterValues.length - 1] + "'), ");
-      }
-    }
-    imageFilenameBuilder.append("." + fileType);
-    String imageFilename = imageFilenameBuilder.toString();
-    rQueryBuilder.append("path = '%s')");
-    String rQuery = rQueryBuilder.toString();
-    File imageFile = new File(this.cachedGraphsDirectory + "/"
-        + imageFilename);
-    return this.generateRObject(rQuery, imageFile, imageFilename,
-        checkCache);
-  }
-
-  public SortedSet<String> getAvailableCsvFiles() {
-    return this.availableCsvFiles;
-  }
-
-  public RObject generateCsv(String requestedCsvFile,
-      boolean checkCache) {
-    /* Prepare filename and R query string. */
-    String rQuery = "export_" + requestedCsvFile.replaceAll("-", "_")
-        + "(path = '%s')";
-    String csvFilename = requestedCsvFile + ".csv";
-
-    /* See if we need to generate this .csv file. */
-    File csvFile = new File(this.cachedGraphsDirectory + "/"
-        + csvFilename);
-    return this.generateRObject(rQuery, csvFile, csvFilename, checkCache);
-  }
-
-  public List<Map<String, String>> generateTable(String tableName,
-      String requestedTable, Map parameterMap, boolean checkCache) {
-
-    Map<String, String[]> checkedParameters = null;
-    if (tableName.equals(requestedTable)) {
-      checkedParameters = TableParameterChecker.
-          getInstance().checkParameters(requestedTable,
-          parameterMap);
-    } else {
-      checkedParameters = TableParameterChecker.
-          getInstance().checkParameters(tableName, null);
-    }
-    if (checkedParameters == null) {
-      /* TODO We're going to take the blame by sending an internal server
-       * error to the client, but really the user is to blame. */
-      return null;
-    }
-    StringBuilder rQueryBuilder = new StringBuilder("write_"
-        + tableName.replaceAll("-", "_") + "("),
-        tableFilenameBuilder = new StringBuilder(tableName);
-
-    for (Map.Entry<String, String[]> parameter :
-        checkedParameters.entrySet()) {
-      String parameterName = parameter.getKey();
-      String[] parameterValues = parameter.getValue();
-      for (String param : parameterValues) {
-        tableFilenameBuilder.append("-" + param);
-      }
-      if (parameterValues.length < 2) {
-        rQueryBuilder.append(parameterName + " = '"
-            + parameterValues[0] + "', ");
-      } else {
-        rQueryBuilder.append(parameterName + " = c(");
-        for (int i = 0; i < parameterValues.length - 1; i++) {
-          rQueryBuilder.append("'" + parameterValues[i] + "', ");
-        }
-        rQueryBuilder.append("'" + parameterValues[
-            parameterValues.length - 1] + "'), ");
-      }
-    }
-    tableFilenameBuilder.append(".tbl");
-    String tableFilename = tableFilenameBuilder.toString();
-    rQueryBuilder.append("path = '%s')");
-    String rQuery = rQueryBuilder.toString();
-    return this.generateTable(rQuery, tableFilename, checkCache);
-  }
-
-  /* Generate table data using the given R query and filename or read
-   * previously generated table data from disk if it's not too old and
-   * return table data. */
-  private List<Map<String, String>> generateTable(String rQuery,
-      String tableFilename, boolean checkCache) {
-
-    /* See if we need to generate this table. */
-    File tableFile = new File(this.cachedGraphsDirectory + "/"
-        + tableFilename);
-    byte[] tableBytes = this.generateRObject(rQuery, tableFile,
-        tableFilename, checkCache).getBytes();
-
-    /* Write the table content to a map. */
-    List<Map<String, String>> result = null;
-    try {
-      result = new ArrayList<Map<String, String>>();
-      BufferedReader br = new BufferedReader(new InputStreamReader(
-          new ByteArrayInputStream(tableBytes)));
-      String line = br.readLine();
-      if (line != null) {
-        List<String> headers = new ArrayList<String>(Arrays.asList(
-            line.split(",")));
-        while ((line = br.readLine()) != null) {
-          String[] parts = line.split(",");
-          if (headers.size() != parts.length) {
-            return null;
-          }
-          Map<String, String> row = new HashMap<String, String>();
-          for (int i = 0; i < headers.size(); i++) {
-            row.put(headers.get(i), parts[i]);
-          }
-          result.add(row);
-        }
-      }
-    } catch (IOException e) {
-      return null;
-    }
-
-    /* Return table values. */
-    return result;
-  }
-
-  /* Generate an R object in a separate worker thread, or wait for an
-   * already running worker thread to finish and get its result. */
-  private RObject generateRObject(String rQuery, File rObjectFile,
-      String fileName, boolean checkCache) {
-    RObjectGeneratorWorker worker = null;
-    synchronized (this.rObjectGeneratorThreads) {
-      if (this.rObjectGeneratorThreads.containsKey(rQuery)) {
-        worker = this.rObjectGeneratorThreads.get(rQuery);
-      } else {
-        worker = new RObjectGeneratorWorker(rQuery, rObjectFile,
-            fileName, checkCache);
-        this.rObjectGeneratorThreads.put(rQuery, worker);
-        worker.start();
-      }
-    }
-    try {
-      worker.join();
-    } catch (InterruptedException e) {
-    }
-    synchronized (this.rObjectGeneratorThreads) {
-      if (this.rObjectGeneratorThreads.containsKey(rQuery) &&
-          this.rObjectGeneratorThreads.get(rQuery) == worker) {
-        this.rObjectGeneratorThreads.remove(rQuery);
-      }
-    }
-    return worker.getRObject();
-  }
-
-  private Map<String, RObjectGeneratorWorker> rObjectGeneratorThreads =
-      new HashMap<String, RObjectGeneratorWorker>();
-
-  private class RObjectGeneratorWorker extends Thread {
-
-    private String rQuery;
-    private File rObjectFile;
-    private String fileName;
-    private boolean checkCache;
-    private RObject result = null;
-
-    public RObjectGeneratorWorker(String rQuery, File rObjectFile,
-        String fileName, boolean checkCache) {
-      this.rQuery = rQuery;
-      this.rObjectFile = rObjectFile;
-      this.fileName = fileName;
-      this.checkCache = checkCache;
-    }
-
-    public void run() {
-
-      /* See if we need to generate this R object. */
-      long now = System.currentTimeMillis();
-      if (!this.checkCache || !this.rObjectFile.exists() ||
-          this.rObjectFile.lastModified() < now - maxCacheAge * 1000L) {
-
-        /* We do. Update the R query to contain the absolute path to the
-         * file to be generated, create a connection to Rserve, run the R
-         * query, and close the connection. The generated object will be
-         * on disk. */
-        this.rQuery = String.format(this.rQuery,
-            this.rObjectFile.getAbsolutePath());
-        try {
-          RConnection rc = new RConnection(rserveHost, rservePort);
-          rc.eval(this.rQuery);
-          rc.close();
-        } catch (RserveException e) {
-          return;
-        }
-
-        /* Check that we really just generated the R object. */
-        if (!this.rObjectFile.exists() || this.rObjectFile.lastModified()
-            < now - maxCacheAge * 1000L) {
-          return;
-        }
-      }
-
-      /* Read the R object from disk and write it to a byte array. */
-      long lastModified = this.rObjectFile.lastModified();
-      try {
-        BufferedInputStream bis = new BufferedInputStream(
-            new FileInputStream(this.rObjectFile), 1024);
-        ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        byte[] buffer = new byte[1024];
-        int length;
-        while ((length = bis.read(buffer)) > 0) {
-          baos.write(buffer, 0, length);
-        }
-        bis.close();
-        this.result = new RObject(baos.toByteArray(), this.fileName,
-            lastModified);
-      } catch (IOException e) {
-        return;
-      }
-    }
-
-    public RObject getRObject() {
-      return this.result;
-    }
-  }
-}
diff --git a/src/org/torproject/ernie/web/graphs/TableParameterChecker.java b/src/org/torproject/ernie/web/graphs/TableParameterChecker.java
deleted file mode 100644
index 02a0c85..0000000
--- a/src/org/torproject/ernie/web/graphs/TableParameterChecker.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/* Copyright 2011, 2012 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.ernie.web.graphs;
-
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.TimeZone;
-
-/**
- * Checks request parameters passed to generate tables.
- */
-public class TableParameterChecker {
-
-  /**
-   * Singleton instance of this class.
-   */
-  private static TableParameterChecker instance =
-      new TableParameterChecker();
-
-  /**
-   * Returns the singleton instance of this class.
-   */
-  public static TableParameterChecker getInstance() {
-    return instance;
-  }
-
-  /* Date format for parsing start and end dates. */
-  private SimpleDateFormat dateFormat;
-
-  /* Available tables with corresponding parameter lists. */
-  private Map<String, String> availableTables;
-
-  /**
-   * Initializes map with valid parameters for each of the graphs.
-   */
-  public TableParameterChecker() {
-    this.dateFormat = new SimpleDateFormat("yyyy-MM-dd");
-    this.dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
-  }
-
-  public void setAvailableTables(Map<String, String> availableTables) {
-    this.availableTables = availableTables;
-  }
-
-  /**
-   * Checks request parameters for the given table type and returns a map
-   * of recognized parameters, or null if the table type doesn't exist or
-   * the parameters are invalid.
-   */
-  public Map<String, String[]> checkParameters(String tableType,
-      Map requestParameters) {
-
-    /* Check if the table type exists. */
-    if (tableType == null ||
-        !this.availableTables.containsKey(tableType)) {
-      return null;
-    }
-
-    /* Find out which other parameters are supported by this table type
-     * and parse them if they are given. */
-    Set<String> supportedTableParameters = new HashSet<String>(Arrays.
-        asList(this.availableTables.get(tableType).split(",")));
-    Map<String, String[]> recognizedTableParameters =
-        new HashMap<String, String[]>();
-
-    /* Parse start and end dates if supported by the table type. If no end
-     * date is provided, set it to today. If no start date is provided,
-     * set it to 90 days before the end date. Make sure that start date
-     * precedes end date. */
-    if (supportedTableParameters.contains("start") ||
-        supportedTableParameters.contains("end")) {
-      String[] startParameter = null;
-      String[] endParameter = null;
-      if (requestParameters != null) {
-        startParameter = (String[]) requestParameters.get("start");
-        endParameter = (String[]) requestParameters.get("end");
-      }
-      long endTimestamp = System.currentTimeMillis();
-      if (endParameter != null && endParameter.length > 0 &&
-          endParameter[0].length() > 0) {
-        try {
-          endTimestamp = dateFormat.parse(endParameter[0]).getTime();
-        } catch (ParseException e)  {
-          return null;
-        }
-        if (!endParameter[0].startsWith("20")) {
-          return null;
-        }
-      }
-      endParameter = new String[] { dateFormat.format(endTimestamp) };
-      long startTimestamp = endTimestamp - 90L * 24L * 60L * 60L * 1000L;
-      if (startParameter != null && startParameter.length > 0 &&
-          startParameter[0].length() > 0) {
-        try {
-          startTimestamp = dateFormat.parse(startParameter[0]).getTime();
-        } catch (ParseException e)  {
-          return null;
-        }
-        if (!startParameter[0].startsWith("20")) {
-          return null;
-        }
-      }
-      startParameter = new String[] { dateFormat.format(startTimestamp) };
-      if (startTimestamp > endTimestamp) {
-       return null;
-      }
-      recognizedTableParameters.put("start", startParameter);
-      recognizedTableParameters.put("end", endParameter);
-    }
-
-    /* We now have a map with all required table parameters. Return it. */
-    return recognizedTableParameters;
-  }
-}
-
diff --git a/src/org/torproject/ernie/web/research/ResearchDataServlet.java b/src/org/torproject/ernie/web/research/ResearchDataServlet.java
deleted file mode 100644
index 6c24e0d..0000000
--- a/src/org/torproject/ernie/web/research/ResearchDataServlet.java
+++ /dev/null
@@ -1,260 +0,0 @@
-/* Copyright 2011, 2012 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.ernie.web.research;
-
-import java.io.File;
-import java.io.IOException;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.SortedMap;
-import java.util.TreeMap;
-
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-/**
- * Controller servlet for the Data page. Prepares the various lists of
- * downloadable metrics data files by parsing a file with URLs on other
- * servers and looking at a local directory with files served by local
- * Apache HTTP server. The file with URLs on other servers may contain
- * comment lines starting with #. Recognizes metrics data file types from
- * the file names.
- */
-public class ResearchDataServlet extends HttpServlet {
-
-  private static final long serialVersionUID = -5168280373350515577L;
-
-  public void doGet(HttpServletRequest request,
-      HttpServletResponse response) throws IOException, ServletException {
-
-    /* Read local directory with files served by the local Apache HTTP
-     * server and add the URLs to the list. */
-    List<String> dataFileUrls = new ArrayList<String>();
-    String localDataDir = getServletConfig().getInitParameter(
-        "localDataDir");
-    if (localDataDir != null) {
-      try {
-        File localDataDirFile = new File(localDataDir);
-        if (localDataDirFile.exists() && localDataDirFile.isDirectory()) {
-          for (File localDataFile : localDataDirFile.listFiles()) {
-            if (!localDataFile.isDirectory()) {
-              dataFileUrls.add("/data/" + localDataFile.getName());
-            }
-          }
-        }
-      } catch (SecurityException e) {
-        /* We're not permitted to read the directory with metrics data
-         * files. Ignore. */
-      }
-    }
-
-    /* Prepare data structures that we're going to pass to the JSP. All
-     * data structures are (nested) maps with the map keys being used for
-     * displaying the files in tables and map values being 2-element
-     * arrays containing the file url and optional signature file. */
-    SortedMap<Date, Map<String, String[]>> relayDescriptors =
-        new TreeMap<Date, Map<String, String[]>>(
-        java.util.Collections.reverseOrder());
-    String[] certs = new String[2];
-    SortedMap<Date, String[]> bridgeDescriptors =
-        new TreeMap<Date, String[]>(java.util.Collections.reverseOrder());
-    String[] relayStatistics = new String[2];
-    SortedMap<Date, String[]> torperfTarballs =
-        new TreeMap<Date, String[]>(java.util.Collections.reverseOrder());
-    SortedMap<String, Map<String, String[]>> torperfData =
-        new TreeMap<String, Map<String, String[]>>();
-    SortedMap<Date, String[]> exitLists =
-        new TreeMap<Date, String[]>(java.util.Collections.reverseOrder());
-    SortedMap<Date, String[]> torperfExperiments =
-        new TreeMap<Date, String[]>();
-    SortedMap<Date, String[]> bridgePoolAssignments =
-        new TreeMap<Date, String[]>(java.util.Collections.reverseOrder());
-
-    /* Prepare rewriting Torperf sources. */
-    Map<String, String> torperfSources = new HashMap<String, String>();
-    torperfSources.put("torperffast", "torperf, fastest");
-    torperfSources.put("torperffastratio", "torperf, best ratio");
-    torperfSources.put("torperfslow", "torperf, slowest");
-    torperfSources.put("torperfslowratio", "torperf, worst ratio");
-
-    /* Go through the file list, decide for each file what metrics data
-     * type it is, and put it in the appropriate map. */
-    SimpleDateFormat monthFormat = new SimpleDateFormat("yyyy-MM");
-    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
-    List<String> torperfFilesizes = Arrays.asList("50kb,1mb,5mb".
-        split(","));
-    for (String url : dataFileUrls) {
-      if (!url.contains("/")) {
-        continue;
-      }
-      String filename = url.substring(url.lastIndexOf("/") + 1);
-
-      /* URL contains relay descriptors. */
-      if (filename.startsWith("tor-20") ||
-          filename.startsWith("statuses-20") ||
-          filename.startsWith("server-descriptors-20") ||
-          filename.startsWith("extra-infos-20") ||
-          filename.startsWith("votes-20") ||
-          filename.startsWith("consensuses-20")) {
-        String type = filename.substring(0, filename.indexOf("-20"));
-        String yearMonth = filename.substring(filename.indexOf("20"));
-        yearMonth = yearMonth.substring(0, 7);
-        Date month = null;
-        try {
-          month = monthFormat.parse(yearMonth);
-        } catch (ParseException e) {
-          /* Ignore this URL. */
-          continue;
-        }
-        int index = filename.endsWith(".asc") ? 1 : 0;
-        if (!relayDescriptors.containsKey(month)) {
-          relayDescriptors.put(month, new HashMap<String, String[]>());
-        }
-        if (!relayDescriptors.get(month).containsKey(type)) {
-          relayDescriptors.get(month).put(type, new String[2]);
-        }
-        relayDescriptors.get(month).get(type)[index] = url;
-
-      /* URL contains v3 certificates. */
-      } else if (filename.startsWith("certs.tar")) {
-        int index = filename.endsWith(".asc") ? 1 : 0;
-        certs[index] = url;
-
-      /* URL contains bridge descriptors. */
-      } else if (filename.startsWith("bridge-descriptors-20")) {
-        String yearMonth = filename.substring(filename.indexOf("20"));
-        yearMonth = yearMonth.substring(0, 7);
-        Date month = null;
-        try {
-          month = monthFormat.parse(yearMonth);
-        } catch (ParseException e) {
-          /* Ignore this URL. */
-          continue;
-        }
-        int index = filename.endsWith(".asc") ? 1 : 0;
-        if (!bridgeDescriptors.containsKey(month)) {
-          bridgeDescriptors.put(month, new String[2]);
-        }
-        bridgeDescriptors.get(month)[index] = url;
-
-      /* URL contains relay statistics. */
-      } else if (filename.startsWith("relay-statistics.tar.bz2")) {
-        int index = filename.endsWith(".asc") ? 1 : 0;
-        relayStatistics[index] = url;
-
-      /* URL contains Torperf tarball. */
-      } else if (filename.startsWith("torperf-20")) {
-        String yearMonth = filename.substring(filename.indexOf("20"));
-        yearMonth = yearMonth.substring(0, 7);
-        Date month = null;
-        try {
-          month = monthFormat.parse(yearMonth);
-        } catch (ParseException e) {
-          /* Ignore this URL. */
-          continue;
-        }
-        if (!torperfTarballs.containsKey(month)) {
-          torperfTarballs.put(month, new String[2]);
-        }
-        torperfTarballs.get(month)[0] = url;
-
-      /* URL contains Torperf data file. */
-      } else if (filename.endsWith("b.data") ||
-          filename.endsWith("b.extradata")) {
-        boolean isExtraData = filename.endsWith("b.extradata");
-        String[] parts = filename.split("-");
-        if (parts.length != 2) {
-          continue;
-        }
-        String source = parts[0];
-        if (torperfSources.containsKey(source)) {
-          source = torperfSources.get(source);
-        }
-        String filesize = parts[1];
-        filesize = filesize.substring(0, filesize.length()
-            - (isExtraData ? 10 : 5));
-        if (!torperfFilesizes.contains(filesize)) {
-          continue;
-        }
-        if (!torperfData.containsKey(source)) {
-          torperfData.put(source, new HashMap<String, String[]>());
-        }
-        if (!torperfData.get(source).containsKey(filesize)) {
-          torperfData.get(source).put(filesize, new String[2]);
-        }
-        torperfData.get(source).get(filesize)[isExtraData ? 1 : 0] = url;
-
-      /* URL contains Torperf experiment tarball. */
-      } else if (filename.startsWith("torperf-experiment-20")) {
-        String dateString = filename.substring(filename.indexOf("20"));
-        dateString = dateString.substring(0, 10);
-        Date date = null;
-        try {
-          date = dateFormat.parse(dateString);
-        } catch (ParseException e) {
-          /* Ignore this URL. */
-          continue;
-        }
-        if (!torperfExperiments.containsKey(date)) {
-          torperfExperiments.put(date, new String[2]);
-        }
-        torperfExperiments.get(date)[0] = url;
-
-      /* URL contains exit list. */
-      } else if (filename.startsWith("exit-list-20")) {
-        String yearMonth = filename.substring(filename.indexOf("20"));
-        yearMonth = yearMonth.substring(0, 7);
-        Date month = null;
-        try {
-          month = monthFormat.parse(yearMonth);
-        } catch (ParseException e) {
-          /* Ignore this URL. */
-          continue;
-        }
-        if (!exitLists.containsKey(month)) {
-          exitLists.put(month, new String[2]);
-        }
-        exitLists.get(month)[0] = url;
-
-      /* URL contains bridge pool assignments. */
-      } else if (filename.startsWith("bridge-pool-assignments-20")) {
-        String yearMonth = filename.substring(filename.indexOf("20"));
-        yearMonth = yearMonth.substring(0, 7);
-        Date month = null;
-        try {
-          month = monthFormat.parse(yearMonth);
-        } catch (ParseException e) {
-          /* Ignore this URL. */
-          continue;
-        }
-        if (!bridgePoolAssignments.containsKey(month)) {
-          bridgePoolAssignments.put(month, new String[2]);
-        }
-        bridgePoolAssignments.get(month)[0] = url;
-      }
-    }
-
-    /* Add the maps to the request and forward it to the JSP to display
-     * the page. */
-    request.setAttribute("relayDescriptors", relayDescriptors);
-    request.setAttribute("certs", certs);
-    request.setAttribute("bridgeDescriptors", bridgeDescriptors);
-    request.setAttribute("relayStatistics", relayStatistics);
-    request.setAttribute("torperfData", torperfData);
-    request.setAttribute("exitLists", exitLists);
-    request.setAttribute("torperfTarballs", torperfTarballs);
-    request.setAttribute("torperfExperiments", torperfExperiments);
-    request.setAttribute("bridgePoolAssignments", bridgePoolAssignments);
-    request.getRequestDispatcher("WEB-INF/data.jsp").forward(request,
-        response);
-  }
-}
-
diff --git a/src/org/torproject/ernie/web/research/ResearchFormatsServlet.java b/src/org/torproject/ernie/web/research/ResearchFormatsServlet.java
deleted file mode 100644
index 3b70ca3..0000000
--- a/src/org/torproject/ernie/web/research/ResearchFormatsServlet.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/* Copyright 2011, 2012 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.ernie.web.research;
-
-import java.io.IOException;
-
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-public class ResearchFormatsServlet extends HttpServlet {
-
-  private static final long serialVersionUID = 5666493868675314116L;
-
-  public void doGet(HttpServletRequest request,
-      HttpServletResponse response) throws IOException, ServletException {
-
-    /* Forward the request to the JSP that does all the hard work. */
-    request.getRequestDispatcher("WEB-INF/formats.jsp").forward(request,
-        response);
-  }
-}
-
diff --git a/src/org/torproject/ernie/web/research/ResearchPapersServlet.java b/src/org/torproject/ernie/web/research/ResearchPapersServlet.java
deleted file mode 100644
index a63eef0..0000000
--- a/src/org/torproject/ernie/web/research/ResearchPapersServlet.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/* Copyright 2011, 2012 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.ernie.web.research;
-
-import java.io.IOException;
-
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-public class ResearchPapersServlet extends HttpServlet {
-
-  private static final long serialVersionUID = -8135459207158536268L;
-
-  public void doGet(HttpServletRequest request,
-      HttpServletResponse response) throws IOException, ServletException {
-
-    /* Forward the request to the JSP that does all the hard work. */
-    request.getRequestDispatcher("WEB-INF/papers.jsp").forward(request,
-        response);
-  }
-}
-
diff --git a/src/org/torproject/ernie/web/research/ResearchStatsServlet.java b/src/org/torproject/ernie/web/research/ResearchStatsServlet.java
deleted file mode 100644
index 2ca93a3..0000000
--- a/src/org/torproject/ernie/web/research/ResearchStatsServlet.java
+++ /dev/null
@@ -1,132 +0,0 @@
-/* Copyright 2013 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.ernie.web.research;
-
-import java.io.BufferedInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.util.SortedSet;
-import java.util.TreeSet;
-
-import javax.servlet.ServletConfig;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-public class ResearchStatsServlet extends HttpServlet {
-
-  private static final long serialVersionUID = 3346710354297653810L;
-
-  private File statsDir;
-
-  private SortedSet<String> availableStatisticsFiles;
-
-  public void init(ServletConfig config) throws ServletException {
-    super.init(config);
-    this.statsDir = new File(config.getInitParameter("statsDir"));
-    this.availableStatisticsFiles = new TreeSet<String>();
-    this.availableStatisticsFiles.add("servers");
-    this.availableStatisticsFiles.add("bandwidth");
-    this.availableStatisticsFiles.add("fast-exits");
-    this.availableStatisticsFiles.add("clients");
-    this.availableStatisticsFiles.add("torperf");
-    this.availableStatisticsFiles.add("connbidirect");
-  }
-
-  public long getLastModified(HttpServletRequest request) {
-    File statsFile = this.determineStatsFile(request);
-    if (statsFile == null || !statsFile.exists()) {
-      return -1L;
-    } else {
-      return statsFile.lastModified();
-    }
-  }
-
-  public void doGet(HttpServletRequest request,
-      HttpServletResponse response) throws IOException, ServletException {
-    String requestURI = request.getRequestURI();
-    if (requestURI.equals("/ernie/stats/")) {
-      this.writeDirectoryListing(request, response);
-    } else if (requestURI.equals("/ernie/stats.html")) {
-      this.writeStatisticsPage(request, response);
-    } else {
-      File statsFile = this.determineStatsFile(request);
-      if (statsFile == null) {
-        response.sendError(HttpServletResponse.SC_NOT_FOUND);
-        return;
-      } else if (!this.writeStatsFile(statsFile, response)) {
-        response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
-      }
-    }
-  }
-
-  private void writeDirectoryListing(HttpServletRequest request,
-      HttpServletResponse response) throws IOException, ServletException {
-    request.setAttribute("directory", "/stats");
-    request.setAttribute("extension", ".csv");
-    request.setAttribute("files", this.availableStatisticsFiles);
-    request.getRequestDispatcher("/WEB-INF/dir.jsp").forward(request,
-        response);
-  }
-
-  private void writeStatisticsPage(HttpServletRequest request,
-      HttpServletResponse response) throws IOException, ServletException {
-    request.getRequestDispatcher("/WEB-INF/stats.jsp").forward(request,
-        response);
-  }
-
-  private File determineStatsFile(HttpServletRequest request) {
-    String requestedStatsFile = request.getRequestURI();
-    if (requestedStatsFile.equals("/ernie/stats/") ||
-        requestedStatsFile.equals("/ernie/stats.html")) {
-      return null;
-    }
-    if (requestedStatsFile.endsWith(".csv")) {
-      requestedStatsFile = requestedStatsFile.substring(0,
-          requestedStatsFile.length() - ".csv".length());
-    }
-    if (requestedStatsFile.contains("/")) {
-      requestedStatsFile = requestedStatsFile.substring(
-          requestedStatsFile.lastIndexOf("/") + 1);
-    }
-    if (!availableStatisticsFiles.contains(requestedStatsFile)) {
-      return null;
-    } else {
-      return new File(this.statsDir, requestedStatsFile + ".csv");
-    }
-  }
-
-  private boolean writeStatsFile(File statsFile,
-      HttpServletResponse response) throws IOException, ServletException {
-    if (!statsFile.exists()) {
-      return false;
-    }
-    byte[] statsFileBytes;
-    try {
-      BufferedInputStream bis = new BufferedInputStream(
-          new FileInputStream(statsFile), 1024);
-      ByteArrayOutputStream baos = new ByteArrayOutputStream();
-      byte[] buffer = new byte[1024];
-      int length;
-      while ((length = bis.read(buffer)) > 0) {
-        baos.write(buffer, 0, length);
-      }
-      bis.close();
-      statsFileBytes = baos.toByteArray();
-    } catch (IOException e) {
-      return false;
-    }
-    String statsFileContent = new String(statsFileBytes);
-    response.setContentType("text/csv");
-    response.setHeader("Content-Length", String.valueOf(
-        statsFileContent.length()));
-    response.setHeader("Content-Disposition",
-        "inline; filename=\"" + statsFile.getName() + "\"");
-    response.getWriter().print(statsFileContent);
-    return true;
-  }
-}
-
diff --git a/src/org/torproject/ernie/web/research/ResearchToolsServlet.java b/src/org/torproject/ernie/web/research/ResearchToolsServlet.java
deleted file mode 100644
index 173a1da..0000000
--- a/src/org/torproject/ernie/web/research/ResearchToolsServlet.java
+++ /dev/null
@@ -1,24 +0,0 @@
-/* Copyright 2011, 2012 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.ernie.web.research;
-
-import java.io.IOException;
-
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-public class ResearchToolsServlet extends HttpServlet {
-
-  private static final long serialVersionUID = -3344204426180358872L;
-
-  public void doGet(HttpServletRequest request,
-      HttpServletResponse response) throws IOException, ServletException {
-
-    /* Forward the request to the JSP that does all the hard work. */
-    request.getRequestDispatcher("WEB-INF/tools.jsp").forward(request,
-        response);
-  }
-}
-
diff --git a/web/WEB-INF/banner.jsp b/web/WEB-INF/banner.jsp
deleted file mode 100644
index 6bff272..0000000
--- a/web/WEB-INF/banner.jsp
+++ /dev/null
@@ -1,81 +0,0 @@
-<table class="banner" border="0" cellpadding="0" cellspacing="0" summary="">
-<tr>
-  <td class="banner-left">
-    <a href="/index.html">
-      <img src="/images/top-left.png" alt="Click to go to home page"
-           width="193" height="79"></a></td>
-  <td class="banner-middle">
-    <% String currentPage = request.getRequestURI(); %>
-    <a <% if (currentPage.endsWith("index.jsp")) {
-        %>class="current"<%} else {%>href="/index.html"<%}%>>Home</a>
-    <a <% if (currentPage.endsWith("graphs.jsp")) {
-        %>class="current"<%} else {%>href="/graphs.html"<%}%>>Graphs</a>
-    <a <% if (currentPage.endsWith("research.jsp")) {
-        %>class="current"<%} else {%>href="/research.html"<%}%>>Research</a>
-    <a <% if (currentPage.endsWith("status.jsp")) {
-        %>class="current"<%} else {%>href="/status.html"<%}%>>Status</a>
-    <%if (currentPage.endsWith("graphs.jsp") ||
-         currentPage.endsWith("network.jsp") ||
-         currentPage.endsWith("bubbles.jsp") ||
-         currentPage.endsWith("fast-exits.jsp") ||
-         currentPage.endsWith("users.jsp") ||
-         currentPage.endsWith("performance.jsp")) {
-     %><br>
-      <font size="2">
-        <a <%if (currentPage.endsWith("network.jsp")){
-            %>class="current"<%} else {%>href="/network.html"<%}
-            %>>Network</a>
-        <a <%if (currentPage.endsWith("fast-exits.jsp")){
-            %>class="current"<%} else {%>href="/fast-exits.html"<%}
-            %>>Fast Exits</a>
-        <a <%if (currentPage.endsWith("bubbles.jsp")){
-            %>class="current"<%} else {%>href="/bubbles.html"<%}
-            %>>Bubbles</a>
-        <a <%if (currentPage.endsWith("users.jsp")) {
-            %>class="current"<%} else {%>href="/users.html"<%}
-            %>>Users</a>
-        <a <%if (currentPage.endsWith("performance.jsp")) {
-            %>class="current"<%} else {%>href="/performance.html"<%}
-            %>>Performance</a>
-      </font>
-    <%} else if (currentPage.endsWith("status.jsp") ||
-                 currentPage.endsWith("exonerator.jsp") ||
-                 currentPage.endsWith("relay-search.jsp") ||
-                 currentPage.endsWith("consensus-health.jsp")) {
-     %><br>
-      <font size="2">
-        <a <%if (currentPage.endsWith("exonerator.jsp")){
-            %>class="current"<%} else {%>href="/exonerator.html"<%}
-            %>>ExoneraTor</a>
-        <a <%if (currentPage.endsWith("relay-search.jsp")){
-            %>class="current"<%} else {%>href="/relay-search.html"<%}
-            %>>Relay Search</a>
-        <a <%if (currentPage.endsWith("consensus-health.jsp")){
-            %>class="current"<%} else {%>href="/consensus-health.html"<%}
-            %>>Consensus Health</a>
-      </font>
-    <%} else if (currentPage.endsWith("research.jsp") ||
-                 currentPage.endsWith("data.jsp") ||
-                 currentPage.endsWith("formats.jsp") ||
-                 currentPage.endsWith("tools.jsp") ||
-                 currentPage.endsWith("stats.jsp")) {
-     %><br>
-      <font size="2">
-        <a <%if (currentPage.endsWith("data.jsp")) {
-            %>class="current"<%} else {%> href="/data.html"<%}
-            %>>Data</a>
-        <a <%if (currentPage.endsWith("formats.jsp")) {
-            %>class="current"<%} else {%> href="/formats.html"<%}
-            %>>Formats</a>
-        <a <%if (currentPage.endsWith("tools.jsp")) {
-            %>class="current"<%} else {%> href="/tools.html"<%}
-            %>>Tools</a>
-        <a <%if (currentPage.endsWith("stats.jsp")) {
-            %>class="current"<%} else {%> href="/stats.html"<%}
-            %>>Statistics</a>
-      </font>
-    <%}%>
-  </td>
-  <td class="banner-right"></td>
-</tr>
-</table>
diff --git a/web/WEB-INF/bubbles.jsp b/web/WEB-INF/bubbles.jsp
deleted file mode 100644
index 59b3025..0000000
--- a/web/WEB-INF/bubbles.jsp
+++ /dev/null
@@ -1,40 +0,0 @@
-<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
-<html>
-<head>
-  <title>Tor Metrics Portal: Network bubble graphs</title>
-  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
-  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
-  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
-  <script src="/js/d3.min.js"></script>
-  <script src="/js/bubbles.js"></script>
-</head>
-<body>
-  <div class="center">
-    <%@ include file="banner.jsp"%>
-    <div class="main-column">
-      <p>
-        All relays:
-        <a href="#no-group" onclick="make_bubble_graph('no-group');">No group</a> |
-        <a href="#as" onclick="make_bubble_graph('as');">Autonomous Systems</a> |
-        <a href="#contact" onclick="make_bubble_graph('contact');">Contact</a>  |
-        <a href="#country" onclick="make_bubble_graph('country');">Country</a> |
-        <a href="#network-family" onclick="make_bubble_graph('network-family');">Network family (/16)</a>
-      </p>
-      <p>
-        Exits only:
-        <a href="#no-group-exits-only" onclick="make_bubble_graph('no-group-exits-only');">No group</a> |
-        <a href="#as-exits-only" onclick="make_bubble_graph('as-exits-only');">Autonomous Systems</a> |
-        <a href="#contact-exits-only" onclick="make_bubble_graph('contact-exits-only');">Contact</a>  |
-        <a href="#country-exits-only" onclick="make_bubble_graph('country-exits-only');">Country</a> |
-        <a href="#network-family-exits-only" onclick="make_bubble_graph('network-family-exits-only');">Network family (/16)</a>
-      </p>
-      <script>make_bubble_graph();</script>
-      <noscript>Sorry, you need to turn on JavaScript.</script>
-    </div>
-  </div>
-  <div class="bottom" id="bottom">
-    <%@ include file="footer.jsp"%>
-  </div>
-</body>
-</html>
diff --git a/web/WEB-INF/data.jsp b/web/WEB-INF/data.jsp
deleted file mode 100644
index 06176db..0000000
--- a/web/WEB-INF/data.jsp
+++ /dev/null
@@ -1,286 +0,0 @@
-<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
-<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
-<fmt:setLocale value="en_US"/>
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
-<html>
-<head>
-  <title>Tor Metrics Portal: Data</title>
-  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
-  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
-  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
-</head>
-<body>
-  <div class="center">
-    <%@ include file="banner.jsp"%>
-    <div class="main-column">
-        <h2>Tor Metrics Portal: Data</h2>
-        <br>
-        <p>One of the main goals of the Tor Metrics Project is to make all
-        gathered data available to the public. This approach enables
-        privacy researchers to perform their own analyses using real data
-        on the Tor network, and it acts as a safeguard to not gather data
-        that are too sensitive to publish. The following data are
-        available (see the <a href="tools.html">Tools</a> section for
-        details on processing the files):</p>
-        <ul>
-          <li><a href="#relaydesc">Relay descriptor archives</a></li>
-          <li><a href="#bridgedesc">Bridge descriptor archives</a></li>
-          <li><a href="#bridgeassignments">Bridge pool assignments</a></li>
-          <li><a href="#performance">Performance data</a></li>
-          <li><a href="#exitlist">Exit lists</a></li>
-        </ul>
-        <p>The tarballs listed on this page and the raw files that were
-        published on the last three days are also available via
-        "rsync metrics.torproject.org::".</p>
-        <br>
-        <a name="relaydesc"></a>
-        <h3><a href="#relaydesc" class="anchor">Relay descriptor
-        archives</a></h3>
-        <br>
-        <p>The relay descriptor archives contain all documents that the
-        directory authorities make available about the network of relays.
-        These documents include network statuses, server (relay)
-        descriptors, and extra-info descriptors.
-        The data formats are described <a href="formats.html">here</a>.</p>
-        <table width="100%" border="0" cellpadding="5" cellspacing="0" summary="">
-          <c:forEach var="item" items="${relayDescriptors}" >
-            <fmt:formatDate var="longDate" pattern="MMMM yyyy"
-                            value="${item.key}"/>
-            <tr>
-              <td>${longDate}</td>
-              <td>
-                <c:if test="${item.value['tor'] ne null}" >
-                  <a href="${item.value['tor'][0]}">v1 directories</a>
-                  <c:if test="${item.value['tor'][1] ne null}">
-                    (<a href="${item.value['tor'][1]}">sig</a>)
-                  </c:if>
-                </c:if>
-              </td>
-              <td>
-                <c:if test="${item.value['statuses'] ne null}" >
-                  <a href="${item.value['statuses'][0]}">v2 statuses</a>
-                  <c:if test="${item.value['statuses'][1] ne null}">
-                    (<a href="${item.value['statuses'][1]}">sig</a>)
-                  </c:if>
-                </c:if>
-              </td>
-              <td>
-                <c:if test="${item.value['server-descriptors'] ne null}" >
-                  <a href="${item.value['server-descriptors'][0]}">server descriptors</a>
-                  <c:if test="${item.value['server-descriptors'][1] ne null}">
-                    (<a href="${item.value['server-descriptors'][1]}">sig</a>)
-                  </c:if>
-                </c:if>
-              </td>
-              <td>
-                <c:if test="${item.value['extra-infos'] ne null}" >
-                  <a href="${item.value['extra-infos'][0]}">extra-infos</a>
-                  <c:if test="${item.value['extra-infos'][1] ne null}">
-                    (<a href="${item.value['extra-infos'][1]}">sig</a>)
-                  </c:if>
-                </c:if>
-              </td>
-              <td>
-                <c:if test="${item.value['votes'] ne null}" >
-                  <a href="${item.value['votes'][0]}">v3 votes</a>
-                  <c:if test="${item.value['votes'][1] ne null}">
-                    (<a href="${item.value['votes'][1]}">sig</a>)
-                  </c:if>
-                </c:if>
-              </td>
-              <td>
-                <c:if test="${item.value['consensuses'] ne null}" >
-                  <a href="${item.value['consensuses'][0]}">v3 consensuses</a>
-                  <c:if test="${item.value['consensuses'][1] ne null}">
-                    (<a href="${item.value['consensuses'][1]}">sig</a>)
-                  </c:if>
-                </c:if>
-              </td>
-            </tr>
-          </c:forEach>
-        </table>
-        <c:if test="${certs[0] ne null}">
-          <br>
-          <p>In order to verify the v3 votes and v3 consensuses, download
-          the tarball of <a href="${certs[0]}">v3 certificates</a>
-          <c:if test="${certs[1] ne null}">
-            (<a href="${certs[1]}">sig</a>)
-          </c:if>
-          which is updated whenever new v3 certificates become available.</p>
-        </c:if>
-        <c:if test="${relayStatistics[0] ne null}">
-          <br>
-          <p>Some of the relays are configured to gather statistics on the
-          number of requests or connecting clients, the number of
-          processed cells per queue, or the number of exiting bytes per
-          port. Relays running version 0.2.2.4-alpha or higher can include
-          these statistics in extra-info descriptors, so that they are
-          included in the relay descriptor archives. This
-          <a href="${relayStatistics[0]}">archive</a>
-          <c:if test="${relayStatistics[1] ne null}">
-            (<a href="${relayStatistics[1]}">sig</a>)
-          </c:if>
-          contains the statistics produced by relays running earlier
-          versions.</p>
-        </c:if>
-        <br>
-        <a name="bridgedesc"></a>
-        <h3><a href="#bridgedesc" class="anchor">Bridge descriptor
-        archives</a></h3>
-        <br>
-        <p>The bridge descriptor archives contain similar documents as the
-        relay descriptor archives, but for the non-public bridges. The
-        descriptors have been sanitized before publication to remove all
-        information that could otherwise be used to locate bridges. The
-        files below contain all documents of a given month, including
-        bridge network statuses, bridge server descriptors, and bridge
-        extra-info descriptors. The sanitizing process is described
-        <a href="formats.html#bridgedesc">here</a>.</p>
-        <table width="100%" border="0" cellpadding="5" cellspacing="0" summary="">
-          <c:forEach var="item" items="${bridgeDescriptors}" >
-            <fmt:formatDate var="longDate" pattern="MMMM yyyy"
-                            value="${item.key}"/>
-            <tr>
-              <td>
-                <a href="${item.value[0]}">${longDate}</a>
-                <c:if test="${item.value[1] ne null}">
-                    (<a href="${item.value[1]}">sig</a>)
-                </c:if>
-              </td>
-            </tr>
-          </c:forEach>
-        </table>
-        <p></p>
-        <br>
-        <a name="bridgeassignments"></a>
-        <h3><a href="#bridgeassignments" class="anchor">Bridge pool
-        assignments</a></h3>
-        <br>
-        <p>BridgeDB periodically dumps the list of running bridges with
-        information about the rings, subrings, and file buckets to which
-        they are assigned to a local file.  We are archiving sanitized
-        versions of these files here to analyze how the pool assignment
-        affects a bridge's usage.</p>
-        The data format and sanitizing process is described
-        <a href="formats.html#bridgepool">here</a>.</p>
-        <table width="100%" border="0" cellpadding="5" cellspacing="0" summary="">
-          <c:forEach var="item" items="${bridgePoolAssignments}" >
-            <fmt:formatDate var="longDate" pattern="MMMM yyyy"
-                            value="${item.key}"/>
-            <tr>
-              <td>
-                <a href="${item.value[0]}">${longDate}</a>
-              </td>
-            </tr>
-          </c:forEach>
-        </table>
-        <br>
-        <a name="performance"></a>
-        <h3><a href="#performance" class="anchor">Performance
-        data</a></h3>
-        <br>
-        <p>We are continuously measuring the performance of the Tor
-        network by periodically requesting files of different sizes and
-        recording the time needed to do so. These measurements take place
-        on moria, siv, and torperf and use an unmodified Tor client.
-        The files below contain the output of the torperf application.
-        The data format is described
-        <a href="formats.html#torperf">here</a>.</p>
-        <table width="100%" border="0" cellpadding="5" cellspacing="0" summary="">
-          <c:forEach var="item" items="${torperfTarballs}" >
-            <fmt:formatDate var="longDate" pattern="MMMM yyyy"
-                            value="${item.key}"/>
-            <tr>
-              <td>
-                <a href="${item.value[0]}">${longDate}</a>
-              </td>
-            </tr>
-          </c:forEach>
-        </table>
-        <br>
-        <p>The output above is the result of combining torperf request
-        data with information about used paths.
-        The raw files are also available below.</p>
-        <table width="100%" border="0" cellpadding="5" cellspacing="0" summary="">
-          <c:forEach var="item" items="${torperfData}" >
-            <tr>
-              <td>${item.key}</td>
-              <td>
-                <c:if test="${item.value['50kb'] ne null}" >
-                  <c:if test="${item.value['50kb'][0] ne null}" >
-                    <a href="${item.value['50kb'][0]}">50 KiB requests</a>
-                  </c:if>
-                  <c:if test="${item.value['50kb'][1] ne null}" >
-                    <a href="${item.value['50kb'][1]}">50 KiB path info</a>
-                  </c:if>
-                </c:if>
-              </td>
-              <td>
-                <c:if test="${item.value['1mb'] ne null}" >
-                  <c:if test="${item.value['1mb'][0] ne null}" >
-                    <a href="${item.value['1mb'][0]}">1 MiB requests</a>
-                  </c:if>
-                  <c:if test="${item.value['1mb'][1] ne null}" >
-                    <a href="${item.value['1mb'][1]}">1 MiB path info</a>
-                  </c:if>
-                </c:if>
-              </td>
-              <td>
-                <c:if test="${item.value['5mb'] ne null}" >
-                  <c:if test="${item.value['5mb'][0] ne null}" >
-                    <a href="${item.value['5mb'][0]}">5 MiB requests</a>
-                  </c:if>
-                  <c:if test="${item.value['5mb'][1] ne null}" >
-                    <a href="${item.value['5mb'][1]}">5 MiB path info</a>
-                  </c:if>
-                </c:if>
-              </td>
-            </tr>
-          </c:forEach>
-        </table>
-        <br>
-        <p>We further conducted additional experiments with Torperf in the
-        past by modifying the guard node selection strategies or circuit
-        build timeouts.  The modified guard node selection strategies are
-        to pick guard nodes from sets of the a) absolute fastest, b)
-        absolute slowest, c) best rated vs. advertised ratio or d) worst
-        rated vs. advertised ratio nodes. The ratio mechanisms provide a
-        way to select the nodes that the bandwidth authorities think stand
-        out in their measurement.  Experiments are listed by the date when
-        they ended.  Details about the experiment setup are contained in a
-        README file in the tarballs.</p>
-        <table width="100%" border="0" cellpadding="5" cellspacing="0" summary="">
-          <c:forEach var="item" items="${torperfExperiments}" >
-            <fmt:formatDate var="endDate" pattern="MMMM dd, yyyy"
-                value="${item.key}"/>
-            <tr><td><a href="${item.value[0]}">${endDate}</a></td></tr>
-          </c:forEach>
-        </table>
-        <br>
-        <a name="exitlist"></a>
-        <h3><a href="#exitlist" class="anchor">Exit lists</a></h3>
-        <br>
-        <p>We are archiving the bulk exit lists used by
-        <a href="https://check.torproject.org/">Tor Check</a>
-        containing the IP addresses that exit relays exit from.
-        The data format is described in
-        <a href="https://www.torproject.org/tordnsel/exitlist-spec.txt">exitlist-spec.txt</a>
-        and <a href="formats.html#exitlist">here</a>.</p>
-        <table width="100%" border="0" cellpadding="5" cellspacing="0" summary="">
-          <c:forEach var="item" items="${exitLists}" >
-            <fmt:formatDate var="longDate" pattern="MMMM yyyy"
-                            value="${item.key}"/>
-            <tr>
-              <td>
-                <a href="${item.value[0]}">${longDate}</a>
-              </td>
-            </tr>
-          </c:forEach>
-        </table>
-    </div>
-  </div>
-  <div class="bottom" id="bottom">
-    <%@ include file="footer.jsp"%>
-  </div>
-</body>
-</html>
diff --git a/web/WEB-INF/dir.jsp b/web/WEB-INF/dir.jsp
deleted file mode 100644
index 1ac1819..0000000
--- a/web/WEB-INF/dir.jsp
+++ /dev/null
@@ -1,25 +0,0 @@
-<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
-<html>
-<head>
-  <title>Tor Metrics Portal: Index of ${directory}</title>
-  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
-  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
-  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
-</head>
-<body>
-  <div class="center">
-    <%@ include file="banner.jsp"%>
-    <div class="main-column">
-        <h2>Tor Metrics Portal: Index of ${directory}</h2>
-        <br>
-        <c:forEach var="file" items="${files}" >
-          <a href="${directory}/${file}${extension}">${file}${extension}</a><br>
-        </c:forEach>
-    </div>
-  </div>
-  <div class="bottom" id="bottom">
-    <%@ include file="footer.jsp"%>
-  </div>
-</body>
-</html>
diff --git a/web/WEB-INF/error.jsp b/web/WEB-INF/error.jsp
deleted file mode 100644
index 9c5150e..0000000
--- a/web/WEB-INF/error.jsp
+++ /dev/null
@@ -1,77 +0,0 @@
-<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
-<%@ page isErrorPage="true" %>
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
-<html>
-<head>
-  <title>Tor Metrics Portal: Error</title>
-  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
-  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
-  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
-</head>
-<body>
-  <div class="center">
-    <%@ include file="banner.jsp"%>
-    <div class="main-column">
-<h2>Tor Metrics Portal: Error</h2>
-<br>
-<p>
-Oops! Something went wrong here! We encountered a
-<b>
-<c:choose>
-<c:when test="${pageContext.errorData.statusCode eq 400}">
-400 Bad Request
-</c:when>
-<c:when test="${pageContext.errorData.statusCode eq 404}">
-404 Not Found
-</c:when>
-<c:when test="${pageContext.errorData.statusCode eq 500}">
-500 Internal Server Error
-</c:when>
-<c:when test="${not empty pageContext.errorData.throwable}">
-${pageContext.exception}
-</c:when>
-<c:otherwise>
-Unknown Error
-</c:otherwise>
-</c:choose>
-</b>
-when processing your request!</p>
-
-<p>
-Maybe you find what you're looking for on our sitemap:
-<ul>
-<li><a href="index.html">Home</a></li>
-<li><a href="graphs.html">Graphs</a>
-<ul>
-<li><a href="network.html">Network</a></li>
-<li><a href="fast-exits.html">Fast Exits</a></li>
-<li><a href="bubbles.html">Bubbles</a></li>
-<li><a href="users.html">Users</a></li>
-<li><a href="performance.html">Performance</a></li>
-</ul></li>
-<li><a href="research.html">Research</a>
-<ul>
-<li><a href="data.html">Data</a></li>
-<li><a href="formats.html">Formats</a></li>
-<li><a href="tools.html">Tools</a></li>
-<li><a href="stats.html">Statistics</a></li>
-</ul></li>
-<li><a href="status.html">Status</a>
-<ul>
-<li><a href="exonerator.html">ExoneraTor</a></li>
-<li><a href="relay-search.html">Relay Search</a></li>
-<li><a href="consensus-health.html">Consensus Health</a></li>
-</ul></li>
-</ul>
-</p>
-
-<p>If this problem persists, please
-<a href="mailto:tor-assistants at torproject.org">let us know</a>!</p>
-
-    </div>
-  </div>
-  <div class="bottom" id="bottom">
-    <%@ include file="footer.jsp"%>
-  </div>
-</body>
-</html>
diff --git a/web/WEB-INF/exonerator.jsp b/web/WEB-INF/exonerator.jsp
deleted file mode 100644
index 0eefe99..0000000
--- a/web/WEB-INF/exonerator.jsp
+++ /dev/null
@@ -1,45 +0,0 @@
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
-<html>
-<head>
-  <title>Tor Metrics Portal: ExoneraTor</title>
-  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
-  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
-  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
-</head>
-<body>
-  <div class="center">
-    <%@ include file="banner.jsp"%>
-  <div class="main-column" style="margin:5; Padding:0;">
-    <h2>ExoneraTor</h2>
-    <h3>or: a website that tells you whether a given IP address was a Tor
-    relay</h3>
-    <br>
-    <p>Just because you see an Internet connection from a particular IP
-    address does not mean you know <i>who</i> originated the traffic. Tor
-    anonymizes Internet traffic by "<a href="https://www.torproject.org/about/overview#thesolution">onion
-    routing</a>," sending packets through a series of encrypted hops
-    before they reach their destination. Therefore, if you see traffic
-    from a Tor node, you may be seeing traffic that originated from
-    someone using Tor, rather than from the node operator itself. The Tor
-    Project and Tor node operators have no records of the traffic that
-    passes over the network, but we do maintain current and historical
-    records of which IP addresses are part of the Tor network.</p>
-    <br>
-    <p>ExoneraTor tells you whether there was a Tor relay running on a
-    given IP address at a given time. ExoneraTor can further indicate
-    whether this relay permitted exiting to a given server and/or TCP
-    port. ExoneraTor learns these facts by parsing the public relay lists
-    and relay descriptors that are collected from the Tor directory
-    authorities and the exit lists collected by TorDNSEL. By inputting an
-    IP address and time, you can determine whether that IP was then a part
-    of the Tor network.</p>
-    <br>
-    <p><font color="red"><b>Notice:</b> This service has moved to:
-    <a href="https://exonerator.torproject.org/">https://exonerator.torproject.org/</a></font></p>
-  </div>
-  </div>
-  <div class="bottom" id="bottom">
-    <%@ include file="footer.jsp"%>
-  </div>
-</body>
-</html>
diff --git a/web/WEB-INF/fast-exits.jsp b/web/WEB-INF/fast-exits.jsp
deleted file mode 100644
index 903c88c..0000000
--- a/web/WEB-INF/fast-exits.jsp
+++ /dev/null
@@ -1,81 +0,0 @@
-<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
-<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
-<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
-<html>
-<head>
-  <title>Tor Metrics Portal: Fast Exits</title>
-  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
-  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
-  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
-</head>
-<body>
-  <div class="center">
-    <%@ include file="banner.jsp"%>
-    <div class="main-column">
-<h2>Tor Metrics Portal: Fast Exits</h2>
-<br>
-<p>This page summarizes progress in operating 125 fast exits, which is a
-deliverable for
-<a href="https://trac.torproject.org/projects/tor/wiki/org/sponsors/SponsorJ">SponsorJ</a>.</p>
-<br>
-
-<a name="fastexits"></a>
-<h3><a href="#fastexits" class="anchor">Relays meeting the fast-exit
-requirements</a></h3>
-<br>
-<img src="fast-exits.png${fast_exits_url}"
-     width="576" height="432" alt="Fast exits graph">
-<form action="fast-exits.html#fastexits">
-  <div class="formrow">
-    <input type="hidden" name="graph" value="fast-exits">
-    <p>
-    <label>Start date (yyyy-mm-dd):</label>
-      <input type="text" name="start" size="10"
-             value="<c:choose><c:when test="${fn:length(fast_exits_start) == 0}">${default_start_date}</c:when><c:otherwise>${fast_exits_start[0]}</c:otherwise></c:choose>">
-    <label>End date (yyyy-mm-dd):</label>
-      <input type="text" name="end" size="10"
-             value="<c:choose><c:when test="${fn:length(fast_exits_end) == 0}">${default_end_date}</c:when><c:otherwise>${fast_exits_end[0]}</c:otherwise></c:choose>">
-    </p><p>
-    <input class="submit" type="submit" value="Update graph">
-    </p>
-  </div>
-</form>
-<p>Download graph as
-<a href="fast-exits.pdf${fast_exits_url}">PDF</a> or
-<a href="fast-exits.svg${fast_exits_url}">SVG</a>.</p>
-<br>
-
-<a name="almostfastexits"></a>
-<h3><a href="#almostfastexits" class="anchor">Relays almost meeting the
-fast-exit requirements</a></h3>
-<br>
-<img src="almost-fast-exits.png${almost_fast_exits_url}"
-     width="576" height="432" alt="Almost fast exits graph">
-<form action="fast-exits.html#almostfastexits">
-  <div class="formrow">
-    <input type="hidden" name="graph" value="almost-fast-exits">
-    <p>
-    <label>Start date (yyyy-mm-dd):</label>
-      <input type="text" name="start" size="10"
-             value="<c:choose><c:when test="${fn:length(almost_fast_exits_start) == 0}">${default_start_date}</c:when><c:otherwise>${almost_fast_exits_start[0]}</c:otherwise></c:choose>">
-    <label>End date (yyyy-mm-dd):</label>
-      <input type="text" name="end" size="10"
-             value="<c:choose><c:when test="${fn:length(almost_fast_exits_end) == 0}">${default_end_date}</c:when><c:otherwise>${almost_fast_exits_end[0]}</c:otherwise></c:choose>">
-    </p><p>
-    <input class="submit" type="submit" value="Update graph">
-    </p>
-  </div>
-</form>
-<p>Download graph as
-<a href="almost-fast-exits.pdf${almost_fast_exits_url}">PDF</a> or
-<a href="almost-fast-exits.svg${almost_fast_exits_url}">SVG</a>.</p>
-<br>
-
-    </div>
-  </div>
-  <div class="bottom" id="bottom">
-    <%@ include file="footer.jsp"%>
-  </div>
-</body>
-</html>
diff --git a/web/WEB-INF/footer.jsp b/web/WEB-INF/footer.jsp
deleted file mode 100644
index cab9e7f..0000000
--- a/web/WEB-INF/footer.jsp
+++ /dev/null
@@ -1,13 +0,0 @@
-      <p>This material is supported in part by the National Science
-      Foundation under Grant No. CNS-0959138. Any opinions,
-      finding, and conclusions or recommendations expressed in this
-      material are those of the author(s) and do not necessarily reflect
-      the views of the National Science Foundation.</p>
-      <p>"Tor" and the "Onion Logo" are <a href="https://www.torproject.org/docs/trademark-faq.html.en">registered trademarks</a> of The Tor Project, Inc.</p>
-      <p>Data on this site is freely available under a
-      <a href="http://creativecommons.org/publicdomain/zero/1.0/">CC0 no
-      copyright declaration</a>: To the extent possible under law, the Tor
-      Project has waived all copyright and related or neighboring rights
-      in the data. Graphs are licensed under a
-      <a href="http://creativecommons.org/licenses/by/3.0/us/">Creative
-      Commons Attribution 3.0 United States License</a>.</p>
diff --git a/web/WEB-INF/formats.jsp b/web/WEB-INF/formats.jsp
deleted file mode 100644
index 4d297b2..0000000
--- a/web/WEB-INF/formats.jsp
+++ /dev/null
@@ -1,986 +0,0 @@
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
-<html>
-<head>
-  <title>Tor Metrics Portal: Data Formats</title>
-  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
-  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
-  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
-</head>
-<body>
-<div class="center">
-<%@ include file="banner.jsp"%>
-<div class="main-column">
-<h2>Tor Metrics Portal: Data Formats</h2>
-<br>
-
-<p>
-Statistical analysis in the Tor network can be performed using various
-kinds of data.
-This page gives an overview of three major data sources for
-statistics in the Tor network:</p>
-
-<ol>
-<li>First, we recap measuring the Tor network from public directory
-information (<a href="https://research.torproject.org/techreports/metrics-2009-08-07.pdf">PDF</a>)
-by describing the data format of
-<a href="#serverdesc">server descriptors and network statuses</a>,
-and we explain the sanitzation process of (non-public)
-<a href="#bridgedesc">bridge directory information</a>.</li>
-<li>Second, we describe the numerous aggregate statistics that relays
-publish about their usage (<a href="http://freehaven.net/anonbib/cache/wecsr10measuring-tor.pdf">PDF</a>),
-including
-<a href="#bytehist">byte histories</a>,
-<a href="#dirreqstats">directory request statistics</a>,
-<a href="#entrystats">connecting client statistics</a>,
-<a href="#bridgestats">bridge user statistics</a>,
-<a href="#cellstats">cell-queue statistics</a>,
-<a href="#exitstats">exit-port statistics</a>, and
-<a href="#bidistats">bidirectional connection use</a>.</li>
-<li>Third, we delineate the output of various Tor services like
-<a href="#bridgepool">BridgeDB</a>, or
-<a href="#exitlist">Tor Check</a> as well as specific measurement tools like
-<a href="#torperf">Torperf</a>.</li>
-</ol>
-
-<p>
-All data described on this page are available for download on the
-<a href="data.html">data</a> page.
-This page is based on a technical report
-(<a href="https://research.torproject.org/techreports/data-2011-03-14.pdf">PDF</a>)
-and is very likely more recent than the report.
-</p>
-<hr>
-<br>
-
-<a name="descriptortypes"></a>
-<h3><a href="#descriptortypes" class="anchor">Descriptor types</a></h3>
-<br>
-<p>
-Any file containing descriptors described on this page may contain meta
-data in its first text line using the format
-<tt>@type $descriptortype $major.$minor</tt>.
-Any tool that processes these descriptors may parse files without meta
-data or with an unknown descriptor type at its own risk, can safely parse
-files with known descriptor type and same major version number, and should
-not parse files with known descriptor type and higher major version
-number.
-</p>
-
-<p>
-The following descriptor types and versions are known.
-Gray entries are deprecated, black entries are recent:
-</p>
-
-<ul>
-<li><tt>@type server-descriptor 1.0</tt></li>
-<li><tt>@type microdescriptor 1.0</tt></li>
-<li><tt>@type extra-info 1.0</tt></li>
-<li><tt>@type directory 1.0</tt></li>
-<li><tt>@type network-status-2 1.0</tt></li>
-<li><tt>@type dir-key-certificate-3 1.0</tt></li>
-<li><tt>@type network-status-consensus-3 1.0</tt></li>
-<li><tt>@type network-status-vote-3 1.0</tt></li>
-<li><tt>@type network-status-microdesc-consensus-3 1.0</tt></li>
-<li><tt>@type bridge-network-status 1.0</tt></li>
-<li><tt>@type bridge-server-descriptor 1.0</tt></li>
-<li><font color="gray"><tt>@type bridge-extra-info 1.0</tt></font></li>
-<li><font color="gray"><tt>@type bridge-extra-info 1.1</tt> contain
-  sanitized <tt>transport</tt> lines</font></li>
-<li><tt>@type bridge-extra-info 1.2</tt> contain <tt>ntor-onion-key</tt>
-  lines</li>
-<li><tt>@type torperf 1.0</tt></li>
-<li><tt>@type bridge-pool-assignment 1.0</tt></li>
-<li><tt>@type tordnsel 1.0</tt></li>
-</ul>
-
-<hr>
-<br>
-
-<a name="serverdesc"></a>
-<h3><a href="#serverdesc" class="anchor">Server descriptors and network
-statuses</a></h3>
-<br>
-<p>
-Relays in the Tor network report their capabilities by publishing server
-descriptors to the directory authorities.
-The directory authorities confirm reachability of relays and assign flags
-to help clients make good path selections.
-Every hour, the directory authorities publish a network status consensus
-with all known running relays at the time.
-Both server descriptors and network statuses constitute a solid data basis
-for statistical analysis in the Tor network.
-We described the approach to measure the Tor network from public directory
-information in the HotPETS 2009 paper
-(<a href="https://research.torproject.org/techreports/metrics-2009-08-07.pdf">PDF</a>)
-and provide interactive
-<a href="graphs.html">graphs</a> on the metrics
-website.
-We briefly describe the most interesting pieces of the
-two descriptor formats that can be used for statistics.
-</p>
-
-<p>
-The server descriptors published by relays at least once every 18 hours
-contain the necessary information for clients to build circuits using a
-given relay.
-These server descriptors can also be useful for statistical analysis of
-the Tor network infrastructure.
-</p>
-
-<p>
-We assume that the majority of server descriptors are correct.
-But when performing statistical analysis on server descriptors, one has to
-keep in mind that only a small subset of the information written to server
-descriptors is confirmed by the trusted directory authorities.
-In theory, relays can provide false information in their server
-descriptors, even though the incentive to do so is probably low.
-</p>
-
-<blockquote>
-<p>
-<i>Server descriptor published by relay <tt>blutmagie</tt> (without
-cryptographic keys and hashes):</i>
-</p>
-<p>
-<tt>router blutmagie 192.251.226.206 443 0 80</tt><br>
-<tt>platform Tor 0.2.2.20-alpha on Linux x86_64</tt><br>
-<tt>opt protocols Link 1 2 Circuit 1</tt><br>
-<tt>published 2010-12-27 14:35:27</tt><br>
-<tt>opt fingerprint 6297 B13A 687B 521A 59C6 BD79 188A 2501 EC03 A065</tt><br>
-<tt>uptime 445412</tt><br>
-<tt>bandwidth 14336000 18432000 15905178</tt><br>
-<tt>opt extra-info-digest 5C1D5D6F8B243304079BC15CD96C7FCCB88322D4</tt><br>
-<tt>opt caches-extra-info</tt><br>
-<tt>onion-key</tt><br>
-<tt>[...]</tt><br>
-<tt>signing-key</tt><br>
-<tt>[...]</tt><br>
-<tt>family $66CA87E164F1CFCE8C3BB5C095217A28578B8BAF
-  $67EC84376D9C4C467DCE8621AACA109160B5264E
-  $7B698D327F1695590408FED95CDEE1565774D136</tt><br>
-<tt>opt hidden-service-dir</tt><br>
-<tt>contact abuse at blutmagie.de</tt><br>
-<tt>reject 0.0.0.0/8:*</tt><br>
-<tt>reject 169.254.0.0/16:*</tt><br>
-<tt>reject 127.0.0.0/8:*</tt><br>
-<tt>reject 192.168.0.0/16:*</tt><br>
-<tt>reject 10.0.0.0/8:*</tt><br>
-<tt>reject 172.16.0.0/12:*</tt><br>
-<tt>reject 192.251.226.206:*</tt><br>
-<tt>reject *:25</tt><br>
-<tt>reject *:119</tt><br>
-<tt>reject *:135-139</tt><br>
-<tt>reject *:445</tt><br>
-<tt>reject *:465</tt><br>
-<tt>reject *:563</tt><br>
-<tt>reject *:587</tt><br>
-<tt>reject *:1214</tt><br>
-<tt>reject *:4661-4666</tt><br>
-<tt>reject *:6346-6429</tt><br>
-<tt>reject *:6660-6999</tt><br>
-<tt>accept *:*</tt><br>
-<tt>router-signature</tt><br>
-<tt>[...]</tt><br>
-</p>
-</blockquote>
-
-<p>
-The document above shows an example server descriptor.
-The following data fields in server descriptors may be relevant to
-statistical analysis:
-</p>
-
-<ul>
-<li><b>IP address and ports:</b> Relays provide their IP address
-and ports where they accept requests to build circuits and directory
-requests.
-These data fields are contained in the first line of a server descriptor
-starting with <tt>router</tt>.
-Note that in rare cases, the IP address provided here can be different
-from the IP address used for exiting to the Internet.
-The latter can be found in the exit lists produced by Tor Check as
-described in the <a href="#exitlist">Tor Check exit lists</a> section below.
-</li>
-<li><b>Operating system and Tor software version:</b> Relays include
-their operating system and Tor software version in their server
-descriptors in the <tt>platform</tt> line.
-While this information is very likely correct in most cases, a few relay
-operators may try to impede hacking attempts by providing false platform
-strings.</li>
-<li><b>Uptime:</b> Relays include the number of seconds since the
-last restart in their server descriptor in the <tt>uptime</tt> line.</li>
-<li><b>Own measured bandwidth:</b> Relays report the bandwidth that
-they are willing to provide on average and for short periods of time.
-Relays also perform periodic bandwidth self-tests and report their actual
-available bandwidth.
-The latter was used by clients to weight relays in the path selection
-algorithm and was sometimes subject to manipulation by malicious relays.
-All three bandwidth values can be found in a server descriptor's
-<tt>bandwidth</tt> line.
-With the introduction of
-<a href="http://gitweb.torproject.org/torflow.git/">bandwidth scanners</a>, the self-reported relay
-bandwidth in server descriptors has become less
-relevant.</li>
-<li><b>Relay family:</b> Some relay operators who run more than one
-relay organize their relays in relay families, so that clients don't pick
-more than one of these relays for a single circuit.
-Each relay belonging to a relay family lists the members of that family
-either by nickname or fingerprint in its server descriptor in the
-<tt>family</tt> line.</li>
-<li><b>Exit policy:</b> Relays define their exit policy by including
-firewall-like rules which outgoing connections they reject or accept in
-the <tt>reject</tt> and <tt>accept</tt> lines.</li>
-</ul>
-
-<p>
-These are just a subset of the fields in a server descriptor that seem
-relevant for statistical analysis.
-For a complete list of fields in server descriptors, see the <a href="https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt">directory
-protocol specification</a>.
-</p>
-
-<p>
-Every hour, the directory authorities publish a new network status that
-contains a list of all running relays.
-The directory authorities confirm reachability of the contained relays and
-assign flags based on the relays' characteristics.
-The entries in a network status reference the last published server
-descriptor of a relay.
-</p>
-
-<p>
-The network statuses are relevant for statistical analysis, because they
-constitute trusted snapshots of the Tor network.
-Anyone can publish as many server descriptors as they want, but only the
-directory authorities can confirm that a relay was running at a given
-time.
-Most statistics on the Tor network infrastructure rely on network statuses
-and possibly combine them with the referenced server descriptors.
-The document below shows the network status entry referencing
-the server descriptor above.
-In addition to the reachability information, network statuses contain the
-following fields that may be relevant for statistical analysis:
-</p>
-
-<blockquote>
-<p>
-<i>Network status entry of relay <tt>blutmagie</tt>:</i>
-</p>
-<p>
-<tt>r blutmagie YpexOmh7UhpZxr15GIolAewDoGU
-  lFY7WmD/yvVFp9drmZzNeTxZ6dw 2010-12-27 14:35:27 192.251.226.206
-  443 80</tt><br>
-<tt>s Exit Fast Guard HSDir Named Running Stable V2Dir Valid</tt><br>
-<tt>v Tor 0.2.2.20-alpha</tt><br>
-<tt>w Bandwidth=30800</tt><br>
-<tt>p reject 25,119,135-139,445,465,563,587,1214,4661-4666,6346-6429,6660-6999</tt><br>
-</p>
-</blockquote>
-
-<ul>
-<li><b>Relay flags:</b> The directory authorities assign flags to
-relays based on their characteristics to the line starting with <tt>s</tt>.
-Examples are the <tt>Exit</tt> flag if a relay permits exiting to the
-Internet and the <tt>Guard</tt> flag if a relay is stable enough to be
-picked as guard node</li>
-<li><b>Relay version:</b> The directory authorities include the
-version part of the platform string written to server descriptors in the
-network status in the line starting with <tt>v</tt>.</li>
-<li><b>Bandwidth weights:</b> The network status contains a bandwidth
-weight for every relay in the lines with <tt>w</tt> that clients shall use
-for weighting relays in their path selection algorithm.
-This bandwidth weight is either the self-reported bandwidth of the relay
-or the bandwidth measured by the bandwidth scanners.</li>
-<li><b>Exit policy summary:</b> Every entry in a network status
-contains a summary version of a relay's exit policy in the line starting
-with <tt>p</tt>.
-This summary is a list of accepted or rejected ports for exit to most IP
-addresses.</li>
-</ul>
-<hr>
-<br>
-
-<a name="bridgedesc"></a>
-<h3><a href="#bridgedesc" class="anchor">Sanitized bridge
-descriptors</a></h3>
-<br>
-Bridges in the Tor network publish server descriptors to the bridge
-authority which in turn generates a bridge network status.
-We cannot, however, make the bridge server descriptors and bridge network
-statuses available for statistical analysis as we do with the relay server
-descriptors and relay network statuses.
-The problem is that bridge server descriptors and network statuses contain
-bridge IP addresses and other sensitive information that shall not be made
-publicly available.
-We therefore sanitize bridge descriptors by removing all potentially
-identifying information and publish sanitized versions of the descriptors.
-The processing steps for sanitizing bridge descriptors are as follows:
-
-<ol>
-<li><b>Replace the bridge identity with its SHA1 value:</b> Clients
-can request a bridge's current descriptor by sending its identity string
-to the bridge authority.
-This is a feature to make bridges on dynamic IP addresses useful.
-Therefore, the original identities (and anything that could be used to
-derive them) need to be removed from the descriptors.
-The bridge identity is replaced with its SHA1 hash value.
-The idea is to have a consistent replacement that remains stable over
-months or even years (without keeping a secret for a keyed hash function).</li>
-<li><b>Remove all cryptographic keys and signatures:</b> It would be
-straightforward to learn about the bridge identity from the bridge's
-public key.
-Replacing keys by newly generated ones seemed to be unnecessary (and would
-involve keeping a state over months/years), so that all cryptographic
-objects have simply been removed.</li>
-<li><b>Replace IP address with IP address hash:</b> Of course, IP
-addresses need to be sanitized, too.
-<ul><li>IPv4 addresses are replaced with <tt>10.x.x.x</tt> with
-<tt>x.x.x</tt> being the 3 byte output of
-<tt>H(IP address | bridge identity | secret)[:3]</tt>.
-The input <tt>IP address</tt> is the 4-byte long binary representation of
-the bridge's current IP address.
-The <tt>bridge identity</tt> is the 20-byte long binary representation of
-the bridge's long-term identity fingerprint.
-The <tt>secret</tt> is a 31-byte long secure random string that changes once
-per month for all descriptors and statuses published in that month.
-<tt>H()</tt> is SHA-256.
-The <tt>[:3]</tt> operator means that we pick the 3 most significant bytes
-of the result.</li>
-<li>IPv6 addresses are replaced with <tt>[fd9f:2e19:3bcf::xx:xxxx]</tt>
-with <tt>xx:xxxx</tt> being the hex-formatted 3 byte output of a similar
-hash function as described for IPv4 addresses.
-The only differences are that the input <tt>IP address</tt> is 16 bytes
-long and the <tt>secret</tt> is only 19 bytes long.</li></ul>
-<li><b>Replace contact information:</b> If there is contact
-information in a descriptor, the contact line is changed to
-<tt>somebody</tt>.</li>
-<li><b>Remove pluggable transport addresses and arguments:</b> Bridges may
-provide transports in addition to the onion-routing protocol and include
-information about these transports in their extra-info descriptors for
-BridgeDB.  In that case, any IP addresses, TCP ports, or additional
-arguments are removed, only leaving in the supported transport names.</li>
-</ol>
-
-<p>
-Apart from these processing steps, sanitized bridge server descriptors
-follow the same format as relay server descriptors.
-The same applies to sanitized bridge extra-info descriptors.
-Sanitized bridge network statuses are similar to version 2 relay network
-statuses, but with only a <tt>published</tt> line in the header and
-without any lines in the footer.
-</p>
-
-<p>
-The two documents below show an example bridge server
-descriptor that is referenced from a bridge network status.
-For more details about this process, see the
-<a href="https://gitweb.torproject.org/metrics-db.git">metrics data processor</a> software.
-</p>
-
-<blockquote>
-<p>
-<i>Sanitized bridge server descriptor:</i>
-</p>
-<p>
-<tt>@type bridge-server-descriptor 1.0</tt><br>
-<tt>router Hawthorne 10.175.105.22 443 0 0</tt><br>
-<tt>platform Tor 0.2.2.19-alpha (git-1988927edecce4c7) on Linux i686</tt><br>
-<tt>opt protocols Link 1 2 Circuit 1</tt><br>
-<tt>published 2010-12-27 18:55:01</tt><br>
-<tt>opt fingerprint A5FA 7F38 B02A 415E 72FE 614C 64A1 E5A9 2BA9 9BBD</tt><br>
-<tt>uptime 2347112</tt><br>
-<tt>bandwidth 5242880 10485760 1016594</tt><br>
-<tt>opt extra-info-digest E729BCB5E06A5657A73151B55354EB003D2BAE0F</tt><br>
-<tt>opt hidden-service-dir</tt><br>
-<tt>contact somebody</tt><br>
-<tt>reject *:*</tt><br>
-<tt>router-digest 46DFDBE7B67B7C90A1962B0B5AA4526FAF406979</tt><br>
-</p>
-</blockquote>
-
-<blockquote>
-<p>
-<i>Sanitized bridge network status:</i>
-</p>
-<p>
-<tt>@type bridge-network-status 1.0</tt><br>
-<tt>published 2010-12-27 22:07:03</tt><br>
-<tt>[...status entries...]</tt><br>
-<tt>r Hawthorne pfp/OLAqQV5y/mFMZKHlqSupm70 Rt/b57Z7fJChlisLWqRSb69AaXk
-  2010-12-27 18:55:01 10.175.105.22 443 0</tt><br>
-<tt>s Fast Guard HSDir Running Stable Valid</tt><br>
-<tt>[...status entries...]</tt><br>
-</p>
-</blockquote>
-<hr>
-<br>
-
-<a name="bytehist"></a>
-<h3><a href="#bytehist" class="anchor">Byte histories</a></h3>
-<br>
-<p>
-Relays include aggregate statistics in their descriptors that they upload
-to the directory authorities.
-These aggregate statistics are contained in extra-info descriptors that
-are published in companion with server descriptors.
-Extra-info descriptors are not required for clients to build circuits.
-An extra-info descriptor belonging to a server descriptor is referenced by
-its SHA1 hash value.
-</p>
-
-<p>
-Byte histories were the first statistical data that relays published about
-their usage.
-Relays report the number of written and read bytes in 15-minute intervals
-throughout the last 24 hours.
-The extra-info descriptor in the document below contains the byte
-histories in the two lines starting with <tt>write-history</tt> and
-<tt>read-history</tt>.
-More details about these statistics can be found in the <a href="https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt">directory protocol
-specification</a>.
-</p>
-
-<blockquote>
-<p>
-<i>Extra-info descriptor published by relay <tt>blutmagie</tt>
-(without cryptographic signature and with long lines being truncated):</i>
-</p>
-<p>
-<tt>extra-info blutmagie 6297B13A687B521A59C6BD79188A2501EC03A065</tt><br>
-<tt>published 2010-12-27 14:35:27</tt><br>
-<tt>write-history 2010-12-27 14:34:05 (900 s) 12902389760,12902402048,12859373568,12894131200,[...]</tt><br>
-<tt>read-history 2010-12-27 14:34:05 (900 s) 12770249728,12833485824,12661140480,12872439808,[...]</tt><br>
-<tt>dirreq-write-history 2010-12-27 14:26:13 (900 s) 51731456,60808192,56740864,54948864,[...]</tt><br>
-<tt>dirreq-read-history 2010-12-27 14:26:13 (900 s) 4747264,4767744,4511744,4752384,[...]</tt><br>
-<tt>dirreq-stats-end 2010-12-27 10:51:09 (86400 s)</tt><br>
-<tt>dirreq-v3-ips us=2000,de=1344,fr=744,kr=712,[...]</tt><br>
-<tt>dirreq-v2-ips ??=8,au=8,cn=8,cz=8,[...]</tt><br>
-<tt>dirreq-v3-reqs us=2368,de=1680,kr=1048,fr=800,[...]</tt><br>
-<tt>dirreq-v2-reqs id=48,??=8,au=8,cn=8,[...]</tt><br>
-<tt>dirreq-v3-resp ok=12504,not-enough-sigs=0,unavailable=0,not-found=0,not-modified=0,busy=128</tt><br>
-<tt>dirreq-v2-resp ok=64,unavailable=0,not-found=8,not-modified=0,busy=8</tt><br>
-<tt>dirreq-v2-share 1.03%</tt><br>
-<tt>dirreq-v3-share 1.03%</tt><br>
-<tt>dirreq-v3-direct-dl complete=316,timeout=4,running=0,min=4649,d1=36436,d2=68056,q1=76600,d3=87891,d4=131294,md=173579,d6=229695,d7=294528,q3=332053,d8=376301,d9=530252,max=2129698</tt><br>
-<tt>dirreq-v2-direct-dl complete=16,timeout=52,running=0,min=9769,d1=9769,d2=9844,q1=9981,d3=9981,d4=27297,md=33640,d6=60814,d7=205884,q3=205884,d8=361137,d9=628256,max=956009</tt><br>
-<tt>dirreq-v3-tunneled-dl complete=12088,timeout=92,running=4,min=534,d1=31351,d2=49166,q1=58490,d3=70774,d4=88192,md=109778,d6=152389,d7=203435,q3=246377,d8=323837,d9=559237,max=26601000</tt><br>
-<tt>dirreq-v2-tunneled-dl complete=0,timeout=0,running=0</tt><br>
-<tt>entry-stats-end 2010-12-27 10:51:09 (86400 s)</tt><br>
-<tt>entry-ips de=11024,us=10672,ir=5936,fr=5040,[...]</tt><br>
-<tt>exit-stats-end 2010-12-27 10:51:09 (86400 s)</tt><br>
-<tt>exit-kibibytes-written 80=6758009,443=498987,4000=227483,5004=1182656,11000=22767,19371=1428809,31551=8212,41500=965584,51413=3772428,56424=1912605,other=175227777</tt><br>
-<tt>exit-kibibytes-read 80=197075167,443=5954607,4000=1660990,5004=1808563,11000=1893893,19371=130360,31551=7588414,41500=756287,51413=2994144,56424=1646509,other=288412366</tt><br>
-<tt>exit-streams-opened 80=5095484,443=359256,4000=4508,5004=22288,11000=124,19371=24,31551=40,41500=96,51413=16840,56424=28,other=1970964</tt><br>
-</p>
-</blockquote>
-<hr>
-<br>
-
-<a name="dirreqstats"></a>
-<h3><a href="#dirreqstats" class="anchor">Directory requests</a></h3>
-<br>
-<p>
-The directory authorities and directory mirrors report statistical data
-about processed directory requests.
-Starting with Tor version 0.2.2.15-alpha, all directories report the
-number of written and read bytes for answering directory requests.
-The format is similar to the format of byte histories as described in the
-previous section.
-The relevant lines are <tt>dirreq-write-history</tt> and
-<tt>dirreq-read-history</tt> in the document listed in the
-<a href="#bytehist">Byte histories</a> section above.
-These two lines contain the subset of total read and written bytes that
-the directory mirror spent on responding to any kind of directory request,
-including network statuses, server descriptors, extra-info descriptors,
-authority certificates, etc.
-</p>
-
-<p>
-The directories further report statistics on answering directory requests
-for network statuses only.
-For Tor versions before 0.2.3.x, relay operators had to manually enable
-these statistics, which is why only a few directories report them.
-The lines starting with <tt>dirreq-v3-</tt> all belong to the directory
-request statistics (the lines starting with <tt>dirreq-v2-</tt> report
-similar statistics for version 2 of the directory protocol which is
-deprecated at the time of writing this report).
-The following fields may be relevant for statistical analysis:
-</p>
-
-<ul>
-<li><b>Unique IP addresses:</b> The numbers in <tt>dirreq-v3-ips</tt>
-denote the unique IP addresses of clients requesting network statuses by
-country.</li>
-<li><b>Network status requests:</b> The numbers in
-<tt>dirreq-v3-reqs</tt> constitute the total network status requests by
-country.</li>
-<li><b>Request share:</b> The percentage in <tt>dirreq-v3-share</tt> is
-an estimate of the share of directory requests that the reporting relay
-expects to see in the Tor network.
-In a tech report (<a href="https://research.torproject.org/techreports/countingusers-2010-11-30.pdf">PDF</a>)
-we found that this estimate isn't very useful
-for statistical analysis because of the different approaches that clients
-take to select directory mirrors.
-The fraction of written directory bytes (<tt>dirreq-write-history</tt>) can
-be used to derive a better metric for the share of directory requests.</li>
-<li><b>Network status responses:</b> The directories also report
-whether they could provide the requested network status to clients in
-<tt>dirreq-v3-resp</tt>.
-This information was mostly used to diagnose error rates in version 2 of
-the directory protocol where a lot of directories replied to network
-status requests with <tt>503 Busy</tt>.
-In version 3 of the directory protocol, most responses contain the status
-code <tt>200 OK</tt>.</li>
-<li><b>Network status download times:</b> The line
-<tt>dirreq-v3-direct-dl</tt> contains statistics on the download of network
-statuses via the relay's directory port.
-The line <tt>dirreq-v3-tunneled-dl</tt> contains similar statistics on
-downloads via a 1-hop circuit between client and directory (which is the
-common approach in version 3 of the directory protocol).
-Relays report how many requests have been completed, have timed out, and
-are still running at the end of a 24-hour time interval as well as the
-minimum, maximum, median, quartiles, and deciles of download times.</li>
-</ul>
-
-<p>
-More details about these statistics can be found in the <a href="https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt">directory protocol
-specification</a>.
-</p>
-<hr>
-<br>
-
-<a name="entrystats"></a>
-<h3><a href="#entrystats" class="anchor">Connecting clients</a></h3>
-<br>
-<p>
-Relays can be configured to report per-country statistics on directly
-connecting clients.
-This metric includes clients connecting to a relay in order to build
-circuits and clients creating a 1-hop circuit to request directory
-information.
-In practice, the latter number outweighs the former number.
-The <tt>entry-ips</tt> line in the document listed in the
-<a href="#bytehist">Byte histories</a> section above
-shows the number
-of unique IP addresses connecting to the relay by country.
-More details about these statistics can be found in the <a href="https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt">directory protocol
-specification</a>.
-</p>
-<hr>
-<br>
-
-<a name="bridgestats"></a>
-<h3><a href="#bridgestats" class="anchor">Bridge users</a></h3>
-<br>
-<p>
-Bridges report statistics on connecting bridge clients in their extra-info
-descriptors.
-The document below shows a bridge extra-info descriptor
-with the bridge user statistics in the <tt>bridge-ips</tt> line.
-
-<blockquote>
-<p>
-<i>Sanitized bridge extra-info descriptor:</i>
-</p>
-<p>
-<tt>extra-info Unnamed A5FA7F38B02A415E72FE614C64A1E5A92BA99BBD</tt><br>
-<tt>published 2010-12-27 18:55:01</tt><br>
-<tt>write-history 2010-12-27 18:43:50 (900 s) 151712768,176698368,180030464,163150848,[...]</tt><br>
-<tt>read-history 2010-12-27 18:43:50 (900 s) 148109312,172274688,172168192,161094656,[...]</tt><br>
-<tt>bridge-stats-end 2010-12-27 14:56:29 (86400 s)</tt><br>
-<tt>bridge-ips sa=48,us=40,de=32,ir=32,[...]</tt><br>
-</p>
-</blockquote>
-
-<p>
-Bridges running Tor version 0.2.2.3-alpha or earlier report bridge users
-in a similar line starting with <tt>geoip-client-origins</tt>.
-The reason for switching to <tt>bridge-ips</tt> was that the measurement
-interval in <tt>geoip-client-origins</tt> had a variable length, whereas the
-measurement interval in 0.2.2.4-alpha and later is set to exactly
-24 hours.
-In order to clearly distinguish the new measurement intervals from the old
-ones, the new keywords have been introduced.
-More details about these statistics can be found in the <a href="https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt">directory protocol
-specification</a>.
-</p>
-<hr>
-<br>
-
-<a name="cellstats"></a>
-<h3><a href="#cellstats" class="anchor">Cell-queue statistics</a></h3>
-<br>
-<p>
-Relays can be configured to report aggregate statistics on their cell
-queues.
-These statistics include average processed cells, average number of queued
-cells, and average time that cells spend in circuits.
-Circuits are split into deciles based on the number of processed cells.
-The statistics are provided for circuit deciles from loudest to quietest
-circuits.
-The document below shows the cell statistics contained in an
-extra-info descriptor by relay <tt>gabelmoo</tt>.
-An early analysis of cell-queue statistics can be found in a tech report
-(<a href="https://research.torproject.org/techreports/bufferstats-2009-08-25.pdf">PDF</a>).
-More details about these statistics can be found in the <a href="https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt">directory protocol
-specification</a>.
-</p>
-
-<blockquote>
-<p>
-<i>Cell statistics in extra-info descriptor by relay <tt>gabelmoo</tt>:</i>
-</p>
-<p>
-<tt>cell-stats-end 2010-12-27 09:59:50 (86400 s)</tt><br>
-<tt>cell-processed-cells 4563,153,42,15,7,7,6,5,4,2</tt><br>
-<tt>cell-queued-cells 9.39,0.98,0.09,0.01,0.00,0.00,0.00,0.01,0.00,
-  0.01</tt><br>
-<tt>cell-time-in-queue 2248,807,277,92,49,22,52,55,81,148</tt><br>
-<tt>cell-circuits-per-decile 7233</tt><br>
-</p>
-</blockquote>
-<hr>
-<br>
-
-<a name="exitstats"></a>
-<h3><a href="#exitstats" class="anchor">Exit-port statistics</a></h3>
-<br>
-<p>
-Exit relays running Tor version 0.2.1.1-alpha or higher can be configured
-to report aggregate statistics on exiting connections.
-These relays report the number of opened streams, written and read bytes
-by exiting port.
-Until version 0.2.2.19-alpha, relays reported all ports exceeding a
-threshold of 0.01 % of all written and read exit bytes.
-Starting with version 0.2.2.20-alpha, relays only report the top 10 ports
-in exit-port statistics in order not to exceed the maximum extra-info
-descriptor length of 50 KB.
-The document listed in the
-<a href="#bytehist">Byte histories</a> section above contains
-exit-port statistics in the lines starting with <tt>exit-</tt>.
-More details about these statistics can be found in the <a href="https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt">directory protocol
-specification</a>.
-</p>
-<hr>
-<br>
-
-<a name="bidistats"></a>
-<h3><a href="#bidistats" class="anchor">Bidirectional connection
-use</a></h3>
-<br>
-<p>
-Relays running Tor version 0.2.3.1-alpha or higher can be configured to
-report what fraction of connections is used uni- or bi-directionally.
-Every 10 seconds, relays determine for every connection whether they read
-and wrote less than a threshold of 20 KiB.
-Connections below this threshold are labeled as "Below Threshold".
-For the remaining connections, relays report whether they read/wrote at
-least 10 times as many bytes as they wrote/read.
-If so, they classify a connection as "Mostly reading" or "Mostly
-writing," respectively.
-All other connections are classified as "Both reading and writing."
-After classifying connections, read and write counters are reset for the
-next 10-second interval.
-Statistics are aggregated over 24 hours.
-The document below shows the bidirectional connection use
-statistics in an extra-info descriptor by relay <tt>zweifaltigkeit</tt>.
-The four numbers denote the number of connections "Below threshold,"
-"Mostly reading," "Mostly writing," and "Both reading and writing."
-More details about these statistics can be found in the <a href="https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt">directory protocol
-specification</a>.
-</p>
-
-<blockquote>
-<p>
-<i>Bidirectional connection use statistic in extra-info descriptor
-by relay <tt>zweifaltigkeit</tt>:</i>
-</p>
-<p>
-<tt>conn-bi-direct 2010-12-28 15:55:11 (86400 s) 387465,45285,55361,81786</tt>
-</p>
-</blockquote>
-<hr>
-<br>
-
-<a name="torperf"></a>
-<h3><a href="#torperf" class="anchor">Torperf output files</a></h3>
-<br>
-<p>
-Torperf is a little tool that measures Tor's performance as users
-experience it.
-Torperf uses a trivial SOCKS client to download files of various sizes
-over the Tor network and notes how long substeps take.
-Torperf can be
-<a href="https://metrics.torproject.org/tools.html">downloaded</a>
-from the metrics
-website.
-A Torperf results file contains a single line per Torperf run with
-<tt>key=value</tt> pairs.
-Such a result line is sufficient to learn about 1) the Tor and Torperf
-configuration, 2) measurement results, and 3) additional information that
-might help explain the results.
-Known keys are explained below.
-</p>
-<ul>
-<li>Configuration
-<ul>
-<li><tt>SOURCE:</tt> Configured name of the data source; required.</li>
-<li><tt>FILESIZE:</tt> Configured file size in bytes; required.</li>
-<li>Other meta data describing the Tor or Torperf configuration, e.g.,
-GUARD for a custom guard choice; optional.</li>
-</ul>
-<li>Measurement results
-<ul>
-<li><tt>START:</tt> Time when the connection process starts;
-required.</li>
-<li><tt>SOCKET:</tt> Time when the socket was created; required.</li>
-<li><tt>CONNECT:</tt> Time when the socket was connected; required.</li>
-<li><tt>NEGOTIATE:</tt> Time when SOCKS 5 authentication methods have been
-negotiated; required.</li>
-<li><tt>REQUEST:</tt> Time when the SOCKS request was sent; required.</li>
-<li><tt>RESPONSE:</tt> Time when the SOCKS response was received;
-required.</li>
-<li><tt>DATAREQUEST:</tt> Time when the HTTP request was written;
-required.</li>
-<li><tt>DATARESPONSE:</tt> Time when the first response was received;
-required.</li>
-<li><tt>DATACOMPLETE:</tt> Time when the payload was complete;
-required.</li>
-<li><tt>WRITEBYTES:</tt> Total number of bytes written; required.</li>
-<li><tt>READBYTES:</tt> Total number of bytes read; required.</li>
-<li><tt>DIDTIMEOUT:</tt> 1 if the request timed out, 0 otherwise;
-optional.</li>
-<li><tt>DATAPERCx:</tt> Time when x% of expected bytes were read for
-x = { 10, 20, 30, 40, 50, 60, 70, 80, 90 }; optional.</li>
-<li>Other measurement results, e.g., START_RENDCIRC, GOT_INTROCIRC, etc.
-for hidden-service measurements; optional.</li>
-</ul>
-<li>Additional information
-<ul>
-<li><tt>LAUNCH:</tt> Time when the circuit was launched; optional.</li>
-<li><tt>USED_AT:</tt> Time when this circuit was used; optional.</li>
-<li><tt>PATH:</tt> List of relays in the circuit, separated by commas;
-optional.</li>
-<li><tt>BUILDTIMES:</tt> List of times when circuit hops were built,
-separated by commas; optional.</li>
-<li><tt>TIMEOUT:</tt> Circuit build timeout that the Tor client used when
-building this circuit; optional.</li>
-<li><tt>QUANTILE:</tt> Circuit build time quantile that the Tor client
-uses to determine its circuit-build timeout; optional.</li>
-<li><tt>CIRC_ID:</tt> Circuit identifier of the circuit used for this
-measurement; optional.</li>
-<li><tt>USED_BY:</tt> Stream identifier of the stream used for this
-measurement; optional.</li>
-<li>Other fields containing additional information; optional.</li>
-</ul>
-</ul>
-
-<blockquote>
-<p>
-<i>Torperf <tt>.tpf</tt> output lines for a single request to download a
-50 KiB file (reformatted):</i>
-</p>
-
-<p>
-<tt>BUILDTIMES=1.16901898384,1.86555600166,2.13295292854</tt><br>
-<tt>CIRC_ID=9878</tt><br>
-<tt>CONNECT=1338357901.42</tt><br>
-<tt>DATACOMPLETE=1338357902.91</tt><br>
-<tt>DATAPERC10=1338357902.48</tt><br>
-<tt>DATAPERC20=1338357902.48</tt><br>
-<tt>DATAPERC30=1338357902.61</tt><br>
-<tt>DATAPERC40=1338357902.64</tt><br>
-<tt>DATAPERC50=1338357902.65</tt><br>
-<tt>DATAPERC60=1338357902.74</tt><br>
-<tt>DATAPERC70=1338357902.74</tt><br>
-<tt>DATAPERC80=1338357902.75</tt><br>
-<tt>DATAPERC90=1338357902.79</tt><br>
-<tt>DATAREQUEST=1338357901.83</tt><br>
-<tt>DATARESPONSE=1338357902.25</tt><br>
-<tt>DIDTIMEOUT=0</tt><br>
-<tt>FILESIZE=51200</tt><br>
-<tt>LAUNCH=1338357661.74</tt><br>
-<tt>NEGOTIATE=1338357901.42</tt><br>
-<tt>PATH=$980D326017CEF4CBBF4089FBABE767DC83D059AF,$03545609092A24C71CCAD2F4523F5CCC6714F159,$CAC3CF7154AE9C656C4096DC38B4EFA145905654</tt><br>
-<tt>QUANTILE=0.800000</tt><br>
-<tt>READBYTES=51442</tt><br>
-<tt>REQUEST=1338357901.42</tt><br>
-<tt>RESPONSE=1338357901.83</tt><br>
-<tt>SOCKET=1338357901.42</tt><br>
-<tt>SOURCE=torperf</tt><br>
-<tt>START=1338357901.42</tt><br>
-<tt>TIMEOUT=5049</tt><br>
-<tt>USED_AT=1338357902.91</tt><br>
-<tt>USED_BY=18869</tt><br>
-<tt>WRITEBYTES=75</tt><br>
-</p>
-</blockquote>
-<br>
-
-<p>
-Torperf can produce two output files: <tt>.data</tt> and
-<tt>.extradata</tt>.
-The <tt>.data</tt> file contains timestamps for request substeps and the
-byte summaries for downloading a test file via Tor.
-The document below shows an example output of a Torperf run.
-The timestamps are seconds and microseconds since 1970-01-01
-00:00:00.000000.
-Torperf can be configured to write <tt>.extradata</tt> files by attaching
-a Tor controller and writing certain controller events to disk.
-The format of a <tt>.extradata</tt> line is similar to the combined format
-as specified above, except that it can only contain "Additional
-information" keywords.
-</p>
-
-<blockquote>
-<p>
-<i>Torperf <tt>.data</tt> and <tt>.extradata</tt> output lines for a
-single request to download a 50 KiB file (reformatted and annotated with
-comments):</i>
-</p>
-
-<p>
-<tt># Timestamps and byte summaries contained in .data files:</tt><br>
-<tt>1338357901 422336   # Connection process started</tt><br>
-<tt>1338357901 422346   # After socket is created</tt><br>
-<tt>1338357901 422521   # After socket is connected</tt><br>
-<tt>1338357901 422604   # After authentication methods are negotiated (SOCKS 5 only)</tt><br>
-<tt>1338357901 423550   # After SOCKS request is sent</tt><br>
-<tt>1338357901 839639   # After SOCKS response is received</tt><br>
-<tt>1338357901 839849   # After HTTP request is written</tt><br>
-<tt>1338357902 258157   # After first response is received</tt><br>
-<tt>1338357902 914263   # After payload is complete</tt><br>
-<tt>75                  # Written bytes</tt><br>
-<tt>51442               # Read bytes</tt><br>
-<tt>0                   # Timeout (optional field)</tt><br>
-<tt>1338357902 481591   # After 10% of expected bytes are read (optional field)</tt><br>
-<tt>1338357902 482719   # After 20% of expected bytes are read (optional field)</tt><br>
-<tt>1338357902 613169   # After 30% of expected bytes are read (optional field)</tt><br>
-<tt>1338357902 647108   # After 40% of expected bytes are read (optional field)</tt><br>
-<tt>1338357902 651764   # After 50% of expected bytes are read (optional field)</tt><br>
-<tt>1338357902 743705   # After 60% of expected bytes are read (optional field)</tt><br>
-<tt>1338357902 743876   # After 70% of expected bytes are read (optional field)</tt><br>
-<tt>1338357902 757475   # After 80% of expected bytes are read (optional field)</tt><br>
-<tt>1338357902 795100   # After 90% of expected bytes are read (optional field)</tt><br>
-</p>
-
-<p>
-<tt># Path information contained in .extradata files:</tt><br>
-<tt>CIRC_ID=9878</tt><br>
-<tt>LAUNCH=1338357661.74</tt><br>
-<tt>PATH=$980D326017CEF4CBBF4089FBABE767DC83D059AF,$03545609092A24C71CCAD2F4523F5CCC6714F159,$CAC3CF7154AE9C656C4096DC38B4EFA145905654</tt><br>
-<tt>BUILDTIMES=1.16901898384,1.86555600166,2.13295292854</tt><br>
-<tt>USED_AT=1338357902.91</tt><br>
-<tt>USED_BY=18869</tt><br>
-<tt>TIMEOUT=5049</tt><br>
-<tt>QUANTILE=0.800000</tt><br>
-</p>
-</blockquote>
-<hr>
-<br>
-
-<a name="bridgepool"></a>
-<h3><a href="#bridgepool" class="anchor">BridgeDB pool assignment
-files</a></h3>
-<br>
-<p>
-BridgeDB is the software that receives bridge network statuses containing
-the information which bridges are running from the bridge authority,
-assigns these bridges to persistent distribution rings, and hands them out
-to bridge users.
-BridgeDB periodically dumps the list of running bridges with information
-about the rings, subrings, and file buckets to which they are assigned to
-a local file.
-The sanitized versions of these lists containing SHA-1 hashes of bridge
-fingerprints instead of the original fingerprints are available for
-statistical analysis.
-</p>
-
-<blockquote>
-<p>
-<i>BridgeDB pool assignment file from March 13, 2011:</i>
-</p>
-<p>
-<tt>bridge-pool-assignment 2011-03-13 14:38:03</tt><br>
-<tt>00b834117566035736fc6bd4ece950eace8e057a unallocated</tt><br>
-<tt>00e923e7a8d87d28954fee7503e480f3a03ce4ee email port=443 flag=stable</tt><br>
-<tt>0103bb5b00ad3102b2dbafe9ce709a0a7c1060e4 https ring=2 port=443 flag=stable</tt><br>
-<tt>[...]</tt><br>
-</p>
-</blockquote>
-
-<p>
-The document above shows a BridgeDB pool assignment file
-from March 13, 2011.
-Every such file begins with a line containing the timestamp when BridgeDB
-wrote this file.
-Subsequent lines always start with the SHA-1 hash of a bridge fingerprint,
-followed by ring, subring, and/or file bucket information.
-There are currently three distributor ring types in BridgeDB:
-</p>
-
-<ol>
-<li><b>unallocated:</b> These bridges are not distributed by BridgeDB,
-but are either reserved for manual distribution or are written to file
-buckets for distribution via an external tool.
-If a bridge in the <tt>unallocated</tt> ring is assigned to a file bucket,
-this is noted by <tt>bucket=$bucketname</tt>.</li>
-<li><b>email:</b> These bridges are distributed via an e-mail
-autoresponder.  Bridges can be assigned to subrings by their OR port or
-relay flag which is defined by <tt>port=$port</tt> and/or <tt>flag=$flag</tt>.
-</li>
-<li><b>https:</b> These bridges are distributed via https server.
-There are multiple https rings to further distribute bridges by IP address
-ranges, which is denoted by <tt>ring=$ring</tt>.
-Bridges in the <tt>https</tt> ring can also be assigned to subrings by
-OR port or relay flag which is defined by <tt>port=$port</tt> and/or
-<tt>flag=$flag</tt>.</li>
-</ol>
-<hr>
-<br>
-
-<a name="exitlist">
-<h3><a href="#exitlist" class="anchor">Tor Check exit lists</a></h3>
-<br>
-<p>
-<a href="https://www.torproject.org/tordnsel/dist/">TorDNSEL</a> is an
-implementation of the active testing, DNS-based exit list
-for Tor exit
-nodes.
-Tor Check makes the list of known exits and corresponding exit IP
-addresses available in a specific format.
-The document below shows an entry of the exit list written on
-December 28, 2010 at 15:21:44 UTC.
-This entry means that the relay with fingerprint <tt>63BA..</tt> which
-published a descriptor at 07:35:55 and was contained in a version 2
-network status from 08:10:11 uses two different IP addresses for exiting.
-The first address <tt>91.102.152.236</tt> was found in a test performed at
-07:10:30.
-When looking at the corresponding server descriptor, one finds that this
-is also the IP address on which the relay accepts connections from inside
-the Tor network.
-A second test performed at 10:35:30 reveals that the relay also uses IP
-address <tt>91.102.152.227</tt> for exiting.
-</p>
-
-<blockquote>
-<p>
-<i>Exit list entry written on December 28, 2010 at 15:21:44 UTC:</i>
-</p>
-<p>
-<tt>ExitNode 63BA28370F543D175173E414D5450590D73E22DC</tt><br>
-<tt>Published 2010-12-28 07:35:55</tt><br>
-<tt>LastStatus 2010-12-28 08:10:11</tt><br>
-<tt>ExitAddress 91.102.152.236 2010-12-28 07:10:30</tt><br>
-<tt>ExitAddress 91.102.152.227 2010-12-28 10:35:30</tt><br>
-</p>
-</blockquote>
-
-</div>
-</div>
-<div class="bottom" id="bottom">
-<%@ include file="footer.jsp"%>
-</div>
-</body>
-</html>
-
diff --git a/web/WEB-INF/graphs.jsp b/web/WEB-INF/graphs.jsp
deleted file mode 100644
index 58c122c..0000000
--- a/web/WEB-INF/graphs.jsp
+++ /dev/null
@@ -1,34 +0,0 @@
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
-<html>
-<head>
-  <title>Tor Metrics Portal: Graphs</title>
-  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
-  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
-  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
-</head>
-<body>
-  <div class="center">
-    <%@ include file="banner.jsp"%>
-    <div class="main-column">
-        <h2>Tor Metrics Portal: Graphs</h2>
-        <br>
-        <p>The graphs on this page visualize a small portion of the data
-        gathered in the Tor Metrics Project. The following graphs are
-        available:</p>
-        <ul>
-          <li>The <a href="network.html">Network page</a> has numerous
-          statistics on the network of relays and bridges.</li>
-          <li>The <a href="users.html">Users page</a> attempts to estimate
-          the number of users in the network.</li>
-          <li>There are active and passive performance measurements of the
-          Tor network available on the
-          <a href="performance.html">Performance page</a>.</li>
-        </ul>
-        <br>
-    </div>
-  </div>
-  <div class="bottom" id="bottom">
-    <%@ include file="footer.jsp"%>
-  </div>
-</body>
-</html>
diff --git a/web/WEB-INF/index.jsp b/web/WEB-INF/index.jsp
deleted file mode 100644
index 9accc2b..0000000
--- a/web/WEB-INF/index.jsp
+++ /dev/null
@@ -1,40 +0,0 @@
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
-<html>
-<head>
-  <title>Tor Metrics Portal</title>
-  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
-  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
-  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
-</head>
-<body>
-  <div class="center">
-    <%@ include file="banner.jsp"%>
-    <div class="main-column">
-        <h2>Tor Metrics Portal</h2>
-        <br>
-        <p>The Tor Metrics Portal aggregates all kinds of interesting
-        data about the Tor network and visualizes them in graphs and
-        reports. This portal also provides easy access to the underlying
-        data and documentation for performing own analyses based on these
-        data. Find out more here:</p>
-        <ul>
-          <li>View daily updated <a href="graphs.html">graphs</a> on
-          estimated client numbers, on network performance, and other
-          statistics on the Tor network</li>
-          <li>Read <a href="http://freehaven.net/anonbib/">papers</a> and
-          <a href="https://research.torproject.org/techreports.html">technical
-          reports</a>
-          on the measurement techniques and results of statistical
-          analysis of metrics data</li>
-          <li>Download the <a href="data.html">data</a> that is behind the
-          graphs and reports to make your own evaluations</li>
-          <li>Try out the <a href="tools.html">tools</a> to parse and
-          evaluate the metrics data</li>
-        </ul>
-    </div>
-  </div>
-  <div class="bottom" id="bottom">
-    <%@ include file="footer.jsp"%>
-  </div>
-</body>
-</html>
diff --git a/web/WEB-INF/network.jsp b/web/WEB-INF/network.jsp
deleted file mode 100644
index e0b297a..0000000
--- a/web/WEB-INF/network.jsp
+++ /dev/null
@@ -1,305 +0,0 @@
-<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
-<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
-<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
-<html>
-<head>
-  <title>Tor Metrics Portal: Network</title>
-  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
-  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
-  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
-</head>
-<body>
-  <div class="center">
-    <%@ include file="banner.jsp"%>
-    <div class="main-column">
-<h2>Tor Metrics Portal: Network</h2>
-<br>
-<a name="networksize"></a>
-<h3><a href="#networksize" class="anchor">Relays and bridges in the
-network</a></h3>
-<br>
-<p>The following graph shows the average daily number of relays and
-bridges in the network.</p>
-<img src="networksize.png${networksize_url}"
-     width="576" height="360" alt="Network size graph">
-<form action="network.html#networksize">
-  <div class="formrow">
-    <input type="hidden" name="graph" value="networksize">
-    <p>
-    <label>Start date (yyyy-mm-dd):</label>
-      <input type="text" name="start" size="10"
-             value="<c:choose><c:when test="${fn:length(networksize_start) == 0}">${default_start_date}</c:when><c:otherwise>${networksize_start[0]}</c:otherwise></c:choose>">
-    <label>End date (yyyy-mm-dd):</label>
-      <input type="text" name="end" size="10"
-             value="<c:choose><c:when test="${fn:length(networksize_end) == 0}">${default_end_date}</c:when><c:otherwise>${networksize_end[0]}</c:otherwise></c:choose>">
-    </p><p>
-    <input class="submit" type="submit" value="Update graph">
-    </p>
-  </div>
-</form>
-<p>Download graph as
-<a href="networksize.pdf${networksize_url}">PDF</a> or
-<a href="networksize.svg${networksize_url}">SVG</a>.</p>
-<p><a href="stats/servers.csv">CSV</a> file containing all data.</p>
-<br>
-
-<a name="relayflags"></a>
-<h3><a href="#relayflags" class="anchor">Relays with Exit, Fast, Guard,
-Stable, and HSDir flags</a></h3>
-<br>
-<p>The directory authorities assign certain flags to relays that clients
-use for their path selection decisions. The following graph shows the
-average number of relays with these flags assigned.</p>
-<img src="relayflags.png${relayflags_url}"
-     width="576" height="360" alt="Relay flags graph">
-<form action="network.html#relayflags">
-  <div class="formrow">
-    <input type="hidden" name="graph" value="relayflags">
-    <p>
-    <label>Start date (yyyy-mm-dd):</label>
-      <input type="text" name="start" size="10"
-             value="<c:choose><c:when test="${fn:length(relayflags_start) == 0}">${default_start_date}</c:when><c:otherwise>${relayflags_start[0]}</c:otherwise></c:choose>">
-    <label>End date (yyyy-mm-dd):</label>
-      <input type="text" name="end" size="10"
-             value="<c:choose><c:when test="${fn:length(relayflags_end) == 0}">${default_end_date}</c:when><c:otherwise>${relayflags_end[0]}</c:otherwise></c:choose>">
-    </p><p>
-      <label>Relay flags: </label>
-      <input type="checkbox" name="flag" value="Running"<c:if test="${fn:length(relayflags_flag) == 0 or fn:contains(fn:join(relayflags_flag, ','), 'Running')}"> checked</c:if>> Running
-      <input type="checkbox" name="flag" value="Exit"<c:if test="${fn:length(relayflags_flag) == 0 or fn:contains(fn:join(relayflags_flag, ','), 'Exit')}"> checked</c:if>> Exit
-      <input type="checkbox" name="flag" value="Fast"<c:if test="${fn:length(relayflags_flag) == 0 or fn:contains(fn:join(relayflags_flag, ','), 'Fast')}"> checked</c:if>> Fast
-      <input type="checkbox" name="flag" value="Guard"<c:if test="${fn:length(relayflags_flag) == 0 or fn:contains(fn:join(relayflags_flag, ','), 'Guard')}"> checked</c:if>> Guard
-      <input type="checkbox" name="flag" value="Stable"<c:if test="${fn:length(relayflags_flag) == 0 or fn:contains(fn:join(relayflags_flag, ','), 'Stable')}"> checked</c:if>> Stable
-      <input type="checkbox" name="flag" value="HSDir"<c:if test="${fn:length(relayflags_flag) > 0 and fn:contains(fn:join(relayflags_flag, ','), 'HSDir')}"> checked</c:if>> HSDir
-    </p><p>
-    <input class="submit" type="submit" value="Update graph">
-    </p>
-  </div>
-</form>
-<p>Download graph as
-<a href="relayflags.pdf${relayflags_url}">PDF</a> or
-<a href="relayflags.svg${relayflags_url}">SVG</a>.</p>
-<p><a href="stats/servers.csv">CSV</a> file containing all data.</p>
-<br>
-
-<a name="versions"></a>
-<h3><a href="#versions" class="anchor">Relays by version</a></h3>
-<br>
-<p>Relays report the Tor version that they are running to the directory
-authorities. See the
-<a href="https://www.torproject.org/download/download.html.en">download
-page</a> and
-<a href="https://gitweb.torproject.org/tor.git/blob/HEAD:/ChangeLog">ChangeLog file</a>
-to find out which Tor versions are stable and unstable.
-The following graph shows the number of relays by version.</p>
-<img src="versions.png${versions_url}"
-     width="576" height="360" alt="Relay versions graph">
-<form action="network.html#versions">
-  <div class="formrow">
-    <input type="hidden" name="graph" value="versions">
-    <p>
-    <label>Start date (yyyy-mm-dd):</label>
-      <input type="text" name="start" size="10"
-             value="<c:choose><c:when test="${fn:length(versions_start) == 0}">${default_start_date}</c:when><c:otherwise>${versions_start[0]}</c:otherwise></c:choose>">
-    <label>End date (yyyy-mm-dd):</label>
-      <input type="text" name="end" size="10"
-             value="<c:choose><c:when test="${fn:length(versions_end) == 0}">${default_end_date}</c:when><c:otherwise>${versions_end[0]}</c:otherwise></c:choose>">
-    </p><p>
-    <input class="submit" type="submit" value="Update graph">
-    </p>
-  </div>
-</form>
-<p>Download graph as
-<a href="versions.pdf${versions_url}">PDF</a> or
-<a href="versions.svg${versions_url}">SVG</a>.</p>
-<p><a href="stats/servers.csv">CSV</a> file containing all data.</p>
-<br>
-
-<a name="platforms"></a>
-<h3><a href="#platforms" class="anchor">Relays by platform</a></h3>
-<br>
-<p>Relays report the operating system they are running to the directory
-authorities. The following graph shows the number of relays by
-platform.</p>
-<img src="platforms.png${platforms_url}"
-     width="576" height="360" alt="Relay platforms graph">
-<form action="network.html#platforms">
-  <div class="formrow">
-    <input type="hidden" name="graph" value="platforms">
-    <p>
-    <label>Start date (yyyy-mm-dd):</label>
-      <input type="text" name="start" size="10"
-             value="<c:choose><c:when test="${fn:length(platforms_start) == 0}">${default_start_date}</c:when><c:otherwise>${platforms_start[0]}</c:otherwise></c:choose>">
-    <label>End date (yyyy-mm-dd):</label>
-      <input type="text" name="end" size="10"
-             value="<c:choose><c:when test="${fn:length(platforms_end) == 0}">${default_end_date}</c:when><c:otherwise>${platforms_end[0]}</c:otherwise></c:choose>">
-    </p><p>
-    <input class="submit" type="submit" value="Update graph">
-    </p>
-  </div>
-</form>
-<p>Download graph as
-<a href="platforms.pdf${platforms_url}">PDF</a> or
-<a href="platforms.svg${platforms_url}">SVG</a>.</p>
-<p><a href="stats/servers.csv">CSV</a> file containing all data.</p>
-<br>
-
-<a name="cloudbridges"></a>
-<h3><a href="#cloudbridges" class="anchor">Tor Cloud bridges</a></h3>
-<br>
-<p>The following graph shows the average daily number of
-<a href="http://cloud.torproject.org/">Tor Cloud</a> bridges in the
-network.</p>
-<img src="cloudbridges.png${cloudbridges_url}"
-     width="576" height="360" alt="Tor Cloud bridges graph">
-<form action="network.html#cloudbridges">
-  <div class="formrow">
-    <input type="hidden" name="graph" value="cloudbridges">
-    <p>
-    <label>Start date (yyyy-mm-dd):</label>
-      <input type="text" name="start" size="10"
-             value="<c:choose><c:when test="${fn:length(cloudbridges_start) == 0}">${default_start_date}</c:when><c:otherwise>${cloudbridges_start[0]}</c:otherwise></c:choose>">
-    <label>End date (yyyy-mm-dd):</label>
-      <input type="text" name="end" size="10"
-             value="<c:choose><c:when test="${fn:length(cloudbridges_end) == 0}">${default_end_date}</c:when><c:otherwise>${cloudbridges_end[0]}</c:otherwise></c:choose>">
-    </p><p>
-    <input class="submit" type="submit" value="Update graph">
-    </p>
-  </div>
-</form>
-<p>Download graph as
-<a href="cloudbridges.pdf${cloudbridges_url}">PDF</a> or
-<a href="cloudbridges.svg${cloudbridges_url}">SVG</a>.</p>
-<p><a href="stats/servers.csv">CSV</a> file containing all data.</p>
-<br>
-
-<a name="bandwidth"></a>
-<h3><a href="#bandwidth" class="anchor">Total relay bandwidth in the
-network</a></h3>
-<br>
-<p>Relays report how much bandwidth they are willing to contribute and how
-many bytes they have read and written in the past 24 hours. The following
-graph shows total advertised bandwidth and bandwidth history of all relays
-in the network.</p>
-<img src="bandwidth.png${bandwidth_url}"
-     width="576" height="360" alt="Relay bandwidth graph">
-<form action="network.html#bandwidth">
-  <div class="formrow">
-    <input type="hidden" name="graph" value="bandwidth">
-    <p>
-    <label>Start date (yyyy-mm-dd):</label>
-      <input type="text" name="start" size="10"
-             value="<c:choose><c:when test="${fn:length(bandwidth_start) == 0}">${default_start_date}</c:when><c:otherwise>${bandwidth_start[0]}</c:otherwise></c:choose>">
-    <label>End date (yyyy-mm-dd):</label>
-      <input type="text" name="end" size="10"
-             value="<c:choose><c:when test="${fn:length(bandwidth_end) == 0}">${default_end_date}</c:when><c:otherwise>${bandwidth_end[0]}</c:otherwise></c:choose>">
-    </p><p>
-    <input class="submit" type="submit" value="Update graph">
-    </p>
-  </div>
-</form>
-<p>Download graph as
-<a href="bandwidth.pdf${bandwidth_url}">PDF</a> or
-<a href="bandwidth.svg${bandwidth_url}">SVG</a>.</p>
-<p><a href="stats/bandwidth.csv">CSV</a> file containing all data.</p>
-<br>
-
-<a name="bwhist-flags"></a>
-<h3><a href="#bwhist-flags" class="anchor">Relay bandwidth by Exit and/or
-Guard flags</a></h3>
-<br>
-<p>The following graph shows the relay bandwidth of all relays with the
-Exit and/or Guard flags assigned by the directory authorities.</p>
-<img src="bwhist-flags.png${bwhist_flags_url}"
-     width="576" height="360" alt="Relay bandwidth by flags graph">
-<form action="network.html#bwhist-flags">
-  <div class="formrow">
-    <input type="hidden" name="graph" value="bwhist-flags">
-    <p>
-    <label>Start date (yyyy-mm-dd):</label>
-      <input type="text" name="start" size="10"
-             value="<c:choose><c:when test="${fn:length(bwhist_flags_start) == 0}">${default_start_date}</c:when><c:otherwise>${bwhist_flags_start[0]}</c:otherwise></c:choose>">
-    <label>End date (yyyy-mm-dd):</label>
-      <input type="text" name="end" size="10"
-             value="<c:choose><c:when test="${fn:length(bwhist_flags_end) == 0}">${default_end_date}</c:when><c:otherwise>${bwhist_flags_end[0]}</c:otherwise></c:choose>">
-    </p><p>
-    <input class="submit" type="submit" value="Update graph">
-    </p>
-  </div>
-</form>
-<p>Download graph as
-<a href="bwhist-flags.pdf${bwhist_flags_url}">PDF</a> or
-<a href="bwhist-flags.svg${bwhist_flags_url}">SVG</a>.</p>
-<p><a href="stats/bandwidth.csv">CSV</a> file containing all data.</p>
-<br>
-
-<a name="bandwidth-flags"></a>
-<h3><a href="#bandwidth-flags" class="anchor">Advertised bandwidth and
-bandwidth history by relay flags</a></h3>
-<br>
-<p>The following graph shows the advertised bandwidth and bandwidth
-history of all relays with the Exit and/or Guard flags assigned by the
-directory authorities.
-Note that these sets possibly overlap with relays having both Exit and
-Guard flag.</p>
-<img src="bandwidth-flags.png${bandwidth_flags_url}"
-     width="576" height="360" alt="Advertised bandwidth and bandwidth history by relay flags graph">
-<form action="network.html#bandwidth-flags">
-  <div class="formrow">
-    <input type="hidden" name="graph" value="bandwidth-flags">
-    <p>
-    <label>Start date (yyyy-mm-dd):</label>
-      <input type="text" name="start" size="10"
-             value="<c:choose><c:when test="${fn:length(bandwidth_flags_start) == 0}">${default_start_date}</c:when><c:otherwise>${bandwidth_flags_start[0]}</c:otherwise></c:choose>">
-    <label>End date (yyyy-mm-dd):</label>
-      <input type="text" name="end" size="10"
-             value="<c:choose><c:when test="${fn:length(bandwidth_flags_end) == 0}">${default_end_date}</c:when><c:otherwise>${bandwidth_flags_end[0]}</c:otherwise></c:choose>">
-    </p><p>
-    <input class="submit" type="submit" value="Update graph">
-    </p>
-  </div>
-</form>
-<p>Download graph as
-<a href="bandwidth-flags.pdf${bandwidth_flags_url}">PDF</a> or
-<a href="bandwidth-flags.svg${bandwidth_flags_url}">SVG</a>.</p>
-<p><a href="stats/bandwidth.csv">CSV</a> file containing all data.</p>
-<br>
-
-<a name="dirbytes"></a>
-<h3><a href="#dirbytes" class="anchor">Number of bytes spent on answering
-directory requests</a></h3>
-<br>
-<p>Relays running on 0.2.2.15-alpha or higher report the number of bytes
-they spend on answering directory requests. The following graph shows
-total written and read bytes as well as written and read dir bytes. The
-dir bytes are extrapolated from those relays who report them to reflect
-the number of written and read dir bytes by all relays.</p>
-<img src="dirbytes.png${dirbytes_url}"
-     width="576" height="360" alt="Dir bytes graph">
-<form action="network.html#dirbytes">
-  <div class="formrow">
-    <input type="hidden" name="graph" value="dirbytes">
-    <p>
-    <label>Start date (yyyy-mm-dd):</label>
-      <input type="text" name="start" size="10"
-             value="<c:choose><c:when test="${fn:length(dirbytes_start) == 0}">${default_start_date}</c:when><c:otherwise>${dirbytes_start[0]}</c:otherwise></c:choose>">
-    <label>End date (yyyy-mm-dd):</label>
-      <input type="text" name="end" size="10"
-             value="<c:choose><c:when test="${fn:length(dirbytes_end) == 0}">${default_end_date}</c:when><c:otherwise>${dirbytes_end[0]}</c:otherwise></c:choose>">
-    </p><p>
-    <input class="submit" type="submit" value="Update graph">
-    </p>
-  </div>
-</form>
-<p>Download graph as
-<a href="dirbytes.pdf${dirbytes_url}">PDF</a> or
-<a href="dirbytes.svg${dirbytes_url}">SVG</a>.</p>
-<p><a href="stats/bandwidth.csv">CSV</a> file containing all data.</p>
-<br>
-    </div>
-  </div>
-  <div class="bottom" id="bottom">
-    <%@ include file="footer.jsp"%>
-  </div>
-</body>
-</html>
diff --git a/web/WEB-INF/papers.jsp b/web/WEB-INF/papers.jsp
deleted file mode 100644
index 1a48491..0000000
--- a/web/WEB-INF/papers.jsp
+++ /dev/null
@@ -1,81 +0,0 @@
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
-<html>
-<head>
-  <title>Tor Metrics Portal: Papers</title>
-  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
-  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
-  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
-</head>
-<body>
-  <div class="center">
-    <%@ include file="banner.jsp"%>
-    <div class="main-column">
-        <h2>Tor Metrics Portal: Papers</h2>
-        <br>
-        <p>The <a href="#papers">papers</a>,
-        <a href="#techreports">technical reports</a>, and
-        <a href="#blogposts">blog posts</a> listed on this page originate
-        from, are based on, or are related to work performed in the Tor
-        Metrics Project.</p>
-        <p><font color="red">This page will be removed soon.
-        Its main purpose was to list metrics-related tech reports, but
-        those have now moved
-        <a href="https://research.torproject.org/techreports.html">here</a>.
-        </font></p>
-        <br>
-        <a name="papers"></a>
-        <h3><a href="#papers" class="anchor">Papers</a></h3>
-        <br>
-        These papers summarize some of the results of of the Tor Metrics
-        Project and have been accepted for publication at academic
-        conferences or workshops.
-        <ul>
-          <li>Karsten Loesing, Steven J. Murdoch, Roger Dingledine. A Case
-          Study on Measuring Statistical Data in the Tor Anonymity
-          Network. Workshop on Ethics in
-          Computer Security Research (WECSR 2010), Tenerife, Spain,
-          January 2010. (<a href="http://freehaven.net/anonbib/cache/wecsr10measuring-tor.pdf">PDF</a>)</li>
-          <li>Karsten Loesing. Measuring the Tor Network from Public
-          Directory Information. 2nd Hot Topics in Privacy Enhancing
-          Technologies (HotPETs 2009), Seattle, WA, USA, August 2009.
-          (<a href="https://research.torproject.org/techreports/metrics-2009-08-07.pdf">PDF</a>)</li>
-        </ul>
-        <br>
-        <a name="techreports"></a>
-        <h3><a href="#techreports" class="anchor">Technical
-        reports</a></h3>
-        <br>
-        <p>
-        Some of the
-        <a href="https://research.torproject.org/techreports.html">Tor
-        Technical Reports</a> have been the first place to
-        publish novel kinds of statistics on the Tor network. Some, but
-        not all, of the results contained in those technical reports have
-        been included in the <a href="#papers">papers</a> above or in the
-        daily updated <a href="graphs.html">graphs</a>.
-        </p>
-        <br>
-        <a name="blogposts"></a>
-        <h3><a href="#blogposts" class="anchor">Blog posts</a></h3>
-        <br>
-        The following blog posts are either the results of metrics
-        research or describe new interesting research questions that can
-        (partly) be answered with metrics data.
-        <ul>
-          <li>Research problems: Ten ways to discover Tor bridges
-          (<a href="https://blog.torproject.org/blog/research-problems-ten-ways-discover-tor-bridges">link</a>,
-          October 31, 2011).</li>
-          <li>Research problem: better guard rotation parameters
-          (<a href="https://blog.torproject.org/blog/research-problem-better-guard-rotation-parameters">link</a>,
-          August 20, 2011).</li>
-          <li>Research problem: measuring the safety of the Tor network
-          (<a href="https://blog.torproject.org/blog/research-problem-measuring-safety-tor-network">link</a>,
-          February 5, 2011).</li>
-        </ul>
-    </div>
-  </div>
-  <div class="bottom" id="bottom">
-    <%@ include file="footer.jsp"%>
-  </div>
-</body>
-</html>
diff --git a/web/WEB-INF/performance.jsp b/web/WEB-INF/performance.jsp
deleted file mode 100644
index e220b59..0000000
--- a/web/WEB-INF/performance.jsp
+++ /dev/null
@@ -1,150 +0,0 @@
-<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
-<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
-<html>
-<head>
-  <title>Tor Metrics Portal: Performance</title>
-  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
-  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
-  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
-</head>
-<body>
-  <div class="center">
-    <%@ include file="banner.jsp"%>
-    <div class="main-column">
-<h2>Tor Metrics Portal: Performance</h2>
-<br>
-<a name="torperf"></a>
-<h3><a href="#torperf" class="anchor">Time to download files over
-Tor</a></h3>
-<br>
-<p>The following graphs show the performance of the Tor network as
-experienced by its users. The graphs contain the average (median) time to
-request files of three different sizes over Tor as well as first and third
-quartile of request times. Medians and quartiles are calculated multiple
-times per day for completed days only, resulting in a delay of 1 to 1.5
-days before changes to network performance become visible in the
-graph.</p>
-<img src="torperf.png${torperf_url}"
-     width="576" height="360" alt="Torperf graph">
-<form action="performance.html#torperf">
-  <div class="formrow">
-    <input type="hidden" name="graph" value="torperf">
-    <p>
-    <label>Start date (yyyy-mm-dd):</label>
-      <input type="text" name="start" size="10"
-             value="<c:choose><c:when test="${fn:length(torperf_start) == 0}">${default_start_date}</c:when><c:otherwise>${torperf_start[0]}</c:otherwise></c:choose>">
-    <label>End date (yyyy-mm-dd):</label>
-      <input type="text" name="end" size="10"
-             value="<c:choose><c:when test="${fn:length(torperf_end) == 0}">${default_end_date}</c:when><c:otherwise>${torperf_end[0]}</c:otherwise></c:choose>">
-    </p><p>
-      Source:
-      <input type="radio" name="source" value="all"<c:if test="${fn:length(torperf_source) == 0 or torperf_source[0] eq 'all'}"> checked</c:if>> all
-      <input type="radio" name="source" value="torperf"<c:if test="${torperf_source[0] eq 'torperf'}"> checked</c:if>> torperf
-      <input type="radio" name="source" value="moria"<c:if test="${torperf_source[0] eq 'moria'}"> checked</c:if>> moria
-      <input type="radio" name="source" value="siv"<c:if test="${torperf_source[0] eq 'siv'}"> checked</c:if>> siv
-    </p><p>
-      <label>File size: </label>
-      <input type="radio" name="filesize" value="50kb"<c:if test="${fn:length(torperf_filesize) == 0 or torperf_filesize[0] eq '50kb'}"> checked</c:if>> 50 KiB
-      <input type="radio" name="filesize" value="1mb"<c:if test="${torperf_filesize[0] eq '1mb'}"> checked</c:if>> 1 MiB
-      <input type="radio" name="filesize" value="5mb"<c:if test="${torperf_filesize[0] eq '5mb'}"> checked</c:if>> 5 MiB
-    </p><p>
-    <input class="submit" type="submit" value="Update graph">
-    </p>
-  </div>
-</form>
-<p>Download graph as
-<a href="torperf.pdf${torperf_url}">PDF</a> or
-<a href="torperf.svg${torperf_url}">SVG</a>.</p>
-<p><a href="stats/torperf.csv">CSV</a> file containing all data.</p>
-
-<br>
-<a name="torperf-failures"></a>
-<h3><a href="#torperf-failures" class="anchor">Timeouts and failures of
-downloading files over Tor</a></h3>
-<br>
-<p>The following graphs show the fraction of timeouts and failures of
-downloading files over Tor as experienced by users.
-A timeout occurs when a 50 KiB (1 MiB, 5 MiB) download does not complete
-within 4:55 minutes (29:55 minutes, 59:55 minutes).
-A failure occurs when the download completes, but the response is smaller
-than 50 KiB (1 MiB, 5 MiB).</p>
-<img src="torperf-failures.png${torperf_failures_url}"
-     width="576" height="360" alt="Torperf failures graph">
-<form action="performance.html#torperf-failures">
-  <div class="formrow">
-    <input type="hidden" name="graph" value="torperf-failures">
-    <p>
-    <label>Start date (yyyy-mm-dd):</label>
-      <input type="text" name="start" size="10"
-             value="<c:choose><c:when test="${fn:length(torperf_failures_start) == 0}">${default_start_date}</c:when><c:otherwise>${torperf_failures_start[0]}</c:otherwise></c:choose>">
-    <label>End date (yyyy-mm-dd):</label>
-      <input type="text" name="end" size="10"
-             value="<c:choose><c:when test="${fn:length(torperf_failures_end) == 0}">${default_end_date}</c:when><c:otherwise>${torperf_failures_end[0]}</c:otherwise></c:choose>">
-    </p><p>
-      Source:
-      <input type="radio" name="source" value="all"<c:if test="${fn:length(torperf_failures_source) == 0 or torperf_failures_source[0] eq 'all'}"> checked</c:if>> all
-      <input type="radio" name="source" value="torperf"<c:if test="${torperf_failures_source[0] eq 'torperf'}"> checked</c:if>> torperf
-      <input type="radio" name="source" value="moria"<c:if test="${torperf_failures_source[0] eq 'moria'}"> checked</c:if>> moria
-      <input type="radio" name="source" value="siv"<c:if test="${torperf_failures_source[0] eq 'siv'}"> checked</c:if>> siv
-    </p><p>
-      <label>File size: </label>
-      <input type="radio" name="filesize" value="50kb"<c:if test="${fn:length(torperf_failures_filesize) == 0 or torperf_failures_filesize[0] eq '50kb'}"> checked</c:if>> 50 KiB
-      <input type="radio" name="filesize" value="1mb"<c:if test="${torperf_failures_filesize[0] eq '1mb'}"> checked</c:if>> 1 MiB
-      <input type="radio" name="filesize" value="5mb"<c:if test="${torperf_failures_filesize[0] eq '5mb'}"> checked</c:if>> 5 MiB
-    </p><p>
-    <input class="submit" type="submit" value="Update graph">
-    </p>
-  </div>
-</form>
-<p>Download graph as
-<a href="torperf-failures.pdf${torperf_failures_url}">PDF</a> or
-<a href="torperf-failures.svg${torperf_failures_url}">SVG</a>.</p>
-<p><a href="stats/torperf.csv">CSV</a> file containing all data.</p>
-
-<br>
-<a name="connbidirect"></a>
-<h3><a href="#connbidirect" class="anchor">Fraction of connections used
-uni-/bidirectionally</a></h3>
-<br>
-<p>The following graph shows the fraction of connections that is used
-uni- or bi-directionally.  Every 10 seconds, relays determine for every
-connection whether they read and wrote less than a threshold of 20 KiB.
-Connections below this threshold are excluded from these statistics.  For
-the remaining connections, relays report whether they read/wrote at least
-10 times as many bytes as they wrote/read.  If so, they classify a
-connection as "Mostly reading" or "Mostly writing," respectively.  All
-other connections are classified as "Both reading and writing."  After
-classifying connections, read and write counters are reset for the next
-10-second interval.  Statistics are aggregated over 24 hours.</p>
-<img src="connbidirect.png${connbidirect_url}"
-     width="576" height="360"
-     alt="Fraction of direct connections used uni-/bidirectionally">
-<form action="performance.html#connbidirect">
-  <div class="formrow">
-    <input type="hidden" name="graph" value="connbidirect">
-    <p>
-    <label>Start date (yyyy-mm-dd):</label>
-      <input type="text" name="start" size="10"
-             value="<c:choose><c:when test="${fn:length(connbidirect_start) == 0}">${default_start_date}</c:when><c:otherwise>${connbidirect_start[0]}</c:otherwise></c:choose>">
-    <label>End date (yyyy-mm-dd):</label>
-      <input type="text" name="end" size="10"
-             value="<c:choose><c:when test="${fn:length(connbidirect_end) == 0}">${default_end_date}</c:when><c:otherwise>${connbidirect_end[0]}</c:otherwise></c:choose>">
-    </p><p>
-    <input class="submit" type="submit" value="Update graph">
-    </p>
-  </div>
-</form>
-<p>Download graph as
-<a href="connbidirect.pdf${connbidirect_url}">PDF</a> or
-<a href="connbidirect.svg${connbidirect_url}">SVG</a>.</p>
-<p><a href="stats/connbidirect.csv">CSV</a> file containing all data.</p>
-<br>
-
-    </div>
-  </div>
-  <div class="bottom" id="bottom">
-    <%@ include file="footer.jsp"%>
-  </div>
-</body>
-</html>
diff --git a/web/WEB-INF/relay-search.jsp b/web/WEB-INF/relay-search.jsp
deleted file mode 100644
index 90fdd86..0000000
--- a/web/WEB-INF/relay-search.jsp
+++ /dev/null
@@ -1,85 +0,0 @@
-<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
-<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
-<html>
-<head>
-  <title>Tor Metrics Portal: Relay Search</title>
-  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
-  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
-  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
-</head>
-<body>
-  <div class="center">
-    <%@ include file="banner.jsp"%>
-    <div class="main-column">
-      <h2>Tor Metrics Portal: Relay Search</h2>
-      <p>Search for a relay in the relay descriptor archive by typing
-      (part of) a <b>nickname</b>, <b>$-prefixed fingerprint</b>, or <b>IP
-      address</b> and optionally a <b>month (yyyy-mm)</b> or up to three
-      <b>days (yyyy-mm-dd)</b> in the following search field and
-      clicking Search. The search will stop after 30 hits or, unless you
-      provide a month or a day, after parsing the last 30 days of relay
-      lists.</p>
-      <br>
-      <form action="relay-search.html">
-        <table>
-          <tr>
-            <td><input type="text" name="search"
-                       value="<c:out value="${param.search}"/>"></td>
-            <td><input type="submit" value="Search"></td>
-          </tr>
-        </table>
-      </form>
-      <br>
-      <c:if test="${not empty invalidQuery}">
-        <p>Sorry, I didn't understand your query. Please provide a
-        nickname (e.g., "gabelmoo"), at least the first 8 hex characters
-        of a fingerprint prefixed by $ (e.g., "$F2044413"), or at least
-        the first two octets of an IPv4 address in dotted-decimal notation
-        (e.g., "80.190"). You can also provide at most three months or
-        days in ISO 8601 format (e.g., "2010-09" or "2010-09-17").</p>
-      </c:if>
-      <c:if test="${not empty outsideInterval}">
-        <p>${outsideInterval}</p>
-      </c:if>
-      <c:if test="${not empty searchNotice}">
-        <p>${searchNotice}</p>
-      </c:if>
-      <c:if test="${not empty query}">
-        <!-- ${query} -->
-      </c:if>
-      <c:if test="${not empty queryTime}">
-        <c:forEach var="consensus" items="${foundDescriptors}">
-          ${rawValidAfterLines[consensus.key]}
-          <c:forEach var="statusentry" items="${consensus.value}">
-            ${rawStatusEntries[statusentry]}
-          </c:forEach>
-          <br>
-        </c:forEach>
-        <p>Found
-        <c:choose>
-          <c:when test="${matches > 30}">
-            more than 30 relays (displaying only those in the last
-            consensuses)
-          </c:when>
-          <c:otherwise>
-            ${matches} relays
-          </c:otherwise>
-        </c:choose>
-        in <fmt:formatNumber value="${queryTime / 1000}" pattern="#.###"/>
-        seconds.</p>
-        <c:if test="${queryTime > 10000}">
-          <p>In theory, search time should not exceed 10 seconds. The
-          query was '${query}'. If this or similar searches remain slow,
-          please <a href="mailto:tor-assistants at torproject.org">let us
-          know</a>!</p>
-        </c:if>
-      </c:if>
-    </div>
-  </div>
-  <div class="bottom" id="bottom">
-    <%@ include file="footer.jsp"%>
-  </div>
-</body>
-</html>
-
diff --git a/web/WEB-INF/research.jsp b/web/WEB-INF/research.jsp
deleted file mode 100644
index b0a7277..0000000
--- a/web/WEB-INF/research.jsp
+++ /dev/null
@@ -1,32 +0,0 @@
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
-<html>
-<head>
-  <title>Tor Metrics Portal: Research</title>
-  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
-  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
-  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
-</head>
-<body>
-  <div class="center">
-    <%@ include file="banner.jsp"%>
-    <div class="main-column">
-        <h2>Tor Metrics Portal: Research</h2>
-        <br>
-        <p>The Tor Metrics Project aims at supporting privacy enhancing
-        technologies research by making gathered network
-        <a href="data.html">data</a>, and
-        <a href="tools.html">tools</a>
-        for processing these data available to the public.
-        Some results from analyzing these data can be found in
-        <a href="https://research.torproject.org/techreports.html">Tor
-        Tech Reports</a>.  If you are
-        missing anything for your Tor-related research or want to share
-        your research results with others, please
-        <a href="mailto:tor-assistants at torproject.org">let us know</a>!
-    </div>
-  </div>
-  <div class="bottom" id="bottom">
-    <%@ include file="footer.jsp"%>
-  </div>
-</body>
-</html>
diff --git a/web/WEB-INF/stats.jsp b/web/WEB-INF/stats.jsp
deleted file mode 100644
index ccc2540..0000000
--- a/web/WEB-INF/stats.jsp
+++ /dev/null
@@ -1,313 +0,0 @@
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
-<html>
-<head>
-  <title>Tor Metrics Portal: Statistics</title>
-  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
-  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
-  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
-</head>
-<body>
-  <div class="center">
-    <%@ include file="banner.jsp"%>
-    <div class="main-column">
-<h2>Tor Metrics Portal: Statistics</h2>
-<br>
-
-<p>The metrics portal aggregates large amounts of Tor network
-<a href="data.html">data</a> and visualizes results in customizable
-<a href="graphs.html">graphs</a> and tables.
-All aggregated data are also available for download, so that people can
-easily plot their own graphs or even develop a prettier metrics website
-without writing their own data aggregation code.
-Data formats of aggregate statistics are specified below.</p>
-<hr>
-<br>
-
-<a name="servers"></a>
-<h3><a href="#servers" class="anchor">Number of relays and
-bridges</a></h3>
-<br>
-<p>Statistics file <a href="stats/servers.csv">servers.csv</a> contains
-the average number of relays and bridges in the Tor network.
-All averages are calculated per day by evaluating the relay and bridge
-lists published by the directory authorities.
-Statistics include subsets of relays or bridges by relay flag (only
-relays), country code (only relays, only until February 2013), Tor
-software version (only relays), operating system (only relays), and EC2
-cloud (only bridges).
-The statistics file contains the following columns:</p>
-
-<ul>
-<li><b>date:</b> UTC date (YYYY-MM-DD) when relays or bridges have been
-listed as running.</li>
-<li><b>flag:</b> Relay flag assigned by the directory authorities.
-Examples are <b>"Exit"</b>, <b>"Guard"</b>, <b>"Fast"</b>,
-<b>"Stable"</b>, and <b>"HSDir"</b>.
-Relays can have none, some, or all these relay flags assigned.
-Relays that don't have the <b>"Running"</b> flag are not included in these
-statistics regardless of their other flags.
-If this column contains the empty string, all running relays are included,
-regardless of assigned flags.
-There are no statistics on the number of bridges by relay flag.</li>
-<li><b>country:</b> Two-letter lower-case country code as found in a GeoIP
-database by resolving the relay's first onion-routing IP address, or
-<b>"??"</b> if an IP addresses could not be resolved.
-If this column contains the empty string, all running relays are included,
-regardless of their resolved country code.
-Statistics on relays by country code are only available until January 31,
-2013.
-There are no statistics on the number of bridges by country code.</li>
-<li><b>version:</b> First three dotted numbers of the Tor software version
-as reported by the relay.
-An example is <b>"0.2.5"</b>.
-If this column contains the empty string, all running relays are included,
-regardless of the Tor software version they run.
-There are no statistics on the number of bridges by Tor software
-version.</li>
-<li><b>platform:</b> Operating system as reported by the relay.
-Examples are <b>"Linux"</b>, <b>"Darwin"</b> (Mac OS X), <b>"FreeBSD"</b>,
-<b>"Windows"</b>, and <b>"Other"</b>.
-If this column contains the empty string, all running relays are included,
-regardless of the operating system they run on.
-There are no statistics on the number of bridges by operating system.</li>
-<li><b>ec2bridge:</b> Whether bridges are running in the EC2 cloud or not.
-More precisely, bridges in the EC2 cloud running an image provided by Tor
-by default set their nickname to <b>"ec2bridger"</b> plus 8 random hex
-characters.
-This column either contains <b>"t"</b> for bridges matching this naming
-scheme, or the empty string for all bridges regardless of their nickname.
-There are no statistics on the number of relays running in the EC2
-cloud.</li>
-<li><b>relays:</b> The average number of relays matching the criteria in
-the previous columns.
-If the values in previous columns are specific to bridges only, this
-column contains the empty string.</li>
-<li><b>bridges:</b> The average number of bridges matching the criteria in
-the previous columns.
-If the values in previous columns are specific to relays only, this column
-contains the empty string.</li>
-</ul>
-<hr>
-<br>
-
-<a name="bandwidth"></a>
-<h3><a href="#bandwidth" class="anchor">Bandwidth provided and consumed by
-relays</a></h3>
-<br>
-<p>Statistics on bandwidth provided and consumed by relays are contained
-in file <a href="stats/bandwidth.csv">bandwidth.csv</a>.
-This file contains three different bandwidth metrics:
-(1) bandwidth that relays are capable to provide and bandwidth that relays
-report to have consumed, either (2) for any traffic, or (3) only traffic
-from serving directory data.
-Relays providing bandwidth statistics are categorized by having the
-<b>"Exit"</b> and <b>"Guard"</b> relay flag, having both, or not having
-either.
-The statistics file contains the following columns:</p>
-
-<ul>
-<li><b>date:</b> UTC date (YYYY-MM-DD) that relays reported bandwidth data
-for.</li>
-<li><b>isexit:</b> Whether relays included in this line have the
-<b>"Exit"</b> relay flag or not, which can be <b>"t"</b> or <b>"f"</b>.
-If this column contains the empty string, bandwidth data from all running
-relays are included, regardless of assigned relay flags.</li>
-<li><b>isguard:</b> Whether relays included in this line have the
-<b>"Guard"</b> relay flag or not, which can be <b>"t"</b> or <b>"f"</b>.
-If this column contains the empty string, bandwidth data from all running
-relays are included, regardless of assigned relay flags.</li>
-<li><b>advbw:</b> Total advertised bandwidth in bytes per second that
-relays are capable to provide.</li>
-<li><b>bwread:</b> Total bandwidth in bytes per second that relays have
-read.
-This metric includes any kind of traffic.</li>
-<li><b>bwwrite:</b> Similar to <b>bwread</b>, but for traffic written by
-relays.</li>
-<li><b>dirread:</b> Bandwidth in bytes per second that relays have read
-when serving directory data.
-Not all relays report how many bytes they read when serving directory data
-which is why this value is an estimate from the available data.
-This metric is not available for subsets of relays with certain relay
-flags, so that this column will contain the empty string if either
-<b>isexit</b> or <b>isguard</b> is non-empty.</li>
-<li><b>dirwrite:</b> Similar to <b>dirread</b>, but for traffic written by
-relays when serving directory data.</li>
-</ul>
-<hr>
-<br>
-
-<a name="fast-exits"></a>
-<h3><a href="#fast-exits" class="anchor">Relays meeting or almost meeting
-fast-exit requirements</a></h3>
-<br>
-<p>Statistics file <a href="stats/fast-exits.csv">fast-exits.csv</a>
-contains the number of relays meeting or almost meeting fast-exit
-requirements.
-These requirements originate from a Tor sponsor contract and are defined as
-follows:
-a Tor relay is fast if it has at least 95 Mbit/s configured bandwidth
-rate, at least 5000 KB/s advertised bandwidth capacity, and permits
-exiting to ports 80, 443, 554, and 1755; furthermore, there may be at most
-2 relays per /24 network in the set of fast exits.
-Similarly, an almost fast exit is one that almost meets the fast-exit
-requirements, but fails at least one of them.
-In particular, an almost fast exit is one that has at least 80 Mbit/s
-configured bandwidth rate, at least 2000 KB/s advertised bandwidth
-capacity, and permits exiting to ports 80 and 443; also, if there are more
-than 2 relays per /24 network meeting fast-exit requirements, all but two
-are considered almost fast.
-The statistics file contains the following columns:</p>
-
-<ul>
-<li><b>date:</b> UTC date (YYYY-MM-DD) when relays have been listed as
-running.</li>
-<li><b>fastnum:</b> Average number of relays matching fast-exit
-requirements.</li>
-<li><b>almostnum:</b> Average number of relays almost matching
-fast-exit requirements.</li>
-<li><b>fastprob:</b> Total exit probability of all relays matching
-fast-exit requirements.</li>
-<li><b>almostprob:</b> Total exit probability of all relays almost
-matching fast-exit requirements.</li>
-</li>
-</ul>
-<hr>
-<br>
-
-<a name="clients"></a>
-<h3><a href="#clients" class="anchor">Estimated number of clients in the
-Tor network</a></h3>
-<br>
-<p>Statistics file <a href="stats/clients.csv">clients.csv</a> contains
-estimates on the number of clients in the Tor network.
-These estimates are based on the number of directory requests counted on
-directory mirrors and bridges.
-Statistics are available for clients connecting directly to the Tor
-network and clients connecting via bridges.
-For relays, there exist statistics on the number of clients by country,
-and for bridges, statistics are available by country, by transport, and by
-IP version.
-Statistics further include expected client numbers from past observations
-which can be used to detect censorship or release of censorship.
-The statistics file contains the following columns:</p>
-
-<ul>
-<li><b>date:</b> UTC date (YYYY-MM-DD) for which client numbers are
-estimated.</li>
-<li><b>node:</b> The node type to which clients connect first, which can
-be either <b>"relay"</b> or <b>"bridge"</b>.</li>
-<li><b>country:</b> Two-letter lower-case country code as found in a GeoIP
-database by resolving clients' IP addresses, or <b>"??"</b> if client IP
-addresses could not be resolved.
-If this column contains the empty string, all clients are included,
-regardless of their country code.</li>
-<li><b>transport:</b> Transport name used by clients to connect to the Tor
-network using bridges.
-Examples are <b>"obfs2"</b>, <b>"obfs3"</b>, <b>"websocket"</b>, or
-<b>"<OR>"</b> (original onion routing protocol).
-If this column contains the empty string, all clients are included,
-regardless of their transport.
-There are no statistics on the number of clients by transport that connect
-to the Tor network via relays.</li>
-<li><b>version:</b> IP version used by clients to connect to the Tor
-network using bridges.
-Examples are <b>"v4"</b> and <b>"v6"</b>.
-If this column contains the empty string, all clients are included,
-regardless of their IP version.
-There are no statistics on the number of clients by IP version that connect
-directly to the Tor network using relays.</li>
-<li><b>lower:</b> Lower number of expected clients under the assumption
-that there has been no censorship event.
-If this column contains the empty string, there are no expectations on the
-number of clients.</li>
-<li><b>upper:</b> Upper number of expected clients under the assumption
-that there has been no release of censorship.
-If this column contains the empty string, there are no expectations on the
-number of clients.</li>
-<li><b>clients:</b> Estimated number of clients.</li>
-<li><b>frac:</b> Fraction of relays or bridges in percent that the
-estimate is based on.
-The higher this value, the more reliable is the estimate.
-Values above 50 can be considered reliable enough for most purposes,
-lower values should be handled with more care.</li>
-</ul>
-<hr>
-<br>
-
-<a name="torperf"></a>
-<h3><a href="#torperf" class="anchor">Performance of downloading static
-files over Tor</a></h3>
-<br>
-<p>Statistics file <a href="stats/torperf.csv">torperf.csv</a> contains
-aggregate statistics on download performance over time.
-These statistics come from the Torperf service that periodically downloads
-static files over Tor.
-The statistics file contains the following columns:</p>
-
-<ul>
-<li><b>date:</b> UTC date (YYYY-MM-DD) when download performance was
-measured.</li>
-<li><b>size:</b> Size of the downloaded file in bytes.</li>
-<li><b>source:</b> Name of the Torperf service performing measurements.
-If this column contains the empty string, all measurements are included,
-regardless of which Torperf service performed them.
-Examples are <b>"moria"</b>, <b>"siv"</b>, and <b>"torperf"</b>.</li>
-<li><b>q1:</b> First quartile of time until receiving the last byte in
-milliseconds.</li>
-<li><b>md:</b> Median of time until receiving the last byte in
-milliseconds.</li>
-<li><b>q3:</b> Third quartile of time until receiving the last byte in
-milliseconds.</li>
-<li><b>timeouts:</b> Number of timeouts that occurred when attempting to
-download the static file over Tor.</li>
-<li><b>failures:</b> Number of failures that occurred when attempting to
-download the static file over Tor.</li>
-<li><b>requests:</b> Total number of requests made to download the static
-file over Tor.</li>
-</ul>
-<hr>
-<br>
-
-<a name="connbidirect"></a>
-<h3><a href="#connbidirect" class="anchor">Fraction of connections used
-uni-/bidirectionally</a></h3>
-<br>
-<p>Statistics file <a href="stats/connbidirect.csv">connbidirect.csv</a>
-contains statistics on the fraction of connections that is used uni- or
-bidirectionally.
-Every 10 seconds, relays determine for every connection whether they read
-and wrote less than a threshold of 20 KiB.
-For the remaining connections, relays report whether they read/wrote at
-least 10 times as many bytes as they wrote/read.
-If so, they classify a connection as "mostly reading" or "mostly writing,"
-respectively.
-All other connections are classified as "both reading and writing."
-After classifying connections, read and write counters are reset for the
-next 10-second interval.
-Statistics are aggregated over 24 hours.
-The statistics file contains the following columns:</p>
-
-<ul>
-<li><b>date:</b> UTC date (YYYY-MM-DD) for which statistics on
-uni-/bidirectional connection usage were reported.</li>
-<li><b>source:</b> Fingerprint of the relay reporting statistics.</li>
-<li><b>below:</b> Number of 10-second intervals of connections with less
-than 20 KiB read and written data.</li>
-<li><b>read:</b> Number of 10-second intervals of connections with 10
-times as many read bytes as written bytes.</li>
-<li><b>write:</b> Number of 10-second intervals of connections with 10
-times as many written bytes as read bytes.</li>
-<li><b>both:</b> Number of 10-second intervals of connections with less
-than 10 times as many written or read bytes as in the other
-direction.</li>
-</ul>
-    </div>
-  </div>
-  <div class="bottom" id="bottom">
-    <%@ include file="footer.jsp"%>
-  </div>
-</body>
-</html>
-</body>
-</html>
-
diff --git a/web/WEB-INF/status.jsp b/web/WEB-INF/status.jsp
deleted file mode 100644
index d6b565e..0000000
--- a/web/WEB-INF/status.jsp
+++ /dev/null
@@ -1,30 +0,0 @@
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
-<html>
-<head>
-  <title>Tor Metrics Portal: Status</title>
-  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
-  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
-  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
-</head>
-<body>
-  <div class="center">
-    <%@ include file="banner.jsp"%>
-    <div class="main-column">
-        <h2>Tor Metrics Portal: Status</h2>
-        <br>
-        <p>The network data collected by the Tor Metrics Project can be
-        used to analyze the Tor network status from a few years ago until
-        an hour ago. There are currently two applications for this data:
-        The <a href="exonerator.html">ExoneraTor</a> tells you whether
-        some IP address was a Tor relay at a given time, the
-        <a href="relay-search.html">Relay Search</a> lets you search the
-        descriptor archive for a relay, and the
-        <a href="consensus-health.html">Consensus Health</a> summarizes
-        information about the latest network consensus voting process.
-    </div>
-  </div>
-  <div class="bottom" id="bottom">
-    <%@ include file="footer.jsp"%>
-  </div>
-</body>
-</html>
diff --git a/web/WEB-INF/tools.jsp b/web/WEB-INF/tools.jsp
deleted file mode 100644
index 8929d74..0000000
--- a/web/WEB-INF/tools.jsp
+++ /dev/null
@@ -1,110 +0,0 @@
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
-<html>
-<head>
-  <title>Tor Metrics Portal: Tools</title>
-  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
-  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
-  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
-</head>
-<body>
-  <div class="center">
-    <%@ include file="banner.jsp"%>
-    <div class="main-column">
-        <h2>Tor Metrics Portal: Tools</h2>
-        <br>
-        <p>The metrics website and related websites depend on a collection
-        of services that measure, archive, process, and finally present Tor
-        network data.
-        This page gives an overview of these services.</p>
-        <img src="/images/tor-metrics-overview.png"/>
-        <br>
-        <a name="measure"></a>
-        <h3><a href="#measure" class="anchor">Measuring Tor network
-        data</a></h3>
-        <br>
-        <p>Tor network data is measured at various places:
-        <ul>
-          <li><a href="https://gitweb.torproject.org/torperf.git">Torperf</a>
-          is a set of utilities for testing Tor performance from a client
-          perspective.</li>
-          <li><a href="https://gitweb.torproject.org/tor.git">tor</a>
-          relays and bridges gather aggregate usage statistics and publish
-          descriptors containing data about Tor network structure and
-          usage.</li>
-          <li><a href="https://gitweb.torproject.org/tordnsel.git">TorDNSEL</a>
-          is a Tor DNS-based exit list that runs periodic checks whether
-          relays use different IP addresses for exiting to the Internet
-          than they use to register in the Tor network.</li>
-          <li><a href="https://gitweb.torproject.org/bridgedb.git">BridgeDB</a>
-          assigns bridges to distributors and gives them out via HTTPS or
-          email.</li>
-        </ul>
-        <br>
-        <a name="archive"></a>
-        <h3><a href="#archive" class="anchor">Archiving Tor network
-        data</a></h3>
-        <br>
-        <p>All Tor network data is downloaded, possibly sanitized, and
-        then archived by a single tool:</p>
-        <ul>
-          <li><a href="https://gitweb.torproject.org/metrics-db.git">metrics-db</a>
-          contains five components for archiving relay descriptors, bridge
-          descriptors, Torperf results, TorDNSEL exit lists, and BridgeDB
-          pool assignments.</li>
-        </ul>
-        <br>
-        <a name="process"></a>
-        <h3><a href="#process" class="anchor">Processing Tor network
-        data</a></h3>
-        <br>
-        <p>In some cases, processing and presenting Tor network data is
-        separated for maximum flexibility.
-        In particular, there are currently two main tools that process Tor
-        network data and write an intermediate data format, but don't
-        directly present results:</p>
-        <ul>
-          <li><a href="https://gitweb.torproject.org/metrics-web.git">metrics-web</a>
-          is the software behind this website, including aggregation code
-          that produces statistics files.</li>
-          <li><a href="https://gitweb.torproject.org/metrics-tasks.git/tree/HEAD:/task-6498">task-6498</a>
-          is a submodule of metric-web that
-          aggregates data to visualize fast exits in the Tor network.</li>
-          <li><a href="https://gitweb.torproject.org/metrics-tasks.git/tree/HEAD:/task-8462">task-8462</a>
-          is another submodule of metric-web that
-          estimates daily users from reported directory request
-          statistics.</li>
-          <li><a href="https://gitweb.torproject.org/onionoo.git">Onionoo</a>
-          provides Tor network status information in JSON format via a
-          RESTful web service.</li>
-        </ul>
-        <br>
-        <a name="present"></a>
-        <h3><a href="#present" class="anchor">Presenting Tor network
-        data</a></h3>
-        <br>
-        <p>There are a few websites and additional tools presenting Tor
-        network data:
-        <ul>
-          <li><a href="https://gitweb.torproject.org/metrics-web.git">metrics-web</a>
-          also contains the code that presents aggregate statistics on
-          this website.</li>
-          <li><a href="https://gitweb.torproject.org/exonerator.git">ExoneraTor</a>
-          is a website that tells you whether a given IP address was a Tor
-          relay.</li>
-          <li><a href="https://gitweb.torproject.org/atlas.git">Atlas</a>
-          is a web application to discover relays that uses Onionoo as its
-          data back-end.</li>
-          <li><a href="https://gitweb.torproject.org/globe.git">Globe</a>
-          is a Tor relay and bridge explorer that also uses Onionoo as its
-          data back-end.</li>
-          <li><a href="https://gitweb.torproject.org/compass.git">Compass</a>
-          is a web application that uses Onionoo's data to display
-          information about fast exits in the Tor network.</li>
-        </ul>
-    </div>
-  </div>
-  <div class="bottom" id="bottom">
-    <%@ include file="footer.jsp"%>
-  </div>
-</body>
-</html>
diff --git a/web/WEB-INF/users.jsp b/web/WEB-INF/users.jsp
deleted file mode 100644
index ec9f200..0000000
--- a/web/WEB-INF/users.jsp
+++ /dev/null
@@ -1,390 +0,0 @@
-<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
-<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
-<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
-<html>
-<head>
-  <title>Tor Metrics Portal: Users</title>
-  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
-  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
-  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
-</head>
-<body>
-  <div class="center">
-    <%@ include file="banner.jsp"%>
-    <div class="main-column">
-<h2>Tor Metrics Portal: Users</h2>
-<br>
-
-<a name="userstats-relay-country"></a>
-<p><b>Direct users by country:</b></p>
-
-<img src="userstats-relay-country.png${userstats_relay_country_url}"
-     width="576" height="360" alt="Direct users by country graph">
-<form action="users.html#userstats-relay-country">
-  <div class="formrow">
-    <input type="hidden" name="graph" value="userstats-relay-country">
-    <p>
-    <label>Start date (yyyy-mm-dd):</label>
-      <input type="text" name="start" size="10"
-             value="<c:choose><c:when test="${fn:length(userstats_relay_country_start) == 0}">${default_start_date}</c:when><c:otherwise>${userstats_relay_country_start[0]}</c:otherwise></c:choose>">
-    <label>End date (yyyy-mm-dd):</label>
-      <input type="text" name="end" size="10"
-             value="<c:choose><c:when test="${fn:length(userstats_relay_country_end) == 0}">${default_end_date}</c:when><c:otherwise>${userstats_relay_country_end[0]}</c:otherwise></c:choose>">
-    </p><p>
-      Source: <select name="country">
-        <option value="all"<c:if test="${userstats_relay_country_country[0] eq 'all'}"> selected</c:if>>All users</option>
-        <c:forEach var="country" items="${countries}" >
-          <option value="${country[0]}"<c:if test="${userstats_relay_country_country[0] eq country[0]}"> selected</c:if>>${country[1]}</option>
-        </c:forEach>
-      </select>
-    </p><p>
-      Show possible censorship events if available (<a
-      href="http://research.torproject.org/techreports/detector-2011-09-09.pdf">BETA</a>)
-      <select name="events">
-        <option value="off">Off</option>
-        <option value="on"<c:if test="${userstats_relay_country_events[0] eq 'on'}"> selected</c:if>>On: both points and expected range</option>
-        <option value="points"<c:if test="${userstats_relay_country_events[0] eq 'points'}"> selected</c:if>>On: points only, no expected range</option>
-      </select>
-    </p><p>
-    <input class="submit" type="submit" value="Update graph">
-    </p>
-  </div>
-</form>
-<p>Download graph as
-<a href="userstats-relay-country.pdf${userstats_relay_country_url}">PDF</a> or
-<a href="userstats-relay-country.svg${userstats_relay_country_url}">SVG</a>.</p>
-<hr>
-<a name="userstats-relay-table"></a>
-<p><b>Top-10 countries by directly connecting users:</b></p>
-<form action="users.html#userstats-relay-table">
-  <div class="formrow">
-    <input type="hidden" name="table" value="userstats-relay">
-    <p>
-    <label>Start date (yyyy-mm-dd):</label>
-      <input type="text" name="start" size="10"
-             value="<c:choose><c:when test="${fn:length(userstats_relay_start) == 0}">${default_start_date}</c:when><c:otherwise>${userstats_relay_start[0]}</c:otherwise></c:choose>">
-    <label>End date (yyyy-mm-dd):</label>
-      <input type="text" name="end" size="10"
-             value="<c:choose><c:when test="${fn:length(userstats_relay_end) == 0}">${default_end_date}</c:when><c:otherwise>${userstats_relay_end[0]}</c:otherwise></c:choose>">
-    </p><p>
-    <input class="submit" type="submit" value="Update table">
-    </p>
-  </div>
-</form>
-<br>
-<table>
-  <tr>
-    <th>Country</th>
-    <th>Mean daily users</th>
-  </tr>
-  <c:forEach var="row" items="${userstats_relay_tabledata}">
-    <tr>
-      <td><a href="users.html?graph=userstats-relay-country&country=${row['cc']}#userstats-relay-country">${row['country']}</a> </td>
-      <td>${row['abs']} (<fmt:formatNumber type="number" minFractionDigits="2" value="${row['rel']}" /> %)</td>
-    </tr>
-  </c:forEach>
-</table>
-<hr>
-<a name="userstats-censorship-events"></a>
-<p><b>Top-10 countries by possible censorship events (<a
-      href="http://research.torproject.org/techreports/detector-2011-09-09.pdf">BETA</a>):</b></p>
-<form action="users.html#userstats-censorship-events">
-  <div class="formrow">
-    <input type="hidden" name="table" value="userstats-censorship-events">
-    <p>
-    <label>Start date (yyyy-mm-dd):</label>
-      <input type="text" name="start" size="10"
-             value="<c:choose><c:when test="${fn:length(userstats_censorship_events_start) == 0}">${default_start_date}</c:when><c:otherwise>${userstats_censorship_events_start[0]}</c:otherwise></c:choose>">
-    <label>End date (yyyy-mm-dd):</label>
-      <input type="text" name="end" size="10"
-             value="<c:choose><c:when test="${fn:length(userstats_censorship_events_end) == 0}">${default_end_date}</c:when><c:otherwise>${userstats_censorship_events_end[0]}</c:otherwise></c:choose>">
-    </p><p>
-    <input class="submit" type="submit" value="Update table">
-    </p>
-  </div>
-</form>
-<br>
-<table>
-  <tr>
-    <th>Country</th>
-    <th>Downturns</th>
-    <th>Upturns</th>
-  </tr>
-  <c:forEach var="row" items="${userstats_censorship_events_tabledata}">
-    <tr>
-      <td><a href="users.html?graph=userstats-relay-country&country=${row['cc']}&events=on#userstats-relay-country">${row['country']}</a> </td>
-      <td>${row['downturns']}</td>
-      <td>${row['upturns']}</td>
-    </tr>
-  </c:forEach>
-</table>
-<hr>
-
-<a name="userstats-bridge-country"></a>
-<p><b>Bridge users by country:</b></p>
-
-<img src="userstats-bridge-country.png${userstats_bridge_country_url}"
-     width="576" height="360" alt="Bridge users by country graph">
-<form action="users.html#userstats-bridge-country">
-  <div class="formrow">
-    <input type="hidden" name="graph" value="userstats-bridge-country">
-    <p>
-    <label>Start date (yyyy-mm-dd):</label>
-      <input type="text" name="start" size="10"
-             value="<c:choose><c:when test="${fn:length(userstats_bridge_country_start) == 0}">${default_start_date}</c:when><c:otherwise>${userstats_bridge_country_start[0]}</c:otherwise></c:choose>">
-    <label>End date (yyyy-mm-dd):</label>
-      <input type="text" name="end" size="10"
-             value="<c:choose><c:when test="${fn:length(userstats_bridge_country_end) == 0}">${default_end_date}</c:when><c:otherwise>${userstats_bridge_country_end[0]}</c:otherwise></c:choose>">
-    </p><p>
-      Source: <select name="country">
-        <option value="all"<c:if test="${userstats_bridge_country_country[0] eq 'all'}"> selected</c:if>>All users</option>
-        <c:forEach var="country" items="${countries}" >
-          <option value="${country[0]}"<c:if test="${userstats_bridge_country_country[0] eq country[0]}"> selected</c:if>>${country[1]}</option>
-        </c:forEach>
-      </select>
-    </p><p>
-    <input class="submit" type="submit" value="Update graph">
-    </p>
-  </div>
-</form>
-<p>Download graph as
-<a href="userstats-bridge-country.pdf${userstats_bridge_country_url}">PDF</a> or
-<a href="userstats-bridge-country.svg${userstats_bridge_country_url}">SVG</a>.</p>
-<hr>
-<a name="userstats-bridge-table"></a>
-<p><b>Top-10 countries by bridge users:</b></p>
-<form action="users.html#userstats-bridge-table">
-  <div class="formrow">
-    <input type="hidden" name="table" value="userstats-bridge">
-    <p>
-    <label>Start date (yyyy-mm-dd):</label>
-      <input type="text" name="start" size="10"
-             value="<c:choose><c:when test="${fn:length(userstats_bridge_start) == 0}">${default_start_date}</c:when><c:otherwise>${userstats_bridge_start[0]}</c:otherwise></c:choose>">
-    <label>End date (yyyy-mm-dd):</label>
-      <input type="text" name="end" size="10"
-             value="<c:choose><c:when test="${fn:length(userstats_bridge_end) == 0}">${default_end_date}</c:when><c:otherwise>${userstats_bridge_end[0]}</c:otherwise></c:choose>">
-    </p><p>
-    <input class="submit" type="submit" value="Update table">
-    </p>
-  </div>
-</form>
-<br>
-<table>
-  <tr>
-    <th>Country</th>
-    <th>Mean daily users</th>
-  </tr>
-  <c:forEach var="row" items="${userstats_bridge_tabledata}">
-    <tr>
-      <td><a href="users.html?graph=userstats-bridge-country&country=${row['cc']}#userstats-bridge-country">${row['country']}</a> </td>
-      <td>${row['abs']} (<fmt:formatNumber type="number" minFractionDigits="2" value="${row['rel']}" /> %)</td>
-    </tr>
-  </c:forEach>
-</table>
-<hr>
-
-<a name="userstats-bridge-transport"></a>
-<p><b>Bridge users by transport:</b></p>
-
-<img src="userstats-bridge-transport.png${userstats_bridge_transport_url}"
-     width="576" height="360" alt="Bridge users by transport graph">
-<form action="users.html#userstats-bridge-transport">
-  <div class="formrow">
-    <input type="hidden" name="graph" value="userstats-bridge-transport">
-    <p>
-    <label>Start date (yyyy-mm-dd):</label>
-      <input type="text" name="start" size="10"
-             value="<c:choose><c:when test="${fn:length(userstats_bridge_transport_start) == 0}">${default_start_date}</c:when><c:otherwise>${userstats_bridge_transport_start[0]}</c:otherwise></c:choose>">
-    <label>End date (yyyy-mm-dd):</label>
-      <input type="text" name="end" size="10"
-             value="<c:choose><c:when test="${fn:length(userstats_bridge_transport_end) == 0}">${default_end_date}</c:when><c:otherwise>${userstats_bridge_transport_end[0]}</c:otherwise></c:choose>">
-    </p><p>
-      Source: <select name="transport">
-        <option value="<OR>"<c:if test="${userstats_bridge_transport_transport[0] eq '<OR>'}"> selected</c:if>>Default OR protocol</option>
-        <option value="obfs2"<c:if test="${userstats_bridge_transport_transport[0] eq 'obfs2'}"> selected</c:if>>obfs2</option>
-        <option value="obfs3"<c:if test="${userstats_bridge_transport_transport[0] eq 'obfs3'}"> selected</c:if>>obfs3</option>
-        <option value="websocket"<c:if test="${userstats_bridge_transport_transport[0] eq 'websocket'}"> selected</c:if>>Flash proxy/websocket</option>
-        <option value="<??>"<c:if test="${userstats_bridge_transport_transport[0] eq '<??>'}"> selected</c:if>>Unknown transport</option>
-      </select>
-    </p><p>
-    <input class="submit" type="submit" value="Update graph">
-    </p>
-  </div>
-</form>
-<p>Download graph as
-<a href="userstats-bridge-transport.pdf${userstats_bridge_transport_url}">PDF</a> or
-<a href="userstats-bridge-transport.svg${userstats_bridge_transport_url}">SVG</a>.</p>
-<hr>
-
-<a name="userstats-bridge-version"></a>
-<p><b>Bridge users by IP version:</b></p>
-
-<img src="userstats-bridge-version.png${userstats_bridge_version_url}"
-     width="576" height="360" alt="Bridge users by IP version graph">
-<form action="users.html#userstats-bridge-version">
-  <div class="formrow">
-    <input type="hidden" name="graph" value="userstats-bridge-version">
-    <p>
-    <label>Start date (yyyy-mm-dd):</label>
-      <input type="text" name="start" size="10"
-             value="<c:choose><c:when test="${fn:length(userstats_bridge_version_start) == 0}">${default_start_date}</c:when><c:otherwise>${userstats_bridge_version_start[0]}</c:otherwise></c:choose>">
-    <label>End date (yyyy-mm-dd):</label>
-      <input type="text" name="end" size="10"
-             value="<c:choose><c:when test="${fn:length(userstats_bridge_version_end) == 0}">${default_end_date}</c:when><c:otherwise>${userstats_bridge_version_end[0]}</c:otherwise></c:choose>">
-    </p><p>
-      Source: <select name="version">
-        <option value="v4"<c:if test="${userstats_bridge_version_version[0] eq 'v4'}"> selected</c:if>>IPv4</option>
-        <option value="v6"<c:if test="${userstats_bridge_version_version[0] eq 'v6'}"> selected</c:if>>IPv6</option>
-      </select>
-    </p><p>
-    <input class="submit" type="submit" value="Update graph">
-    </p>
-  </div>
-</form>
-<p>Download graph as
-<a href="userstats-bridge-version.pdf${userstats_bridge_version_url}">PDF</a> or
-<a href="userstats-bridge-version.svg${userstats_bridge_version_url}">SVG</a>.</p>
-<hr>
-
-<p><a href="stats/clients.csv">CSV</a> file containing new user
-estimates.</p>
-<br>
-
-<hr>
-<a name="questions-and-answers"></a>
-<p><b>Questions and answers</b></p>
-<p>
-Q: How is it even possible to count users in an anonymity network?<br/>
-A: We actually don't count users, but we count requests to the directories
-that clients make periodically to update their list of relays and estimate
-user numbers indirectly from there.
-</p>
-<p>
-Q: Do all directories report these directory request numbers?<br/>
-A: No, but we can see what fraction of directories reported them, and then
-we can extrapolate the total number in the network.
-</p>
-
-<p>
-Q: How do you get from these directory requests to user numbers?<br/>
-A: We put in the assumption that the average client makes 10 such requests
-per day.  A tor client that is connected 24/7 makes about 15 requests per
-day, but not all clients are connected 24/7, so we picked the number 10
-for the average client.  We simply divide directory requests by 10 and
-consider the result as the number of users.
-</p>
-
-<p>
-Q: So, are these distinct users per day, average number of users connected
-over the day, or what?<br/>
-A: Average number of users connected over the day.  We can't say how many
-distinct users there are.
-</p>
-
-<p>
-Q: Are these tor clients or users?  What if there's more than one user
-behind a tor client?<br/>
-A: Then we count those users as one.  We really count clients, but it's
-more intuitive for most people to think of users, that's why we say users
-and not clients.
-</p>
-
-<p>
-Q: What if a user runs tor on a laptop and changes their IP address a few
-times per day?  Don't you overcount that user?<br/>
-A: No, because that user updates their list of relays as often as a user
-that doesn't change IP address over the day.
-</p>
-
-<p>
-Q: How do you know which countries users come from?<br/>
-A: The directories resolve IP addresses to country codes and report these
-numbers in aggregate form.  This is one of the reasons why tor ships with
-a GeoIP database.
-</p>
-
-<p>
-Q: Why are there so few bridge users that are not using the default OR
-protocol or that are using IPv6?<br/>
-A: Very few bridges report data on transports or IP versions yet, and by
-default we consider requests to use the default OR protocol and IPv4.
-Once more bridges report these data, the numbers will become more
-accurate.
-</p>
-
-<p>
-Q: Why do the graphs end 2 days in the past and not today?<br/>
-A: Relays and bridges report some of the data in 24-hour intervals which
-may end at any time of the day.  And after such an interval is over relays
-and bridges might take another 18 hours to report the data.  We cut off
-the last two days from the graphs, because we want to avoid that the last
-data point in a graph indicates a recent trend change which is in fact
-just an artifact of the algorithm.
-</p>
-
-<p>
-Q: But I noticed that the last data point went up/down a bit since I last
-looked a few hours ago.  Why is that?<br/>
-A: You're an excellent observer!  The reason is that we publish user
-numbers once we're confident enough that they won't change significantly
-anymore.  But it's always possible that a directory reports data a few
-hours after we were confident enough, but which then slightly changed the
-graph.
-</p>
-
-<p>
-Q: Why are no numbers available before September 2011?<br/>
-A: We do have descriptor archives from before that time, but those
-descriptors didn't contain all the data we use to estimate user numbers.
-We do have older user numbers from an earlier estimation approach
-<a href="/data/old-user-number-estimates.tar.gz">here</a>, but we believe
-the current approach is more accurate.
-</p>
-
-<p>
-Q: Why do you believe the current approach to estimate user numbers is
-more accurate?<br/>
-A: For direct users, we include all directories which we didn't do in the
-old approach.  We also use histories that only contain bytes written to
-answer directory requests, which is more precise than using general byte
-histories.
-</p>
-
-<p>
-Q: And what about the advantage of the current approach over the old one
-when it comes to bridge users?<br/>
-A: Oh, that's a whole different story.  We wrote a 13 page long
-<a href="https://research.torproject.org/techreports/counting-daily-bridge-users-2012-10-24.pdf">technical
-report</a> explaining the reasons for retiring the old approach.
-tl;dr: in the old approach we measured the wrong thing, and now we measure
-the right thing.
-</p>
-
-<p>
-Q: Are the data and the source code for estimating these user numbers
-available?<br/>
-A: Sure, <a href="/data.html">data</a> and
-<a href="https://gitweb.torproject.org/metrics-tasks.git/tree/HEAD:/task-8462">source
-code</a> are publicly available.
-</p>
-
-<p>
-Q: What are these red and blue dots indicating possible censorship
-events?<br/>
-A: We run an anomaly-based censorship-detection system that looks at
-estimated user numbers over a series of days and predicts the user number
-in the next days.  If the actual number is higher or lower, this might
-indicate a possible censorship event or release of censorship.  For more
-details, see our
-<a href="https://research.torproject.org/techreports/detector-2011-09-09.pdf">technical
-report</a>.
-</p>
-
-    </div>
-  </div>
-  <div class="bottom" id="bottom">
-    <%@ include file="footer.jsp"%>
-  </div>
-</body>
-</html>
diff --git a/web/css/stylesheet-ltr.css b/web/css/stylesheet-ltr.css
deleted file mode 100644
index ce0c54e..0000000
--- a/web/css/stylesheet-ltr.css
+++ /dev/null
@@ -1,161 +0,0 @@
-body {
-    background-color: white;
-    margin-top: 0px;
-    font-family: Arial, Helvetica, sans-serif;
-    font-size: 1em;
-    font-style: normal;
-    color: #000000;
-    padding-top: 0px;
-}
-
-/* images */
-
-img {
-    border: 0;
-}
-
-
-li {
-   margin: .2em .2em .2em 1em;
-}
-
-/* this centers the page */
-
-.center {
-    text-align: center;
-    background-color: white;
-    margin: 0px auto 0 auto;
-    width: 85%;
-}
-
-.center table {
-    margin-left: auto;
-    margin-right: auto;
-    text-align: left;
-}
-
-div.bottom {
-    font-size: 0.8em;
-    margin-top: 0.5cm;
-    margin-left: 1em;
-    margin-right: 1em;
-    text-align: center;
-}
-
-/* The main column (left text) */
-
-div.main-column {
-    padding: 15px 0 10px 10px;
-    text-indent: 0pt;
-    font-size: 1em;
-    direction: ltr;
-    text-align: left;
-}
-
-/* formatting styles */
-
-h2 {
-    font-size: 1.4em;
-    margin-bottom: 0em;
-    font-weight: bold;
-    margin-top: 0;
-}
-
-h3 {
-    font-size: 1.2em;
-    margin-bottom: 0em;
-    font-weight: bold;
-    margin-top: 0;
-}
-
-p {
-    margin-top: 0;
-    margin-bottom: 1em;
-}
-
-a:link {
-    color: blue;
-    font-size: 1em;
-}
-
-a:visited {
-    color: purple;
-    font-size: 1em;
-}
-
-a.anchor {
-    font-size: 1em;
-    color: black;
-    font-weight: bold;
-    text-decoration: none;
-}
-
-td {
-    vertical-align: top;
-}
-
-/* the banner */
-
-table.banner {
-    width: 100%;
-    height: 79px;
-    margin-left: auto;
-    margin-right: auto;
-}
-
-td.banner-left {
-	/* This is done with an <img> in the HTML so it can be clickable
-    background-image: url("images/top-left.png");
-    background-repeat: no-repeat; */
-    width: 193px;
-}
-
-td.banner-middle {
-    background-color: #00802B;
-    background-image: url("/images/top-middle.png");
-    background-repeat: repeat-x;
-    vertical-align: bottom;
-    padding-bottom: 10px;
-    color: white;
-    font-weight: bold;
-    font-size: 1.2em;
-}
-
-td.banner-middle a, td.banner-middle a:visited {
-    margin-right: 5px;
-    color: white;
-    font-weight: bold;
-    font-size: 1em;
-}
-
-td.banner-middle a:hover {
-    color: #FF7F00;
-    font-weight: bold;
-    font-size: 1em;
-}
-
-td.banner-right {
-    background-image: url("/images/top-right.png");
-    background-repeat: no-repeat;
-    width: 15px;
-    background-position: right;
-    padding-top: 8px;
-}
-
-.banner-middle a.current {
-    text-decoration: none;
-    color: #FF7F00;
-    font-weight: bold;
-    font-size: 1em;
-    width: auto;
-    left: -50px;
-}
-
-hr {
-	background-color:#002200;
-	color:#666666;
-	font-size:1px;
-	height:1px;
-	line-height:0;
-	margin:15px 0 5px;
-}
diff --git a/web/images/favicon.ico b/web/images/favicon.ico
deleted file mode 100644
index 48060b1..0000000
Binary files a/web/images/favicon.ico and /dev/null differ
diff --git a/web/images/top-left.png b/web/images/top-left.png
deleted file mode 100644
index 9927355..0000000
Binary files a/web/images/top-left.png and /dev/null differ
diff --git a/web/images/top-middle.png b/web/images/top-middle.png
deleted file mode 100644
index 1035dc5..0000000
Binary files a/web/images/top-middle.png and /dev/null differ
diff --git a/web/images/top-right.png b/web/images/top-right.png
deleted file mode 100644
index 54623ef..0000000
Binary files a/web/images/top-right.png and /dev/null differ
diff --git a/web/images/tor-metrics-overview.png b/web/images/tor-metrics-overview.png
deleted file mode 100644
index ee2bc07..0000000
Binary files a/web/images/tor-metrics-overview.png and /dev/null differ
diff --git a/web/js/bubbles.js b/web/js/bubbles.js
deleted file mode 100644
index 8030e20..0000000
--- a/web/js/bubbles.js
+++ /dev/null
@@ -1,288 +0,0 @@
-function get_prefix(relay) { return /^[0-9]+\.[0-9]+\./.exec(relay.or_addresses[0]); }
-
-var graphs = {
-     'default': {
-         extra_fields: [],
-         group: undefined,
-         group_id_func: function(relay) { return undefined; },
-         group_name_func: function(relay) { return 'Relays'; },
-  }, 'as': {
-         extra_fields: ['as_number', 'as_name'],
-         group: "autonomous systems",
-         group_id_func: function(relay) { return relay.as_number; },
-         group_name_func: function(relay) { return relay.as_name; },
-  }, 'contact': {
-         extra_fields: ['contact'],
-         group: "contact infos",
-         group_id_func: function(relay) { return relay.contact; },
-         group_name_func: function(relay) { return relay.contact; },
-  }, 'country': {
-         extra_fields: ['country', 'country_name'],
-         group: "countries",
-         group_id_func: function(relay) { return relay.country; },
-         group_name_func: function(relay) { return relay.country_name; },
-  }, 'network-family': {
-         extra_fields: ['or_addresses'],
-         group: "network families (/16)",
-         group_id_func: function(relay) { return get_prefix(relay); },
-         group_name_func: function(relay) { return get_prefix(relay) + "0.0/16"; },
-  },
-};
-
-function make_bubble_graph(graph_name) {
-  var onionoo_url = "https://onionoo.torproject.org/details?type=relay&running=true&fields=consensus_weight,running,nickname,exit_probability,advertised_bandwidth";
-  var diameter = 800;
-  var legendWidth = 270;
-  var legendIconSize = 50;
-  var legendItems = 3;
-  var legendIconMargin = 15;
-  var legendHeight = legendItems * (legendIconMargin * 2 + legendIconSize) - legendIconMargin;
-
-  var cutOff = 100 / 8.0 * 1000.0 * 1000.0; // 100 Mbit/s
-
-  var format = d3.format(",d");
-  var color = d3.scale.category20c();
-
-  var old_graph = document.getElementById("bubble-graph");
-  if (old_graph) {
-     old_graph.parentNode.removeChild(old_graph);
-  }
-
-  var bubble = d3.layout.pack()
-    .sort(null)
-    .size([diameter, diameter])
-    .padding(1.5);
-
-  var svg = d3.select("body").append("svg")
-    .attr("id", "bubble-graph")
-    .attr("width", diameter + legendWidth)
-    .attr("height", diameter)
-    .attr("class", "bubble");
-
-  var defs = svg.append("defs")
-
-  defs.append("filter")
-      .attr("id", "middle-filter")
-      .append("feColorMatrix")
-        .attr("type", "hueRotate")
-        .attr("in", "SourceGraphic")
-        .attr("values", "90");
-
-  var node_circle = defs.append("svg").attr("id", "node-circle").attr("viewBox", "0 0 120.50655 184.78298");
-
-  node_circle.append("path")
-      .attr("style", "fill:#7d4698;fill-opacity:1;stroke:none")
-      .attr("d", "m 119.19492,135.63045 a 56.525425,56.525425 0 1 1 -113.0508541,0 56.525425,56.525425 0 1 1 113.0508541,0 z")
-      .attr("transform", "matrix(1.048913,0,0,0.95108692,-5.4815686,2.0260454)");
-
-  var node_onion = defs.append("svg").attr("id", "node-onion").attr("viewBox", "0 0 120.50655 184.78298")
-      .append("g")
-        .attr("transform", "translate(-195.35846,-64.183782)");
-
-  node_onion.append("path")
-        .attr("style", "fill:#abcd03;fill-rule:evenodd;stroke:none")
-        .attr("d", "m 264.513,77.977773 -4.917,19.529001 c 6.965,-13.793001 18.027,-24.172001 30.729,-33.323001 -9.287,10.789 -17.754,21.579001 -22.944,32.368001 8.741,-12.292001 20.486,-19.120001 33.733,-23.627001 -17.618,15.706001 -31.60228,32.559277 -42.25528,49.494277 l -8.467,-3.687 c 1.501,-13.521 6.60928,-27.369276 14.12128,-40.754277 z");
-  node_onion.append("path")
-        .attr("style", "fill:#fffcdb;fill-rule:evenodd;stroke:none")
-        .attr("d", "m 241.90113,115.14152 16.116,6.68594 c 0,4.098 -0.33313,16.59703 2.22938,20.28403 26.80289,34.5191 22.29349,103.71329 -5.42951,105.48829 -42.21656,0 -58.317,-28.679 -58.317,-55.03801 0,-24.037 28.816,-40.016 46.025,-54.219 4.37,-3.824 3.61113,-12.27525 -0.62387,-23.20125 z")
-  node_onion.append("path")
-        .attr("style", "fill:#7d4698;fill-rule:evenodd;stroke:none")
-        .attr("d", "m 258.02197,121.58695 5.80803,2.96282 c -0.546,3.823 0.273,12.292 4.096,14.476 16.936,10.516 32.914,21.988 39.197,33.46 22.398,40.42601 -15.706,77.84601 -48.62,74.29501 17.891,-13.248 23.081,-40.42501 16.389,-70.06201 -2.731,-11.609 -6.966,-22.125 -14.478,-34.007 -3.25421,-5.83246 -2.11803,-13.06582 -2.39203,-21.12482 z");
-  node_onion.append("path")
-        .attr("style", "fill:#000000;fill-opacity:1;stroke:none")
-        .attr("d", "m 255.226,120.58877 12.018,1.639 c -3.551,11.745 6.966,19.939 10.38,21.852 7.64801,4.234 15.02301,8.604 20.89601,13.93 11.063,10.106 17.345,24.31 17.345,39.333 0,14.886 -6.829,29.226 -18.301,38.786 -10.789,9.014 -25.67501,12.838 -40.15201,12.838 -9.014,0 -17.072,-0.409 -25.812,-3.278 -19.939,-6.692 -34.826,-23.763 -36.055,-44.25 -1.093,-15.979 2.458,-28.134 14.887,-40.835 6.418,-6.692 19.393,-14.34 28.271,-20.486 4.371,-3.005 9.014,-11.473 0.136,-27.451 l 1.776,-1.366 13.15659,8.81203 -11.10759,-4.57803 c 0.956,1.366 3.551,7.512 4.098,9.287 1.229,5.053 0.683,9.971 -0.41,12.155 -5.599,10.107 -15.159,12.838 -22.124,18.574 -12.292,10.106 -25.676,18.164 -24.174,45.888 0.683,13.657 11.336,30.319 27.314,38.104 9.014,4.371 19.394,6.146 29.91,6.692 9.423,0.41 27.45101,-5.19 37.28401,-13.384 10.516,-8.74 16.389,-21.988 16.389,-35.508 0,-13.658 -5.463,-26.632 -15.706,-35.783 -5.873,-5.326 -15.56901,-11.745 -21.57801,-15.16 -6.009,-3.414 -13.521,-12.974 -11.063,-22.124 z");
-  node_onion.append("path")
-        .attr("style", "fill:#000000;fill-opacity:1;stroke:none")
-        .attr("d", "m 251.539,140.80177 c -1.229,6.283 -2.595,17.618 -8.058,21.852 -2.322,1.638 -4.644,3.278 -7.102,4.916 -9.833,6.693 -19.667,12.974 -24.173,29.09 -0.956,3.415 -0.136,7.102 0.684,10.516 2.458,9.833 9.423,20.486 14.886,26.769 0,0.273 1.093,0.956 1.093,1.229 4.507,5.327 5.873,6.829 22.944,10.652 l -0.41,1.913 c -10.243,-2.731 -18.71,-5.189 -24.037,-11.336 0,-0.136 -0.956,-1.093 -0.956,-1.093 -5.736,-6.556 -12.702,-17.481 -15.296,-27.724 -0.956,-4.098 -1.775,-7.238 -0.683,-11.473 4.643,-16.661 14.75,-23.217 24.993,-30.182 2.322,-1.502 5.053,-2.869 7.238,-4.644 4.233,-3.14 6.554,-12.701 8.877,-20.485 z");
-  node_onion.append("path")
-        .attr("style", "fill:#000000;fill-opacity:1;stroke:none")
-        .attr("d", "m 255.90625,166.74951 c 0.137,7.102 -0.55625,10.66475 1.21875,15.71875 1.092,3.004 4.782,7.1015 5.875,11.0625 1.502,5.327 3.138,11.19901 3,14.75001 0,4.09799 -0.25625,11.74249 -2.03125,19.93749 -1.35362,6.77108 -4.47323,12.58153 -9.71875,15.875 -5.37327,-1.10644 -11.68224,-2.99521 -15.40625,-6.1875 -7.238,-6.282 -13.64875,-16.7865 -14.46875,-25.9375 -0.682,-7.51099 6.27275,-18.5885 15.96875,-24.1875 8.194,-4.78 10.1,-10.22775 11.875,-18.96875 -2.458,7.648 -4.7665,14.05925 -12.6875,18.15625 -11.472,6.009 -17.3585,16.09626 -16.8125,25.65625 0.819,12.291 5.7415,20.6195 15.4375,27.3125 4.097,2.868 11.75125,5.89875 16.53125,6.71875 l 0,-0.625 c 3.62493,-0.67888 8.31818,-6.63267 10.65625,-14.6875 2.049,-7.238 2.85675,-16.502 2.71875,-22.37499 -0.137,-3.414 -1.643,-10.80801 -4.375,-17.50001 -1.502,-3.687 -3.8095,-7.37375 -5.3125,-9.96875 -1.637,-2.597 -1.64875,-8.195 -2.46875,-14.75 z");
-  node_onion.append("path")
-        .attr("style", "fill:#000000;fill-opacity:1;stroke:none")
-        .attr("d", "m 255.09375,193.53076 c 0.136,4.78 2.056,10.90451 2.875,17.18751 0.684,4.64399 0.387,9.30824 0.25,13.40624 -0.13495,4.74323 -1.7152,13.24218 -3.875,17.375 -2.03673,-0.93403 -2.83294,-1.99922 -4.15625,-3.71875 -1.638,-2.322 -2.75075,-4.644 -3.84375,-7.375 -0.819,-2.049 -1.7765,-4.394 -2.1875,-7.125 -0.546,-4.097 -0.393,-10.5065 4.25,-17.06249 3.551,-5.19001 4.36475,-5.58476 5.59375,-11.59376 -1.64,5.326 -2.8625,5.869 -6.6875,10.37501 -4.233,4.917 -4.9375,12.15924 -4.9375,18.03124 0,2.459 0.9805,5.18725 1.9375,7.78125 1.092,2.732 2.02925,5.452 3.53125,7.5 2.25796,3.32082 5.14798,5.20922 6.5625,5.5625 0.009,0.002 0.022,-0.002 0.0312,0 0.0303,0.007 0.0649,0.0255 0.0937,0.0312 l 0,-0.15625 c 2.64982,-2.95437 4.24444,-5.88934 4.78125,-8.84375 0.683,-3.551 0.84,-7.10975 1.25,-11.34375 0.409,-3.551 0.11225,-8.334 -0.84375,-13.24999 -1.365,-6.146 -3.669,-12.41226 -4.625,-16.78126 z");
-  node_onion.append("path")
-        .attr("style", "fill:#000000;fill-opacity:1;stroke:none")
-        .attr("d", "m 255.499,135.06577 c 0.137,7.101 0.683,20.35 2.595,25.539 0.546,1.775 5.599,9.56 9.149,18.983 2.459,6.556 3.005,12.565 3.415,14.34 1.639,7.785 -0.41,20.896 -3.142,33.324 -1.365,6.692 -6.009,15.023 -11.335,18.301 l -1.092,1.912 c 3.005,-0.137 10.379,-7.375 12.974,-16.389 4.371,-15.296 6.146,-22.398 4.098,-39.333 -0.273,-1.64 -0.956,-7.238 -3.551,-13.248 -3.824,-9.151 -9.287,-17.891 -9.969,-19.667 -1.23,-2.867 -2.869,-15.295 -3.142,-23.762 z");
-  node_onion.append("path")
-        .attr("style", "fill:#000000;fill-opacity:1;stroke:none")
-        .attr("d", "m 258.06151,125.35303 c -0.40515,7.29812 -0.51351,9.98574 0.85149,15.31174 1.502,5.873 9.151,14.34 12.292,24.037 6.009,18.574 4.507,42.884 0.136,61.867 -1.638,6.691 -9.424,16.389 -17.208,19.529 l 5.736,1.366 c 3.141,-0.137 11.198,-7.648 14.34,-16.252 5.052,-13.521 6.009,-29.636 3.96,-46.571 -0.137,-1.639 -2.869,-16.252 -5.463,-22.398 -3.688,-9.15 -10.244,-17.345 -10.926,-19.119 -1.228,-3.005 -3.92651,-9.24362 -3.71849,-17.77074 z");
-
-  if (!graph_name) {
-    graph_name = window.location.hash.substring(1);
-  }
-  var exits_only = false;
-  if (/-exits-only$/.exec(graph_name)) {
-    exits_only = true;
-    graph_name = graph_name.replace("-exits-only", "");
-  }
-
-  var graph = graphs[graph_name];
-  if (!graph) {
-    graph = graphs['default'];
-  }
-
-  onionoo_url += ',' + graph.extra_fields.join(',')
-
-  d3.json(onionoo_url, function(error, data) {
-    var groups = {};
-    var relay_count = 0;
-    data.relays.forEach(function(relay) {
-      if (0 == relay.consensus_weight || !relay.running) {
-        return;
-      }
-      if (exits_only && relay.exit_probability == 0) {
-        return;
-      }
-      group_id = graph.group_id_func(relay);
-      group_name = graph.group_name_func(relay);
-      if (!group_id) {
-        group_id = 'unknown';
-        group_name = 'Unknown';
-      }
-      if (!groups.hasOwnProperty(group_id)) {
-        groups[group_id] = { name: group_name, children: [] };
-      }
-      groups[group_id].children.push(
-          { name: relay.nickname ? relay.fingerprint : relay.nickname,
-            value: relay.consensus_weight,
-            exit: relay.exit_probability > 0,
-            bandwidth: relay.advertised_bandwidth,
-          });
-      relay_count++;
-    });
-
-    var bubbles = svg.selectAll(".node")
-        .data(bubble.nodes({ children: d3.values(groups) }));
-    var node = bubbles.enter().append("g")
-        .attr("class", "node")
-        .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
-
-    if (graph.group) {
-      node.append("circle")
-          .filter(function(d) { return d.children && d.name; })
-          .attr("r", function(d) { return d.r; })
-          .style("fill", "#888888")
-          .style("fill-opacity", ".25");
-    }
-
-    var relays = node.filter(function(d) { return !d.children && d.r > 1;})
-      .append("use")
-        .attr("xlink:href", function(d) { return "#node-" + (d.bandwidth > cutOff ? "onion" : "circle"); })
-        .attr("transform", function(d) { return "translate(" + -d.r + "," + -d.r + ")"; })
-        .attr("width", function(d) { return d.r * 2; })
-        .attr("height", function(d) { return d.r * 2; })
-        .attr("preserveAspectRatio", "xMidYMin")
-        .attr("filter", function(d) { return d.exit ? "" : "url(#middle-filter)"; });
-
-    if (!graph.group) {
-      relays
-        .on("mouseover", function(d) {
-             svg.append("text")
-                 .attr("transform", "translate(" + diameter + "," + (diameter - legendHeight - 50) + ")")
-                 .attr("id", "relay-bw")
-                 .style("text-anchor", "start")
-                 .style("font-size", "14pt")
-                 .text(d.className.substring(0, 20) + ": " + (d.bandwidth * 8.0 / 1000.0 / 1000.0).toFixed(2) + " Mbit/s");
-           })
-        .on("mouseout", function() {
-             d3.select("#relay-bw").remove();
-           })
-    };
-
-    if (graph.group) {
-      node.filter(function(d) { return d.children && d.name; })
-        .each(function(d) {
-          var g = svg.append("g")
-            .attr("transform", "translate(" + d.x + "," + d.y + ")");
-          g.append("circle")
-            .attr("r", d.r)
-            .style("fill", "#000000")
-            .style("fill-opacity", "0")
-            .style("stroke", "none");
-          g.on("mouseover", function() {
-                svg.append("text")
-                    .attr("transform", "translate(" + d.x + "," + (d.y - d.r) + ")")
-                    .attr("id", "group-name")
-                    .style("text-anchor", "middle")
-                    .style("font-size", "14pt")
-                    .text((d.name + "").substring(0, 50));
-              })
-           .on("mouseout", function() {
-                d3.select("#group-name").remove();
-              });
-        });
-    }
-
-    var titleText = relay_count + " " + (exits_only ? "exits" : "relays") + " (" +
-        (node.filter(function(d) { return !d.children && d.r > 1; }).size())  + " visible)";
-    if (graph.group) {
-      titleText = Object.keys(groups).length + " " + graph.group + " with " + titleText;
-    }
-    var title = svg.append("g")
-        .attr("transform", "translate(10, " + (diameter - 30) + ")");
-    title.append("text")
-        .text(titleText)
-        .attr("text-anchor", "start")
-        .attr("style", "font-size: 18pt");
-    title.append("text")
-        .text(data['relays_published'])
-        .attr("text-anchor", "start")
-        .attr("dy", "15")
-        .attr("style", "font-size: 10pt");
-
-    var legend = svg.append("g")
-        .attr("transform", "translate(" + (diameter - 10) +", " + (diameter - legendHeight - 10) + ")")
-    legend.append("rect")
-        .attr("width", legendWidth)
-        .attr("height", legendHeight)
-        .attr("fill", "#cccccc")
-        .attr("stroke", "#000000");
-    var legendOnion = legend.append("g")
-        .attr("transform", "translate(0, " + legendIconMargin + ")");
-    legendOnion.append("use")
-      .attr("xlink:href", "#node-onion")
-        .attr("width", legendIconSize)
-        .attr("height", legendIconSize)
-        .attr("preserveAspectRatio", "xMidYMin");
-    legendOnion.append("text")
-        .text((cutOff * 8 / 1000 / 1000) + "+ Mbit/s relays")
-        .attr("text-anchor", "start")
-        .attr("dx", legendIconSize)
-        .attr("dy", legendIconSize / 2)
-    var legendCircle = legend.append("g")
-        .attr("transform", "translate(0, " + (legendIconSize + legendIconMargin * 2) + ")");
-    legendCircle.append("use")
-      .attr("xlink:href", "#node-circle")
-        .attr("width", legendIconSize)
-        .attr("height", legendIconSize)
-        .attr("preserveAspectRatio", "xMidYMin");
-    legendCircle.append("text")
-        .text("smaller relays")
-        .attr("text-anchor", "start")
-        .attr("dx", legendIconSize)
-        .attr("dy", legendIconSize / 2)
-    var legendExit = legend.append("g")
-        .attr("transform", "translate(0, " + ((legendIconSize + legendIconMargin * 2) * 2) + ")");
-    legendExit.append("use")
-      .attr("xlink:href", "#node-onion")
-        .attr("width", legendIconSize / 2)
-        .attr("height", legendIconSize / 2)
-        .attr("preserveAspectRatio", "xMidYMin")
-        .attr("filter", "url(#middle-filter)");
-    legendExit.append("use")
-      .attr("xlink:href", "#node-circle")
-        .attr("width", legendIconSize / 2)
-        .attr("height", legendIconSize / 2)
-        .attr("preserveAspectRatio", "xMidYMin")
-        .attr("transform", "translate(" + (legendIconSize / 2) + ", " + (legendIconSize / 2) + ")")
-        .attr("filter", "url(#middle-filter)");
-    legendExit.append("text")
-        .text("non-exits")
-        .attr("text-anchor", "start")
-        .attr("dx", legendIconSize)
-        .attr("dy", legendIconSize / 2);
-
-  });
-
-  d3.select(self.frameElement).style("height", diameter + "px");
-}
diff --git a/web/js/d3.min.js b/web/js/d3.min.js
deleted file mode 100644
index 5c30406..0000000
--- a/web/js/d3.min.js
+++ /dev/null
@@ -1,5 +0,0 @@
-d3=function(){function n(n){return null!=n&&!isNaN(n)}function t(n){return n.length}function e(n){for(var t=1;n*t%1;)t*=10;return t}function r(n,t){try{for(var e in t)Object.defineProperty(n.prototype,e,{value:t[e],enumerable:!1})}catch(r){n.prototype=t}}function i(){}function u(){}function a(n,t,e){return function(){var r=e.apply(t,arguments);return r===t?n:r}}function o(){}function c(n){function t(){for(var t,r=e,i=-1,u=r.length;++i<u;)(t=r[i].on)&&t.apply(this,arguments);return n}var e=[],r=new i;return t.on=function(t,i){var u,a=r.get(t);return arguments.length<2?a&&a.on:(a&&(a.on=null,e=e.slice(0,u=e.indexOf(a)).concat(e.slice(u+1)),r.remove(t)),i&&e.push(r.set(t,{on:i})),n)},t}function l(){va.event.stopPropagation(),va.event.preventDefault()}function f(){for(var n,t=va.event;n=t.sourceEvent;)t=n;return t}function s(n,t){function e(){n.on(t,null)}n.on(t,function(){l(),e()},!0),setTimeout(e,0)}function h(n){for(var t=new o,e=0,r=arguments.length;++e<r;)t[arguments[e]]=c(t);retur
 n t.of=function(e,r){return function(i){try{var u=i.sourceEvent=va.event;i.target=n,va.event=i,t[i.type].apply(e,r)}finally{va.event=u}}},t}function g(n,t){var e=n.ownerSVGElement||n;if(e.createSVGPoint){var r=e.createSVGPoint();if(0>Aa&&(xa.scrollX||xa.scrollY)){e=va.select("body").append("svg").style({position:"absolute",top:0,left:0,margin:0,padding:0,border:"none"},"important");var i=e[0][0].getScreenCTM();Aa=!(i.f||i.e),e.remove()}return Aa?(r.x=t.pageX,r.y=t.pageY):(r.x=t.clientX,r.y=t.clientY),r=r.matrixTransform(n.getScreenCTM().inverse()),[r.x,r.y]}var u=n.getBoundingClientRect();return[t.clientX-u.left-n.clientLeft,t.clientY-u.top-n.clientTop]}function p(n){for(var t=-1,e=n.length,r=[];++t<e;)r.push(n[t]);return r}function d(n){return Array.prototype.slice.call(n)}function m(n,t){if(t in n)return t;t=t.charAt(0).toUpperCase()+t.substring(1);for(var e=0,r=Ca.length;r>e;++e){var i=Ca[e]+t;if(i in n)return i}}function v(n){return Ta(n,Pa),n}function y(n){return function(){ret
 urn ja(n,this)}}function M(n){return function(){return La(n,this)}}function x(n,t){function e(){this.removeAttribute(n)}function r(){this.removeAttributeNS(n.space,n.local)}function i(){this.setAttribute(n,t)}function u(){this.setAttributeNS(n.space,n.local,t)}function a(){var e=t.apply(this,arguments);null==e?this.removeAttribute(n):this.setAttribute(n,e)}function o(){var e=t.apply(this,arguments);null==e?this.removeAttributeNS(n.space,n.local):this.setAttributeNS(n.space,n.local,e)}return n=va.ns.qualify(n),null==t?n.local?r:e:"function"==typeof t?n.local?o:a:n.local?u:i}function b(n){return n.trim().replace(/\s+/g," ")}function _(n){return RegExp("(?:^|\\s+)"+va.requote(n)+"(?:\\s+|$)","g")}function w(n,t){function e(){for(var e=-1;++e<i;)n[e](this,t)}function r(){for(var e=-1,r=t.apply(this,arguments);++e<i;)n[e](this,r)}n=n.trim().split(/\s+/).map(S);var i=n.length;return"function"==typeof t?r:e}function S(n){var t=_(n);return function(e,r){if(i=e.classList)return r?i.add(n):i.
 remove(n);var i=e.getAttribute("class")||"";r?(t.lastIndex=0,t.test(i)||e.setAttribute("class",b(i+" "+n))):e.setAttribute("class",b(i.replace(t," ")))}}function E(n,t,e){function r(){this.style.removeProperty(n)}function i(){this.style.setProperty(n,t,e)}function u(){var r=t.apply(this,arguments);null==r?this.style.removeProperty(n):this.style.setProperty(n,r,e)}return null==t?r:"function"==typeof t?u:i}function k(n,t){function e(){delete this[n]}function r(){this[n]=t}function i(){var e=t.apply(this,arguments);null==e?delete this[n]:this[n]=e}return null==t?e:"function"==typeof t?i:r}function A(n){return{__data__:n}}function N(n){return function(){return Fa(this,n)}}function q(n){return arguments.length||(n=va.ascending),function(t,e){return!t-!e||n(t.__data__,e.__data__)}}function T(){}function C(n,t,e){function r(){var t=this[a];t&&(this.removeEventListener(n,t,t.$),delete this[a])}function i(){var i=c(t,Na(arguments));r.call(this),this.addEventListener(n,this[a]=i,i.$=e),i._=t}
 function u(){var t,e=RegExp("^__on([^.]+)"+va.requote(n)+"$");for(var r in this)if(t=r.match(e)){var i=this[r];this.removeEventListener(t[1],i,i.$),delete this[r]}}var a="__on"+n,o=n.indexOf("."),c=z;o>0&&(n=n.substring(0,o));var l=Ya.get(n);return l&&(n=l,c=D),o?t?i:r:t?T:u}function z(n,t){return function(e){var r=va.event;va.event=e,t[0]=this.__data__;try{n.apply(this,t)}finally{va.event=r}}}function D(n,t){var e=z(n,t);return function(n){var t=this,r=n.relatedTarget;r&&(r===t||r.compareDocumentPosition(t)&8)||e.call(t,n)}}function j(n,t){for(var e=0,r=n.length;r>e;e++)for(var i,u=n[e],a=0,o=u.length;o>a;a++)(i=u[a])&&t(i,a,e);return n}function L(n){return Ta(n,Ua),n}function H(){}function F(n,t,e){return new P(n,t,e)}function P(n,t,e){this.h=n,this.s=t,this.l=e}function O(n,t,e){function r(n){return n>360?n-=360:0>n&&(n+=360),60>n?u+(a-u)*n/60:180>n?a:240>n?u+(a-u)*(240-n)/60:u}function i(n){return Math.round(r(n)*255)}var u,a;return n=isNaN(n)?0:(n%=360)<0?n+360:n,t=isNaN(t)?0:0
 >t?0:t>1?1:t,e=0>e?0:e>1?1:e,a=.5>=e?e*(1+t):e+t-e*t,u=2*e-a,et(i(n+120),i(n),i(n-120))}function R(n){return n>0?1:0>n?-1:0}function Y(n){return n>1?0:-1>n?$a:Math.acos(n)}function U(n){return n>1?$a/2:-1>n?-$a/2:Math.asin(n)}function I(n){return(Math.exp(n)-Math.exp(-n))/2}function V(n){return(Math.exp(n)+Math.exp(-n))/2}function X(n){return(n=Math.sin(n/2))*n}function Z(n,t,e){return new B(n,t,e)}function B(n,t,e){this.h=n,this.c=t,this.l=e}function $(n,t,e){return isNaN(n)&&(n=0),isNaN(t)&&(t=0),W(e,Math.cos(n*=Ga)*t,Math.sin(n)*t)}function W(n,t,e){return new J(n,t,e)}function J(n,t,e){this.l=n,this.a=t,this.b=e}function G(n,t,e){var r=(n+16)/116,i=r+t/500,u=r-e/200;return i=Q(i)*to,r=Q(r)*eo,u=Q(u)*ro,et(tt(3.2404542*i-1.5371385*r-.4985314*u),tt(-.969266*i+1.8760108*r+.041556*u),tt(.0556434*i-.2040259*r+1.0572252*u))}function K(n,t,e){return n>0?Z(Math.atan2(e,t)*Ka,Math.sqrt(t*t+e*e),n):Z(0/0,0/0,n)}function Q(n){return n>.206893034?n*n*n:(n-4/29)/7.787037}function nt(n){retur
 n n>.008856?Math.pow(n,1/3):7.787037*n+4/29}function tt(n){return Math.round(255*(.00304>=n?12.92*n:1.055*Math.pow(n,1/2.4)-.055))}function et(n,t,e){return new rt(n,t,e)}function rt(n,t,e){this.r=n,this.g=t,this.b=e}function it(n){return 16>n?"0"+Math.max(0,n).toString(16):Math.min(255,n).toString(16)}function ut(n,t,e){var r,i,u,a=0,o=0,c=0;if(r=/([a-z]+)\((.*)\)/i.exec(n))switch(i=r[2].split(","),r[1]){case"hsl":return e(parseFloat(i[0]),parseFloat(i[1])/100,parseFloat(i[2])/100);case"rgb":return t(lt(i[0]),lt(i[1]),lt(i[2]))}return(u=ao.get(n))?t(u.r,u.g,u.b):(null!=n&&n.charAt(0)==="#"&&(n.length===4?(a=n.charAt(1),a+=a,o=n.charAt(2),o+=o,c=n.charAt(3),c+=c):n.length===7&&(a=n.substring(1,3),o=n.substring(3,5),c=n.substring(5,7)),a=parseInt(a,16),o=parseInt(o,16),c=parseInt(c,16)),t(a,o,c))}function at(n,t,e){var r,i,u=Math.min(n/=255,t/=255,e/=255),a=Math.max(n,t,e),o=a-u,c=(a+u)/2;return o?(i=.5>c?o/(a+u):o/(2-a-u),r=n==a?(t-e)/o+(e>t?6:0):t==a?(e-n)/o+2:(n-t)/o+4,r*=60):(r=0
 /0,i=c>0&&1>c?0:r),F(r,i,c)}function ot(n,t,e){n=ct(n),t=ct(t),e=ct(e);var r=nt((.4124564*n+.3575761*t+.1804375*e)/to),i=nt((.2126729*n+.7151522*t+.072175*e)/eo),u=nt((.0193339*n+.119192*t+.9503041*e)/ro);return W(116*i-16,500*(r-i),200*(i-u))}function ct(n){return(n/=255)<=.04045?n/12.92:Math.pow((n+.055)/1.055,2.4)}function lt(n){var t=parseFloat(n);return n.charAt(n.length-1)==="%"?Math.round(2.55*t):t}function ft(n){return"function"==typeof n?n:function(){return n}}function st(n){return n}function ht(n){return function(t,e,r){return arguments.length===2&&"function"==typeof e&&(r=e,e=null),gt(t,e,n,r)}}function gt(n,t,e,r){function i(){var n,t=c.status;if(!t&&c.responseText||t>=200&&300>t||304===t){try{n=e.call(u,c)}catch(r){return a.error.call(u,r),void 0}a.load.call(u,n)}else a.error.call(u,c)}var u={},a=va.dispatch("progress","load","error"),o={},c=new XMLHttpRequest,l=null;return!xa.XDomainRequest||"withCredentials"in c||!/^(http(s)?:)?\/\//.test(n)||(c=new XDomainRequest),"o
 nload"in c?c.onload=c.onerror=i:c.onreadystatechange=function(){c.readyState>3&&i()},c.onprogress=function(n){var t=va.event;va.event=n;try{a.progress.call(u,c)}finally{va.event=t}},u.header=function(n,t){return n=(n+"").toLowerCase(),arguments.length<2?o[n]:(null==t?delete o[n]:o[n]=t+"",u)},u.mimeType=function(n){return arguments.length?(t=null==n?null:n+"",u):t},u.responseType=function(n){return arguments.length?(l=n,u):l},u.response=function(n){return e=n,u},["get","post"].forEach(function(n){u[n]=function(){return u.send.apply(u,[n].concat(Na(arguments)))}}),u.send=function(e,r,i){if(arguments.length===2&&"function"==typeof r&&(i=r,r=null),c.open(e,n,!0),null==t||"accept"in o||(o.accept=t+",*/*"),c.setRequestHeader)for(var a in o)c.setRequestHeader(a,o[a]);return null!=t&&c.overrideMimeType&&c.overrideMimeType(t),null!=l&&(c.responseType=l),null!=i&&u.on("error",i).on("load",function(n){i(null,n)}),c.send(null==r?null:r),u},u.abort=function(){return c.abort(),u},va.rebind(u,a,"
 on"),null==r?u:u.get(pt(r))}function pt(n){return n.length===1?function(t,e){n(null==t?e:null)}:n}function dt(){var n=mt(),t=vt()-n;t>24?(isFinite(t)&&(clearTimeout(fo),fo=setTimeout(dt,t)),lo=0):(lo=1,so(dt))}function mt(){for(var n=Date.now(),t=oo;t;)n>=t.time&&(t.flush=t.callback(n-t.time)),t=t.next;return n}function vt(){for(var n,t=oo,e=1/0;t;)t.flush?t=n?n.next=t.next:oo=t.next:(t.time<e&&(e=t.time),t=(n=t).next);return co=n,e}function yt(n,t){var e=Math.pow(10,Math.abs(8-t)*3);return{scale:t>8?function(n){return n/e}:function(n){return n*e},symbol:n}}function Mt(n,t){return t-(n?Math.ceil(Math.log(n)/Math.LN10):1)}function xt(n){return n+""}function bt(){}function _t(n,t,e){var r=e.s=n+t,i=r-n,u=r-i;e.t=n-u+(t-i)}function wt(n,t){n&&wo.hasOwnProperty(n.type)&&wo[n.type](n,t)}function St(n,t,e){var r,i=-1,u=n.length-e;for(t.lineStart();++i<u;)r=n[i],t.point(r[0],r[1]);t.lineEnd()}function Et(n,t){var e=-1,r=n.length;for(t.polygonStart();++e<r;)St(n[e],t,1);t.polygonEnd()}funct
 ion kt(){function n(n,t){n*=Ga,t=t*Ga/2+$a/4;var e=n-r,a=Math.cos(t),o=Math.sin(t),c=u*o,l=i*a+c*Math.cos(e),f=c*Math.sin(e);Eo.add(Math.atan2(f,l)),r=n,i=a,u=o}var t,e,r,i,u;ko.point=function(a,o){ko.point=n,r=(t=a)*Ga,i=Math.cos(o=(e=o)*Ga/2+$a/4),u=Math.sin(o)},ko.lineEnd=function(){n(t,e)}}function At(n){var t=n[0],e=n[1],r=Math.cos(e);return[r*Math.cos(t),r*Math.sin(t),Math.sin(e)]}function Nt(n,t){return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]}function qt(n,t){return[n[1]*t[2]-n[2]*t[1],n[2]*t[0]-n[0]*t[2],n[0]*t[1]-n[1]*t[0]]}function Tt(n,t){n[0]+=t[0],n[1]+=t[1],n[2]+=t[2]}function Ct(n,t){return[n[0]*t,n[1]*t,n[2]*t]}function zt(n){var t=Math.sqrt(n[0]*n[0]+n[1]*n[1]+n[2]*n[2]);n[0]/=t,n[1]/=t,n[2]/=t}function Dt(n){return[Math.atan2(n[1],n[0]),U(n[2])]}function jt(n,t){return Math.abs(n[0]-t[0])<Wa&&Math.abs(n[1]-t[1])<Wa}function Lt(n,t){n*=Ga;var e=Math.cos(t*=Ga);Ht(e*Math.cos(n),e*Math.sin(n),Math.sin(t))}function Ht(n,t,e){++Ao,qo+=(n-qo)/Ao,To+=(t-To)/Ao,Co+=(e-Co)/Ao}functio
 n Ft(){function n(n,i){n*=Ga;var u=Math.cos(i*=Ga),a=u*Math.cos(n),o=u*Math.sin(n),c=Math.sin(i),l=Math.atan2(Math.sqrt((l=e*c-r*o)*l+(l=r*a-t*c)*l+(l=t*o-e*a)*l),t*a+e*o+r*c);No+=l,zo+=l*(t+(t=a)),Do+=l*(e+(e=o)),jo+=l*(r+(r=c)),Ht(t,e,r)}var t,e,r;Po.point=function(i,u){i*=Ga;var a=Math.cos(u*=Ga);t=a*Math.cos(i),e=a*Math.sin(i),r=Math.sin(u),Po.point=n,Ht(t,e,r)}}function Pt(){Po.point=Lt}function Ot(){function n(n,t){n*=Ga;var e=Math.cos(t*=Ga),a=e*Math.cos(n),o=e*Math.sin(n),c=Math.sin(t),l=i*c-u*o,f=u*a-r*c,s=r*o-i*a,h=Math.sqrt(l*l+f*f+s*s),g=r*a+i*o+u*c,p=h&&-Y(g)/h,d=Math.atan2(h,g);Lo+=p*l,Ho+=p*f,Fo+=p*s,No+=d,zo+=d*(r+(r=a)),Do+=d*(i+(i=o)),jo+=d*(u+(u=c)),Ht(r,i,u)}var t,e,r,i,u;Po.point=function(a,o){t=a,e=o,Po.point=n,a*=Ga;var c=Math.cos(o*=Ga);r=c*Math.cos(a),i=c*Math.sin(a),u=Math.sin(o),Ht(r,i,u)},Po.lineEnd=function(){n(t,e),Po.lineEnd=Pt,Po.point=Lt}}function Rt(){return!0}function Yt(n,t,e,r,i){var u=[],a=[];if(n.forEach(function(n){if(!((t=n.length-1)<=0)){var
  t,e=n[0],r=n[t];if(jt(e,r)){i.lineStart();for(var o=0;t>o;++o)i.point((e=n[o])[0],e[1]);return i.lineEnd(),void 0}var c={point:e,points:n,other:null,visited:!1,entry:!0,subject:!0},l={point:e,points:[e],other:c,visited:!1,entry:!1,subject:!1};c.other=l,u.push(c),a.push(l),c={point:r,points:[r],other:null,visited:!1,entry:!1,subject:!0},l={point:r,points:[r],other:c,visited:!1,entry:!0,subject:!1},c.other=l,u.push(c),a.push(l)}}),a.sort(t),Ut(u),Ut(a),u.length){if(e)for(var o=1,c=!e(a[0].point),l=a.length;l>o;++o)a[o].entry=c=!c;for(var f,s,h,g=u[0];;){for(f=g;f.visited;)if((f=f.next)===g)return;s=f.points,i.lineStart();do{if(f.visited=f.other.visited=!0,f.entry){if(f.subject)for(var o=0;o<s.length;o++)i.point((h=s[o])[0],h[1]);else r(f.point,f.next.point,1,i);f=f.next}else{if(f.subject){s=f.prev.points;for(var o=s.length;--o>=0;)i.point((h=s[o])[0],h[1])}else r(f.point,f.prev.point,-1,i);f=f.prev}f=f.other,s=f.points}while(!f.visited);i.lineEnd()}}}function Ut(n){if(t=n.length){for
 (var t,e,r=0,i=n[0];++r<t;)i.next=e=n[r],e.prev=i,i=e;i.next=e=n[0],e.prev=i}}function It(n,t,e,r){return function(i){function u(t,e){n(t,e)&&i.point(t,e)}function a(n,t){d.point(n,t)}function o(){m.point=a,d.lineStart()}function c(){m.point=u,d.lineEnd()}function l(n,t){y.point(n,t),p.push([n,t])}function f(){y.lineStart(),p=[]}function s(){l(p[0][0],p[0][1]),y.lineEnd();var n,t=y.clean(),e=v.buffer(),r=e.length;if(p.pop(),g.push(p),p=null,r){if(1&t){n=e[0];var u,r=n.length-1,a=-1;for(i.lineStart();++a<r;)i.point((u=n[a])[0],u[1]);return i.lineEnd(),void 0}r>1&&2&t&&e.push(e.pop().concat(e.shift())),h.push(e.filter(Vt))}}var h,g,p,d=t(i),m={point:u,lineStart:o,lineEnd:c,polygonStart:function(){m.point=l,m.lineStart=f,m.lineEnd=s,h=[],g=[],i.polygonStart()},polygonEnd:function(){m.point=u,m.lineStart=o,m.lineEnd=c,h=va.merge(h),h.length?Yt(h,Zt,null,e,i):r(g)&&(i.lineStart(),e(null,null,1,i),i.lineEnd()),i.polygonEnd(),h=g=null},sphere:function(){i.polygonStart(),i.lineStart(),e(nul
 l,null,1,i),i.lineEnd(),i.polygonEnd()}},v=Xt(),y=t(v);return m}}function Vt(n){return n.length>1}function Xt(){var n,t=[];return{lineStart:function(){t.push(n=[])},point:function(t,e){n.push([t,e])},lineEnd:T,buffer:function(){var e=t;return t=[],n=null,e},rejoin:function(){t.length>1&&t.push(t.pop().concat(t.shift()))}}}function Zt(n,t){return((n=n.point)[0]<0?n[1]-$a/2-Wa:$a/2-n[1])-((t=t.point)[0]<0?t[1]-$a/2-Wa:$a/2-t[1])}function Bt(n,t){var e=n[0],r=n[1],i=[Math.sin(e),-Math.cos(e),0],u=0,a=!1,o=!1,c=0;Eo.reset();for(var l=0,f=t.length;f>l;++l){var s=t[l],h=s.length;if(h){for(var g=s[0],p=g[0],d=g[1]/2+$a/4,m=Math.sin(d),v=Math.cos(d),y=1;;){y===h&&(y=0),n=s[y];var M=n[0],x=n[1]/2+$a/4,b=Math.sin(x),_=Math.cos(x),w=M-p,S=Math.abs(w)>$a,E=m*b;if(Eo.add(Math.atan2(E*Math.sin(w),v*_+E*Math.cos(w))),Math.abs(x)<Wa&&(o=!0),u+=S?w+(w>=0?2:-2)*$a:w,S^p>=e^M>=e){var k=qt(At(g),At(n));zt(k);var A=qt(i,k);zt(A);var N=(S^w>=0?-1:1)*U(A[2]);r>N&&(c+=S^w>=0?1:-1)}if(!y++)break;p=M,m=b,v=_
 ,g=n}Math.abs(u)>Wa&&(a=!0)}}return(!o&&!a&&0>Eo||-Wa>u)^1&c}function $t(n){var t,e=0/0,r=0/0,i=0/0;return{lineStart:function(){n.lineStart(),t=1},point:function(u,a){var o=u>0?$a:-$a,c=Math.abs(u-e);Math.abs(c-$a)<Wa?(n.point(e,r=(r+a)/2>0?$a/2:-$a/2),n.point(i,r),n.lineEnd(),n.lineStart(),n.point(o,r),n.point(u,r),t=0):i!==o&&c>=$a&&(Math.abs(e-i)<Wa&&(e-=i*Wa),Math.abs(u-o)<Wa&&(u-=o*Wa),r=Wt(e,r,u,a),n.point(i,r),n.lineEnd(),n.lineStart(),n.point(o,r),t=0),n.point(e=u,r=a),i=o},lineEnd:function(){n.lineEnd(),e=r=0/0},clean:function(){return 2-t}}}function Wt(n,t,e,r){var i,u,a=Math.sin(n-e);return Math.abs(a)>Wa?Math.atan((Math.sin(t)*(u=Math.cos(r))*Math.sin(e)-Math.sin(r)*(i=Math.cos(t))*Math.sin(n))/(i*u*a)):(t+r)/2}function Jt(n,t,e,r){var i;if(null==n)i=e*$a/2,r.point(-$a,i),r.point(0,i),r.point($a,i),r.point($a,0),r.point($a,-i),r.point(0,-i),r.point(-$a,-i),r.point(-$a,0),r.point(-$a,i);else if(Math.abs(n[0]-t[0])>Wa){var u=(n[0]<t[0]?1:-1)*$a;i=e*u/2,r.point(-u,i),r.poin
 t(0,i),r.point(u,i)}else r.point(t[0],t[1])}function Gt(n){return Bt(Ro,n)}function Kt(n){function t(n,t){return Math.cos(n)*Math.cos(t)>a}function e(n){var e,u,a,c,f;return{lineStart:function(){c=a=!1,f=1},point:function(s,h){var g,p=[s,h],d=t(s,h),m=o?d?0:i(s,h):d?i(s+(0>s?$a:-$a),h):0;if(!e&&(c=a=d)&&n.lineStart(),d!==a&&(g=r(e,p),(jt(e,g)||jt(p,g))&&(p[0]+=Wa,p[1]+=Wa,d=t(p[0],p[1]))),d!==a)f=0,d?(n.lineStart(),g=r(p,e),n.point(g[0],g[1])):(g=r(e,p),n.point(g[0],g[1]),n.lineEnd()),e=g;else if(l&&e&&o^d){var v;m&u||!(v=r(p,e,!0))||(f=0,o?(n.lineStart(),n.point(v[0][0],v[0][1]),n.point(v[1][0],v[1][1]),n.lineEnd()):(n.point(v[1][0],v[1][1]),n.lineEnd(),n.lineStart(),n.point(v[0][0],v[0][1])))}!d||e&&jt(e,p)||n.point(p[0],p[1]),e=p,a=d,u=m},lineEnd:function(){a&&n.lineEnd(),e=null},clean:function(){return f|(c&&a)<<1}}}function r(n,t,e){var r=At(n),i=At(t),u=[1,0,0],o=qt(r,i),c=Nt(o,o),l=o[0],f=c-l*l;if(!f)return!e&&n;var s=a*c/f,h=-a*l/f,g=qt(u,o),p=Ct(u,s),d=Ct(o,h);Tt(p,d);var m
 =g,v=Nt(p,m),y=Nt(m,m),M=v*v-y*(Nt(p,p)-1);if(!(0>M)){var x=Math.sqrt(M),b=Ct(m,(-v-x)/y);if(Tt(b,p),b=Dt(b),!e)return b;var _,w=n[0],S=t[0],E=n[1],k=t[1];w>S&&(_=w,w=S,S=_);var A=S-w,N=Math.abs(A-$a)<Wa,q=N||Wa>A;if(!N&&E>k&&(_=E,E=k,k=_),q?N?E+k>0^b[1]<(Math.abs(b[0]-w)<Wa?E:k):E<=b[1]&&b[1]<=k:A>$a^(w<=b[0]&&b[0]<=S)){var T=Ct(m,(-v+x)/y);return Tt(T,p),[b,Dt(T)]}}}function i(t,e){var r=o?n:$a-n,i=0;return-r>t?i|=1:t>r&&(i|=2),-r>e?i|=4:e>r&&(i|=8),i}function u(n){return Bt(c,n)}var a=Math.cos(n),o=a>0,c=[n,0],l=Math.abs(a)>Wa,f=we(n,6*Ga);return It(t,e,f,u)}function Qt(n,t,e,r){function i(r,i){return Math.abs(r[0]-n)<Wa?i>0?0:3:Math.abs(r[0]-e)<Wa?i>0?2:1:Math.abs(r[1]-t)<Wa?i>0?1:0:i>0?3:2}function u(n,t){return a(n.point,t.point)}function a(n,t){var e=i(n,1),r=i(t,1);return e!==r?e-r:0===e?t[1]-n[1]:1===e?n[0]-t[0]:2===e?n[1]-t[1]:t[0]-n[0]}function o(i,u){var a=u[0]-i[0],o=u[1]-i[1],c=[0,1];return Math.abs(a)<Wa&&Math.abs(o)<Wa?n<=i[0]&&i[0]<=e&&t<=i[1]&&i[1]<=r:ne(n-i[0],a,c
 )&&ne(i[0]-e,-a,c)&&ne(t-i[1],o,c)&&ne(i[1]-r,-o,c)?(c[1]<1&&(u[0]=i[0]+c[1]*a,u[1]=i[1]+c[1]*o),c[0]>0&&(i[0]+=c[0]*a,i[1]+=c[0]*o),!0):!1}return function(c){function l(u){var a=i(u,-1),o=f([0===a||3===a?n:e,a>1?r:t]);return o}function f(n){for(var t=0,e=M.length,r=n[1],i=0;e>i;++i)for(var u,a=1,o=M[i],c=o.length,l=o[0];c>a;++a)u=o[a],l[1]<=r?u[1]>r&&s(l,u,n)>0&&++t:u[1]<=r&&s(l,u,n)<0&&--t,l=u;return 0!==t}function s(n,t,e){return(t[0]-n[0])*(e[1]-n[1])-(e[0]-n[0])*(t[1]-n[1])}function h(u,o,c,l){var f=0,s=0;if(null==u||(f=i(u,c))!==(s=i(o,c))||a(u,o)<0^c>0){do l.point(0===f||3===f?n:e,f>1?r:t);while((f=(f+c+4)%4)!==s)}else l.point(o[0],o[1])}function g(i,u){return i>=n&&e>=i&&u>=t&&r>=u}function p(n,t){g(n,t)&&c.point(n,t)}function d(){T.point=v,M&&M.push(x=[]),A=!0,k=!1,S=E=0/0}function m(){y&&(v(b,_),w&&k&&q.rejoin(),y.push(q.buffer())),T.point=p,k&&c.lineEnd()}function v(n,t){n=Math.max(-Yo,Math.min(Yo,n)),t=Math.max(-Yo,Math.min(Yo,t));var e=g(n,t);if(M&&x.push([n,t]),A)b=n,_
 =t,w=e,A=!1,e&&(c.lineStart(),c.point(n,t));else if(e&&k)c.point(n,t);else{var r=[S,E],i=[n,t];o(r,i)?(k||(c.lineStart(),c.point(r[0],r[1])),c.point(i[0],i[1]),e||c.lineEnd()):e&&(c.lineStart(),c.point(n,t))}S=n,E=t,k=e}var y,M,x,b,_,w,S,E,k,A,N=c,q=Xt(),T={point:p,lineStart:d,lineEnd:m,polygonStart:function(){c=q,y=[],M=[]},polygonEnd:function(){c=N,(y=va.merge(y)).length?(c.polygonStart(),Yt(y,u,l,h,c),c.polygonEnd()):f([n,t])&&(c.polygonStart(),c.lineStart(),h(null,null,1,c),c.lineEnd(),c.polygonEnd()),y=M=x=null}};return T}}function ne(n,t,e){if(Math.abs(t)<Wa)return 0>=n;var r=n/t;if(t>0){if(r>e[1])return!1;r>e[0]&&(e[0]=r)}else{if(r<e[0])return!1;r<e[1]&&(e[1]=r)}return!0}function te(n,t){function e(e,r){return e=n(e,r),t(e[0],e[1])}return n.invert&&t.invert&&(e.invert=function(e,r){return e=t.invert(e,r),e&&n.invert(e[0],e[1])}),e}function ee(n){var t=0,e=$a/3,r=me(n),i=r(t,e);return i.parallels=function(n){return arguments.length?r(t=n[0]*$a/180,e=n[1]*$a/180):[180*(t/$a),18
 0*(e/$a)]},i}function re(n,t){function e(n,t){var e=Math.sqrt(u-2*i*Math.sin(t))/i;return[e*Math.sin(n*=i),a-e*Math.cos(n)]}var r=Math.sin(n),i=(r+Math.sin(t))/2,u=1+r*(2*i-r),a=Math.sqrt(u)/i;return e.invert=function(n,t){var e=a-t;return[Math.atan2(n,e)/i,U((u-(n*n+e*e)*i*i)/(2*i))]},e}function ie(){function n(n,t){Io+=i*n-r*t,r=n,i=t}var t,e,r,i;$o.point=function(u,a){$o.point=n,t=r=u,e=i=a},$o.lineEnd=function(){n(t,e)}}function ue(n,t){Vo>n&&(Vo=n),n>Zo&&(Zo=n),Xo>t&&(Xo=t),t>Bo&&(Bo=t)}function ae(){function n(n,t){a.push("M",n,",",t,u)}function t(n,t){a.push("M",n,",",t),o.point=e}function e(n,t){a.push("L",n,",",t)}function r(){o.point=n}function i(){a.push("Z")}var u=oe(4.5),a=[],o={point:n,lineStart:function(){o.point=t},lineEnd:r,polygonStart:function(){o.lineEnd=i},polygonEnd:function(){o.lineEnd=r,o.point=n},pointRadius:function(n){return u=oe(n),o},result:function(){if(a.length){var n=a.join("");return a=[],n}}};return o}function oe(n){return"m0,"+n+"a"+n+","+n+" 0 1,1
  0,"+-2*n+"a"+n+","+n+" 0 1,1 0,"+2*n+"z"}function ce(n,t){qo+=n,To+=t,++Co}function le(){function n(n,r){var i=n-t,u=r-e,a=Math.sqrt(i*i+u*u);zo+=a*(t+n)/2,Do+=a*(e+r)/2,jo+=a,ce(t=n,e=r)}var t,e;Jo.point=function(r,i){Jo.point=n,ce(t=r,e=i)}}function fe(){Jo.point=ce}function se(){function n(n,t){var e=n-r,u=t-i,a=Math.sqrt(e*e+u*u);zo+=a*(r+n)/2,Do+=a*(i+t)/2,jo+=a,a=i*n-r*t,Lo+=a*(r+n),Ho+=a*(i+t),Fo+=3*a,ce(r=n,i=t)}var t,e,r,i;Jo.point=function(u,a){Jo.point=n,ce(t=r=u,e=i=a)},Jo.lineEnd=function(){n(t,e)}}function he(n){function t(t,e){n.moveTo(t,e),n.arc(t,e,a,0,2*$a)}function e(t,e){n.moveTo(t,e),o.point=r}function r(t,e){n.lineTo(t,e)}function i(){o.point=t}function u(){n.closePath()}var a=4.5,o={point:t,lineStart:function(){o.point=e},lineEnd:i,polygonStart:function(){o.lineEnd=u},polygonEnd:function(){o.lineEnd=i,o.point=t},pointRadius:function(n){return a=n,o},result:T};return o}function ge(n){function t(t){function r(e,r){e=n(e,r),t.point(e[0],e[1])}function u(){x=0/0,
 E.point=a,t.lineStart()}function a(r,u){var a=At([r,u]),o=n(r,u),c=[];e(x,b,M,_,w,S,x=o[0],b=o[1],M=r,_=a[0],w=a[1],S=a[2],i,c),s(c,t),t.point(x,b)}function o(){E.point=r,t.lineEnd()}function c(){u(),E.point=l,E.lineEnd=f}function l(n,t){a(h=n,g=t),p=x,d=b,m=_,v=w,y=S,E.point=a}function f(){var n=[];e(x,b,M,_,w,S,p,d,h,m,v,y,i,n),s(n,t),E.lineEnd=o,o()}function s(n,t){for(var e,r=0,i=n.length;i>r;++r)t.point((e=n[r])[0],e[1])}var h,g,p,d,m,v,y,M,x,b,_,w,S,E={point:r,lineStart:u,lineEnd:o,polygonStart:function(){t.polygonStart(),E.lineStart=c},polygonEnd:function(){t.polygonEnd(),E.lineStart=u}};return E}function e(t,i,u,a,o,c,l,f,s,h,g,p,d,m){var v=l-t,y=f-i,M=v*v+y*y;if(M>4*r&&d--){var x=a+h,b=o+g,_=c+p,w=Math.sqrt(x*x+b*b+_*_),S=Math.asin(_/=w),E=Math.abs(Math.abs(_)-1)<Wa?(u+s)/2:Math.atan2(b,x),k=n(E,S),A=k[0],N=k[1],q=A-t,T=N-i,C=y*q-v*T,z=!1;if(C*C/M>r||Math.abs((v*q+y*T)/M-.5)>.3||(z=q*q+T*T>256*r)){var D=e(t,i,u,a,o,c,A,N,E,x/=w,b/=w,_,d,m);m.push(k);var j=e(A,N,E,x,b,_,l,f,
 s,h,g,p,d,m);return!z||D||j||(m.pop(),!1)}}}var r=.5,i=16;return t.precision=function(n){return arguments.length?(i=(r=n*n)>0&&16,t):Math.sqrt(r)},t}function pe(n){var t=ge(function(t,e){return n([t*Ka,e*Ka])});return function(n){return n=t(n),{point:function(t,e){n.point(t*Ga,e*Ga)},sphere:function(){n.sphere()},lineStart:function(){n.lineStart()},lineEnd:function(){n.lineEnd()},polygonStart:function(){n.polygonStart()},polygonEnd:function(){n.polygonEnd()}}}}function de(n){return me(function(){return n})()}function me(n){function t(n){return n=o(n[0]*Ga,n[1]*Ga),[n[0]*h+c,l-n[1]*h]}function e(n){return n=o.invert((n[0]-c)/h,(l-n[1])/h),n&&[n[0]*Ka,n[1]*Ka]}function r(){o=te(a=Me(v,y,M),u);var n=u(d,m);return c=g-n[0]*h,l=p+n[1]*h,i()}function i(){return f&&(f.valid=!1,f=null),t}var u,a,o,c,l,f,s=ge(function(n,t){return n=u(n,t),[n[0]*h+c,l-n[1]*h]}),h=150,g=480,p=250,d=0,m=0,v=0,y=0,M=0,x=Oo,b=st,_=null,w=null;return t.stream=function(n){return f&&(f.valid=!1),f=ve(a,x(s(b(n)))),f
 .valid=!0,f},t.clipAngle=function(n){return arguments.length?(x=null==n?(_=n,Oo):Kt((_=+n)*Ga),i()):_},t.clipExtent=function(n){return arguments.length?(w=n,b=null==n?st:Qt(n[0][0],n[0][1],n[1][0],n[1][1]),i()):w},t.scale=function(n){return arguments.length?(h=+n,r()):h},t.translate=function(n){return arguments.length?(g=+n[0],p=+n[1],r()):[g,p]},t.center=function(n){return arguments.length?(d=n[0]%360*Ga,m=n[1]%360*Ga,r()):[d*Ka,m*Ka]},t.rotate=function(n){return arguments.length?(v=n[0]%360*Ga,y=n[1]%360*Ga,M=n.length>2?n[2]%360*Ga:0,r()):[v*Ka,y*Ka,M*Ka]},va.rebind(t,s,"precision"),function(){return u=n.apply(this,arguments),t.invert=u.invert&&e,r()}}function ve(n,t){return{point:function(e,r){r=n(e*Ga,r*Ga),e=r[0],t.point(e>$a?e-2*$a:-$a>e?e+2*$a:e,r[1])},sphere:function(){t.sphere()},lineStart:function(){t.lineStart()},lineEnd:function(){t.lineEnd()},polygonStart:function(){t.polygonStart()},polygonEnd:function(){t.polygonEnd()}}}function ye(n,t){return[n,t]}function Me(n,t,e){
 return n?t||e?te(be(n),_e(t,e)):be(n):t||e?_e(t,e):ye}function xe(n){return function(t,e){return t+=n,[t>$a?t-2*$a:-$a>t?t+2*$a:t,e]}}function be(n){var t=xe(n);return t.invert=xe(-n),t}function _e(n,t){function e(n,t){var e=Math.cos(t),o=Math.cos(n)*e,c=Math.sin(n)*e,l=Math.sin(t),f=l*r+o*i;return[Math.atan2(c*u-f*a,o*r-l*i),U(f*u+c*a)]}var r=Math.cos(n),i=Math.sin(n),u=Math.cos(t),a=Math.sin(t);return e.invert=function(n,t){var e=Math.cos(t),o=Math.cos(n)*e,c=Math.sin(n)*e,l=Math.sin(t),f=l*u-c*a;return[Math.atan2(c*u+l*a,o*r+f*i),U(f*r-o*i)]},e}function we(n,t){var e=Math.cos(n),r=Math.sin(n);return function(i,u,a,o){null!=i?(i=Se(e,i),u=Se(e,u),(a>0?u>i:i>u)&&(i+=2*a*$a)):(i=n+2*a*$a,u=n);for(var c,l=a*t,f=i;a>0?f>u:u>f;f-=l)o.point((c=Dt([e,-r*Math.cos(f),-r*Math.sin(f)]))[0],c[1])}}function Se(n,t){var e=At(t);e[0]-=n,zt(e);var r=Y(-e[1]);return((-e[2]<0?-r:r)+2*Math.PI-Wa)%(2*Math.PI)}function Ee(n,t,e){var r=va.range(n,t-Wa,e).concat(t);return function(n){return r.map(functi
 on(t){return[n,t]})}}function ke(n,t,e){var r=va.range(n,t-Wa,e).concat(t);return function(n){return r.map(function(t){return[t,n]})}}function Ae(n){return n.source}function Ne(n){return n.target}function qe(n,t,e,r){var i=Math.cos(t),u=Math.sin(t),a=Math.cos(r),o=Math.sin(r),c=i*Math.cos(n),l=i*Math.sin(n),f=a*Math.cos(e),s=a*Math.sin(e),h=2*Math.asin(Math.sqrt(X(r-t)+i*a*X(e-n))),g=1/Math.sin(h),p=h?function(n){var t=Math.sin(n*=h)*g,e=Math.sin(h-n)*g,r=e*c+t*f,i=e*l+t*s,a=e*u+t*o;return[Math.atan2(i,r)*Ka,Math.atan2(a,Math.sqrt(r*r+i*i))*Ka]}:function(){return[n*Ka,t*Ka]};return p.distance=h,p}function Te(){function n(n,i){var u=Math.sin(i*=Ga),a=Math.cos(i),o=Math.abs((n*=Ga)-t),c=Math.cos(o);Go+=Math.atan2(Math.sqrt((o=a*Math.sin(o))*o+(o=r*u-e*a*c)*o),e*u+r*a*c),t=n,e=u,r=a}var t,e,r;Ko.point=function(i,u){t=i*Ga,e=Math.sin(u*=Ga),r=Math.cos(u),Ko.point=n},Ko.lineEnd=function(){Ko.point=Ko.lineEnd=T}}function Ce(n,t){function e(t,e){var r=Math.cos(t),i=Math.cos(e),u=n(r*i);ret
 urn[u*i*Math.sin(t),u*Math.sin(e)]}return e.invert=function(n,e){var r=Math.sqrt(n*n+e*e),i=t(r),u=Math.sin(i),a=Math.cos(i);return[Math.atan2(n*u,r*a),Math.asin(r&&e*u/r)]},e}function ze(n,t){function e(n,t){var e=Math.abs(Math.abs(t)-$a/2)<Wa?0:a/Math.pow(i(t),u);return[e*Math.sin(u*n),a-e*Math.cos(u*n)]}var r=Math.cos(n),i=function(n){return Math.tan($a/4+n/2)},u=n===t?Math.sin(n):Math.log(r/Math.cos(t))/Math.log(i(t)/i(n)),a=r*Math.pow(i(n),u)/u;return u?(e.invert=function(n,t){var e=a-t,r=R(u)*Math.sqrt(n*n+e*e);return[Math.atan2(n,e)/u,2*Math.atan(Math.pow(a/r,1/u))-$a/2]},e):je}function De(n,t){function e(n,t){var e=u-t;return[e*Math.sin(i*n),u-e*Math.cos(i*n)]}var r=Math.cos(n),i=n===t?Math.sin(n):(r-Math.cos(t))/(t-n),u=r/i+n;return Math.abs(i)<Wa?ye:(e.invert=function(n,t){var e=u-t;return[Math.atan2(n,e)/i,u-R(i)*Math.sqrt(n*n+e*e)]},e)}function je(n,t){return[n,Math.log(Math.tan($a/4+t/2))]}function Le(n){var t,e=de(n),r=e.scale,i=e.translate,u=e.clipExtent;return e.scal
 e=function(){var n=r.apply(e,arguments);return n===e?t?e.clipExtent(null):e:n},e.translate=function(){var n=i.apply(e,arguments);return n===e?t?e.clipExtent(null):e:n},e.clipExtent=function(n){var a=u.apply(e,arguments);if(a===e){if(t=null==n){var o=$a*r(),c=i();u([[c[0]-o,c[1]-o],[c[0]+o,c[1]+o]])}}else t&&(a=null);return a},e.clipExtent(null)}function He(n,t){var e=Math.cos(t)*Math.sin(n);return[Math.log((1+e)/(1-e))/2,Math.atan2(Math.tan(t),Math.cos(n))]}function Fe(n){function t(t){function a(){l.push("M",u(n(f),o))}for(var c,l=[],f=[],s=-1,h=t.length,g=ft(e),p=ft(r);++s<h;)i.call(this,c=t[s],s)?f.push([+g.call(this,c,s),+p.call(this,c,s)]):f.length&&(a(),f=[]);return f.length&&a(),l.length?l.join(""):null}var e=Pe,r=Oe,i=Rt,u=Re,a=u.key,o=.7;return t.x=function(n){return arguments.length?(e=n,t):e},t.y=function(n){return arguments.length?(r=n,t):r},t.defined=function(n){return arguments.length?(i=n,t):i},t.interpolate=function(n){return arguments.length?(a="function"==typeof n?
 u=n:(u=ic.get(n)||Re).key,t):a},t.tension=function(n){return arguments.length?(o=n,t):o},t}function Pe(n){return n[0]}function Oe(n){return n[1]}function Re(n){return n.join("L")}function Ye(n){return Re(n)+"Z"}function Ue(n){for(var t=0,e=n.length,r=n[0],i=[r[0],",",r[1]];++t<e;)i.push("H",(r[0]+(r=n[t])[0])/2,"V",r[1]);return e>1&&i.push("H",r[0]),i.join("")}function Ie(n){for(var t=0,e=n.length,r=n[0],i=[r[0],",",r[1]];++t<e;)i.push("V",(r=n[t])[1],"H",r[0]);return i.join("")}function Ve(n){for(var t=0,e=n.length,r=n[0],i=[r[0],",",r[1]];++t<e;)i.push("H",(r=n[t])[0],"V",r[1]);return i.join("")}function Xe(n,t){return n.length<4?Re(n):n[1]+$e(n.slice(1,n.length-1),We(n,t))}function Ze(n,t){return n.length<3?Re(n):n[0]+$e((n.push(n[0]),n),We([n[n.length-2]].concat(n,[n[1]]),t))}function Be(n,t){return n.length<3?Re(n):n[0]+$e(n,We(n,t))}function $e(n,t){if(t.length<1||n.length!=t.length&&n.length!=t.length+2)return Re(n);var e=n.length!=t.length,r="",i=n[0],u=n[1],a=t[0],o=a,c=1;i
 f(e&&(r+="Q"+(u[0]-a[0]*2/3)+","+(u[1]-a[1]*2/3)+","+u[0]+","+u[1],i=n[1],c=2),t.length>1){o=t[1],u=n[c],c++,r+="C"+(i[0]+a[0])+","+(i[1]+a[1])+","+(u[0]-o[0])+","+(u[1]-o[1])+","+u[0]+","+u[1];for(var l=2;l<t.length;l++,c++)u=n[c],o=t[l],r+="S"+(u[0]-o[0])+","+(u[1]-o[1])+","+u[0]+","+u[1]}if(e){var f=n[c];r+="Q"+(u[0]+o[0]*2/3)+","+(u[1]+o[1]*2/3)+","+f[0]+","+f[1]}return r}function We(n,t){for(var e,r=[],i=(1-t)/2,u=n[0],a=n[1],o=1,c=n.length;++o<c;)e=u,u=a,a=n[o],r.push([i*(a[0]-e[0]),i*(a[1]-e[1])]);return r}function Je(n){if(n.length<3)return Re(n);var t=1,e=n.length,r=n[0],i=r[0],u=r[1],a=[i,i,i,(r=n[1])[0]],o=[u,u,u,r[1]],c=[i,",",u];for(tr(c,a,o);++t<e;)r=n[t],a.shift(),a.push(r[0]),o.shift(),o.push(r[1]),tr(c,a,o);for(t=-1;++t<2;)a.shift(),a.push(r[0]),o.shift(),o.push(r[1]),tr(c,a,o);return c.join("")}function Ge(n){if(n.length<4)return Re(n);for(var t,e=[],r=-1,i=n.length,u=[0],a=[0];++r<3;)t=n[r],u.push(t[0]),a.push(t[1]);for(e.push(nr(oc,u)+","+nr(oc,a)),--r;++r<i;)t=n
 [r],u.shift(),u.push(t[0]),a.shift(),a.push(t[1]),tr(e,u,a);return e.join("")}function Ke(n){for(var t,e,r=-1,i=n.length,u=i+4,a=[],o=[];++r<4;)e=n[r%i],a.push(e[0]),o.push(e[1]);for(t=[nr(oc,a),",",nr(oc,o)],--r;++r<u;)e=n[r%i],a.shift(),a.push(e[0]),o.shift(),o.push(e[1]),tr(t,a,o);return t.join("")}function Qe(n,t){var e=n.length-1;if(e)for(var r,i,u=n[0][0],a=n[0][1],o=n[e][0]-u,c=n[e][1]-a,l=-1;++l<=e;)r=n[l],i=l/e,r[0]=t*r[0]+(1-t)*(u+i*o),r[1]=t*r[1]+(1-t)*(a+i*c);return Je(n)}function nr(n,t){return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]+n[3]*t[3]}function tr(n,t,e){n.push("C",nr(uc,t),",",nr(uc,e),",",nr(ac,t),",",nr(ac,e),",",nr(oc,t),",",nr(oc,e))}function er(n,t){return(t[1]-n[1])/(t[0]-n[0])}function rr(n){for(var t=0,e=n.length-1,r=[],i=n[0],u=n[1],a=r[0]=er(i,u);++t<e;)r[t]=(a+(a=er(i=u,u=n[t+1])))/2;return r[t]=a,r}function ir(n){for(var t,e,r,i,u=[],a=rr(n),o=-1,c=n.length-1;++o<c;)t=er(n[o],n[o+1]),Math.abs(t)<1e-6?a[o]=a[o+1]=0:(e=a[o]/t,r=a[o+1]/t,i=e*e+r*r,i>9&&(i=3*t/Ma
 th.sqrt(i),a[o]=i*e,a[o+1]=i*r));for(o=-1;++o<=c;)i=(n[Math.min(c,o+1)][0]-n[Math.max(0,o-1)][0])/(6*(1+a[o]*a[o])),u.push([i||0,a[o]*i||0]);
-return u}function ur(n){return n.length<3?Re(n):n[0]+$e(n,ir(n))}function ar(n,t,e,r){var i,u,a,o,c,l,f;return i=r[n],u=i[0],a=i[1],i=r[t],o=i[0],c=i[1],i=r[e],l=i[0],f=i[1],(f-a)*(o-u)-(c-a)*(l-u)>0}function or(n,t,e){return(e[0]-t[0])*(n[1]-t[1])<(e[1]-t[1])*(n[0]-t[0])}function cr(n,t,e,r){var i=n[0],u=e[0],a=t[0]-i,o=r[0]-u,c=n[1],l=e[1],f=t[1]-c,s=r[1]-l,h=(o*(c-l)-s*(i-u))/(s*a-o*f);return[i+h*a,c+h*f]}function lr(n,t){var e={list:n.map(function(n,t){return{index:t,x:n[0],y:n[1]}}).sort(function(n,t){return n.y<t.y?-1:n.y>t.y?1:n.x<t.x?-1:n.x>t.x?1:0}),bottomSite:null},r={list:[],leftEnd:null,rightEnd:null,init:function(){r.leftEnd=r.createHalfEdge(null,"l"),r.rightEnd=r.createHalfEdge(null,"l"),r.leftEnd.r=r.rightEnd,r.rightEnd.l=r.leftEnd,r.list.unshift(r.leftEnd,r.rightEnd)},createHalfEdge:function(n,t){return{edge:n,side:t,vertex:null,l:null,r:null}},insert:function(n,t){t.l=n,t.r=n.r,n.r.l=t,n.r=t},leftBound:function(n){var t=r.leftEnd;do t=t.r;while(t!=r.rightEnd&&i.righ
 tOf(t,n));return t=t.l},del:function(n){n.l.r=n.r,n.r.l=n.l,n.edge=null},right:function(n){return n.r},left:function(n){return n.l},leftRegion:function(n){return n.edge==null?e.bottomSite:n.edge.region[n.side]},rightRegion:function(n){return n.edge==null?e.bottomSite:n.edge.region[cc[n.side]]}},i={bisect:function(n,t){var e={region:{l:n,r:t},ep:{l:null,r:null}},r=t.x-n.x,i=t.y-n.y,u=r>0?r:-r,a=i>0?i:-i;return e.c=n.x*r+n.y*i+.5*(r*r+i*i),u>a?(e.a=1,e.b=i/r,e.c/=r):(e.b=1,e.a=r/i,e.c/=i),e},intersect:function(n,t){var e=n.edge,r=t.edge;if(!e||!r||e.region.r==r.region.r)return null;var i=e.a*r.b-e.b*r.a;if(Math.abs(i)<1e-10)return null;var u,a,o=(e.c*r.b-r.c*e.b)/i,c=(r.c*e.a-e.c*r.a)/i,l=e.region.r,f=r.region.r;l.y<f.y||l.y==f.y&&l.x<f.x?(u=n,a=e):(u=t,a=r);var s=o>=a.region.r.x;return s&&u.side==="l"||!s&&u.side==="r"?null:{x:o,y:c}},rightOf:function(n,t){var e=n.edge,r=e.region.r,i=t.x>r.x;if(i&&n.side==="l")return 1;if(!i&&n.side==="r")return 0;if(e.a===1){var u=t.y-r.y,a=t.x-r.x,
 o=0,c=0;if(!i&&e.b<0||i&&e.b>=0?c=o=u>=e.b*a:(c=t.x+t.y*e.b>e.c,e.b<0&&(c=!c),c||(o=1)),!o){var l=r.x-e.region.l.x;c=e.b*(a*a-u*u)<l*u*(1+2*a/l+e.b*e.b),e.b<0&&(c=!c)}}else{var f=e.c-e.a*t.x,s=t.y-f,h=t.x-r.x,g=f-r.y;c=s*s>h*h+g*g}return n.side==="l"?c:!c},endPoint:function(n,e,r){n.ep[e]=r,n.ep[cc[e]]&&t(n)},distance:function(n,t){var e=n.x-t.x,r=n.y-t.y;return Math.sqrt(e*e+r*r)}},u={list:[],insert:function(n,t,e){n.vertex=t,n.ystar=t.y+e;for(var r=0,i=u.list,a=i.length;a>r;r++){var o=i[r];if(!(n.ystar>o.ystar||n.ystar==o.ystar&&t.x>o.vertex.x))break}i.splice(r,0,n)},del:function(n){for(var t=0,e=u.list,r=e.length;r>t&&e[t]!=n;++t);e.splice(t,1)},empty:function(){return u.list.length===0},nextEvent:function(n){for(var t=0,e=u.list,r=e.length;r>t;++t)if(e[t]==n)return e[t+1];return null},min:function(){var n=u.list[0];return{x:n.vertex.x,y:n.ystar}},extractMin:function(){return u.list.shift()}};r.init(),e.bottomSite=e.list.shift();for(var a,o,c,l,f,s,h,g,p,d,m,v,y,M=e.list.shift();
 ;)if(u.empty()||(a=u.min()),M&&(u.empty()||M.y<a.y||M.y==a.y&&M.x<a.x))o=r.leftBound(M),c=r.right(o),h=r.rightRegion(o),v=i.bisect(h,M),s=r.createHalfEdge(v,"l"),r.insert(o,s),d=i.intersect(o,s),d&&(u.del(o),u.insert(o,d,i.distance(d,M))),o=s,s=r.createHalfEdge(v,"r"),r.insert(o,s),d=i.intersect(s,c),d&&u.insert(s,d,i.distance(d,M)),M=e.list.shift();else{if(u.empty())break;o=u.extractMin(),l=r.left(o),c=r.right(o),f=r.right(c),h=r.leftRegion(o),g=r.rightRegion(c),m=o.vertex,i.endPoint(o.edge,o.side,m),i.endPoint(c.edge,c.side,m),r.del(o),u.del(c),r.del(c),y="l",h.y>g.y&&(p=h,h=g,g=p,y="r"),v=i.bisect(h,g),s=r.createHalfEdge(v,y),r.insert(l,s),i.endPoint(v,cc[y],m),d=i.intersect(l,s),d&&(u.del(l),u.insert(l,d,i.distance(d,h))),d=i.intersect(s,f),d&&u.insert(s,d,i.distance(d,h))}for(o=r.right(r.leftEnd);o!=r.rightEnd;o=r.right(o))t(o.edge)}function fr(n){return n.x}function sr(n){return n.y}function hr(){return{leaf:!0,nodes:[],point:null,x:null,y:null}}function gr(n,t,e,r,i,u){if(!n(
 t,e,r,i,u)){var a=.5*(e+i),o=.5*(r+u),c=t.nodes;c[0]&&gr(n,c[0],e,r,a,o),c[1]&&gr(n,c[1],a,r,i,o),c[2]&&gr(n,c[2],e,o,a,u),c[3]&&gr(n,c[3],a,o,i,u)}}function pr(n,t){n=va.rgb(n),t=va.rgb(t);var e=n.r,r=n.g,i=n.b,u=t.r-e,a=t.g-r,o=t.b-i;return function(n){return"#"+it(Math.round(e+u*n))+it(Math.round(r+a*n))+it(Math.round(i+o*n))}}function dr(n){var t=[n.a,n.b],e=[n.c,n.d],r=vr(t),i=mr(t,e),u=vr(yr(e,t,-i))||0;t[0]*e[1]<e[0]*t[1]&&(t[0]*=-1,t[1]*=-1,r*=-1,i*=-1),this.rotate=(r?Math.atan2(t[1],t[0]):Math.atan2(-e[0],e[1]))*Ka,this.translate=[n.e,n.f],this.scale=[r,u],this.skew=u?Math.atan2(i,u)*Ka:0}function mr(n,t){return n[0]*t[0]+n[1]*t[1]}function vr(n){var t=Math.sqrt(mr(n,n));return t&&(n[0]/=t,n[1]/=t),t}function yr(n,t,e){return n[0]+=e*t[0],n[1]+=e*t[1],n}function Mr(n,t){return t-=n=+n,function(e){return n+t*e}}function xr(n,t){var e,r=[],i=[],u=va.transform(n),a=va.transform(t),o=u.translate,c=a.translate,l=u.rotate,f=a.rotate,s=u.skew,h=a.skew,g=u.scale,p=a.scale;return o[
 0]!=c[0]||o[1]!=c[1]?(r.push("translate(",null,",",null,")"),i.push({i:1,x:Mr(o[0],c[0])},{i:3,x:Mr(o[1],c[1])})):c[0]||c[1]?r.push("translate("+c+")"):r.push(""),l!=f?(l-f>180?f+=360:f-l>180&&(l+=360),i.push({i:r.push(r.pop()+"rotate(",null,")")-2,x:Mr(l,f)})):f&&r.push(r.pop()+"rotate("+f+")"),s!=h?i.push({i:r.push(r.pop()+"skewX(",null,")")-2,x:Mr(s,h)}):h&&r.push(r.pop()+"skewX("+h+")"),g[0]!=p[0]||g[1]!=p[1]?(e=r.push(r.pop()+"scale(",null,",",null,")"),i.push({i:e-4,x:Mr(g[0],p[0])},{i:e-2,x:Mr(g[1],p[1])})):(p[0]!=1||p[1]!=1)&&r.push(r.pop()+"scale("+p+")"),e=i.length,function(n){for(var t,u=-1;++u<e;)r[(t=i[u]).i]=t.x(n);return r.join("")}}function br(n,t){var e,r={},i={};for(e in n)e in t?r[e]=Sr(e)(n[e],t[e]):i[e]=n[e];for(e in t)e in n||(i[e]=t[e]);return function(n){for(e in r)i[e]=r[e](n);return i}}function _r(n,t){var e,r,i,u,a,o=0,c=0,l=[],f=[];for(n+="",t+="",fc.lastIndex=0,r=0;e=fc.exec(t);++r)e.index&&l.push(t.substring(o,c=e.index)),f.push({i:l.length,x:e[0]}),l.p
 ush(null),o=fc.lastIndex;for(o<t.length&&l.push(t.substring(o)),r=0,u=f.length;(e=fc.exec(n))&&u>r;++r)if(a=f[r],a.x==e[0]){if(a.i)if(l[a.i+1]==null)for(l[a.i-1]+=a.x,l.splice(a.i,1),i=r+1;u>i;++i)f[i].i--;else for(l[a.i-1]+=a.x+l[a.i+1],l.splice(a.i,2),i=r+1;u>i;++i)f[i].i-=2;else if(l[a.i+1]==null)l[a.i]=a.x;else for(l[a.i]=a.x+l[a.i+1],l.splice(a.i+1,1),i=r+1;u>i;++i)f[i].i--;f.splice(r,1),u--,r--}else a.x=Mr(parseFloat(e[0]),parseFloat(a.x));for(;u>r;)a=f.pop(),l[a.i+1]==null?l[a.i]=a.x:(l[a.i]=a.x+l[a.i+1],l.splice(a.i+1,1)),u--;return l.length===1?l[0]==null?(a=f[0].x,function(n){return a(n)+""}):function(){return t}:function(n){for(r=0;u>r;++r)l[(a=f[r]).i]=a.x(n);return l.join("")}}function wr(n,t){for(var e,r=va.interpolators.length;--r>=0&&!(e=va.interpolators[r](n,t)););return e}function Sr(n){return"transform"==n?xr:wr}function Er(n,t){var e,r=[],i=[],u=n.length,a=t.length,o=Math.min(n.length,t.length);for(e=0;o>e;++e)r.push(wr(n[e],t[e]));for(;u>e;++e)i[e]=n[e];for(;a>e
 ;++e)i[e]=t[e];return function(n){for(e=0;o>e;++e)i[e]=r[e](n);return i}}function kr(n){return function(t){return 0>=t?0:t>=1?1:n(t)}}function Ar(n){return function(t){return 1-n(1-t)}}function Nr(n){return function(t){return.5*(.5>t?n(2*t):2-n(2-2*t))}}function qr(n){return n*n}function Tr(n){return n*n*n}function Cr(n){if(0>=n)return 0;if(n>=1)return 1;var t=n*n,e=t*n;return 4*(.5>n?e:3*(n-t)+e-.75)}function zr(n){return function(t){return Math.pow(t,n)}}function Dr(n){return 1-Math.cos(n*$a/2)}function jr(n){return Math.pow(2,10*(n-1))}function Lr(n){return 1-Math.sqrt(1-n*n)}function Hr(n,t){var e;return arguments.length<2&&(t=.45),arguments.length?e=t/(2*$a)*Math.asin(1/n):(n=1,e=t/4),function(r){return 1+n*Math.pow(2,10*-r)*Math.sin(2*(r-e)*$a/t)}}function Fr(n){return n||(n=1.70158),function(t){return t*t*((n+1)*t-n)}}function Pr(n){return 1/2.75>n?7.5625*n*n:2/2.75>n?7.5625*(n-=1.5/2.75)*n+.75:2.5/2.75>n?7.5625*(n-=2.25/2.75)*n+.9375:7.5625*(n-=2.625/2.75)*n+.984375}function
  Or(n,t){n=va.hcl(n),t=va.hcl(t);var e=n.h,r=n.c,i=n.l,u=t.h-e,a=t.c-r,o=t.l-i;return isNaN(a)&&(a=0,r=isNaN(r)?t.c:r),isNaN(u)?(u=0,e=isNaN(e)?t.h:e):u>180?u-=360:-180>u&&(u+=360),function(n){return $(e+u*n,r+a*n,i+o*n)+""}}function Rr(n,t){n=va.hsl(n),t=va.hsl(t);var e=n.h,r=n.s,i=n.l,u=t.h-e,a=t.s-r,o=t.l-i;return isNaN(a)&&(a=0,r=isNaN(r)?t.s:r),isNaN(u)?(u=0,e=isNaN(e)?t.h:e):u>180?u-=360:-180>u&&(u+=360),function(n){return O(e+u*n,r+a*n,i+o*n)+""}}function Yr(n,t){n=va.lab(n),t=va.lab(t);var e=n.l,r=n.a,i=n.b,u=t.l-e,a=t.a-r,o=t.b-i;return function(n){return G(e+u*n,r+a*n,i+o*n)+""}}function Ur(n,t){return t-=n,function(e){return Math.round(n+t*e)}}function Ir(n,t){return t=t-(n=+n)?1/(t-n):0,function(e){return(e-n)*t}}function Vr(n,t){return t=t-(n=+n)?1/(t-n):0,function(e){return Math.max(0,Math.min(1,(e-n)*t))}}function Xr(n){for(var t=n.source,e=n.target,r=Br(t,e),i=[t];t!==r;)t=t.parent,i.push(t);for(var u=i.length;e!==r;)i.splice(u,0,e),e=e.parent;return i}function Zr(n)
 {for(var t=[],e=n.parent;null!=e;)t.push(n),n=e,e=e.parent;return t.push(n),t}function Br(n,t){if(n===t)return n;for(var e=Zr(n),r=Zr(t),i=e.pop(),u=r.pop(),a=null;i===u;)a=i,i=e.pop(),u=r.pop();return a}function $r(n){n.fixed|=2}function Wr(n){n.fixed&=-7}function Jr(n){n.fixed|=4,n.px=n.x,n.py=n.y}function Gr(n){n.fixed&=-5}function Kr(n,t,e){var r=0,i=0;if(n.charge=0,!n.leaf)for(var u,a=n.nodes,o=a.length,c=-1;++c<o;)u=a[c],null!=u&&(Kr(u,t,e),n.charge+=u.charge,r+=u.charge*u.cx,i+=u.charge*u.cy);if(n.point){n.leaf||(n.point.x+=Math.random()-.5,n.point.y+=Math.random()-.5);var l=t*e[n.point.index];n.charge+=n.pointCharge=l,r+=l*n.point.x,i+=l*n.point.y}n.cx=r/n.charge,n.cy=i/n.charge}function Qr(n,t){return va.rebind(n,t,"sort","children","value"),n.nodes=n,n.links=ri,n}function ni(n){return n.children}function ti(n){return n.value}function ei(n,t){return t.value-n.value}function ri(n){return va.merge(n.map(function(n){return(n.children||[]).map(function(t){return{source:n,target
 :t}})}))}function ii(n){return n.x}function ui(n){return n.y}function ai(n,t,e){n.y0=t,n.y=e}function oi(n){return va.range(n.length)}function ci(n){for(var t=-1,e=n[0].length,r=[];++t<e;)r[t]=0;return r}function li(n){for(var t,e=1,r=0,i=n[0][1],u=n.length;u>e;++e)(t=n[e][1])>i&&(r=e,i=t);return r}function fi(n){return n.reduce(si,0)}function si(n,t){return n+t[1]}function hi(n,t){return gi(n,Math.ceil(Math.log(t.length)/Math.LN2+1))}function gi(n,t){for(var e=-1,r=+n[0],i=(n[1]-r)/t,u=[];++e<=t;)u[e]=i*e+r;return u}function pi(n){return[va.min(n),va.max(n)]}function di(n,t){return n.parent==t.parent?1:2}function mi(n){var t=n.children;return t&&t.length?t[0]:n._tree.thread}function vi(n){var t,e=n.children;return e&&(t=e.length)?e[t-1]:n._tree.thread}function yi(n,t){var e=n.children;if(e&&(i=e.length))for(var r,i,u=-1;++u<i;)t(r=yi(e[u],t),n)>0&&(n=r);return n}function Mi(n,t){return n.x-t.x}function xi(n,t){return t.x-n.x}function bi(n,t){return n.depth-t.depth}function _i(n,t){
 function e(n,r){var i=n.children;if(i&&(a=i.length))for(var u,a,o=null,c=-1;++c<a;)u=i[c],e(u,o),o=u;t(n,r)}e(n,null)}function wi(n){for(var t,e=0,r=0,i=n.children,u=i.length;--u>=0;)t=i[u]._tree,t.prelim+=e,t.mod+=e,e+=t.shift+(r+=t.change)}function Si(n,t,e){n=n._tree,t=t._tree;var r=e/(t.number-n.number);n.change+=r,t.change-=r,t.shift+=e,t.prelim+=e,t.mod+=e}function Ei(n,t,e){return n._tree.ancestor.parent==t.parent?n._tree.ancestor:e}function ki(n,t){return n.value-t.value}function Ai(n,t){var e=n._pack_next;n._pack_next=t,t._pack_prev=n,t._pack_next=e,e._pack_prev=t}function Ni(n,t){n._pack_next=t,t._pack_prev=n}function qi(n,t){var e=t.x-n.x,r=t.y-n.y,i=n.r+t.r;return.999*i*i>e*e+r*r}function Ti(n){function t(n){f=Math.min(n.x-n.r,f),s=Math.max(n.x+n.r,s),h=Math.min(n.y-n.r,h),g=Math.max(n.y+n.r,g)}if((e=n.children)&&(l=e.length)){var e,r,i,u,a,o,c,l,f=1/0,s=-1/0,h=1/0,g=-1/0;if(e.forEach(Ci),r=e[0],r.x=-r.r,r.y=0,t(r),l>1&&(i=e[1],i.x=i.r,i.y=0,t(i),l>2))for(u=e[2],ji(r,i,u
 ),t(u),Ai(r,u),r._pack_prev=u,Ai(u,i),i=r._pack_next,a=3;l>a;a++){ji(r,i,u=e[a]);var p=0,d=1,m=1;for(o=i._pack_next;o!==i;o=o._pack_next,d++)if(qi(o,u)){p=1;break}if(1==p)for(c=r._pack_prev;c!==o._pack_prev&&!qi(c,u);c=c._pack_prev,m++);p?(m>d||d==m&&i.r<r.r?Ni(r,i=o):Ni(r=c,i),a--):(Ai(r,u),i=u,t(u))}var v=(f+s)/2,y=(h+g)/2,M=0;for(a=0;l>a;a++)u=e[a],u.x-=v,u.y-=y,M=Math.max(M,u.r+Math.sqrt(u.x*u.x+u.y*u.y));n.r=M,e.forEach(zi)}}function Ci(n){n._pack_next=n._pack_prev=n}function zi(n){delete n._pack_next,delete n._pack_prev}function Di(n,t,e,r){var i=n.children;if(n.x=t+=r*n.x,n.y=e+=r*n.y,n.r*=r,i)for(var u=-1,a=i.length;++u<a;)Di(i[u],t,e,r)}function ji(n,t,e){var r=n.r+e.r,i=t.x-n.x,u=t.y-n.y;if(r&&(i||u)){var a=t.r+e.r,o=i*i+u*u;a*=a,r*=r;var c=.5+(r-a)/(2*o),l=Math.sqrt(Math.max(0,2*a*(r+o)-(r-=o)*r-a*a))/(2*o);e.x=n.x+c*i+l*u,e.y=n.y+c*u-l*i}else e.x=n.x+r,e.y=n.y}function Li(n){return 1+va.max(n,function(n){return n.y})}function Hi(n){return n.reduce(function(n,t){return n+
 t.x},0)/n.length}function Fi(n){var t=n.children;return t&&t.length?Fi(t[0]):n}function Pi(n){var t,e=n.children;return e&&(t=e.length)?Pi(e[t-1]):n}function Oi(n){return{x:n.x,y:n.y,dx:n.dx,dy:n.dy}}function Ri(n,t){var e=n.x+t[3],r=n.y+t[0],i=n.dx-t[1]-t[3],u=n.dy-t[0]-t[2];return 0>i&&(e+=i/2,i=0),0>u&&(r+=u/2,u=0),{x:e,y:r,dx:i,dy:u}}function Yi(n){var t=n[0],e=n[n.length-1];return e>t?[t,e]:[e,t]}function Ui(n){return n.rangeExtent?n.rangeExtent():Yi(n.range())}function Ii(n,t,e,r){var i=e(n[0],n[1]),u=r(t[0],t[1]);return function(n){return u(i(n))}}function Vi(n,t){var e,r=0,i=n.length-1,u=n[r],a=n[i];return u>a&&(e=r,r=i,i=e,e=u,u=a,a=e),n[r]=t.floor(u),n[i]=t.ceil(a),n}function Xi(n){return n?{floor:function(t){return Math.floor(t/n)*n},ceil:function(t){return Math.ceil(t/n)*n}}:Mc}function Zi(n,t,e,r){var i=[],u=[],a=0,o=Math.min(n.length,t.length)-1;for(n[o]<n[0]&&(n=n.slice().reverse(),t=t.slice().reverse());++a<=o;)i.push(e(n[a-1],n[a])),u.push(r(t[a-1],t[a]));return fun
 ction(t){var e=va.bisect(n,t,1,o)-1;return u[e](i[e](t))}}function Bi(n,t,e,r){function i(){var i=Math.min(n.length,t.length)>2?Zi:Ii,c=r?Vr:Ir;return a=i(n,t,c,e),o=i(t,n,c,wr),u}function u(n){return a(n)}var a,o;return u.invert=function(n){return o(n)},u.domain=function(t){return arguments.length?(n=t.map(Number),i()):n},u.range=function(n){return arguments.length?(t=n,i()):t},u.rangeRound=function(n){return u.range(n).interpolate(Ur)},u.clamp=function(n){return arguments.length?(r=n,i()):r},u.interpolate=function(n){return arguments.length?(e=n,i()):e},u.ticks=function(t){return Ki(n,t)},u.tickFormat=function(t,e){return Qi(n,t,e)},u.nice=function(t){return Wi(n,t),i()},u.copy=function(){return Bi(n,t,e,r)},i()}function $i(n,t){return va.rebind(n,t,"range","rangeRound","interpolate","clamp")}function Wi(n,t){return Vi(n,Xi(t?Gi(n,t)[2]:Ji(n)))}function Ji(n){var t=Yi(n),e=t[1]-t[0];return Math.pow(10,Math.round(Math.log(e)/Math.LN10)-1)}function Gi(n,t){var e=Yi(n),r=e[1]-e[0],i=
 Math.pow(10,Math.floor(Math.log(r/t)/Math.LN10)),u=t/r*i;return.15>=u?i*=10:.35>=u?i*=5:.75>=u&&(i*=2),e[0]=Math.ceil(e[0]/i)*i,e[1]=Math.floor(e[1]/i)*i+.5*i,e[2]=i,e}function Ki(n,t){return va.range.apply(va,Gi(n,t))}function Qi(n,t,e){var r=-Math.floor(Math.log(Gi(n,t)[2])/Math.LN10+.01);return va.format(e?e.replace(vo,function(n,t,e,i,u,a,o,c,l,f){return[t,e,i,u,a,o,c,l||"."+(r-2*("%"===f)),f].join("")}):",."+r+"f")}function nu(n,t,e,r,i){function u(t){return n(e(t))}return u.invert=function(t){return r(n.invert(t))},u.domain=function(t){return arguments.length?(t[0]<0?(e=ru,r=iu):(e=tu,r=eu),n.domain((i=t.map(Number)).map(e)),u):i},u.base=function(n){return arguments.length?(t=+n,u):t},u.nice=function(){function r(n){return Math.pow(t,Math.floor(Math.log(n)/Math.log(t)))}function a(n){return Math.pow(t,Math.ceil(Math.log(n)/Math.log(t)))}return n.domain(Vi(i,e===tu?{floor:r,ceil:a}:{floor:function(n){return-a(-n)},ceil:function(n){return-r(-n)}}).map(e)),u},u.ticks=function(){v
 ar i=Yi(n.domain()),u=[];if(i.every(isFinite)){var a=Math.log(t),o=Math.floor(i[0]/a),c=Math.ceil(i[1]/a),l=r(i[0]),f=r(i[1]),s=t%1?2:t;if(e===ru)for(u.push(-Math.pow(t,-o));o++<c;)for(var h=s-1;h>0;h--)u.push(-Math.pow(t,-o)*h);else{for(;c>o;o++)for(var h=1;s>h;h++)u.push(Math.pow(t,o)*h);u.push(Math.pow(t,o))}for(o=0;u[o]<l;o++);for(c=u.length;u[c-1]>f;c--);u=u.slice(o,c)}return u},u.tickFormat=function(n,i){if(!arguments.length)return xc;arguments.length<2?i=xc:"function"!=typeof i&&(i=va.format(i));var a,o=Math.log(t),c=Math.max(.1,n/u.ticks().length),l=e===ru?(a=-1e-12,Math.floor):(a=1e-12,Math.ceil);return function(n){return n/r(o*l(e(n)/o+a))<=c?i(n):""}},u.copy=function(){return nu(n.copy(),t,e,r,i)},$i(u,n)}function tu(n){return Math.log(0>n?0:n)}function eu(n){return Math.exp(n)}function ru(n){return-Math.log(n>0?0:-n)}function iu(n){return-Math.exp(-n)}function uu(n,t,e){function r(t){return n(i(t))}var i=au(t),u=au(1/t);return r.invert=function(t){return u(n.invert(t))},
 r.domain=function(t){return arguments.length?(n.domain((e=t.map(Number)).map(i)),r):e},r.ticks=function(n){return Ki(e,n)},r.tickFormat=function(n,t){return Qi(e,n,t)},r.nice=function(n){return r.domain(Wi(e,n))},r.exponent=function(a){return arguments.length?(i=au(t=a),u=au(1/t),n.domain(e.map(i)),r):t},r.copy=function(){return uu(n.copy(),t,e)},$i(r,n)}function au(n){return function(t){return 0>t?-Math.pow(-t,n):Math.pow(t,n)}}function ou(n,t){function e(t){return a[((u.get(t)||u.set(t,n.push(t)))-1)%a.length]}function r(t,e){return va.range(n.length).map(function(n){return t+e*n})}var u,a,o;return e.domain=function(r){if(!arguments.length)return n;n=[],u=new i;for(var a,o=-1,c=r.length;++o<c;)u.has(a=r[o])||u.set(a,n.push(a));return e[t.t].apply(e,t.a)},e.range=function(n){return arguments.length?(a=n,o=0,t={t:"range",a:arguments},e):a},e.rangePoints=function(i,u){arguments.length<2&&(u=0);var c=i[0],l=i[1],f=(l-c)/(Math.max(1,n.length-1)+u);return a=r(n.length<2?(c+l)/2:c+f*u/2,
 f),o=0,t={t:"rangePoints",a:arguments},e},e.rangeBands=function(i,u,c){arguments.length<2&&(u=0),arguments.length<3&&(c=u);var l=i[1]<i[0],f=i[l-0],s=i[1-l],h=(s-f)/(n.length-u+2*c);return a=r(f+h*c,h),l&&a.reverse(),o=h*(1-u),t={t:"rangeBands",a:arguments},e},e.rangeRoundBands=function(i,u,c){arguments.length<2&&(u=0),arguments.length<3&&(c=u);var l=i[1]<i[0],f=i[l-0],s=i[1-l],h=Math.floor((s-f)/(n.length-u+2*c)),g=s-f-(n.length-u)*h;return a=r(f+Math.round(g/2),h),l&&a.reverse(),o=Math.round(h*(1-u)),t={t:"rangeRoundBands",a:arguments},e},e.rangeBand=function(){return o},e.rangeExtent=function(){return Yi(t.a[0])},e.copy=function(){return ou(n,t)},e.domain(n)}function cu(n,t){function e(){var e=0,u=t.length;for(i=[];++e<u;)i[e-1]=va.quantile(n,e/u);return r}function r(n){return isNaN(n=+n)?void 0:t[va.bisect(i,n)]}var i;return r.domain=function(t){return arguments.length?(n=t.filter(function(n){return!isNaN(n)}).sort(va.ascending),e()):n},r.range=function(n){return arguments.lengt
 h?(t=n,e()):t},r.quantiles=function(){return i},r.copy=function(){return cu(n,t)},e()}function lu(n,t,e){function r(t){return e[Math.max(0,Math.min(a,Math.floor(u*(t-n))))]}function i(){return u=e.length/(t-n),a=e.length-1,r}var u,a;return r.domain=function(e){return arguments.length?(n=+e[0],t=+e[e.length-1],i()):[n,t]},r.range=function(n){return arguments.length?(e=n,i()):e},r.copy=function(){return lu(n,t,e)},r.invertExtent=function(t){return t=e.indexOf(t),t=0>t?0/0:t/u+n,[t,t+1/u]},i()}function fu(n,t){function e(e){return e>=e?t[va.bisect(n,e)]:void 0}return e.domain=function(t){return arguments.length?(n=t,e):n},e.range=function(n){return arguments.length?(t=n,e):t},e.invertExtent=function(e){return e=t.indexOf(e),[n[e-1],n[e]]},e.copy=function(){return fu(n,t)},e}function su(n){function t(n){return+n}return t.invert=t,t.domain=t.range=function(e){return arguments.length?(n=e.map(t),t):n},t.ticks=function(t){return Ki(n,t)},t.tickFormat=function(t,e){return Qi(n,t,e)},t.copy=
 function(){return su(n)},t}function hu(n){return n.innerRadius}function gu(n){return n.outerRadius}function pu(n){return n.startAngle}function du(n){return n.endAngle}function mu(n){for(var t,e,r,i=-1,u=n.length;++i<u;)t=n[i],e=t[0],r=t[1]+Ec,t[0]=e*Math.cos(r),t[1]=e*Math.sin(r);return n}function vu(n){function t(t){function c(){d.push("M",o(n(v),s),f,l(n(m.reverse()),s),"Z")}for(var h,g,p,d=[],m=[],v=[],y=-1,M=t.length,x=ft(e),b=ft(i),_=e===r?function(){return g}:ft(r),w=i===u?function(){return p}:ft(u);++y<M;)a.call(this,h=t[y],y)?(m.push([g=+x.call(this,h,y),p=+b.call(this,h,y)]),v.push([+_.call(this,h,y),+w.call(this,h,y)])):m.length&&(c(),m=[],v=[]);return m.length&&c(),d.length?d.join(""):null}var e=Pe,r=Pe,i=0,u=Oe,a=Rt,o=Re,c=o.key,l=o,f="L",s=.7;return t.x=function(n){return arguments.length?(e=r=n,t):r},t.x0=function(n){return arguments.length?(e=n,t):e},t.x1=function(n){return arguments.length?(r=n,t):r},t.y=function(n){return arguments.length?(i=u=n,t):u},t.y0=function(
 n){return arguments.length?(i=n,t):i},t.y1=function(n){return arguments.length?(u=n,t):u},t.defined=function(n){return arguments.length?(a=n,t):a},t.interpolate=function(n){return arguments.length?(c="function"==typeof n?o=n:(o=ic.get(n)||Re).key,l=o.reverse||o,f=o.closed?"M":"L",t):c},t.tension=function(n){return arguments.length?(s=n,t):s},t}function yu(n){return n.radius}function Mu(n){return[n.x,n.y]}function xu(n){return function(){var t=n.apply(this,arguments),e=t[0],r=t[1]+Ec;return[e*Math.cos(r),e*Math.sin(r)]}}function bu(){return 64}function _u(){return"circle"}function wu(n){var t=Math.sqrt(n/$a);return"M0,"+t+"A"+t+","+t+" 0 1,1 0,"+-t+"A"+t+","+t+" 0 1,1 0,"+t+"Z"}function Su(n,t){return Ta(n,Cc),n.id=t,n}function Eu(n,t,e,r){var i=n.id;return j(n,"function"==typeof e?function(n,u,a){n.__transition__[i].tween.set(t,r(e.call(n,n.__data__,u,a)))}:(e=r(e),function(n){n.__transition__[i].tween.set(t,e)}))}function ku(n){return null==n&&(n=""),function(){this.textContent=n}}
 function Au(n,t,e,r){var u=n.__transition__||(n.__transition__={active:0,count:0}),a=u[e];if(!a){var o=r.time;return a=u[e]={tween:new i,event:va.dispatch("start","end"),time:o,ease:r.ease,delay:r.delay,duration:r.duration},++u.count,va.timer(function(r){function i(r){return u.active>e?l():(u.active=e,h.start.call(n,f,t),a.tween.forEach(function(e,r){(r=r.call(n,f,t))&&d.push(r)}),c(r)||va.timer(c,0,o),1)}function c(r){if(u.active!==e)return l();for(var i=(r-g)/p,a=s(i),o=d.length;o>0;)d[--o].call(n,a);return i>=1?(l(),h.end.call(n,f,t),1):void 0}function l(){return--u.count?delete u[e]:delete n.__transition__,1}var f=n.__data__,s=a.ease,h=a.event,g=a.delay,p=a.duration,d=[];return r>=g?i(r):va.timer(i,g,o),1},0,o),a}}function Nu(n,t){n.attr("transform",function(n){return"translate("+t(n)+",0)"})}function qu(n,t){n.attr("transform",function(n){return"translate(0,"+t(n)+")"})}function Tu(n,t,e){if(r=[],e&&t.length>1){for(var r,i,u,a=Yi(n.domain()),o=-1,c=t.length,l=(t[1]-t[0])/++e;++
 o<c;)for(i=e;--i>0;)(u=+t[o]-i*l)>=a[0]&&r.push(u);for(--o,i=0;++i<e&&(u=+t[o]+i*l)<a[1];)r.push(u)}return r}function Cu(){this._=new Date(arguments.length>1?Date.UTC.apply(this,arguments):arguments[0])}function zu(n,t,e){function r(t){var e=n(t),r=u(e,1);return r-t>t-e?e:r}function i(e){return t(e=n(new Pc(e-1)),1),e}function u(n,e){return t(n=new Pc(+n),e),n}function a(n,r,u){var a=i(n),o=[];if(u>1)for(;r>a;)e(a)%u||o.push(new Date(+a)),t(a,1);else for(;r>a;)o.push(new Date(+a)),t(a,1);return o}function o(n,t,e){try{Pc=Cu;var r=new Cu;return r._=n,a(r,t,e)}finally{Pc=Date}}n.floor=n,n.round=r,n.ceil=i,n.offset=u,n.range=a;var c=n.utc=Du(n);return c.floor=c,c.round=Du(r),c.ceil=Du(i),c.offset=Du(u),c.range=o,n}function Du(n){return function(t,e){try{Pc=Cu;var r=new Cu;return r._=t,n(r,e)._}finally{Pc=Date}}}function ju(n,t,e,r){for(var i,u,a=0,o=t.length,c=e.length;o>a;){if(r>=c)return-1;if(i=t.charCodeAt(a++),37===i){if(u=ul[t.charAt(a++)],!u||(r=u(n,e,r))<0)return-1}else if(i!=e.
 charCodeAt(r++))return-1}return r}function Lu(n){return RegExp("^(?:"+n.map(va.requote).join("|")+")","i")}function Hu(n){for(var t=new i,e=-1,r=n.length;++e<r;)t.set(n[e].toLowerCase(),e);return t}function Fu(n,t,e){var r=0>n?"-":"",i=(r?-n:n)+"",u=i.length;return r+(e>u?Array(e-u+1).join(t)+i:i)}function Pu(n,t,e){Jc.lastIndex=0;var r=Jc.exec(t.substring(e));return r?(n.w=Gc.get(r[0].toLowerCase()),e+r[0].length):-1}function Ou(n,t,e){$c.lastIndex=0;var r=$c.exec(t.substring(e));return r?(n.w=Wc.get(r[0].toLowerCase()),e+r[0].length):-1}function Ru(n,t,e){al.lastIndex=0;var r=al.exec(t.substring(e,e+1));return r?(n.w=+r[0],e+r[0].length):-1}function Yu(n,t,e){al.lastIndex=0;var r=al.exec(t.substring(e));return r?(n.U=+r[0],e+r[0].length):-1}function Uu(n,t,e){al.lastIndex=0;var r=al.exec(t.substring(e));return r?(n.W=+r[0],e+r[0].length):-1}function Iu(n,t,e){nl.lastIndex=0;var r=nl.exec(t.substring(e));return r?(n.m=tl.get(r[0].toLowerCase()),e+r[0].length):-1}function Vu(n,t,e){
 Kc.lastIndex=0;var r=Kc.exec(t.substring(e));return r?(n.m=Qc.get(r[0].toLowerCase()),e+r[0].length):-1}function Xu(n,t,e){return ju(n,""+il.c,t,e)}function Zu(n,t,e){return ju(n,""+il.x,t,e)}function Bu(n,t,e){return ju(n,""+il.X,t,e)}function $u(n,t,e){al.lastIndex=0;var r=al.exec(t.substring(e,e+4));return r?(n.y=+r[0],e+r[0].length):-1}function Wu(n,t,e){al.lastIndex=0;var r=al.exec(t.substring(e,e+2));return r?(n.y=Ju(+r[0]),e+r[0].length):-1}function Ju(n){return n+(n>68?1900:2e3)}function Gu(n,t,e){al.lastIndex=0;var r=al.exec(t.substring(e,e+2));return r?(n.m=r[0]-1,e+r[0].length):-1}function Ku(n,t,e){al.lastIndex=0;var r=al.exec(t.substring(e,e+2));return r?(n.d=+r[0],e+r[0].length):-1}function Qu(n,t,e){al.lastIndex=0;var r=al.exec(t.substring(e,e+3));return r?(n.j=+r[0],e+r[0].length):-1}function na(n,t,e){al.lastIndex=0;var r=al.exec(t.substring(e,e+2));return r?(n.H=+r[0],e+r[0].length):-1}function ta(n,t,e){al.lastIndex=0;var r=al.exec(t.substring(e,e+2));return r?(n.
 M=+r[0],e+r[0].length):-1}function ea(n,t,e){al.lastIndex=0;var r=al.exec(t.substring(e,e+2));return r?(n.S=+r[0],e+r[0].length):-1}function ra(n,t,e){al.lastIndex=0;var r=al.exec(t.substring(e,e+3));return r?(n.L=+r[0],e+r[0].length):-1}function ia(n,t,e){var r=ol.get(t.substring(e,e+=2).toLowerCase());return null==r?-1:(n.p=r,e)}function ua(n){var t=n.getTimezoneOffset(),e=t>0?"-":"+",r=~~(Math.abs(t)/60),i=Math.abs(t)%60;return e+Fu(r,"0",2)+Fu(i,"0",2)}function aa(n,t,e){el.lastIndex=0;var r=el.exec(t.substring(e,e+1));return r?e+r[0].length:-1}function oa(n){return n.toISOString()}function ca(n,t,e){function r(t){return n(t)}return r.invert=function(t){return la(n.invert(t))},r.domain=function(t){return arguments.length?(n.domain(t),r):n.domain().map(la)},r.nice=function(n){return r.domain(Vi(r.domain(),n))},r.ticks=function(e,i){var u=Yi(r.domain());if("function"!=typeof e){var a=u[1]-u[0],o=a/e,c=va.bisect(ll,o);if(c==ll.length)return t.year(u,e);if(!c)return n.ticks(e).map(l
 a);Math.log(o/ll[c-1])<Math.log(ll[c]/o)&&--c,e=t[c],i=e[1],e=e[0].range}return e(u[0],new Date(+u[1]+1),i)},r.tickFormat=function(){return e},r.copy=function(){return ca(n.copy(),t,e)},$i(r,n)}function la(n){return new Date(n)}function fa(n){return function(t){for(var e=n.length-1,r=n[e];!r[1](t);)r=n[--e];return r[0](t)}}function sa(n){var t=new Date(n,0,1);return t.setFullYear(n),t}function ha(n){var t=n.getFullYear(),e=sa(t),r=sa(t+1);return t+(n-e)/(r-e)}function ga(n){var t=new Date(Date.UTC(n,0,1));return t.setUTCFullYear(n),t}function pa(n){var t=n.getUTCFullYear(),e=ga(t),r=ga(t+1);return t+(n-e)/(r-e)}function da(n){return JSON.parse(n.responseText)}function ma(n){var t=ya.createRange();return t.selectNode(ya.body),t.createContextualFragment(n.responseText)}var va={version:"3.2.1"};Date.now||(Date.now=function(){return+new Date});var ya=document,Ma=ya.documentElement,xa=window;try{ya.createElement("div").style.setProperty("opacity",0,"")}catch(ba){var _a=xa.CSSStyleDeclara
 tion.prototype,wa=_a.setProperty;_a.setProperty=function(n,t,e){wa.call(this,n,t+"",e)}}va.ascending=function(n,t){return t>n?-1:n>t?1:n>=t?0:0/0},va.descending=function(n,t){return n>t?-1:t>n?1:t>=n?0:0/0},va.min=function(n,t){var e,r,i=-1,u=n.length;if(arguments.length===1){for(;++i<u&&!((e=n[i])!=null&&e>=e);)e=void 0;for(;++i<u;)(r=n[i])!=null&&e>r&&(e=r)}else{for(;++i<u&&!((e=t.call(n,n[i],i))!=null&&e>=e);)e=void 0;for(;++i<u;)(r=t.call(n,n[i],i))!=null&&e>r&&(e=r)}return e},va.max=function(n,t){var e,r,i=-1,u=n.length;if(arguments.length===1){for(;++i<u&&!((e=n[i])!=null&&e>=e);)e=void 0;for(;++i<u;)(r=n[i])!=null&&r>e&&(e=r)}else{for(;++i<u&&!((e=t.call(n,n[i],i))!=null&&e>=e);)e=void 0;for(;++i<u;)(r=t.call(n,n[i],i))!=null&&r>e&&(e=r)}return e},va.extent=function(n,t){var e,r,i,u=-1,a=n.length;if(arguments.length===1){for(;++u<a&&!((e=i=n[u])!=null&&e>=e);)e=i=void 0;for(;++u<a;)(r=n[u])!=null&&(e>r&&(e=r),r>i&&(i=r))}else{for(;++u<a&&!((e=i=t.call(n,n[u],u))!=null&&e>=e);
 )e=void 0;for(;++u<a;)(r=t.call(n,n[u],u))!=null&&(e>r&&(e=r),r>i&&(i=r))}return[e,i]},va.sum=function(n,t){var e,r=0,i=n.length,u=-1;if(arguments.length===1)for(;++u<i;)isNaN(e=+n[u])||(r+=e);else for(;++u<i;)isNaN(e=+t.call(n,n[u],u))||(r+=e);return r},va.mean=function(t,e){var r,i=t.length,u=0,a=-1,o=0;if(arguments.length===1)for(;++a<i;)n(r=t[a])&&(u+=(r-u)/++o);else for(;++a<i;)n(r=e.call(t,t[a],a))&&(u+=(r-u)/++o);return o?u:void 0},va.quantile=function(n,t){var e=(n.length-1)*t+1,r=Math.floor(e),i=+n[r-1],u=e-r;return u?i+u*(n[r]-i):i},va.median=function(t,e){return arguments.length>1&&(t=t.map(e)),t=t.filter(n),t.length?va.quantile(t.sort(va.ascending),.5):void 0},va.bisector=function(n){return{left:function(t,e,r,i){for(arguments.length<3&&(r=0),arguments.length<4&&(i=t.length);i>r;){var u=r+i>>>1;n.call(t,t[u],u)<e?r=u+1:i=u}return r},right:function(t,e,r,i){for(arguments.length<3&&(r=0),arguments.length<4&&(i=t.length);i>r;){var u=r+i>>>1;e<n.call(t,t[u],u)?i=u:r=u+1}retu
 rn r}}};var Sa=va.bisector(function(n){return n});va.bisectLeft=Sa.left,va.bisect=va.bisectRight=Sa.right,va.shuffle=function(n){for(var t,e,r=n.length;r;)e=Math.random()*r--|0,t=n[r],n[r]=n[e],n[e]=t;return n},va.permute=function(n,t){for(var e=[],r=-1,i=t.length;++r<i;)e[r]=n[t[r]];return e},va.zip=function(){if(!(i=arguments.length))return[];for(var n=-1,e=va.min(arguments,t),r=Array(e);++n<e;)for(var i,u=-1,a=r[n]=Array(i);++u<i;)a[u]=arguments[u][n];return r},va.transpose=function(n){return va.zip.apply(va,n)},va.keys=function(n){var t=[];for(var e in n)t.push(e);return t},va.values=function(n){var t=[];for(var e in n)t.push(n[e]);return t},va.entries=function(n){var t=[];for(var e in n)t.push({key:e,value:n[e]});return t},va.merge=function(n){return Array.prototype.concat.apply([],n)},va.range=function(n,t,r){if(arguments.length<3&&(r=1,arguments.length<2&&(t=n,n=0)),1/0===(t-n)/r)throw Error("infinite range");var i,u=[],a=e(Math.abs(r)),o=-1;if(n*=a,t*=a,r*=a,0>r)for(;(i=n+r*
 ++o)>t;)u.push(i/a);else for(;(i=n+r*++o)<t;)u.push(i/a);return u},va.map=function(n){var t=new i;for(var e in n)t.set(e,n[e]);return t},r(i,{has:function(n){return Ea+n in this},get:function(n){return this[Ea+n]},set:function(n,t){return this[Ea+n]=t},remove:function(n){return n=Ea+n,n in this&&delete this[n]},keys:function(){var n=[];return this.forEach(function(t){n.push(t)}),n},values:function(){var n=[];return this.forEach(function(t,e){n.push(e)}),n},entries:function(){var n=[];return this.forEach(function(t,e){n.push({key:t,value:e})}),n},forEach:function(n){for(var t in this)t.charCodeAt(0)===ka&&n.call(this,t.substring(1),this[t])}});var Ea="\0",ka=Ea.charCodeAt(0);va.nest=function(){function n(t,o,c){if(c>=a.length)return r?r.call(u,o):e?o.sort(e):o;for(var l,f,s,h,g=-1,p=o.length,d=a[c++],m=new i;++g<p;)(h=m.get(l=d(f=o[g])))?h.push(f):m.set(l,[f]);return t?(f=t(),s=function(e,r){f.set(e,n(t,r,c))}):(f={},s=function(e,r){f[e]=n(t,r,c)}),m.forEach(s),f}function t(n,e){if(e
 >=a.length)return n;var r=[],i=o[e++];return n.forEach(function(n,i){r.push({key:n,values:t(i,e)})
-}),i?r.sort(function(n,t){return i(n.key,t.key)}):r}var e,r,u={},a=[],o=[];return u.map=function(t,e){return n(e,t,0)},u.entries=function(e){return t(n(va.map,e,0),0)},u.key=function(n){return a.push(n),u},u.sortKeys=function(n){return o[a.length-1]=n,u},u.sortValues=function(n){return e=n,u},u.rollup=function(n){return r=n,u},u},va.set=function(n){var t=new u;if(n)for(var e=0;e<n.length;e++)t.add(n[e]);return t},r(u,{has:function(n){return Ea+n in this},add:function(n){return this[Ea+n]=!0,n},remove:function(n){return n=Ea+n,n in this&&delete this[n]},values:function(){var n=[];return this.forEach(function(t){n.push(t)}),n},forEach:function(n){for(var t in this)t.charCodeAt(0)===ka&&n.call(this,t.substring(1))}}),va.behavior={},va.rebind=function(n,t){for(var e,r=1,i=arguments.length;++r<i;)n[e=arguments[r]]=a(n,t,t[e]);return n},va.dispatch=function(){for(var n=new o,t=-1,e=arguments.length;++t<e;)n[arguments[t]]=c(n);return n},o.prototype.on=function(n,t){var e=n.indexOf("."),r="
 ";if(e>=0&&(r=n.substring(e+1),n=n.substring(0,e)),n)return arguments.length<2?this[n].on(r):this[n].on(r,t);if(arguments.length===2){if(null==t)for(n in this)this.hasOwnProperty(n)&&this[n].on(r,null);return this}},va.event=null,va.mouse=function(n){return g(n,f())};var Aa=/WebKit/.test(xa.navigator.userAgent)?-1:0,Na=d;try{Na(Ma.childNodes)[0].nodeType}catch(qa){Na=p}var Ta=[].__proto__?function(n,t){n.__proto__=t}:function(n,t){for(var e in t)n[e]=t[e]};va.touches=function(n,t){return arguments.length<2&&(t=f().touches),t?Na(t).map(function(t){var e=g(n,t);return e.identifier=t.identifier,e}):[]};var Ca=["webkit","ms","moz","Moz","o","O"],za=m(Ma.style,"userSelect"),Da=za?function(){var n=Ma.style,t=n[za];return n[za]="none",function(){n[za]=t}}:function(n){var t=va.select(xa).on("selectstart."+n,l);return function(){t.on("selectstart."+n,null)}};va.behavior.drag=function(){function n(){this.on("mousedown.drag",t).on("touchstart.drag",t)}function t(){function n(){var n=a.parentNo
 de;return null!=f?va.touches(n).filter(function(n){return n.identifier===f})[0]:va.mouse(n)}function t(){if(!a.parentNode)return i();var t=n(),e=t[0]-h[0],r=t[1]-h[1];g|=e|r,h=t,l(),o({type:"drag",x:t[0]+u[0],y:t[1]+u[1],dx:e,dy:r})}function i(){o({type:"dragend"}),g&&(l(),va.event.target===c&&s(d,"click")),d.on(null!=f?"touchmove.drag-"+f:"mousemove.drag",null).on(null!=f?"touchend.drag-"+f:"mouseup.drag",null),p()}var u,a=this,o=e.of(a,arguments),c=va.event.target,f=va.event.touches?va.event.changedTouches[0].identifier:null,h=n(),g=0,p=Da(null!=f?"drag-"+f:"drag"),d=va.select(xa).on(null!=f?"touchmove.drag-"+f:"mousemove.drag",t).on(null!=f?"touchend.drag-"+f:"mouseup.drag",i,!0);r?(u=r.apply(a,arguments),u=[u.x-h[0],u.y-h[1]]):u=[0,0],o({type:"dragstart"})}var e=h(n,"drag","dragstart","dragend"),r=null;return n.origin=function(t){return arguments.length?(r=t,n):r},va.rebind(n,e,"on")};var ja=function(n,t){return t.querySelector(n)},La=function(n,t){return t.querySelectorAll(n)},
 Ha=Ma[m(Ma,"matchesSelector")],Fa=function(n,t){return Ha.call(n,t)};"function"==typeof Sizzle&&(ja=function(n,t){return Sizzle(n,t)[0]||null},La=function(n,t){return Sizzle.uniqueSort(Sizzle(n,t))},Fa=Sizzle.matchesSelector),va.selection=function(){return Ia};var Pa=va.selection.prototype=[];Pa.select=function(n){var t,e,r,i,u=[];"function"!=typeof n&&(n=y(n));for(var a=-1,o=this.length;++a<o;){u.push(t=[]),t.parentNode=(r=this[a]).parentNode;for(var c=-1,l=r.length;++c<l;)(i=r[c])?(t.push(e=n.call(i,i.__data__,c)),e&&"__data__"in i&&(e.__data__=i.__data__)):t.push(null)}return v(u)},Pa.selectAll=function(n){var t,e,r=[];"function"!=typeof n&&(n=M(n));for(var i=-1,u=this.length;++i<u;)for(var a=this[i],o=-1,c=a.length;++o<c;)(e=a[o])&&(r.push(t=Na(n.call(e,e.__data__,o))),t.parentNode=e);return v(r)};var Oa={svg:"http://www.w3.org/2000/svg",xhtml:"http://www.w3.org/1999/xhtml",xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2
 000/xmlns/"};va.ns={prefix:Oa,qualify:function(n){var t=n.indexOf(":"),e=n;return t>=0&&(e=n.substring(0,t),n=n.substring(t+1)),Oa.hasOwnProperty(e)?{space:Oa[e],local:n}:n}},Pa.attr=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node();return n=va.ns.qualify(n),n.local?e.getAttributeNS(n.space,n.local):e.getAttribute(n)}for(t in n)this.each(x(t,n[t]));return this}return this.each(x(n,t))},va.requote=function(n){return n.replace(Ra,"\\$&")};var Ra=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;Pa.classed=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node(),r=(n=n.trim().split(/^|\s+/g)).length,i=-1;if(t=e.classList){for(;++i<r;)if(!t.contains(n[i]))return!1}else for(t=e.getAttribute("class");++i<r;)if(!_(n[i]).test(t))return!1;return!0}for(t in n)this.each(w(t,n[t]));return this}return this.each(w(n,t))},Pa.style=function(n,t,e){var r=arguments.length;if(3>r){if("string"!=typeof n){2>r&&(t="");for(e in n)this.each(E(e,n[e],t));return this}if(2>r)r
 eturn xa.getComputedStyle(this.node(),null).getPropertyValue(n);e=""}return this.each(E(n,t,e))},Pa.property=function(n,t){if(arguments.length<2){if("string"==typeof n)return this.node()[n];for(t in n)this.each(k(t,n[t]));return this}return this.each(k(n,t))},Pa.text=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.textContent=null==t?"":t}:null==n?function(){this.textContent=""}:function(){this.textContent=n}):this.node().textContent},Pa.html=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.innerHTML=null==t?"":t}:null==n?function(){this.innerHTML=""}:function(){this.innerHTML=n}):this.node().innerHTML},Pa.append=function(n){function t(){return this.appendChild(ya.createElementNS(this.namespaceURI,n))}function e(){return this.appendChild(ya.createElementNS(n.space,n.local))}return n=va.ns.qualify(n),this.select(n.local?e:t)},Pa.insert=function(n,t){functio
 n e(e,r){return this.insertBefore(ya.createElementNS(this.namespaceURI,n),t.call(this,e,r))}function r(e,r){return this.insertBefore(ya.createElementNS(n.space,n.local),t.call(this,e,r))}return n=va.ns.qualify(n),"function"!=typeof t&&(t=y(t)),this.select(n.local?r:e)},Pa.remove=function(){return this.each(function(){var n=this.parentNode;n&&n.removeChild(this)})},Pa.data=function(n,t){function e(n,e){var r,u,a,o=n.length,s=e.length,h=Math.min(o,s),g=Array(s),p=Array(s),d=Array(o);if(t){var m,v=new i,y=new i,M=[];for(r=-1;++r<o;)m=t.call(u=n[r],u.__data__,r),v.has(m)?d[r]=u:v.set(m,u),M.push(m);for(r=-1;++r<s;)m=t.call(e,a=e[r],r),(u=v.get(m))?(g[r]=u,u.__data__=a):y.has(m)||(p[r]=A(a)),y.set(m,a),v.remove(m);for(r=-1;++r<o;)v.has(M[r])&&(d[r]=n[r])}else{for(r=-1;++r<h;)u=n[r],a=e[r],u?(u.__data__=a,g[r]=u):p[r]=A(a);for(;s>r;++r)p[r]=A(e[r]);for(;o>r;++r)d[r]=n[r]}p.update=g,p.parentNode=g.parentNode=d.parentNode=n.parentNode,c.push(p),l.push(g),f.push(d)}var r,u,a=-1,o=this.length
 ;if(!arguments.length){for(n=Array(o=(r=this[0]).length);++a<o;)(u=r[a])&&(n[a]=u.__data__);return n}var c=L([]),l=v([]),f=v([]);if("function"==typeof n)for(;++a<o;)e(r=this[a],n.call(r,r.parentNode.__data__,a));else for(;++a<o;)e(r=this[a],n);return l.enter=function(){return c},l.exit=function(){return f},l},Pa.datum=function(n){return arguments.length?this.property("__data__",n):this.property("__data__")},Pa.filter=function(n){var t,e,r,i=[];"function"!=typeof n&&(n=N(n));for(var u=0,a=this.length;a>u;u++){i.push(t=[]),t.parentNode=(e=this[u]).parentNode;for(var o=0,c=e.length;c>o;o++)(r=e[o])&&n.call(r,r.__data__,o)&&t.push(r)}return v(i)},Pa.order=function(){for(var n=-1,t=this.length;++n<t;)for(var e,r=this[n],i=r.length-1,u=r[i];--i>=0;)(e=r[i])&&(u&&u!==e.nextSibling&&u.parentNode.insertBefore(e,u),u=e);return this},Pa.sort=function(n){n=q.apply(this,arguments);for(var t=-1,e=this.length;++t<e;)this[t].sort(n);return this.order()},Pa.on=function(n,t,e){var r=arguments.length;
 if(3>r){if("string"!=typeof n){2>r&&(t=!1);for(e in n)this.each(C(e,n[e],t));return this}if(2>r)return(r=this.node()["__on"+n])&&r._;e=!1}return this.each(C(n,t,e))};var Ya=va.map({mouseenter:"mouseover",mouseleave:"mouseout"});Ya.forEach(function(n){"on"+n in ya&&Ya.remove(n)}),Pa.each=function(n){return j(this,function(t,e,r){n.call(t,t.__data__,e,r)})},Pa.call=function(n){var t=Na(arguments);return n.apply(t[0]=this,t),this},Pa.empty=function(){return!this.node()},Pa.node=function(){for(var n=0,t=this.length;t>n;n++)for(var e=this[n],r=0,i=e.length;i>r;r++){var u=e[r];if(u)return u}return null},Pa.size=function(){var n=0;return this.each(function(){++n}),n};var Ua=[];va.selection.enter=L,va.selection.enter.prototype=Ua,Ua.append=Pa.append,Ua.insert=Pa.insert,Ua.empty=Pa.empty,Ua.node=Pa.node,Ua.call=Pa.call,Ua.size=Pa.size,Ua.select=function(n){for(var t,e,r,i,u,a=[],o=-1,c=this.length;++o<c;){r=(i=this[o]).update,a.push(t=[]),t.parentNode=i.parentNode;for(var l=-1,f=i.length;++l
 <f;)(u=i[l])?(t.push(r[l]=e=n.call(i.parentNode,u.__data__,l)),e.__data__=u.__data__):t.push(null)}return v(a)},Pa.transition=function(){var n,t,e=Nc||++zc,r=[],i=Object.create(Dc);i.time=Date.now();for(var u=-1,a=this.length;++u<a;){r.push(n=[]);for(var o=this[u],c=-1,l=o.length;++c<l;)(t=o[c])&&Au(t,c,e,i),n.push(t)}return Su(r,e)},va.select=function(n){var t=["string"==typeof n?ja(n,ya):n];return t.parentNode=Ma,v([t])},va.selectAll=function(n){var t=Na("string"==typeof n?La(n,ya):n);return t.parentNode=Ma,v([t])};var Ia=va.select(Ma);va.behavior.zoom=function(){function n(){this.on("mousedown.zoom",o).on("mousemove.zoom",f).on(Za+".zoom",c).on("dblclick.zoom",g).on("touchstart.zoom",p).on("touchmove.zoom",d).on("touchend.zoom",p)}function t(n){return[(n[0]-S[0])/E,(n[1]-S[1])/E]}function e(n){return[n[0]*E+S[0],n[1]*E+S[1]]}function r(n){E=Math.max(k[0],Math.min(k[1],n))}function i(n,t){t=e(t),S[0]+=n[0]-t[0],S[1]+=n[1]-t[1]}function u(){x&&x.domain(M.range().map(function(n){ret
 urn(n-S[0])/E}).map(M.invert)),_&&_.domain(b.range().map(function(n){return(n-S[1])/E}).map(b.invert))}function a(n){u(),va.event.preventDefault(),n({type:"zoom",scale:E,translate:S})}function o(){function n(){c=1,i(va.mouse(r),h),a(u)}function e(){c&&l(),f.on("mousemove.zoom",null).on("mouseup.zoom",null),g(),c&&va.event.target===o&&s(f,"click.zoom")}var r=this,u=A.of(r,arguments),o=va.event.target,c=0,f=va.select(xa).on("mousemove.zoom",n).on("mouseup.zoom",e),h=t(va.mouse(r)),g=Da("zoom")}function c(){m||(m=t(va.mouse(this))),r(Math.pow(2,Va()*.002)*E),i(va.mouse(this),m),a(A.of(this,arguments))}function f(){m=null}function g(){var n=va.mouse(this),e=t(n),u=Math.log(E)/Math.LN2;r(Math.pow(2,va.event.shiftKey?Math.ceil(u)-1:Math.floor(u)+1)),i(n,e),a(A.of(this,arguments))}function p(){var n=va.touches(this),e=Date.now();if(y=E,m={},v=0,n.forEach(function(n){m[n.identifier]=t(n)}),n.length===1){if(500>e-w){var u=n[0],o=t(n[0]);r(2*E),i(u,o),a(A.of(this,arguments))}w=e}else if(n.len
 gth>1){var u=n[0],c=n[1],l=u[0]-c[0],f=u[1]-c[1];v=l*l+f*f}}function d(){var n=va.touches(this),t=n[0],e=m[t.identifier];if(u=n[1]){var u,o=m[u.identifier],c=va.event.scale;if(null==c){var l=(l=u[0]-t[0])*l+(l=u[1]-t[1])*l;c=v&&Math.sqrt(l/v)}t=[(t[0]+u[0])/2,(t[1]+u[1])/2],e=[(e[0]+o[0])/2,(e[1]+o[1])/2],r(c*y)}i(t,e),w=null,a(A.of(this,arguments))}var m,v,y,M,x,b,_,w,S=[0,0],E=1,k=Xa,A=h(n,"zoom");return n.translate=function(t){return arguments.length?(S=t.map(Number),u(),n):S},n.scale=function(t){return arguments.length?(E=+t,u(),n):E},n.scaleExtent=function(t){return arguments.length?(k=null==t?Xa:t.map(Number),n):k},n.x=function(t){return arguments.length?(x=t,M=t.copy(),S=[0,0],E=1,n):x},n.y=function(t){return arguments.length?(_=t,b=t.copy(),S=[0,0],E=1,n):_},va.rebind(n,A,"on")};var Va,Xa=[0,1/0],Za="onwheel"in ya?(Va=function(){return-va.event.deltaY*(va.event.deltaMode?120:1)},"wheel"):"onmousewheel"in ya?(Va=function(){return va.event.wheelDelta},"mousewheel"):(Va=functio
 n(){return-va.event.detail},"MozMousePixelScroll");H.prototype.toString=function(){return this.rgb()+""},va.hsl=function(n,t,e){return arguments.length===1?n instanceof P?F(n.h,n.s,n.l):ut(""+n,at,F):F(+n,+t,+e)};var Ba=P.prototype=new H;Ba.brighter=function(n){return n=Math.pow(.7,arguments.length?n:1),F(this.h,this.s,this.l/n)},Ba.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),F(this.h,this.s,n*this.l)},Ba.rgb=function(){return O(this.h,this.s,this.l)};var $a=Math.PI,Wa=1e-6,Ja=Wa*Wa,Ga=$a/180,Ka=180/$a;va.hcl=function(n,t,e){return arguments.length===1?n instanceof B?Z(n.h,n.c,n.l):n instanceof J?K(n.l,n.a,n.b):K((n=ot((n=va.rgb(n)).r,n.g,n.b)).l,n.a,n.b):Z(+n,+t,+e)};var Qa=B.prototype=new H;Qa.brighter=function(n){return Z(this.h,this.c,Math.min(100,this.l+no*(arguments.length?n:1)))},Qa.darker=function(n){return Z(this.h,this.c,Math.max(0,this.l-no*(arguments.length?n:1)))},Qa.rgb=function(){return $(this.h,this.c,this.l).rgb()},va.lab=function(n,t,e){return arg
 uments.length===1?n instanceof J?W(n.l,n.a,n.b):n instanceof B?$(n.l,n.c,n.h):ot((n=va.rgb(n)).r,n.g,n.b):W(+n,+t,+e)};var no=18,to=.95047,eo=1,ro=1.08883,io=J.prototype=new H;io.brighter=function(n){return W(Math.min(100,this.l+no*(arguments.length?n:1)),this.a,this.b)},io.darker=function(n){return W(Math.max(0,this.l-no*(arguments.length?n:1)),this.a,this.b)},io.rgb=function(){return G(this.l,this.a,this.b)},va.rgb=function(n,t,e){return arguments.length===1?n instanceof rt?et(n.r,n.g,n.b):ut(""+n,et,O):et(~~n,~~t,~~e)};var uo=rt.prototype=new H;uo.brighter=function(n){n=Math.pow(.7,arguments.length?n:1);var t=this.r,e=this.g,r=this.b,i=30;return t||e||r?(t&&i>t&&(t=i),e&&i>e&&(e=i),r&&i>r&&(r=i),et(Math.min(255,Math.floor(t/n)),Math.min(255,Math.floor(e/n)),Math.min(255,Math.floor(r/n)))):et(i,i,i)},uo.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),et(Math.floor(n*this.r),Math.floor(n*this.g),Math.floor(n*this.b))},uo.hsl=function(){return at(this.r,this.g,this.b)}
 ,uo.toString=function(){return"#"+it(this.r)+it(this.g)+it(this.b)};var ao=va.map({aliceblue:"#f0f8ff",antiquewhite:"#faebd7",aqua:"#00ffff",aquamarine:"#7fffd4",azure:"#f0ffff",beige:"#f5f5dc",bisque:"#ffe4c4",black:"#000000",blanchedalmond:"#ffebcd",blue:"#0000ff",blueviolet:"#8a2be2",brown:"#a52a2a",burlywood:"#deb887",cadetblue:"#5f9ea0",chartreuse:"#7fff00",chocolate:"#d2691e",coral:"#ff7f50",cornflowerblue:"#6495ed",cornsilk:"#fff8dc",crimson:"#dc143c",cyan:"#00ffff",darkblue:"#00008b",darkcyan:"#008b8b",darkgoldenrod:"#b8860b",darkgray:"#a9a9a9",darkgreen:"#006400",darkgrey:"#a9a9a9",darkkhaki:"#bdb76b",darkmagenta:"#8b008b",darkolivegreen:"#556b2f",darkorange:"#ff8c00",darkorchid:"#9932cc",darkred:"#8b0000",darksalmon:"#e9967a",darkseagreen:"#8fbc8f",darkslateblue:"#483d8b",darkslategray:"#2f4f4f",darkslategrey:"#2f4f4f",darkturquoise:"#00ced1",darkviolet:"#9400d3",deeppink:"#ff1493",deepskyblue:"#00bfff",dimgray:"#696969",dimgrey:"#696969",dodgerblue:"#1e90ff",firebrick:"#b
 22222",floralwhite:"#fffaf0",forestgreen:"#228b22",fuchsia:"#ff00ff",gainsboro:"#dcdcdc",ghostwhite:"#f8f8ff",gold:"#ffd700",goldenrod:"#daa520",gray:"#808080",green:"#008000",greenyellow:"#adff2f",grey:"#808080",honeydew:"#f0fff0",hotpink:"#ff69b4",indianred:"#cd5c5c",indigo:"#4b0082",ivory:"#fffff0",khaki:"#f0e68c",lavender:"#e6e6fa",lavenderblush:"#fff0f5",lawngreen:"#7cfc00",lemonchiffon:"#fffacd",lightblue:"#add8e6",lightcoral:"#f08080",lightcyan:"#e0ffff",lightgoldenrodyellow:"#fafad2",lightgray:"#d3d3d3",lightgreen:"#90ee90",lightgrey:"#d3d3d3",lightpink:"#ffb6c1",lightsalmon:"#ffa07a",lightseagreen:"#20b2aa",lightskyblue:"#87cefa",lightslategray:"#778899",lightslategrey:"#778899",lightsteelblue:"#b0c4de",lightyellow:"#ffffe0",lime:"#00ff00",limegreen:"#32cd32",linen:"#faf0e6",magenta:"#ff00ff",maroon:"#800000",mediumaquamarine:"#66cdaa",mediumblue:"#0000cd",mediumorchid:"#ba55d3",mediumpurple:"#9370db",mediumseagreen:"#3cb371",mediumslateblue:"#7b68ee",mediumspringgreen:"#00
 fa9a",mediumturquoise:"#48d1cc",mediumvioletred:"#c71585",midnightblue:"#191970",mintcream:"#f5fffa",mistyrose:"#ffe4e1",moccasin:"#ffe4b5",navajowhite:"#ffdead",navy:"#000080",oldlace:"#fdf5e6",olive:"#808000",olivedrab:"#6b8e23",orange:"#ffa500",orangered:"#ff4500",orchid:"#da70d6",palegoldenrod:"#eee8aa",palegreen:"#98fb98",paleturquoise:"#afeeee",palevioletred:"#db7093",papayawhip:"#ffefd5",peachpuff:"#ffdab9",peru:"#cd853f",pink:"#ffc0cb",plum:"#dda0dd",powderblue:"#b0e0e6",purple:"#800080",red:"#ff0000",rosybrown:"#bc8f8f",royalblue:"#4169e1",saddlebrown:"#8b4513",salmon:"#fa8072",sandybrown:"#f4a460",seagreen:"#2e8b57",seashell:"#fff5ee",sienna:"#a0522d",silver:"#c0c0c0",skyblue:"#87ceeb",slateblue:"#6a5acd",slategray:"#708090",slategrey:"#708090",snow:"#fffafa",springgreen:"#00ff7f",steelblue:"#4682b4",tan:"#d2b48c",teal:"#008080",thistle:"#d8bfd8",tomato:"#ff6347",turquoise:"#40e0d0",violet:"#ee82ee",wheat:"#f5deb3",white:"#ffffff",whitesmoke:"#f5f5f5",yellow:"#ffff00",yell
 owgreen:"#9acd32"});ao.forEach(function(n,t){ao.set(n,ut(t,et,O))}),va.functor=ft,va.xhr=ht(st),va.dsv=function(n,t){function e(n,e,u){arguments.length<3&&(u=e,e=null);var a=va.xhr(n,t,u);return a.row=function(n){return arguments.length?a.response((e=n)==null?r:i(n)):e},a.row(e)}function r(n){return e.parse(n.responseText)}function i(n){return function(t){return e.parse(t.responseText,n)}}function a(t){return t.map(o).join(n)}function o(n){return c.test(n)?'"'+n.replace(/\"/g,'""')+'"':n}var c=RegExp('["'+n+"\n]"),l=n.charCodeAt(0);return e.parse=function(n,t){var r;return e.parseRows(n,function(n,e){if(r)return r(n,e-1);var i=Function("d","return {"+n.map(function(n,t){return JSON.stringify(n)+": d["+t+"]"}).join(",")+"}");r=t?function(n,e){return t(i(n),e)}:i})},e.parseRows=function(n,t){function e(){if(f>=c)return a;if(i)return i=!1,u;var t=f;if(n.charCodeAt(t)===34){for(var e=t;e++<c;)if(n.charCodeAt(e)===34){if(n.charCodeAt(e+1)!==34)break;++e}f=e+2;var r=n.charCodeAt(e+1);retu
 rn 13===r?(i=!0,n.charCodeAt(e+2)===10&&++f):10===r&&(i=!0),n.substring(t+1,e).replace(/""/g,'"')}for(;c>f;){var r=n.charCodeAt(f++),o=1;if(10===r)i=!0;else if(13===r)i=!0,n.charCodeAt(f)===10&&(++f,++o);else if(r!==l)continue;return n.substring(t,f-o)}return n.substring(t)}for(var r,i,u={},a={},o=[],c=n.length,f=0,s=0;(r=e())!==a;){for(var h=[];r!==u&&r!==a;)h.push(r),r=e();(!t||(h=t(h,s++)))&&o.push(h)}return o},e.format=function(t){if(Array.isArray(t[0]))return e.formatRows(t);var r=new u,i=[];return t.forEach(function(n){for(var t in n)r.has(t)||i.push(r.add(t))}),[i.map(o).join(n)].concat(t.map(function(t){return i.map(function(n){return o(t[n])}).join(n)})).join("\n")},e.formatRows=function(n){return n.map(a).join("\n")},e},va.csv=va.dsv(",","text/csv"),va.tsv=va.dsv("	","text/tab-separated-values");var oo,co,lo,fo;va.timer=function(n,t,e){if(arguments.length<3){if(arguments.length<2)t=0;else if(!isFinite(t))return;e=Date.now()}var r=e+t,i={callback:n,time:r,next:null};co?co.n
 ext=i:oo=i,co=i,lo||(fo=clearTimeout(fo),lo=1,so(dt))},va.timer.flush=function(){mt(),vt()};var so=xa[m(xa,"requestAnimationFrame")]||function(n){setTimeout(n,17)},ho=".",go=",",po=[3,3],mo=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"].map(yt);va.formatPrefix=function(n,t){var e=0;return n&&(0>n&&(n*=-1),t&&(n=va.round(n,Mt(n,t))),e=1+Math.floor(1e-12+Math.log(n)/Math.LN10),e=Math.max(-24,Math.min(24,Math.floor((0>=e?e+1:e-1)/3)*3))),mo[8+e/3]},va.round=function(n,t){return t?Math.round(n*(t=Math.pow(10,t)))/t:Math.round(n)},va.format=function(n){var t=vo.exec(n),e=t[1]||" ",r=t[2]||">",i=t[3]||"",u=t[4]||"",a=t[5],o=+t[6],c=t[7],l=t[8],f=t[9],s=1,h="",g=!1;switch(l&&(l=+l.substring(1)),(a||"0"===e&&"="===r)&&(a=e="0",r="=",c&&(o-=Math.floor((o-1)/4))),f){case"n":c=!0,f="g";break;case"%":s=100,h="%",f="f";break;case"p":s=100,h="%",f="r";break;case"b":case"o":case"x":case"X":u&&(u="0"+f.toLowerCase());case"c":case"d":g=!0,l=0;break;case"s":s=-1,f="r"}"#"===u&&(
 u=""),"r"!=f||l||(f="g"),null!=l&&("g"==f?l=Math.max(1,Math.min(21,l)):("e"==f||"f"==f)&&(l=Math.max(0,Math.min(20,l)))),f=yo.get(f)||xt;var p=a&&c;return function(n){if(g&&n%1)return"";var t=0>n||0===n&&0>1/n?(n=-n,"-"):i;if(0>s){var d=va.formatPrefix(n,l);n=d.scale(n),h=d.symbol}else n*=s;n=f(n,l),!a&&c&&(n=Mo(n));var m=u.length+n.length+(p?0:t.length),v=o>m?Array(m=o-m+1).join(e):"";return p&&(n=Mo(v+n)),ho&&n.replace(".",ho),t+=u,("<"===r?t+n+v:">"===r?v+t+n:"^"===r?v.substring(0,m>>=1)+t+n+v.substring(m):t+(p?n:v+n))+h}};var vo=/(?:([^{])?([<>=^]))?([+\- ])?(#)?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i,yo=va.map({b:function(n){return n.toString(2)},c:function(n){return String.fromCharCode(n)},o:function(n){return n.toString(8)},x:function(n){return n.toString(16)},X:function(n){return n.toString(16).toUpperCase()},g:function(n,t){return n.toPrecision(t)},e:function(n,t){return n.toExponential(t)},f:function(n,t){return n.toFixed(t)},r:function(n,t){return(n=va.round(n,Mt(n,t))).toFix
 ed(Math.max(0,Math.min(20,Mt(n*(1+1e-15),t))))}}),Mo=st;if(po){var xo=po.length;Mo=function(n){for(var t=n.lastIndexOf("."),e=t>=0?"."+n.substring(t+1):(t=n.length,""),r=[],i=0,u=po[0];t>0&&u>0;)r.push(n.substring(t-=u,t+u)),u=po[i=(i+1)%xo];return r.reverse().join(go||"")+e}}va.geo={},bt.prototype={s:0,t:0,add:function(n){_t(n,this.t,bo),_t(bo.s,this.s,this),this.s?this.t+=bo.t:this.s=bo.t},reset:function(){this.s=this.t=0},valueOf:function(){return this.s}};var bo=new bt;va.geo.stream=function(n,t){n&&_o.hasOwnProperty(n.type)?_o[n.type](n,t):wt(n,t)};var _o={Feature:function(n,t){wt(n.geometry,t)},FeatureCollection:function(n,t){for(var e=n.features,r=-1,i=e.length;++r<i;)wt(e[r].geometry,t)}},wo={Sphere:function(n,t){t.sphere()},Point:function(n,t){var e=n.coordinates;t.point(e[0],e[1])},MultiPoint:function(n,t){for(var e,r=n.coordinates,i=-1,u=r.length;++i<u;)e=r[i],t.point(e[0],e[1])},LineString:function(n,t){St(n.coordinates,t,0)},MultiLineString:function(n,t){for(var e=n.coo
 rdinates,r=-1,i=e.length;++r<i;)St(e[r],t,0)},Polygon:function(n,t){Et(n.coordinates,t)},MultiPolygon:function(n,t){for(var e=n.coordinates,r=-1,i=e.length;++r<i;)Et(e[r],t)},GeometryCollection:function(n,t){for(var e=n.geometries,r=-1,i=e.length;++r<i;)wt(e[r],t)}};va.geo.area=function(n){return So=0,va.geo.stream(n,ko),So};var So,Eo=new bt,ko={sphere:function(){So+=4*$a},point:T,lineStart:T,lineEnd:T,polygonStart:function(){Eo.reset(),ko.lineStart=kt},polygonEnd:function(){var n=2*Eo;So+=0>n?4*$a+n:n,ko.lineStart=ko.lineEnd=ko.point=T}};va.geo.bounds=function(){function n(n,t){M.push(x=[f=n,h=n]),s>t&&(s=t),t>g&&(g=t)}function t(t,e){var r=At([t*Ga,e*Ga]);if(v){var i=qt(v,r),u=[i[1],-i[0],0],a=qt(u,i);zt(a),a=Dt(a);var c=t-p,l=c>0?1:-1,d=a[0]*Ka*l,m=Math.abs(c)>180;if(m^(d>l*p&&l*t>d)){var y=a[1]*Ka;y>g&&(g=y)}else if(d=(d+360)%360-180,m^(d>l*p&&l*t>d)){var y=-a[1]*Ka;s>y&&(s=y)}else s>e&&(s=e),e>g&&(g=e);m?p>t?o(f,t)>o(f,h)&&(h=t):o(t,h)>o(f,h)&&(f=t):h>=f?(f>t&&(f=t),t>h&&(h=t))
 :t>p?o(f,t)>o(f,h)&&(h=t):o(t,h)>o(f,h)&&(f=t)}else n(t,e);v=r,p=t}function e(){b.point=t}function r(){x[0]=f,x[1]=h,b.point=n,v=null}function i(n,e){if(v){var r=n-p;y+=Math.abs(r)>180?r+(r>0?360:-360):r}else d=n,m=e;ko.point(n,e),t(n,e)}function u(){ko.lineStart()}function a(){i(d,m),ko.lineEnd(),Math.abs(y)>Wa&&(f=-(h=180)),x[0]=f,x[1]=h,v=null}function o(n,t){return(t-=n)<0?t+360:t}function c(n,t){return n[0]-t[0]}function l(n,t){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:n<t[0]||t[1]<n}var f,s,h,g,p,d,m,v,y,M,x,b={point:n,lineStart:e,lineEnd:r,polygonStart:function(){b.point=i,b.lineStart=u,b.lineEnd=a,y=0,ko.polygonStart()},polygonEnd:function(){ko.polygonEnd(),b.point=n,b.lineStart=e,b.lineEnd=r,0>Eo?(f=-(h=180),s=-(g=90)):y>Wa?g=90:-Wa>y&&(s=-90),x[0]=f,x[1]=h}};return function(n){g=h=-(f=s=1/0),M=[],va.geo.stream(n,b);var t=M.length;if(t){M.sort(c);for(var e,r=1,i=M[0],u=[i];t>r;++r)e=M[r],l(e[0],i)||l(e[1],i)?(o(i[0],e[1])>o(i[0],i[1])&&(i[1]=e[1]),o(e[0],i[1])>o(i[0],i[1])&&(i[0]=
 e[0])):u.push(i=e);for(var a,e,p=-1/0,t=u.length-1,r=0,i=u[t];t>=r;i=e,++r)e=u[r],(a=o(i[1],e[0]))>p&&(p=a,f=e[0],h=i[1])}return M=x=null,1/0===f||1/0===s?[[0/0,0/0],[0/0,0/0]]:[[f,s],[h,g]]}}(),va.geo.centroid=function(n){Ao=No=qo=To=Co=zo=Do=jo=Lo=Ho=Fo=0,va.geo.stream(n,Po);var t=Lo,e=Ho,r=Fo,i=t*t+e*e+r*r;return Ja>i&&(t=zo,e=Do,r=jo,Wa>No&&(t=qo,e=To,r=Co),i=t*t+e*e+r*r,Ja>i)?[0/0,0/0]:[Math.atan2(e,t)*Ka,U(r/Math.sqrt(i))*Ka]};var Ao,No,qo,To,Co,zo,Do,jo,Lo,Ho,Fo,Po={sphere:T,point:Lt,lineStart:Ft,lineEnd:Pt,polygonStart:function(){Po.lineStart=Ot},polygonEnd:function(){Po.lineStart=Ft}},Oo=It(Rt,$t,Jt,Gt),Ro=[-$a,0],Yo=1e9;(va.geo.conicEqualArea=function(){return ee(re)}).raw=re,va.geo.albers=function(){return va.geo.conicEqualArea().rotate([96,0]).center([-.6,38.7]).parallels([29.5,45.5]).scale(1070)},va.geo.albersUsa=function(){function n(n){var u=n[0],a=n[1];return t=null,e(u,a),t||(r(u,a),t)||i(u,a),t}var t,e,r,i,u=va.geo.albers(),a=va.geo.conicEqualArea().rotate([154,0])
 .center([-2,58.5]).parallels([55,65]),o=va.geo.conicEqualArea().rotate([157,0]).center([-3,19.9]).parallels([8,18]),c={point:function(n,e){t=[n,e]}};return n.invert=function(n){var t=u.scale(),e=u.translate(),r=(n[0]-e[0])/t,i=(n[1]-e[1])/t;return(i>=.12&&.234>i&&r>=-.425&&-.214>r?a:i>=.166&&.234>i&&r>=-.214&&-.115>r?o:u).invert(n)},n.stream=function(n){var t=u.stream(n),e=a.stream(n),r=o.stream(n);return{point:function(n,i){t.point(n,i),e.point(n,i),r.point(n,i)},sphere:function(){t.sphere(),e.sphere(),r.sphere()},lineStart:function(){t.lineStart(),e.lineStart(),r.lineStart()},lineEnd:function(){t.lineEnd(),e.lineEnd(),r.lineEnd()},polygonStart:function(){t.polygonStart(),e.polygonStart(),r.polygonStart()},polygonEnd:function(){t.polygonEnd(),e.polygonEnd(),r.polygonEnd()}}},n.precision=function(t){return arguments.length?(u.precision(t),a.precision(t),o.precision(t),n):u.precision()},n.scale=function(t){return arguments.length?(u.scale(t),a.scale(.35*t),o.scale(t),n.translate(u.tr
 anslate())):u.scale()},n.translate=function(t){if(!arguments.length)return u.translate();var l=u.scale(),f=+t[0],s=+t[1];return e=u.translate(t).clipExtent([[f-.455*l,s-.238*l],[f+.455*l,s+.238*l]]).stream(c).point,r=a.translate([f-.307*l,s+.201*l]).clipExtent([[f-.425*l+Wa,s+.12*l+Wa],[f-.214*l-Wa,s+.234*l-Wa]]).stream(c).point,i=o.translate([f-.205*l,s+.212*l]).clipExtent([[f-.214*l+Wa,s+.166*l+Wa],[f-.115*l-Wa,s+.234*l-Wa]]).stream(c).point,n},n.scale(1070)};var Uo,Io,Vo,Xo,Zo,Bo,$o={point:T,lineStart:T,lineEnd:T,polygonStart:function(){Io=0,$o.lineStart=ie},polygonEnd:function(){$o.lineStart=$o.lineEnd=$o.point=T,Uo+=Math.abs(Io/2)}},Wo={point:ue,lineStart:T,lineEnd:T,polygonStart:T,polygonEnd:T},Jo={point:ce,lineStart:le,lineEnd:fe,polygonStart:function(){Jo.lineStart=se},polygonEnd:function(){Jo.point=ce,Jo.lineStart=le,Jo.lineEnd=fe}};va.geo.path=function(){function n(n){return n&&("function"==typeof o&&u.pointRadius(+o.apply(this,arguments)),a&&a.valid||(a=i(u)),va.geo.strea
 m(n,a)),u.result()}function t(){return a=null,n}var e,r,i,u,a,o=4.5;return n.area=function(n){return Uo=0,va.geo.stream(n,i($o)),Uo},n.centroid=function(n){return qo=To=Co=zo=Do=jo=Lo=Ho=Fo=0,va.geo.stream(n,i(Jo)),Fo?[Lo/Fo,Ho/Fo]:jo?[zo/jo,Do/jo]:Co?[qo/Co,To/Co]:[0/0,0/0]},n.bounds=function(n){return Zo=Bo=-(Vo=Xo=1/0),va.geo.stream(n,i(Wo)),[[Vo,Xo],[Zo,Bo]]},n.projection=function(n){return arguments.length?(i=(e=n)?n.stream||pe(n):st,t()):e},n.context=function(n){return arguments.length?(u=(r=n)==null?new ae:new he(n),"function"!=typeof o&&u.pointRadius(o),t()):r},n.pointRadius=function(t){return arguments.length?(o="function"==typeof t?t:(u.pointRadius(+t),+t),n):o},n.projection(va.geo.albersUsa()).context(null)},va.geo.projection=de,va.geo.projectionMutator=me,(va.geo.equirectangular=function(){return de(ye)}).raw=ye.invert=ye,va.geo.rotation=function(n){function t(t){return t=n(t[0]*Ga,t[1]*Ga),t[0]*=Ka,t[1]*=Ka,t}return n=Me(n[0]%360*Ga,n[1]*Ga,n.length>2?n[2]*Ga:0),t.inver
 t=function(t){return t=n.invert(t[0]*Ga,t[1]*Ga),t[0]*=Ka,t[1]*=Ka,t},t},va.geo.circle=function(){function n(){var n="function"==typeof r?r.apply(this,arguments):r,t=Me(-n[0]*Ga,-n[1]*Ga,0).invert,i=[];return e(null,null,1,{point:function(n,e){i.push(n=t(n,e)),n[0]*=Ka,n[1]*=Ka}}),{type:"Polygon",coordinates:[i]}}var t,e,r=[0,0],i=6;return n.origin=function(t){return arguments.length?(r=t,n):r},n.angle=function(r){return arguments.length?(e=we((t=+r)*Ga,i*Ga),n):t},n.precision=function(r){return arguments.length?(e=we(t*Ga,(i=+r)*Ga),n):i},n.angle(90)},va.geo.distance=function(n,t){var e,r=(t[0]-n[0])*Ga,i=n[1]*Ga,u=t[1]*Ga,a=Math.sin(r),o=Math.cos(r),c=Math.sin(i),l=Math.cos(i),f=Math.sin(u),s=Math.cos(u);return Math.atan2(Math.sqrt((e=s*a)*e+(e=l*f-c*s*o)*e),c*f+l*s*o)},va.geo.graticule=function(){function n(){return{type:"MultiLineString",coordinates:t()}}function t(){return va.range(Math.ceil(u/m)*m,i,m).map(h).concat(va.range(Math.ceil(l/v)*v,c,v).map(g)).concat(va.range(Math.c
 eil(r/p)*p,e,p).filter(function(n){return Math.abs(n%m)>Wa}).map(f)).concat(va.range(Math.ceil(o/d)*d,a,d).filter(function(n){return Math.abs(n%v)>Wa}).map(s))}var e,r,i,u,a,o,c,l,f,s,h,g,p=10,d=p,m=90,v=360,y=2.5;return n.lines=function(){return t().map(function(n){return{type:"LineString",coordinates:n}})},n.outline=function(){return{type:"Polygon",coordinates:[h(u).concat(g(c).slice(1),h(i).reverse().slice(1),g(l).reverse().slice(1))]}},n.extent=function(t){return arguments.length?n.majorExtent(t).minorExtent(t):n.minorExtent()},n.majorExtent=function(t){return arguments.length?(u=+t[0][0],i=+t[1][0],l=+t[0][1],c=+t[1][1],u>i&&(t=u,u=i,i=t),l>c&&(t=l,l=c,c=t),n.precision(y)):[[u,l],[i,c]]},n.minorExtent=function(t){return arguments.length?(r=+t[0][0],e=+t[1][0],o=+t[0][1],a=+t[1][1],r>e&&(t=r,r=e,e=t),o>a&&(t=o,o=a,a=t),n.precision(y)):[[r,o],[e,a]]},n.step=function(t){return arguments.length?n.majorStep(t).minorStep(t):n.minorStep()},n.majorStep=function(t){return arguments.leng
 th?(m=+t[0],v=+t[1],n):[m,v]},n.minorStep=function(t){return arguments.length?(p=+t[0],d=+t[1],n):[p,d]},n.precision=function(t){return arguments.length?(y=+t,f=Ee(o,a,90),s=ke(r,e,y),h=Ee(l,c,90),g=ke(u,i,y),n):y},n.majorExtent([[-180,-90+Wa],[180,90-Wa]]).minorExtent([[-180,-80-Wa],[180,80+Wa]])},va.geo.greatArc=function(){function n(){return{type:"LineString",coordinates:[t||r.apply(this,arguments),e||i.apply(this,arguments)]}}var t,e,r=Ae,i=Ne;return n.distance=function(){return va.geo.distance(t||r.apply(this,arguments),e||i.apply(this,arguments))},n.source=function(e){return arguments.length?(r=e,t="function"==typeof e?null:e,n):r},n.target=function(t){return arguments.length?(i=t,e="function"==typeof t?null:t,n):i},n.precision=function(){return arguments.length?n:0},n},va.geo.interpolate=function(n,t){return qe(n[0]*Ga,n[1]*Ga,t[0]*Ga,t[1]*Ga)},va.geo.length=function(n){return Go=0,va.geo.stream(n,Ko),Go};var Go,Ko={sphere:T,point:T,lineStart:Te,lineEnd:T,polygonStart:T,polyg
 onEnd:T},Qo=Ce(function(n){return Math.sqrt(2/(1+n))},function(n){return 2*Math.asin(n/2)});(va.geo.azimuthalEqualArea=function(){return de(Qo)}).raw=Qo;var nc=Ce(function(n){var t=Math.acos(n);return t&&t/Math.sin(t)},st);(va.geo.azimuthalEquidistant=function(){return de(nc)}).raw=nc,(va.geo.conicConformal=function(){return ee(ze)}).raw=ze,(va.geo.conicEquidistant=function(){return ee(De)}).raw=De;var tc=Ce(function(n){return 1/n},Math.atan);(va.geo.gnomonic=function(){return de(tc)}).raw=tc,je.invert=function(n,t){return[n,2*Math.atan(Math.exp(t))-$a/2]},(va.geo.mercator=function(){return Le(je)}).raw=je;var ec=Ce(function(){return 1},Math.asin);(va.geo.orthographic=function(){return de(ec)}).raw=ec;var rc=Ce(function(n){return 1/(1+n)},function(n){return 2*Math.atan(n)});(va.geo.stereographic=function(){return de(rc)}).raw=rc,He.invert=function(n,t){return[Math.atan2(I(n),Math.cos(t)),U(Math.sin(t)/V(n))]},(va.geo.transverseMercator=function(){return Le(He)}).raw=He,va.geom={},va
 .svg={},va.svg.line=function(){return Fe(st)};var ic=va.map({linear:Re,"linear-closed":Ye,step:Ue,"step-before":Ie,"step-after":Ve,basis:Je,"basis-open":Ge,"basis-closed":Ke,bundle:Qe,cardinal:Be,"cardinal-open":Xe,"cardinal-closed":Ze,monotone:ur});
-ic.forEach(function(n,t){t.key=n,t.closed=/-closed$/.test(n)});var uc=[0,2/3,1/3,0],ac=[0,1/3,2/3,0],oc=[0,1/6,2/3,1/6];va.geom.hull=function(n){function t(n){if(n.length<3)return[];var t,i,u,a,o,c,l,f,s,h,g,p,d=ft(e),m=ft(r),v=n.length,y=v-1,M=[],x=[],b=0;if(d===Pe&&r===Oe)t=n;else for(u=0,t=[];v>u;++u)t.push([+d.call(this,i=n[u],u),+m.call(this,i,u)]);for(u=1;v>u;++u)(t[u][1]<t[b][1]||t[u][1]==t[b][1]&&t[u][0]<t[b][0])&&(b=u);for(u=0;v>u;++u)u!==b&&(c=t[u][1]-t[b][1],o=t[u][0]-t[b][0],M.push({angle:Math.atan2(c,o),index:u}));for(M.sort(function(n,t){return n.angle-t.angle}),g=M[0].angle,h=M[0].index,s=0,u=1;y>u;++u){if(a=M[u].index,g==M[u].angle){if(o=t[h][0]-t[b][0],c=t[h][1]-t[b][1],l=t[a][0]-t[b][0],f=t[a][1]-t[b][1],o*o+c*c>=l*l+f*f){M[u].index=-1;continue}M[s].index=-1}g=M[u].angle,s=u,h=a}for(x.push(b),u=0,a=0;2>u;++a)M[a].index>-1&&(x.push(M[a].index),u++);for(p=x.length;y>a;++a)if(!(M[a].index<0)){for(;!ar(x[p-2],x[p-1],M[a].index,t);)--p;x[p++]=M[a].index}var _=[];for(u=p
 -1;u>=0;--u)_.push(n[x[u]]);return _}var e=Pe,r=Oe;return arguments.length?t(n):(t.x=function(n){return arguments.length?(e=n,t):e},t.y=function(n){return arguments.length?(r=n,t):r},t)},va.geom.polygon=function(n){return n.area=function(){for(var t=0,e=n.length,r=n[e-1][1]*n[0][0]-n[e-1][0]*n[0][1];++t<e;)r+=n[t-1][1]*n[t][0]-n[t-1][0]*n[t][1];return.5*r},n.centroid=function(t){var e,r,i=-1,u=n.length,a=0,o=0,c=n[u-1];for(arguments.length||(t=-1/(6*n.area()));++i<u;)e=c,c=n[i],r=e[0]*c[1]-c[0]*e[1],a+=(e[0]+c[0])*r,o+=(e[1]+c[1])*r;return[a*t,o*t]},n.clip=function(t){for(var e,r,i,u,a,o,c=-1,l=n.length,f=n[l-1];++c<l;){for(e=t.slice(),t.length=0,u=n[c],a=e[(i=e.length)-1],r=-1;++r<i;)o=e[r],or(o,f,u)?(or(a,f,u)||t.push(cr(a,o,f,u)),t.push(o)):or(a,f,u)&&t.push(cr(a,o,f,u)),a=o;f=u}return t},n},va.geom.delaunay=function(n){var t=n.map(function(){return[]}),e=[];return lr(n,function(e){t[e.region.l.index].push(n[e.region.r.index])}),t.forEach(function(t,r){var i=n[r],u=i[0],a=i[1];t.
 forEach(function(n){n.angle=Math.atan2(n[0]-u,n[1]-a)}),t.sort(function(n,t){return n.angle-t.angle});for(var o=0,c=t.length-1;c>o;o++)e.push([i,t[o],t[o+1]])}),e},va.geom.voronoi=function(n){function t(n){var t,u,a,o=n.map(function(){return[]}),c=ft(e),l=ft(r),f=n.length,s=1e6;if(c===Pe&&l===Oe)t=n;else for(t=Array(f),a=0;f>a;++a)t[a]=[+c.call(this,u=n[a],a),+l.call(this,u,a)];if(lr(t,function(n){var t,e,r,i,u,a;n.a===1&&n.b>=0?(t=n.ep.r,e=n.ep.l):(t=n.ep.l,e=n.ep.r),n.a===1?(u=t?t.y:-s,r=n.c-n.b*u,a=e?e.y:s,i=n.c-n.b*a):(r=t?t.x:-s,u=n.c-n.a*r,i=e?e.x:s,a=n.c-n.a*i);var c=[r,u],l=[i,a];o[n.region.l.index].push(c,l),o[n.region.r.index].push(c,l)}),o=o.map(function(n,e){var r=t[e][0],i=t[e][1],u=n.map(function(n){return Math.atan2(n[0]-r,n[1]-i)}),a=va.range(n.length).sort(function(n,t){return u[n]-u[t]});return a.filter(function(n,t){return!t||u[n]-u[a[t-1]]>Wa}).map(function(t){return n[t]})}),o.forEach(function(n,e){var r=n.length;if(!r)return n.push([-s,-s],[-s,s],[s,s],[s,-s]);
 if(!(r>2)){var i=t[e],u=n[0],a=n[1],o=i[0],c=i[1],l=u[0],f=u[1],h=a[0],g=a[1],p=Math.abs(h-l),d=g-f;if(Math.abs(d)<Wa){var m=f>c?-s:s;n.push([-s,m],[s,m])}else if(Wa>p){var v=l>o?-s:s;n.push([v,-s],[v,s])}else{var m=(l-o)*(g-f)>(h-l)*(f-c)?s:-s,y=Math.abs(d)-p;Math.abs(y)<Wa?n.push([0>d?m:-m,m]):(y>0&&(m*=-1),n.push([-s,m],[s,m]))}}}),i)for(a=0;f>a;++a)i.clip(o[a]);for(a=0;f>a;++a)o[a].point=n[a];return o}var e=Pe,r=Oe,i=null;return arguments.length?t(n):(t.x=function(n){return arguments.length?(e=n,t):e},t.y=function(n){return arguments.length?(r=n,t):r},t.clipExtent=function(n){if(!arguments.length)return i&&[i[0],i[2]];if(null==n)i=null;else{var e=+n[0][0],r=+n[0][1],u=+n[1][0],a=+n[1][1];i=va.geom.polygon([[e,r],[e,a],[u,a],[u,r]])}return t},t.size=function(n){return arguments.length?t.clipExtent(n&&[[0,0],n]):i&&i[2]},t.links=function(n){var t,i,u,a=n.map(function(){return[]}),o=[],c=ft(e),l=ft(r),f=n.length;if(c===Pe&&l===Oe)t=n;else for(t=Array(f),u=0;f>u;++u)t[u]=[+c.call(th
 is,i=n[u],u),+l.call(this,i,u)];return lr(t,function(t){var e=t.region.l.index,r=t.region.r.index;a[e][r]||(a[e][r]=a[r][e]=!0,o.push({source:n[e],target:n[r]}))}),o},t.triangles=function(n){if(e===Pe&&r===Oe)return va.geom.delaunay(n);for(var t,i=Array(c),u=ft(e),a=ft(r),o=-1,c=n.length;++o<c;)(i[o]=[+u.call(this,t=n[o],o),+a.call(this,t,o)]).data=t;return va.geom.delaunay(i).map(function(n){return n.map(function(n){return n.data})})},t)};var cc={l:"r",r:"l"};va.geom.quadtree=function(n,t,e,r,i){function u(n){function u(n,t,e,r,i,u,a,o){if(!isNaN(e)&&!isNaN(r))if(n.leaf){var c=n.x,f=n.y;if(null!=c)if(Math.abs(c-e)+Math.abs(f-r)<.01)l(n,t,e,r,i,u,a,o);else{var s=n.point;n.x=n.y=n.point=null,l(n,s,c,f,i,u,a,o),l(n,t,e,r,i,u,a,o)}else n.x=e,n.y=r,n.point=t}else l(n,t,e,r,i,u,a,o)}function l(n,t,e,r,i,a,o,c){var l=.5*(i+o),f=.5*(a+c),s=e>=l,h=r>=f,g=(h<<1)+s;n.leaf=!1,n=n.nodes[g]||(n.nodes[g]=hr()),s?i=l:o=l,h?a=f:c=f,u(n,t,e,r,i,a,o,c)}var f,s,h,g,p,d,m,v,y,M=ft(o),x=ft(c);if(null!=t
 )d=t,m=e,v=r,y=i;else if(v=y=-(d=m=1/0),s=[],h=[],p=n.length,a)for(g=0;p>g;++g)f=n[g],f.x<d&&(d=f.x),f.y<m&&(m=f.y),f.x>v&&(v=f.x),f.y>y&&(y=f.y),s.push(f.x),h.push(f.y);else for(g=0;p>g;++g){var b=+M(f=n[g],g),_=+x(f,g);d>b&&(d=b),m>_&&(m=_),b>v&&(v=b),_>y&&(y=_),s.push(b),h.push(_)}var w=v-d,S=y-m;w>S?y=m+w:v=d+S;var E=hr();if(E.add=function(n){u(E,n,+M(n,++g),+x(n,g),d,m,v,y)},E.visit=function(n){gr(n,E,d,m,v,y)},g=-1,null==t){for(;++g<p;)u(E,n[g],s[g],h[g],d,m,v,y);--g}else n.forEach(E.add);return s=h=n=f=null,E}var a,o=Pe,c=Oe;return(a=arguments.length)?(o=fr,c=sr,3===a&&(i=e,r=t,e=t=0),u(n)):(u.x=function(n){return arguments.length?(o=n,u):o},u.y=function(n){return arguments.length?(c=n,u):c},u.extent=function(n){return arguments.length?(null==n?t=e=r=i=null:(t=+n[0][0],e=+n[0][1],r=+n[1][0],i=+n[1][1]),u):null==t?null:[[t,e],[r,i]]},u.size=function(n){return arguments.length?(null==n?t=e=r=i=null:(t=e=0,r=+n[0],i=+n[1]),u):null==t?null:[r-t,i-e]},u)},va.interpolateRgb=pr,va.t
 ransform=function(n){var t=ya.createElementNS(va.ns.prefix.svg,"g");return(va.transform=function(n){if(null!=n){t.setAttribute("transform",n);var e=t.transform.baseVal.consolidate()}return new dr(e?e.matrix:lc)})(n)},dr.prototype.toString=function(){return"translate("+this.translate+")rotate("+this.rotate+")skewX("+this.skew+")scale("+this.scale+")"};var lc={a:1,b:0,c:0,d:1,e:0,f:0};va.interpolateNumber=Mr,va.interpolateTransform=xr,va.interpolateObject=br,va.interpolateString=_r;var fc=/[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g;va.interpolate=wr,va.interpolators=[function(n,t){var e=typeof t;return("string"===e?ao.has(t)||/^(#|rgb\(|hsl\()/.test(t)?pr:_r:t instanceof H?pr:"object"===e?Array.isArray(t)?Er:br:Mr)(n,t)}],va.interpolateArray=Er;var sc=function(){return st},hc=va.map({linear:sc,poly:zr,quad:function(){return qr},cubic:function(){return Tr},sin:function(){return Dr},exp:function(){return jr},circle:function(){return Lr},elastic:Hr,back:Fr,bounce:function(){return Pr}}
 ),gc=va.map({"in":st,out:Ar,"in-out":Nr,"out-in":function(n){return Nr(Ar(n))}});va.ease=function(n){var t=n.indexOf("-"),e=t>=0?n.substring(0,t):n,r=t>=0?n.substring(t+1):"in";return e=hc.get(e)||sc,r=gc.get(r)||st,kr(r(e.apply(null,Array.prototype.slice.call(arguments,1))))},va.interpolateHcl=Or,va.interpolateHsl=Rr,va.interpolateLab=Yr,va.interpolateRound=Ur,va.layout={},va.layout.bundle=function(){return function(n){for(var t=[],e=-1,r=n.length;++e<r;)t.push(Xr(n[e]));return t}},va.layout.chord=function(){function n(){var n,l,s,h,g,p={},d=[],m=va.range(u),v=[];for(e=[],r=[],n=0,h=-1;++h<u;){for(l=0,g=-1;++g<u;)l+=i[h][g];d.push(l),v.push(va.range(u)),n+=l}for(a&&m.sort(function(n,t){return a(d[n],d[t])}),o&&v.forEach(function(n,t){n.sort(function(n,e){return o(i[t][n],i[t][e])})}),n=(2*$a-f*u)/n,l=0,h=-1;++h<u;){for(s=l,g=-1;++g<u;){var y=m[h],M=v[y][g],x=i[y][M],b=l,_=l+=x*n;p[y+"-"+M]={index:y,subindex:M,startAngle:b,endAngle:_,value:x}}r[y]={index:y,startAngle:s,endAngle:l,va
 lue:(l-s)/n},l+=f}for(h=-1;++h<u;)for(g=h-1;++g<u;){var w=p[h+"-"+g],S=p[g+"-"+h];(w.value||S.value)&&e.push(w.value<S.value?{source:S,target:w}:{source:w,target:S})}c&&t()}function t(){e.sort(function(n,t){return c((n.source.value+n.target.value)/2,(t.source.value+t.target.value)/2)})}var e,r,i,u,a,o,c,l={},f=0;return l.matrix=function(n){return arguments.length?(u=(i=n)&&i.length,e=r=null,l):i},l.padding=function(n){return arguments.length?(f=n,e=r=null,l):f},l.sortGroups=function(n){return arguments.length?(a=n,e=r=null,l):a},l.sortSubgroups=function(n){return arguments.length?(o=n,e=null,l):o},l.sortChords=function(n){return arguments.length?(c=n,e&&t(),l):c},l.chords=function(){return e||n(),e},l.groups=function(){return r||n(),r},l},va.layout.force=function(){function n(n){return function(t,e,r,i){if(t.point!==n){var u=t.cx-n.x,a=t.cy-n.y,o=1/Math.sqrt(u*u+a*a);if(d>(i-e)*o){var c=t.charge*o*o;return n.px-=u*c,n.py-=a*c,!0}if(t.point&&isFinite(o)){var c=t.pointCharge*o*o;n.px-
 =u*c,n.py-=a*c}}return!t.charge}}function t(n){n.px=va.event.x,n.py=va.event.y,o.resume()}var e,r,i,u,a,o={},c=va.dispatch("start","tick","end"),l=[1,1],f=.9,s=pc,h=dc,g=-30,p=.1,d=.8,m=[],v=[];return o.tick=function(){if((r*=.99)<.005)return c.end({type:"end",alpha:r=0}),!0;var t,e,o,s,h,d,y,M,x,b=m.length,_=v.length;for(e=0;_>e;++e)o=v[e],s=o.source,h=o.target,M=h.x-s.x,x=h.y-s.y,(d=M*M+x*x)&&(d=r*u[e]*((d=Math.sqrt(d))-i[e])/d,M*=d,x*=d,h.x-=M*(y=s.weight/(h.weight+s.weight)),h.y-=x*y,s.x+=M*(y=1-y),s.y+=x*y);if((y=r*p)&&(M=l[0]/2,x=l[1]/2,e=-1,y))for(;++e<b;)o=m[e],o.x+=(M-o.x)*y,o.y+=(x-o.y)*y;if(g)for(Kr(t=va.geom.quadtree(m),r,a),e=-1;++e<b;)(o=m[e]).fixed||t.visit(n(o));for(e=-1;++e<b;)o=m[e],o.fixed?(o.x=o.px,o.y=o.py):(o.x-=(o.px-(o.px=o.x))*f,o.y-=(o.py-(o.py=o.y))*f);c.tick({type:"tick",alpha:r})},o.nodes=function(n){return arguments.length?(m=n,o):m},o.links=function(n){return arguments.length?(v=n,o):v},o.size=function(n){return arguments.length?(l=n,o):l},o.linkDistan
 ce=function(n){return arguments.length?(s="function"==typeof n?n:+n,o):s},o.distance=o.linkDistance,o.linkStrength=function(n){return arguments.length?(h="function"==typeof n?n:+n,o):h},o.friction=function(n){return arguments.length?(f=+n,o):f},o.charge=function(n){return arguments.length?(g="function"==typeof n?n:+n,o):g},o.gravity=function(n){return arguments.length?(p=+n,o):p},o.theta=function(n){return arguments.length?(d=+n,o):d},o.alpha=function(n){return arguments.length?(n=+n,r?r=n>0?n:0:n>0&&(c.start({type:"start",alpha:r=n}),va.timer(o.tick)),o):r},o.start=function(){function n(n,r){for(var i,u=t(e),a=-1,o=u.length;++a<o;)if(!isNaN(i=u[a][n]))return i;return Math.random()*r}function t(){if(!c){for(c=[],r=0;p>r;++r)c[r]=[];for(r=0;d>r;++r){var n=v[r];c[n.source.index].push(n.target),c[n.target.index].push(n.source)}}return c[e]}var e,r,c,f,p=m.length,d=v.length,y=l[0],M=l[1];for(e=0;p>e;++e)(f=m[e]).index=e,f.weight=0;for(e=0;d>e;++e)f=v[e],typeof f.source=="number"&&(f.sou
 rce=m[f.source]),typeof f.target=="number"&&(f.target=m[f.target]),++f.source.weight,++f.target.weight;for(e=0;p>e;++e)f=m[e],isNaN(f.x)&&(f.x=n("x",y)),isNaN(f.y)&&(f.y=n("y",M)),isNaN(f.px)&&(f.px=f.x),isNaN(f.py)&&(f.py=f.y);if(i=[],"function"==typeof s)for(e=0;d>e;++e)i[e]=+s.call(this,v[e],e);else for(e=0;d>e;++e)i[e]=s;if(u=[],"function"==typeof h)for(e=0;d>e;++e)u[e]=+h.call(this,v[e],e);else for(e=0;d>e;++e)u[e]=h;if(a=[],"function"==typeof g)for(e=0;p>e;++e)a[e]=+g.call(this,m[e],e);else for(e=0;p>e;++e)a[e]=g;return o.resume()},o.resume=function(){return o.alpha(.1)},o.stop=function(){return o.alpha(0)},o.drag=function(){return e||(e=va.behavior.drag().origin(st).on("dragstart.force",$r).on("drag.force",t).on("dragend.force",Wr)),arguments.length?(this.on("mouseover.force",Jr).on("mouseout.force",Gr).call(e),void 0):e},va.rebind(o,c,"on")};var pc=20,dc=1;va.layout.hierarchy=function(){function n(t,a,o){var c=i.call(e,t,a);if(t.depth=a,o.push(t),c&&(l=c.length)){for(var l,f
 ,s=-1,h=t.children=[],g=0,p=a+1;++s<l;)f=n(c[s],p,o),f.parent=t,h.push(f),g+=f.value;r&&h.sort(r),u&&(t.value=g)}else u&&(t.value=+u.call(e,t,a)||0);return t}function t(n,r){var i=n.children,a=0;if(i&&(o=i.length))for(var o,c=-1,l=r+1;++c<o;)a+=t(i[c],l);else u&&(a=+u.call(e,n,r)||0);return u&&(n.value=a),a}function e(t){var e=[];return n(t,0,e),e}var r=ei,i=ni,u=ti;return e.sort=function(n){return arguments.length?(r=n,e):r},e.children=function(n){return arguments.length?(i=n,e):i},e.value=function(n){return arguments.length?(u=n,e):u},e.revalue=function(n){return t(n,0),n},e},va.layout.partition=function(){function n(t,e,r,i){var u=t.children;if(t.x=e,t.y=t.depth*i,t.dx=r,t.dy=i,u&&(a=u.length)){var a,o,c,l=-1;for(r=t.value?r/t.value:0;++l<a;)n(o=u[l],e,c=o.value*r,i),e+=c}}function t(n){var e=n.children,r=0;if(e&&(i=e.length))for(var i,u=-1;++u<i;)r=Math.max(r,t(e[u]));return 1+r}function e(e,u){var a=r.call(this,e,u);return n(a[0],0,i[0],i[1]/t(a[0])),a}var r=va.layout.hierarchy
 (),i=[1,1];return e.size=function(n){return arguments.length?(i=n,e):i},Qr(e,r)},va.layout.pie=function(){function n(u){var a=u.map(function(e,r){return+t.call(n,e,r)}),o=+("function"==typeof r?r.apply(this,arguments):r),c=(("function"==typeof i?i.apply(this,arguments):i)-o)/va.sum(a),l=va.range(u.length);null!=e&&l.sort(e===mc?function(n,t){return a[t]-a[n]}:function(n,t){return e(u[n],u[t])});var f=[];return l.forEach(function(n){var t;f[n]={data:u[n],value:t=a[n],startAngle:o,endAngle:o+=t*c}}),f}var t=Number,e=mc,r=0,i=2*$a;return n.value=function(e){return arguments.length?(t=e,n):t},n.sort=function(t){return arguments.length?(e=t,n):e},n.startAngle=function(t){return arguments.length?(r=t,n):r},n.endAngle=function(t){return arguments.length?(i=t,n):i},n};var mc={};va.layout.stack=function(){function n(o,c){var l=o.map(function(e,r){return t.call(n,e,r)}),f=l.map(function(t){return t.map(function(t,e){return[u.call(n,t,e),a.call(n,t,e)]})}),s=e.call(n,f,c);l=va.permute(l,s),f=v
 a.permute(f,s);var h,g,p,d=r.call(n,f,c),m=l.length,v=l[0].length;for(g=0;v>g;++g)for(i.call(n,l[0][g],p=d[g],f[0][g][1]),h=1;m>h;++h)i.call(n,l[h][g],p+=f[h-1][g][1],f[h][g][1]);return o}var t=st,e=oi,r=ci,i=ai,u=ii,a=ui;return n.values=function(e){return arguments.length?(t=e,n):t},n.order=function(t){return arguments.length?(e="function"==typeof t?t:vc.get(t)||oi,n):e},n.offset=function(t){return arguments.length?(r="function"==typeof t?t:yc.get(t)||ci,n):r},n.x=function(t){return arguments.length?(u=t,n):u},n.y=function(t){return arguments.length?(a=t,n):a},n.out=function(t){return arguments.length?(i=t,n):i},n};var vc=va.map({"inside-out":function(n){var t,e,r=n.length,i=n.map(li),u=n.map(fi),a=va.range(r).sort(function(n,t){return i[n]-i[t]}),o=0,c=0,l=[],f=[];for(t=0;r>t;++t)e=a[t],c>o?(o+=u[e],l.push(e)):(c+=u[e],f.push(e));return f.reverse().concat(l)},reverse:function(n){return va.range(n.length).reverse()},"default":oi}),yc=va.map({silhouette:function(n){var t,e,r,i=n.len
 gth,u=n[0].length,a=[],o=0,c=[];for(e=0;u>e;++e){for(t=0,r=0;i>t;t++)r+=n[t][e][1];r>o&&(o=r),a.push(r)}for(e=0;u>e;++e)c[e]=(o-a[e])/2;return c},wiggle:function(n){var t,e,r,i,u,a,o,c,l,f=n.length,s=n[0],h=s.length,g=[];for(g[0]=c=l=0,e=1;h>e;++e){for(t=0,i=0;f>t;++t)i+=n[t][e][1];for(t=0,u=0,o=s[e][0]-s[e-1][0];f>t;++t){for(r=0,a=(n[t][e][1]-n[t][e-1][1])/(2*o);t>r;++r)a+=(n[r][e][1]-n[r][e-1][1])/o;u+=a*n[t][e][1]}g[e]=c-=i?u/i*o:0,l>c&&(l=c)}for(e=0;h>e;++e)g[e]-=l;return g},expand:function(n){var t,e,r,i=n.length,u=n[0].length,a=1/i,o=[];for(e=0;u>e;++e){for(t=0,r=0;i>t;t++)r+=n[t][e][1];if(r)for(t=0;i>t;t++)n[t][e][1]/=r;else for(t=0;i>t;t++)n[t][e][1]=a}for(e=0;u>e;++e)o[e]=0;return o},zero:ci});va.layout.histogram=function(){function n(n,u){for(var a,o,c=[],l=n.map(e,this),f=r.call(this,l,u),s=i.call(this,f,l,u),u=-1,h=l.length,g=s.length-1,p=t?1:1/h;++u<g;)a=c[u]=[],a.dx=s[u+1]-(a.x=s[u]),a.y=0;if(g>0)for(u=-1;++u<h;)o=l[u],o>=f[0]&&o<=f[1]&&(a=c[va.bisect(s,o,1,g)-1],a.y+=
 p,a.push(n[u]));return c}var t=!0,e=Number,r=pi,i=hi;return n.value=function(t){return arguments.length?(e=t,n):e},n.range=function(t){return arguments.length?(r=ft(t),n):r},n.bins=function(t){return arguments.length?(i="number"==typeof t?function(n){return gi(n,t)}:ft(t),n):i},n.frequency=function(e){return arguments.length?(t=!!e,n):t},n},va.layout.tree=function(){function n(n,u){function a(n,t){var r=n.children,i=n._tree;if(r&&(u=r.length)){for(var u,o,l,f=r[0],s=f,h=-1;++h<u;)l=r[h],a(l,o),s=c(l,o,s),o=l;wi(n);var g=.5*(f._tree.prelim+l._tree.prelim);t?(i.prelim=t._tree.prelim+e(n,t),i.mod=i.prelim-g):i.prelim=g}else t&&(i.prelim=t._tree.prelim+e(n,t))}function o(n,t){n.x=n._tree.prelim+t;var e=n.children;if(e&&(r=e.length)){var r,i=-1;for(t+=n._tree.mod;++i<r;)o(e[i],t)}}function c(n,t,r){if(t){for(var i,u=n,a=n,o=t,c=n.parent.children[0],l=u._tree.mod,f=a._tree.mod,s=o._tree.mod,h=c._tree.mod;o=vi(o),u=mi(u),o&&u;)c=mi(c),a=vi(a),a._tree.ancestor=n,i=o._tree.prelim+s-u._tree.p
 relim-l+e(o,u),i>0&&(Si(Ei(o,n,r),n,i),l+=i,f+=i),s+=o._tree.mod,l+=u._tree.mod,h+=c._tree.mod,f+=a._tree.mod;o&&!vi(a)&&(a._tree.thread=o,a._tree.mod+=s-f),u&&!mi(c)&&(c._tree.thread=u,c._tree.mod+=l-h,r=n)}return r}var l=t.call(this,n,u),f=l[0];_i(f,function(n,t){n._tree={ancestor:n,prelim:0,mod:0,change:0,shift:0,number:t?t._tree.number+1:0}}),a(f),o(f,-f._tree.prelim);var s=yi(f,xi),h=yi(f,Mi),g=yi(f,bi),p=s.x-e(s,h)/2,d=h.x+e(h,s)/2,m=g.depth||1;return _i(f,i?function(n){n.x*=r[0],n.y=n.depth*r[1],delete n._tree}:function(n){n.x=(n.x-p)/(d-p)*r[0],n.y=n.depth/m*r[1],delete n._tree}),l}var t=va.layout.hierarchy().sort(null).value(null),e=di,r=[1,1],i=!1;return n.separation=function(t){return arguments.length?(e=t,n):e},n.size=function(t){return arguments.length?(i=(r=t)==null,n):i?null:r},n.nodeSize=function(t){return arguments.length?(i=(r=t)!=null,n):i?r:null},Qr(n,t)},va.layout.pack=function(){function n(n,u){var a=e.call(this,n,u),o=a[0],c=i[0],l=i[1],f=t||Math.sqrt;if(o.x=o
 .y=0,_i(o,function(n){n.r=f(n.value)}),_i(o,Ti),r){var s=r*(t?1:Math.max(2*o.r/c,2*o.r/l))/2;_i(o,function(n){n.r+=s}),_i(o,Ti),_i(o,function(n){n.r-=s})}return Di(o,c/2,l/2,t?1:1/Math.max(2*o.r/c,2*o.r/l)),a}var t,e=va.layout.hierarchy().sort(ki),r=0,i=[1,1];return n.size=function(t){return arguments.length?(i=t,n):i},n.radius=function(e){return arguments.length?(t=e,n):t},n.padding=function(t){return arguments.length?(r=+t,n):r},Qr(n,e)},va.layout.cluster=function(){function n(n,u){var a,o=t.call(this,n,u),c=o[0],l=0;_i(c,function(n){var t=n.children;t&&t.length?(n.x=Hi(t),n.y=Li(t)):(n.x=a?l+=e(n,a):0,n.y=0,a=n)});var f=Fi(c),s=Pi(c),h=f.x-e(f,s)/2,g=s.x+e(s,f)/2;return _i(c,i?function(n){n.x=(n.x-c.x)*r[0],n.y=(c.y-n.y)*r[1]}:function(n){n.x=(n.x-h)/(g-h)*r[0],n.y=(1-(c.y?n.y/c.y:1))*r[1]}),o}var t=va.layout.hierarchy().sort(null).value(null),e=di,r=[1,1],i=!1;return n.separation=function(t){return arguments.length?(e=t,n):e},n.size=function(t){return arguments.length?(i=(r=t)==
 null,n):i?null:r},n.nodeSize=function(t){return arguments.length?(i=(r=t)!=null,n):i?r:null},Qr(n,t)},va.layout.treemap=function(){function n(n,t){for(var e,r,i=-1,u=n.length;++i<u;)r=(e=n[i]).value*(0>t?0:t),e.area=isNaN(r)||0>=r?0:r}function t(e){var u=e.children;if(u&&u.length){var a,o,c,l=s(e),f=[],h=u.slice(),p=1/0,d="slice"===g?l.dx:"dice"===g?l.dy:"slice-dice"===g?e.depth&1?l.dy:l.dx:Math.min(l.dx,l.dy);for(n(h,l.dx*l.dy/e.value),f.area=0;(c=h.length)>0;)f.push(a=h[c-1]),f.area+=a.area,"squarify"!==g||(o=r(f,d))<=p?(h.pop(),p=o):(f.area-=f.pop().area,i(f,d,l,!1),d=Math.min(l.dx,l.dy),f.length=f.area=0,p=1/0);f.length&&(i(f,d,l,!0),f.length=f.area=0),u.forEach(t)}}function e(t){var r=t.children;if(r&&r.length){var u,a=s(t),o=r.slice(),c=[];for(n(o,a.dx*a.dy/t.value),c.area=0;u=o.pop();)c.push(u),c.area+=u.area,u.z!=null&&(i(c,u.z?a.dx:a.dy,a,!o.length),c.length=c.area=0);r.forEach(e)}}function r(n,t){for(var e,r=n.area,i=0,u=1/0,a=-1,o=n.length;++a<o;)(e=n[a].area)&&(u>e&&(u=e
 ),e>i&&(i=e));return r*=r,t*=t,r?Math.max(t*i*p/r,r/(t*u*p)):1/0}function i(n,t,e,r){var i,u=-1,a=n.length,o=e.x,l=e.y,f=t?c(n.area/t):0;if(t==e.dx){for((r||f>e.dy)&&(f=e.dy);++u<a;)i=n[u],i.x=o,i.y=l,i.dy=f,o+=i.dx=Math.min(e.x+e.dx-o,f?c(i.area/f):0);i.z=!0,i.dx+=e.x+e.dx-o,e.y+=f,e.dy-=f}else{for((r||f>e.dx)&&(f=e.dx);++u<a;)i=n[u],i.x=o,i.y=l,i.dx=f,l+=i.dy=Math.min(e.y+e.dy-l,f?c(i.area/f):0);i.z=!1,i.dy+=e.y+e.dy-l,e.x+=f,e.dx-=f}}function u(r){var i=a||o(r),u=i[0];return u.x=0,u.y=0,u.dx=l[0],u.dy=l[1],a&&o.revalue(u),n([u],u.dx*u.dy/u.value),(a?e:t)(u),h&&(a=i),i}var a,o=va.layout.hierarchy(),c=Math.round,l=[1,1],f=null,s=Oi,h=!1,g="squarify",p=.5*(1+Math.sqrt(5));return u.size=function(n){return arguments.length?(l=n,u):l},u.padding=function(n){function t(t){var e=n.call(u,t,t.depth);return null==e?Oi(t):Ri(t,"number"==typeof e?[e,e,e,e]:e)}function e(t){return Ri(t,n)}if(!arguments.length)return f;var r;return s=(f=n)==null?Oi:(r=typeof n)=="function"?t:"number"===r?(n=[n,
 n,n,n],e):e,u},u.round=function(n){return arguments.length?(c=n?Math.round:Number,u):c!=Number},u.sticky=function(n){return arguments.length?(h=n,a=null,u):h},u.ratio=function(n){return arguments.length?(p=n,u):p},u.mode=function(n){return arguments.length?(g=n+"",u):g},Qr(u,o)},va.random={normal:function(n,t){var e=arguments.length;return 2>e&&(t=1),1>e&&(n=0),function(){var e,r,i;do e=Math.random()*2-1,r=Math.random()*2-1,i=e*e+r*r;while(!i||i>1);return n+t*e*Math.sqrt(-2*Math.log(i)/i)}},logNormal:function(){var n=va.random.normal.apply(va,arguments);return function(){return Math.exp(n())}},irwinHall:function(n){return function(){for(var t=0,e=0;n>e;e++)t+=Math.random();return t/n}}},va.scale={};var Mc={floor:st,ceil:st};va.scale.linear=function(){return Bi([0,1],[0,1],wr,!1)},va.scale.log=function(){return nu(va.scale.linear().domain([0,Math.LN10]),10,tu,eu,[1,10])};var xc=va.format(".0e");va.scale.pow=function(){return uu(va.scale.linear(),1,[0,1])},va.scale.sqrt=function(){ret
 urn va.scale.pow().exponent(.5)},va.scale.ordinal=function(){return ou([],{t:"range",a:[[]]})},va.scale.category10=function(){return va.scale.ordinal().range(bc)},va.scale.category20=function(){return va.scale.ordinal().range(_c)},va.scale.category20b=function(){return va.scale.ordinal().range(wc)},va.scale.category20c=function(){return va.scale.ordinal().range(Sc)};var bc=["#1f77b4","#ff7f0e","#2ca02c","#d62728","#9467bd","#8c564b","#e377c2","#7f7f7f","#bcbd22","#17becf"],_c=["#1f77b4","#aec7e8","#ff7f0e","#ffbb78","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5","#8c564b","#c49c94","#e377c2","#f7b6d2","#7f7f7f","#c7c7c7","#bcbd22","#dbdb8d","#17becf","#9edae5"],wc=["#393b79","#5254a3","#6b6ecf","#9c9ede","#637939","#8ca252","#b5cf6b","#cedb9c","#8c6d31","#bd9e39","#e7ba52","#e7cb94","#843c39","#ad494a","#d6616b","#e7969c","#7b4173","#a55194","#ce6dbd","#de9ed6"],Sc=["#3182bd","#6baed6","#9ecae1","#c6dbef","#e6550d","#fd8d3c","#fdae6b","#fdd0a2","#31a354","#74c476","#a1d
 99b","#c7e9c0","#756bb1","#9e9ac8","#bcbddc","#dadaeb","#636363","#969696","#bdbdbd","#d9d9d9"];va.scale.quantile=function(){return cu([],[])},va.scale.quantize=function(){return lu(0,1,[0,1])},va.scale.threshold=function(){return fu([.5],[0,1])},va.scale.identity=function(){return su([0,1])},va.svg.arc=function(){function n(){var n=t.apply(this,arguments),u=e.apply(this,arguments),a=r.apply(this,arguments)+Ec,o=i.apply(this,arguments)+Ec,c=(a>o&&(c=a,a=o,o=c),o-a),l=$a>c?"0":"1",f=Math.cos(a),s=Math.sin(a),h=Math.cos(o),g=Math.sin(o);return c>=kc?n?"M0,"+u+"A"+u+","+u+" 0 1,1 0,"+-u+"A"+u+","+u+" 0 1,1 0,"+u+"M0,"+n+"A"+n+","+n+" 0 1,0 0,"+-n+"A"+n+","+n+" 0 1,0 0,"+n+"Z":"M0,"+u+"A"+u+","+u+" 0 1,1 0,"+-u+"A"+u+","+u+" 0 1,1 0,"+u+"Z":n?"M"+u*f+","+u*s+"A"+u+","+u+" 0 "+l+",1 "+u*h+","+u*g+"L"+n*h+","+n*g+"A"+n+","+n+" 0 "+l+",0 "+n*f+","+n*s+"Z":"M"+u*f+","+u*s+"A"+u+","+u+" 0 "+l+",1 "+u*h+","+u*g+"L0,0"+"Z"}var t=hu,e=gu,r=pu,i=du;return n.innerRadius=function(e){return argumen
 ts.length?(t=ft(e),n):t},n.outerRadius=function(t){return arguments.length?(e=ft(t),n):e},n.startAngle=function(t){return arguments.length?(r=ft(t),n):r},n.endAngle=function(t){return arguments.length?(i=ft(t),n):i},n.centroid=function(){var n=(t.apply(this,arguments)+e.apply(this,arguments))/2,u=(r.apply(this,arguments)+i.apply(this,arguments))/2+Ec;return[Math.cos(u)*n,Math.sin(u)*n]},n};var Ec=-$a/2,kc=2*$a-1e-6;va.svg.line.radial=function(){var n=Fe(mu);return n.radius=n.x,delete n.x,n.angle=n.y,delete n.y,n},Ie.reverse=Ve,Ve.reverse=Ie,va.svg.area=function(){return vu(st)},va.svg.area.radial=function(){var n=vu(mu);return n.radius=n.x,delete n.x,n.innerRadius=n.x0,delete n.x0,n.outerRadius=n.x1,delete n.x1,n.angle=n.y,delete n.y,n.startAngle=n.y0,delete n.y0,n.endAngle=n.y1,delete n.y1,n},va.svg.chord=function(){function n(n,o){var c=t(this,u,n,o),l=t(this,a,n,o);return"M"+c.p0+r(c.r,c.p1,c.a1-c.a0)+(e(c,l)?i(c.r,c.p1,c.r,c.p0):i(c.r,c.p1,l.r,l.p0)+r(l.r,l.p1,l.a1-l.a0)+i(l.r,l
 .p1,c.r,c.p0))+"Z"}function t(n,t,e,r){var i=t.call(n,e,r),u=o.call(n,i,r),a=c.call(n,i,r)+Ec,f=l.call(n,i,r)+Ec;return{r:u,a0:a,a1:f,p0:[u*Math.cos(a),u*Math.sin(a)],p1:[u*Math.cos(f),u*Math.sin(f)]}}function e(n,t){return n.a0==t.a0&&n.a1==t.a1}function r(n,t,e){return"A"+n+","+n+" 0 "+ +(e>$a)+",1 "+t}function i(n,t,e,r){return"Q 0,0 "+r}var u=Ae,a=Ne,o=yu,c=pu,l=du;return n.radius=function(t){return arguments.length?(o=ft(t),n):o},n.source=function(t){return arguments.length?(u=ft(t),n):u},n.target=function(t){return arguments.length?(a=ft(t),n):a},n.startAngle=function(t){return arguments.length?(c=ft(t),n):c},n.endAngle=function(t){return arguments.length?(l=ft(t),n):l},n},va.svg.diagonal=function(){function n(n,i){var u=t.call(this,n,i),a=e.call(this,n,i),o=(u.y+a.y)/2,c=[u,{x:u.x,y:o},{x:a.x,y:o},a];return c=c.map(r),"M"+c[0]+"C"+c[1]+" "+c[2]+" "+c[3]}var t=Ae,e=Ne,r=Mu;return n.source=function(e){return arguments.length?(t=ft(e),n):t},n.target=function(t){return arguments.
 length?(e=ft(t),n):e},n.projection=function(t){return arguments.length?(r=t,n):r},n},va.svg.diagonal.radial=function(){var n=va.svg.diagonal(),t=Mu,e=n.projection;return n.projection=function(n){return arguments.length?e(xu(t=n)):t},n},va.svg.symbol=function(){function n(n,r){return(Ac.get(t.call(this,n,r))||wu)(e.call(this,n,r))}var t=_u,e=bu;return n.type=function(e){return arguments.length?(t=ft(e),n):t},n.size=function(t){return arguments.length?(e=ft(t),n):e},n};var Ac=va.map({circle:wu,cross:function(n){var t=Math.sqrt(n/5)/2;return"M"+-3*t+","+-t+"H"+-t+"V"+-3*t+"H"+t+"V"+-t+"H"+3*t+"V"+t+"H"+t+"V"+3*t+"H"+-t+"V"+t+"H"+-3*t+"Z"},diamond:function(n){var t=Math.sqrt(n/(2*Tc)),e=t*Tc;return"M0,"+-t+"L"+e+",0"+" 0,"+t+" "+-e+",0"+"Z"},square:function(n){var t=Math.sqrt(n)/2;return"M"+-t+","+-t+"L"+t+","+-t+" "+t+","+t+" "+-t+","+t+"Z"},"triangle-down":function(n){var t=Math.sqrt(n/qc),e=t*qc/2;return"M0,"+e+"L"+t+","+-e+" "+-t+","+-e+"Z"},"triangle-up":function(n){var t=Math.sqrt
 (n/qc),e=t*qc/2;return"M0,"+-e+"L"+t+","+e+" "+-t+","+e+"Z"}});va.svg.symbolTypes=Ac.keys();var Nc,qc=Math.sqrt(3),Tc=Math.tan(30*Ga),Cc=[],zc=0,Dc={ease:Cr,delay:0,duration:250};Cc.call=Pa.call,Cc.empty=Pa.empty,Cc.node=Pa.node,va.transition=function(n){return arguments.length?Nc?n.transition():n:Ia.transition()},va.transition.prototype=Cc,Cc.select=function(n){var t,e,r,i=this.id,u=[];"function"!=typeof n&&(n=y(n));for(var a=-1,o=this.length;++a<o;){u.push(t=[]);for(var c=this[a],l=-1,f=c.length;++l<f;)(r=c[l])&&(e=n.call(r,r.__data__,l))?("__data__"in r&&(e.__data__=r.__data__),Au(e,l,i,r.__transition__[i]),t.push(e)):t.push(null)}return Su(u,i)},Cc.selectAll=function(n){var t,e,r,i,u,a=this.id,o=[];"function"!=typeof n&&(n=M(n));for(var c=-1,l=this.length;++c<l;)for(var f=this[c],s=-1,h=f.length;++s<h;)if(r=f[s]){u=r.__transition__[a],e=n.call(r,r.__data__,s),o.push(t=[]);for(var g=-1,p=e.length;++g<p;)(i=e[g])&&Au(i,g,a,u),t.push(i)}return Su(o,a)},Cc.filter=function(n){var t,e
 ,r,i=[];"function"!=typeof n&&(n=N(n));for(var u=0,a=this.length;a>u;u++){i.push(t=[]);for(var e=this[u],o=0,c=e.length;c>o;o++)(r=e[o])&&n.call(r,r.__data__,o)&&t.push(r)}return Su(i,this.id,this.time).ease(this.ease())},Cc.tween=function(n,t){var e=this.id;return arguments.length<2?this.node().__transition__[e].tween.get(n):j(this,null==t?function(t){t.__transition__[e].tween.remove(n)}:function(r){r.__transition__[e].tween.set(n,t)})},Cc.attr=function(n,t){function e(){this.removeAttribute(o)}function r(){this.removeAttributeNS(o.space,o.local)}function i(n){return null==n?e:(n+="",function(){var t,e=this.getAttribute(o);return e!==n&&(t=a(e,n),function(n){this.setAttribute(o,t(n))})})}function u(n){return null==n?r:(n+="",function(){var t,e=this.getAttributeNS(o.space,o.local);return e!==n&&(t=a(e,n),function(n){this.setAttributeNS(o.space,o.local,t(n))})})}if(arguments.length<2){for(t in n)this.attr(t,n[t]);return this}var a=Sr(n),o=va.ns.qualify(n);return Eu(this,"attr."+n,t,o
 .local?u:i)},Cc.attrTween=function(n,t){function e(n,e){var r=t.call(this,n,e,this.getAttribute(i));return r&&function(n){this.setAttribute(i,r(n))}}function r(n,e){var r=t.call(this,n,e,this.getAttributeNS(i.space,i.local));return r&&function(n){this.setAttributeNS(i.space,i.local,r(n))}}var i=va.ns.qualify(n);return this.tween("attr."+n,i.local?r:e)},Cc.style=function(n,t,e){function r(){this.style.removeProperty(n)}function i(t){return null==t?r:(t+="",function(){var r,i=xa.getComputedStyle(this,null).getPropertyValue(n);return i!==t&&(r=a(i,t),function(t){this.style.setProperty(n,r(t),e)})})}var u=arguments.length;if(3>u){if("string"!=typeof n){2>u&&(t="");for(e in n)this.style(e,n[e],t);return this}e=""}var a=Sr(n);return Eu(this,"style."+n,t,i)},Cc.styleTween=function(n,t,e){function r(r,i){var u=t.call(this,r,i,xa.getComputedStyle(this,null).getPropertyValue(n));return u&&function(t){this.style.setProperty(n,u(t),e)}}return arguments.length<3&&(e=""),this.tween("style."+n,r)}
 ,Cc.text=function(n){return Eu(this,"text",n,ku)},Cc.remove=function(){return this.each("end.transition",function(){var n;!this.__transition__&&(n=this.parentNode)&&n.removeChild(this)})},Cc.ease=function(n){var t=this.id;return arguments.length<1?this.node().__transition__[t].ease:("function"!=typeof n&&(n=va.ease.apply(va,arguments)),j(this,function(e){e.__transition__[t].ease=n}))},Cc.delay=function(n){var t=this.id;return j(this,"function"==typeof n?function(e,r,i){e.__transition__[t].delay=n.call(e,e.__data__,r,i)|0}:(n|=0,function(e){e.__transition__[t].delay=n}))},Cc.duration=function(n){var t=this.id;return j(this,"function"==typeof n?function(e,r,i){e.__transition__[t].duration=Math.max(1,n.call(e,e.__data__,r,i)|0)}:(n=Math.max(1,0|n),function(e){e.__transition__[t].duration=n}))},Cc.each=function(n,t){var e=this.id;if(arguments.length<2){var r=Dc,i=Nc;Nc=e,j(this,function(t,r,i){Dc=t.__transition__[e],n.call(t,t.__data__,r,i)}),Dc=r,Nc=i}else j(this,function(r){r.__transi
 tion__[e].event.on(n,t)});return this},Cc.transition=function(){for(var n,t,e,r,i=this.id,u=++zc,a=[],o=0,c=this.length;c>o;o++){a.push(n=[]);for(var t=this[o],l=0,f=t.length;f>l;l++)(e=t[l])&&(r=Object.create(e.__transition__[i]),r.delay+=r.duration,Au(e,l,u,r)),n.push(e)}return Su(a,u)},va.svg.axis=function(){function n(n){n.each(function(){var n,s=va.select(this),h=null==l?e.ticks?e.ticks.apply(e,c):e.domain():l,g=null==t?e.tickFormat?e.tickFormat.apply(e,c):String:t,p=Tu(e,h,f),d=s.selectAll(".tick.minor").data(p,String),m=d.enter().insert("line",".tick").attr("class","tick minor").style("opacity",1e-6),v=va.transition(d.exit()).style("opacity",1e-6).remove(),y=va.transition(d).style("opacity",1),M=s.selectAll(".tick.major").data(h,String),x=M.enter().insert("g",".domain").attr("class","tick major").style("opacity",1e-6),b=va.transition(M.exit()).style("opacity",1e-6).remove(),_=va.transition(M).style("opacity",1),w=Ui(e),S=s.selectAll(".domain").data([0]),E=(S.enter().append("p
 ath").attr("class","domain"),va.transition(S)),k=e.copy(),A=this.__chart__||k;this.__chart__=k,x.append("line"),x.append("text");
-var N=x.select("line"),q=_.select("line"),T=M.select("text").text(g),C=x.select("text"),z=_.select("text");switch(r){case"bottom":n=Nu,m.attr("y2",u),y.attr("x2",0).attr("y2",u),N.attr("y2",i),C.attr("y",Math.max(i,0)+o),q.attr("x2",0).attr("y2",i),z.attr("x",0).attr("y",Math.max(i,0)+o),T.attr("dy",".71em").style("text-anchor","middle"),E.attr("d","M"+w[0]+","+a+"V0H"+w[1]+"V"+a);break;case"top":n=Nu,m.attr("y2",-u),y.attr("x2",0).attr("y2",-u),N.attr("y2",-i),C.attr("y",-(Math.max(i,0)+o)),q.attr("x2",0).attr("y2",-i),z.attr("x",0).attr("y",-(Math.max(i,0)+o)),T.attr("dy","0em").style("text-anchor","middle"),E.attr("d","M"+w[0]+","+-a+"V0H"+w[1]+"V"+-a);break;case"left":n=qu,m.attr("x2",-u),y.attr("x2",-u).attr("y2",0),N.attr("x2",-i),C.attr("x",-(Math.max(i,0)+o)),q.attr("x2",-i).attr("y2",0),z.attr("x",-(Math.max(i,0)+o)).attr("y",0),T.attr("dy",".32em").style("text-anchor","end"),E.attr("d","M"+-a+","+w[0]+"H0V"+w[1]+"H"+-a);break;case"right":n=qu,m.attr("x2",u),y.attr("x2",u).
 attr("y2",0),N.attr("x2",i),C.attr("x",Math.max(i,0)+o),q.attr("x2",i).attr("y2",0),z.attr("x",Math.max(i,0)+o).attr("y",0),T.attr("dy",".32em").style("text-anchor","start"),E.attr("d","M"+a+","+w[0]+"H0V"+w[1]+"H"+a)}if(e.ticks)x.call(n,A),_.call(n,k),b.call(n,k),m.call(n,A),y.call(n,k),v.call(n,k);else{var D=k.rangeBand()/2,j=function(n){return k(n)+D};x.call(n,j),_.call(n,j)}})}var t,e=va.scale.linear(),r=jc,i=6,u=6,a=6,o=3,c=[10],l=null,f=0;return n.scale=function(t){return arguments.length?(e=t,n):e},n.orient=function(t){return arguments.length?(r=t in Lc?t+"":jc,n):r},n.ticks=function(){return arguments.length?(c=arguments,n):c},n.tickValues=function(t){return arguments.length?(l=t,n):l},n.tickFormat=function(e){return arguments.length?(t=e,n):t},n.tickSize=function(t,e){if(!arguments.length)return i;var r=arguments.length-1;return i=+t,u=r>1?+e:i,a=r>0?+arguments[r]:i,n},n.tickPadding=function(t){return arguments.length?(o=+t,n):o},n.tickSubdivide=function(t){return arguments
 .length?(f=+t,n):f},n};var jc="bottom",Lc={top:1,right:1,bottom:1,left:1};va.svg.brush=function(){function n(u){u.each(function(){var u,a=va.select(this),l=a.selectAll(".background").data([0]),s=a.selectAll(".extent").data([0]),h=a.selectAll(".resize").data(f,String);a.style("pointer-events","all").on("mousedown.brush",i).on("touchstart.brush",i),l.enter().append("rect").attr("class","background").style("visibility","hidden").style("cursor","crosshair"),s.enter().append("rect").attr("class","extent").style("cursor","move"),h.enter().append("g").attr("class",function(n){return"resize "+n}).style("cursor",function(n){return Hc[n]}).append("rect").attr("x",function(n){return/[ew]$/.test(n)?-3:null}).attr("y",function(n){return/^[ns]/.test(n)?-3:null}).attr("width",6).attr("height",6).style("visibility","hidden"),h.style("display",n.empty()?"none":null),h.exit().remove(),o&&(u=Ui(o),l.attr("x",u[0]).attr("width",u[1]-u[0]),e(a)),c&&(u=Ui(c),l.attr("y",u[0]).attr("height",u[1]-u[0]),r(a)
 ),t(a)})}function t(n){n.selectAll(".resize").attr("transform",function(n){return"translate("+s[+/e$/.test(n)][0]+","+s[+/^s/.test(n)][1]+")"})}function e(n){n.select(".extent").attr("x",s[0][0]),n.selectAll(".extent,.n>rect,.s>rect").attr("width",s[1][0]-s[0][0])}function r(n){n.select(".extent").attr("y",s[0][1]),n.selectAll(".extent,.e>rect,.w>rect").attr("height",s[1][1]-s[0][1])}function i(){function i(){var n=va.event.changedTouches;return n?va.touches(M,n)[0]:va.mouse(M)}function f(){va.event.keyCode==32&&(k||(v=null,A[0]-=s[1][0],A[1]-=s[1][1],k=2),l())}function h(){va.event.keyCode==32&&2==k&&(A[0]+=s[1][0],A[1]+=s[1][1],k=0,l())}function p(){var n=i(),u=!1;y&&(n[0]+=y[0],n[1]+=y[1]),k||(va.event.altKey?(v||(v=[(s[0][0]+s[1][0])/2,(s[0][1]+s[1][1])/2]),A[0]=s[+(n[0]<v[0])][0],A[1]=s[+(n[1]<v[1])][1]):v=null),S&&d(n,o,0)&&(e(_),u=!0),E&&d(n,c,1)&&(r(_),u=!0),u&&(t(_),b({type:"brush",mode:k?"move":"resize"}))}function d(n,t,e){var r,i,a=Ui(t),o=a[0],c=a[1],l=A[e],f=s[1][e]-s[
 0][e];return k&&(o-=l,c-=f+l),r=g[e]?Math.max(o,Math.min(c,n[e])):n[e],k?i=(r+=l)+f:(v&&(l=Math.max(o,Math.min(c,2*v[e]-r))),r>l?(i=r,r=l):i=l),s[0][e]!==r||s[1][e]!==i?(u=null,s[0][e]=r,s[1][e]=i,!0):void 0}function m(){p(),_.style("pointer-events","all").selectAll(".resize").style("display",n.empty()?"none":null),va.select("body").style("cursor",null),N.on("mousemove.brush",null).on("mouseup.brush",null).on("touchmove.brush",null).on("touchend.brush",null).on("keydown.brush",null).on("keyup.brush",null),b({type:"brushend"}),l()}var v,y,M=this,x=va.select(va.event.target),b=a.of(M,arguments),_=va.select(M),w=x.datum(),S=!/^(n|s)$/.test(w)&&o,E=!/^(e|w)$/.test(w)&&c,k=x.classed("extent"),A=i(),N=va.select(xa).on("mousemove.brush",p).on("mouseup.brush",m).on("touchmove.brush",p).on("touchend.brush",m).on("keydown.brush",f).on("keyup.brush",h);if(k)A[0]=s[0][0]-A[0],A[1]=s[0][1]-A[1];else if(w){var q=+/w$/.test(w),T=+/^n/.test(w);y=[s[1-q][0]-A[0],s[1-T][1]-A[1]],A[0]=s[q][0],A[1]=s[T
 ][1]}else va.event.altKey&&(v=A.slice());_.style("pointer-events","none").selectAll(".resize").style("display",null),va.select("body").style("cursor",x.style("cursor")),b({type:"brushstart"}),p(),l()}var u,a=h(n,"brushstart","brush","brushend"),o=null,c=null,f=Fc[0],s=[[0,0],[0,0]],g=[!0,!0];return n.x=function(t){return arguments.length?(o=t,f=Fc[!o<<1|!c],n):o},n.y=function(t){return arguments.length?(c=t,f=Fc[!o<<1|!c],n):c},n.clamp=function(t){return arguments.length?(o&&c?g=[!!t[0],!!t[1]]:(o||c)&&(g[+!o]=!!t),n):o&&c?g:o||c?g[+!o]:null},n.extent=function(t){var e,r,i,a,l;return arguments.length?(u=[[0,0],[0,0]],o&&(e=t[0],r=t[1],c&&(e=e[0],r=r[0]),u[0][0]=e,u[1][0]=r,o.invert&&(e=o(e),r=o(r)),e>r&&(l=e,e=r,r=l),s[0][0]=0|e,s[1][0]=0|r),c&&(i=t[0],a=t[1],o&&(i=i[1],a=a[1]),u[0][1]=i,u[1][1]=a,c.invert&&(i=c(i),a=c(a)),i>a&&(l=i,i=a,a=l),s[0][1]=0|i,s[1][1]=0|a),n):(t=u||s,o&&(e=t[0][0],r=t[1][0],u||(e=s[0][0],r=s[1][0],o.invert&&(e=o.invert(e),r=o.invert(r)),e>r&&(l=e,e=r,r=l))
 ),c&&(i=t[0][1],a=t[1][1],u||(i=s[0][1],a=s[1][1],c.invert&&(i=c.invert(i),a=c.invert(a)),i>a&&(l=i,i=a,a=l))),o&&c?[[e,i],[r,a]]:o?[e,r]:c&&[i,a])},n.clear=function(){return u=null,s[0][0]=s[0][1]=s[1][0]=s[1][1]=0,n},n.empty=function(){return o&&s[0][0]===s[1][0]||c&&s[0][1]===s[1][1]},va.rebind(n,a,"on")};var Hc={n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},Fc=[["n","e","s","w","nw","ne","se","sw"],["e","w"],["n","s"],[]];va.time={};var Pc=Date,Oc=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];Cu.prototype={getDate:function(){return this._.getUTCDate()},getDay:function(){return this._.getUTCDay()},getFullYear:function(){return this._.getUTCFullYear()},getHours:function(){return this._.getUTCHours()},getMilliseconds:function(){return this._.getUTCMilliseconds()},getMinutes:function(){return this._.getUTCMinutes()},getMonth:function(){return this._.getUTCMonth()},getSeconds:fun
 ction(){return this._.getUTCSeconds()},getTime:function(){return this._.getTime()},getTimezoneOffset:function(){return 0},valueOf:function(){return this._.valueOf()},setDate:function(){Rc.setUTCDate.apply(this._,arguments)},setDay:function(){Rc.setUTCDay.apply(this._,arguments)},setFullYear:function(){Rc.setUTCFullYear.apply(this._,arguments)},setHours:function(){Rc.setUTCHours.apply(this._,arguments)},setMilliseconds:function(){Rc.setUTCMilliseconds.apply(this._,arguments)},setMinutes:function(){Rc.setUTCMinutes.apply(this._,arguments)},setMonth:function(){Rc.setUTCMonth.apply(this._,arguments)},setSeconds:function(){Rc.setUTCSeconds.apply(this._,arguments)},setTime:function(){Rc.setTime.apply(this._,arguments)}};var Rc=Date.prototype,Yc="%a %b %e %X %Y",Uc="%m/%d/%Y",Ic="%H:%M:%S",Vc=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],Xc=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],Zc=["January","February","March","April","May","June","July","August","September
 ","October","November","December"],Bc=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];va.time.year=zu(function(n){return n=va.time.day(n),n.setMonth(0,1),n},function(n,t){n.setFullYear(n.getFullYear()+t)},function(n){return n.getFullYear()}),va.time.years=va.time.year.range,va.time.years.utc=va.time.year.utc.range,va.time.day=zu(function(n){var t=new Pc(2e3,0);return t.setFullYear(n.getFullYear(),n.getMonth(),n.getDate()),t},function(n,t){n.setDate(n.getDate()+t)},function(n){return n.getDate()-1}),va.time.days=va.time.day.range,va.time.days.utc=va.time.day.utc.range,va.time.dayOfYear=function(n){var t=va.time.year(n);return Math.floor((n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*6e4)/864e5)},Oc.forEach(function(n,t){n=n.toLowerCase(),t=7-t;var e=va.time[n]=zu(function(n){return(n=va.time.day(n)).setDate(n.getDate()-(n.getDay()+t)%7),n},function(n,t){n.setDate(n.getDate()+Math.floor(t)*7)},function(n){var e=va.time.year(n).getDay();return Math.floor((
 va.time.dayOfYear(n)+(e+t)%7)/7)-(e!==t)});va.time[n+"s"]=e.range,va.time[n+"s"].utc=e.utc.range,va.time[n+"OfYear"]=function(n){var e=va.time.year(n).getDay();return Math.floor((va.time.dayOfYear(n)+(e+t)%7)/7)}}),va.time.week=va.time.sunday,va.time.weeks=va.time.sunday.range,va.time.weeks.utc=va.time.sunday.utc.range,va.time.weekOfYear=va.time.sundayOfYear,va.time.format=function(n){function t(t){for(var r,i,u,a=[],o=-1,c=0;++o<e;)n.charCodeAt(o)===37&&(a.push(n.substring(c,o)),(i=rl[r=n.charAt(++o)])!=null&&(r=n.charAt(++o)),(u=il[r])&&(r=u(t,null==i?"e"===r?" ":"0":i)),a.push(r),c=o+1);return a.push(n.substring(c,o)),a.join("")}var e=n.length;return t.parse=function(t){var e={y:1900,m:0,d:1,H:0,M:0,S:0,L:0},r=ju(e,n,t,0);if(r!=t.length)return null;"p"in e&&(e.H=e.H%12+e.p*12);var i=new Pc;return"j"in e?i.setFullYear(e.y,0,e.j):"w"in e&&("W"in e||"U"in e)?(i.setFullYear(e.y,0,1),i.setFullYear(e.y,0,"W"in e?(e.w+6)%7+e.W*7-(i.getDay()+5)%7:e.w+e.U*7-(i.getDay()+6)%7)):i.setFullYea
 r(e.y,e.m,e.d),i.setHours(e.H,e.M,e.S,e.L),i},t.toString=function(){return n},t};var $c=Lu(Vc),Wc=Hu(Vc),Jc=Lu(Xc),Gc=Hu(Xc),Kc=Lu(Zc),Qc=Hu(Zc),nl=Lu(Bc),tl=Hu(Bc),el=/^%/,rl={"-":"",_:" ",0:"0"},il={a:function(n){return Xc[n.getDay()]},A:function(n){return Vc[n.getDay()]},b:function(n){return Bc[n.getMonth()]},B:function(n){return Zc[n.getMonth()]},c:va.time.format(Yc),d:function(n,t){return Fu(n.getDate(),t,2)},e:function(n,t){return Fu(n.getDate(),t,2)},H:function(n,t){return Fu(n.getHours(),t,2)},I:function(n,t){return Fu(n.getHours()%12||12,t,2)},j:function(n,t){return Fu(1+va.time.dayOfYear(n),t,3)},L:function(n,t){return Fu(n.getMilliseconds(),t,3)},m:function(n,t){return Fu(n.getMonth()+1,t,2)},M:function(n,t){return Fu(n.getMinutes(),t,2)},p:function(n){return n.getHours()>=12?"PM":"AM"},S:function(n,t){return Fu(n.getSeconds(),t,2)},U:function(n,t){return Fu(va.time.sundayOfYear(n),t,2)},w:function(n){return n.getDay()},W:function(n,t){return Fu(va.time.mondayOfYear(n),t,
 2)},x:va.time.format(Uc),X:va.time.format(Ic),y:function(n,t){return Fu(n.getFullYear()%100,t,2)},Y:function(n,t){return Fu(n.getFullYear()%1e4,t,4)},Z:ua,"%":function(){return"%"}},ul={a:Pu,A:Ou,b:Iu,B:Vu,c:Xu,d:Ku,e:Ku,H:na,I:na,j:Qu,L:ra,m:Gu,M:ta,p:ia,S:ea,U:Yu,w:Ru,W:Uu,x:Zu,X:Bu,y:Wu,Y:$u,"%":aa},al=/^\s*\d+/,ol=va.map({am:0,pm:1});va.time.format.utc=function(n){function t(n){try{Pc=Cu;var t=new Pc;return t._=n,e(t)}finally{Pc=Date}}var e=va.time.format(n);return t.parse=function(n){try{Pc=Cu;var t=e.parse(n);return t&&t._}finally{Pc=Date}},t.toString=e.toString,t};var cl=va.time.format.utc("%Y-%m-%dT%H:%M:%S.%LZ");va.time.format.iso=Date.prototype.toISOString&&+new Date("2000-01-01T00:00:00.000Z")?oa:cl,oa.parse=function(n){var t=new Date(n);return isNaN(t)?null:t},oa.toString=cl.toString,va.time.second=zu(function(n){return new Pc(Math.floor(n/1e3)*1e3)},function(n,t){n.setTime(n.getTime()+Math.floor(t)*1e3)},function(n){return n.getSeconds()}),va.time.seconds=va.time.second
 .range,va.time.seconds.utc=va.time.second.utc.range,va.time.minute=zu(function(n){return new Pc(Math.floor(n/6e4)*6e4)},function(n,t){n.setTime(n.getTime()+Math.floor(t)*6e4)},function(n){return n.getMinutes()}),va.time.minutes=va.time.minute.range,va.time.minutes.utc=va.time.minute.utc.range,va.time.hour=zu(function(n){var t=n.getTimezoneOffset()/60;return new Pc((Math.floor(n/36e5-t)+t)*36e5)},function(n,t){n.setTime(n.getTime()+Math.floor(t)*36e5)},function(n){return n.getHours()}),va.time.hours=va.time.hour.range,va.time.hours.utc=va.time.hour.utc.range,va.time.month=zu(function(n){return n=va.time.day(n),n.setDate(1),n},function(n,t){n.setMonth(n.getMonth()+t)},function(n){return n.getMonth()}),va.time.months=va.time.month.range,va.time.months.utc=va.time.month.utc.range;var ll=[1e3,5e3,15e3,3e4,6e4,3e5,9e5,18e5,36e5,108e5,216e5,432e5,864e5,1728e5,6048e5,2592e6,7776e6,31536e6],fl=[[va.time.second,1],[va.time.second,5],[va.time.second,15],[va.time.second,30],[va.time.minute,1],[
 va.time.minute,5],[va.time.minute,15],[va.time.minute,30],[va.time.hour,1],[va.time.hour,3],[va.time.hour,6],[va.time.hour,12],[va.time.day,1],[va.time.day,2],[va.time.week,1],[va.time.month,1],[va.time.month,3],[va.time.year,1]],sl=[[va.time.format("%Y"),Rt],[va.time.format("%B"),function(n){return n.getMonth()}],[va.time.format("%b %d"),function(n){return n.getDate()!=1}],[va.time.format("%a %d"),function(n){return n.getDay()&&n.getDate()!=1}],[va.time.format("%I %p"),function(n){return n.getHours()}],[va.time.format("%I:%M"),function(n){return n.getMinutes()}],[va.time.format(":%S"),function(n){return n.getSeconds()}],[va.time.format(".%L"),function(n){return n.getMilliseconds()}]],hl=va.scale.linear(),gl=fa(sl);fl.year=function(n,t){return hl.domain(n.map(ha)).ticks(t).map(sa)},va.time.scale=function(){return ca(va.scale.linear(),fl,gl)};var pl=fl.map(function(n){return[n[0].utc,n[1]]}),dl=[[va.time.format.utc("%Y"),Rt],[va.time.format.utc("%B"),function(n){return n.getUTCMonth(
 )}],[va.time.format.utc("%b %d"),function(n){return n.getUTCDate()!=1}],[va.time.format.utc("%a %d"),function(n){return n.getUTCDay()&&n.getUTCDate()!=1}],[va.time.format.utc("%I %p"),function(n){return n.getUTCHours()}],[va.time.format.utc("%I:%M"),function(n){return n.getUTCMinutes()}],[va.time.format.utc(":%S"),function(n){return n.getUTCSeconds()}],[va.time.format.utc(".%L"),function(n){return n.getUTCMilliseconds()}]],ml=fa(dl);return pl.year=function(n,t){return hl.domain(n.map(pa)).ticks(t).map(ga)},va.time.scale.utc=function(){return ca(va.scale.linear(),pl,ml)},va.text=ht(function(n){return n.responseText}),va.json=function(n,t){return gt(n,"application/json",da,t)},va.html=function(n,t){return gt(n,"text/html",ma,t)},va.xml=ht(function(n){return n.responseXML}),va}();
\ No newline at end of file
diff --git a/web/robots.txt b/web/robots.txt
deleted file mode 100644
index c59aca1..0000000
--- a/web/robots.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-User-agent: *
-Disallow: /relay.html
-Disallow: /csv/
-Disallow: /consensus-health.html
-
diff --git a/website/build.xml b/website/build.xml
new file mode 100644
index 0000000..fe75453
--- /dev/null
+++ b/website/build.xml
@@ -0,0 +1,64 @@
+<project default="war" name="metrics-web" basedir=".">
+
+  <!-- Define build paths. -->
+  <property name="sources" value="src"/>
+  <property name="classes" value="classes"/>
+  <property name="config" value="etc"/>
+  <property name="webxmlfile" value="${config}/web.xml"/>
+  <property name="warfile" value="ernie.war"/>
+  <path id="classpath">
+    <pathelement path="${classes}"/>
+    <fileset dir="/usr/share/java">
+      <include name="commons-codec.jar"/>
+      <include name="servlet-api-3.0.jar"/>
+    </fileset>
+    <fileset dir="lib">
+      <include name="REngine.jar"/>
+      <include name="RserveEngine.jar"/>
+    </fileset>
+  </path>
+
+  <target name="init">
+    <mkdir dir="${classes}"/>
+  </target>
+
+  <!-- Compile all servlets. -->
+  <target name="compile" depends="init">
+    <javac destdir="${classes}"
+           srcdir="${sources}"
+           source="1.5"
+           target="1.5"
+           debug="true"
+           deprecation="true"
+           optimize="false"
+           failonerror="true"
+           includeantruntime="false">
+      <classpath refid="classpath"/>
+    </javac>
+  </target>
+
+  <!-- Create a .war file for deployment. -->
+  <target name="war"
+          depends="compile">
+    <war destfile="${warfile}"
+         webxml="${webxmlfile}">
+      <fileset dir="web"/>
+      <lib dir="lib">
+          <include name="jstl.jar"/>
+          <include name="REngine.jar"/>
+          <include name="RserveEngine.jar"/>
+          <include name="standard.jar"/>
+      </lib>
+      <lib dir="/usr/share/java">
+          <include name="commons-codec.jar"/>
+      </lib>
+      <classes dir="${classes}"/>
+      <zipfileset dir="${config}"
+                  prefix="WEB-INF/classes"
+                  includes="logging.properties"/>
+      <metainf dir="${config}"
+               includes="context.xml"/>
+    </war>
+  </target>
+</project>
+
diff --git a/website/etc/context.xml b/website/etc/context.xml
new file mode 100644
index 0000000..09e5bac
--- /dev/null
+++ b/website/etc/context.xml
@@ -0,0 +1,3 @@
+<Context cookies="false">
+</Context>
+
diff --git a/website/etc/logging.properties b/website/etc/logging.properties
new file mode 100644
index 0000000..6f12902
--- /dev/null
+++ b/website/etc/logging.properties
@@ -0,0 +1,6 @@
+handlers = org.apache.juli.FileHandler
+
+org.apache.juli.FileHandler.level = FINE
+org.apache.juli.FileHandler.directory = ${catalina.base}/logs
+org.apache.juli.FileHandler.prefix = ernie.
+
diff --git a/website/etc/web.xml b/website/etc/web.xml
new file mode 100644
index 0000000..992de93
--- /dev/null
+++ b/website/etc/web.xml
@@ -0,0 +1,320 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<web-app version="2.4"
+  xmlns="http://java.sun.com/xml/ns/j2ee"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee 
+  http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" >
+
+  <servlet>
+    <servlet-name>Index</servlet-name>
+    <servlet-class>
+      org.torproject.ernie.web.IndexServlet
+    </servlet-class>
+  </servlet>
+  <servlet-mapping>
+    <servlet-name>Index</servlet-name>
+    <url-pattern>/index.html</url-pattern>
+  </servlet-mapping>
+
+  <servlet>
+    <servlet-name>Graphs</servlet-name>
+    <servlet-class>
+      org.torproject.ernie.web.GraphsServlet
+    </servlet-class>
+  </servlet>
+  <servlet-mapping>
+    <servlet-name>Graphs</servlet-name>
+    <url-pattern>/graphs.html</url-pattern>
+  </servlet-mapping>
+
+  <servlet>
+    <servlet-name>GraphsSubpages</servlet-name>
+    <servlet-class>
+      org.torproject.ernie.web.graphs.GraphsSubpagesServlet
+    </servlet-class>
+  </servlet>
+  <servlet-mapping>
+    <servlet-name>GraphsSubpages</servlet-name>
+    <url-pattern>/network.html</url-pattern>
+  </servlet-mapping>
+  <servlet-mapping>
+    <servlet-name>GraphsSubpages</servlet-name>
+    <url-pattern>/fast-exits.html</url-pattern>
+  </servlet-mapping>
+  <servlet-mapping>
+    <servlet-name>GraphsSubpages</servlet-name>
+    <url-pattern>/users.html</url-pattern>
+  </servlet-mapping>
+  <servlet-mapping>
+    <servlet-name>GraphsSubpages</servlet-name>
+    <url-pattern>/performance.html</url-pattern>
+  </servlet-mapping>
+
+  <servlet>
+    <servlet-name>Bubbles</servlet-name>
+    <servlet-class>
+      org.torproject.ernie.web.graphs.BubblesServlet
+    </servlet-class>
+  </servlet>
+  <servlet-mapping>
+    <servlet-name>Bubbles</servlet-name>
+    <url-pattern>/bubbles.html</url-pattern>
+  </servlet-mapping>
+
+  <servlet>
+    <servlet-name>Research</servlet-name>
+    <servlet-class>
+      org.torproject.ernie.web.ResearchServlet
+    </servlet-class>
+  </servlet>
+  <servlet-mapping>
+    <servlet-name>Research</servlet-name>
+    <url-pattern>/research.html</url-pattern>
+  </servlet-mapping>
+
+  <servlet>
+    <servlet-name>ResearchData</servlet-name>
+    <servlet-class>
+      org.torproject.ernie.web.research.ResearchDataServlet
+    </servlet-class>
+    <init-param>
+      <param-name>localDataDir</param-name>
+      <param-value>
+        /srv/metrics.torproject.org/db/data/
+      </param-value>
+    </init-param>
+  </servlet>
+  <servlet-mapping>
+    <servlet-name>ResearchData</servlet-name>
+    <url-pattern>/data.html</url-pattern>
+  </servlet-mapping>
+
+  <servlet>
+    <servlet-name>ResearchPapers</servlet-name>
+    <servlet-class>
+      org.torproject.ernie.web.research.ResearchPapersServlet
+    </servlet-class>
+  </servlet>
+  <servlet-mapping>
+    <servlet-name>ResearchPapers</servlet-name>
+    <url-pattern>/papers.html</url-pattern>
+  </servlet-mapping>
+
+  <servlet>
+    <servlet-name>ResearchTools</servlet-name>
+    <servlet-class>
+      org.torproject.ernie.web.research.ResearchToolsServlet
+    </servlet-class>
+  </servlet>
+  <servlet-mapping>
+    <servlet-name>ResearchTools</servlet-name>
+    <url-pattern>/tools.html</url-pattern>
+  </servlet-mapping>
+
+  <servlet>
+    <servlet-name>ResearchFormats</servlet-name>
+    <servlet-class>
+      org.torproject.ernie.web.research.ResearchFormatsServlet
+    </servlet-class>
+  </servlet>
+  <servlet-mapping>
+    <servlet-name>ResearchFormats</servlet-name>
+    <url-pattern>/formats.html</url-pattern>
+  </servlet-mapping>
+
+  <servlet>
+    <servlet-name>ResearchStats</servlet-name>
+    <servlet-class>
+      org.torproject.ernie.web.research.ResearchStatsServlet
+    </servlet-class>
+    <init-param>
+      <param-name>statsDir</param-name>
+      <param-value>
+        /srv/metrics.torproject.org/web/stats/
+      </param-value>
+    </init-param>
+  </servlet>
+  <servlet-mapping>
+    <servlet-name>ResearchStats</servlet-name>
+    <url-pattern>/stats/*</url-pattern>
+    <url-pattern>/stats.html</url-pattern>
+  </servlet-mapping>
+
+  <servlet>
+    <servlet-name>Status</servlet-name>
+    <servlet-class>
+      org.torproject.ernie.web.StatusServlet
+    </servlet-class>
+  </servlet>
+  <servlet-mapping>
+    <servlet-name>Status</servlet-name>
+    <url-pattern>/status.html</url-pattern>
+  </servlet-mapping>
+
+  <servlet>
+    <servlet-name>RelaySearch</servlet-name>
+    <servlet-class>
+      org.torproject.ernie.status.relaysearch.RelaySearchServlet
+    </servlet-class>
+  </servlet>
+  <servlet-mapping>
+    <servlet-name>RelaySearch</servlet-name>
+    <url-pattern>/relay-search.html</url-pattern>
+  </servlet-mapping>
+
+  <servlet>
+    <servlet-name>GraphImage</servlet-name>
+    <servlet-class>
+      org.torproject.ernie.web.graphs.GraphImageServlet
+    </servlet-class>
+  </servlet>
+  <servlet-mapping>
+    <servlet-name>GraphImage</servlet-name>
+    <url-pattern>/networksize.png</url-pattern>
+    <url-pattern>/networksize.pdf</url-pattern>
+    <url-pattern>/networksize.svg</url-pattern>
+    <url-pattern>/cloudbridges.png</url-pattern>
+    <url-pattern>/cloudbridges.pdf</url-pattern>
+    <url-pattern>/cloudbridges.svg</url-pattern>
+    <url-pattern>/relaycountries.png</url-pattern>
+    <url-pattern>/relaycountries.pdf</url-pattern>
+    <url-pattern>/relaycountries.svg</url-pattern>
+    <url-pattern>/relayflags.png</url-pattern>
+    <url-pattern>/relayflags.pdf</url-pattern>
+    <url-pattern>/relayflags.svg</url-pattern>
+    <url-pattern>/versions.png</url-pattern>
+    <url-pattern>/versions.pdf</url-pattern>
+    <url-pattern>/versions.svg</url-pattern>
+    <url-pattern>/platforms.png</url-pattern>
+    <url-pattern>/platforms.pdf</url-pattern>
+    <url-pattern>/platforms.svg</url-pattern>
+    <url-pattern>/bandwidth.png</url-pattern>
+    <url-pattern>/bandwidth.pdf</url-pattern>
+    <url-pattern>/bandwidth.svg</url-pattern>
+    <url-pattern>/bwhist-flags.png</url-pattern>
+    <url-pattern>/bwhist-flags.pdf</url-pattern>
+    <url-pattern>/bwhist-flags.svg</url-pattern>
+    <url-pattern>/bandwidth-flags.png</url-pattern>
+    <url-pattern>/bandwidth-flags.pdf</url-pattern>
+    <url-pattern>/bandwidth-flags.svg</url-pattern>
+    <url-pattern>/dirbytes.png</url-pattern>
+    <url-pattern>/dirbytes.pdf</url-pattern>
+    <url-pattern>/dirbytes.svg</url-pattern>
+    <url-pattern>/torperf.png</url-pattern>
+    <url-pattern>/torperf.pdf</url-pattern>
+    <url-pattern>/torperf.svg</url-pattern>
+    <url-pattern>/torperf-failures.png</url-pattern>
+    <url-pattern>/torperf-failures.pdf</url-pattern>
+    <url-pattern>/torperf-failures.svg</url-pattern>
+    <url-pattern>/connbidirect.png</url-pattern>
+    <url-pattern>/connbidirect.pdf</url-pattern>
+    <url-pattern>/connbidirect.svg</url-pattern>
+    <url-pattern>/fast-exits.png</url-pattern>
+    <url-pattern>/fast-exits.pdf</url-pattern>
+    <url-pattern>/fast-exits.svg</url-pattern>
+    <url-pattern>/almost-fast-exits.png</url-pattern>
+    <url-pattern>/almost-fast-exits.pdf</url-pattern>
+    <url-pattern>/almost-fast-exits.svg</url-pattern>
+    <url-pattern>/userstats-relay-country.png</url-pattern>
+    <url-pattern>/userstats-relay-country.pdf</url-pattern>
+    <url-pattern>/userstats-relay-country.svg</url-pattern>
+    <url-pattern>/userstats-bridge-country.png</url-pattern>
+    <url-pattern>/userstats-bridge-country.pdf</url-pattern>
+    <url-pattern>/userstats-bridge-country.svg</url-pattern>
+    <url-pattern>/userstats-bridge-transport.png</url-pattern>
+    <url-pattern>/userstats-bridge-transport.pdf</url-pattern>
+    <url-pattern>/userstats-bridge-transport.svg</url-pattern>
+    <url-pattern>/userstats-bridge-version.png</url-pattern>
+    <url-pattern>/userstats-bridge-version.pdf</url-pattern>
+    <url-pattern>/userstats-bridge-version.svg</url-pattern>
+  </servlet-mapping>
+
+  <servlet>
+    <servlet-name>Csv</servlet-name>
+    <servlet-class>
+      org.torproject.ernie.web.graphs.CsvServlet
+    </servlet-class>
+  </servlet>
+  <servlet-mapping>
+    <servlet-name>Csv</servlet-name>
+    <url-pattern>/csv/*</url-pattern>
+  </servlet-mapping>
+
+  <servlet>
+    <servlet-name>ExoneraTor</servlet-name>
+    <servlet-class>
+      org.torproject.ernie.status.exonerator.ExoneraTorServlet
+    </servlet-class>
+  </servlet>
+  <servlet-mapping>
+    <servlet-name>ExoneraTor</servlet-name>
+    <url-pattern>/exonerator.html</url-pattern>
+  </servlet-mapping>
+
+  <servlet>
+    <servlet-name>ConsensusHealthServlet</servlet-name>
+    <servlet-class>
+      org.torproject.ernie.status.doctor.ConsensusHealthServlet
+    </servlet-class>
+  </servlet>
+  <servlet-mapping>
+    <servlet-name>ConsensusHealthServlet</servlet-name>
+    <url-pattern>/consensus-health.html</url-pattern>
+  </servlet-mapping>
+
+  <welcome-file-list>
+    <welcome-file>index.html</welcome-file>
+  </welcome-file-list>
+
+  <error-page>
+    <error-code>400</error-code>
+    <location>/WEB-INF/error.jsp</location>
+  </error-page>
+  <error-page>
+    <error-code>404</error-code>
+    <location>/WEB-INF/error.jsp</location>
+  </error-page>
+  <error-page>
+    <error-code>500</error-code>
+    <location>/WEB-INF/error.jsp</location>
+  </error-page>
+  <error-page>
+    <exception-type>java.lang.Throwable</exception-type>
+    <location>/WEB-INF/error.jsp</location>
+  </error-page>
+
+  <resource-ref>
+    <description>Tor Metrics Database</description>
+    <res-ref-name>jdbc/tordir</res-ref-name>
+    <res-type>javax.sql.DataSource</res-type>
+    <res-auth>Container</res-auth>
+  </resource-ref>
+
+  <context-param>
+    <param-name>rserveHost</param-name>
+    <param-value>localhost</param-value>
+  </context-param>
+  <context-param>
+    <param-name>rservePort</param-name>
+    <param-value>6311</param-value>
+  </context-param>
+  <context-param>
+    <param-name>maxCacheAge</param-name>
+    <param-value>21600</param-value>
+  </context-param>
+  <context-param>
+    <param-name>cachedGraphsDir</param-name>
+    <param-value>
+      /srv/metrics.torproject.org/web/rserve/graphs/
+    </param-value>
+  </context-param>
+
+  <listener>
+    <listener-class>
+      org.torproject.ernie.web.graphs.RObjectGenerator
+    </listener-class>
+  </listener>
+
+</web-app>
+
diff --git a/website/lib/REngine.jar b/website/lib/REngine.jar
new file mode 100644
index 0000000..ddf4059
Binary files /dev/null and b/website/lib/REngine.jar differ
diff --git a/website/lib/RserveEngine.jar b/website/lib/RserveEngine.jar
new file mode 100644
index 0000000..77b22ce
Binary files /dev/null and b/website/lib/RserveEngine.jar differ
diff --git a/website/lib/jstl.jar b/website/lib/jstl.jar
new file mode 100644
index 0000000..6b41358
Binary files /dev/null and b/website/lib/jstl.jar differ
diff --git a/website/lib/standard.jar b/website/lib/standard.jar
new file mode 100644
index 0000000..258daae
Binary files /dev/null and b/website/lib/standard.jar differ
diff --git a/website/rserve/Rserv.conf b/website/rserve/Rserv.conf
new file mode 100644
index 0000000..a23af49
--- /dev/null
+++ b/website/rserve/Rserv.conf
@@ -0,0 +1,2 @@
+workdir /srv/metrics.torproject.org/web/rserve/workdir
+source rserve-init.R
diff --git a/website/rserve/csv.R b/website/rserve/csv.R
new file mode 100644
index 0000000..2ec6e7b
--- /dev/null
+++ b/website/rserve/csv.R
@@ -0,0 +1,213 @@
+options(scipen = 15)
+
+export_networksize <- function(path) {
+  s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv",
+    stringsAsFactors = FALSE)
+  s <- s[s$flag == '' & s$country == '' & s$version == '' &
+         s$platform == '' & s$ec2bridge == '',
+         c("date", "relays", "bridges")]
+  write.csv(s, path, quote = FALSE, row.names = FALSE)
+}
+
+export_cloudbridges <- function(path) {
+  s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv",
+    stringsAsFactors = FALSE)
+  s <- s[s$flag == '' & s$country == '' & s$version == '' &
+         s$platform == '' & s$ec2bridge == 't', ]
+  cloudbridges <- data.frame(date = s$date, cloudbridges = s$bridges)
+  write.csv(cloudbridges, path, quote = FALSE, row.names = FALSE)
+}
+
+export_relaycountries <- function(path) {
+  s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv",
+    stringsAsFactors = FALSE)
+  s <- s[s$flag == '' & s$country != '' & s$version == '' &
+         s$platform == '' & s$ec2bridge == '',
+         c("date", "country", "relays")]
+  write.csv(s, path, quote = FALSE, row.names = FALSE)
+}
+
+export_versions <- function(path) {
+  s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv",
+    stringsAsFactors = FALSE)
+  s <- s[s$flag == '' & s$country == '' & s$version != '' &
+         s$platform == '' & s$ec2bridge == '',
+         c("date", "version", "relays")]
+  versions <- cast(s, date ~ version, value = "relays")
+  versions <- versions[order(versions$date), ]
+  write.csv(versions, path, quote = FALSE, row.names = FALSE)
+}
+
+export_platforms <- function(path) {
+  s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv",
+    stringsAsFactors = FALSE)
+  s <- s[s$flag == '' & s$country == '' & s$version == '' &
+         s$platform != '' & s$ec2bridge == '',
+         c("date", "platform", "relays")]
+  s <- data.frame(date = s$date,
+                  platform = ifelse(s$platform == 'FreeBSD', 'bsd',
+                  tolower(s$platform)), relays = s$relays)
+  s <- cast(s, date ~ platform, value = "relays")
+  platforms <- s[order(s$date), ]
+  write.csv(platforms, path, quote = FALSE, row.names = FALSE)
+}
+
+export_bandwidth <- function(path) {
+  b <- read.csv("/srv/metrics.torproject.org/web/stats/bandwidth.csv",
+    stringsAsFactors = FALSE)
+  b <- b[b$isexit == '' & b$isguard == '', ]
+  b <- data.frame(date = as.Date(b$date, "%Y-%m-%d"),
+                  bwadv = b$advbw,
+                  bwhist = floor((b$bwread + b$bwwrite) / 2))
+  b <- b[order(b$date), ]
+  write.csv(b, path, quote = FALSE, row.names = FALSE)
+}
+
+export_bwhist_flags <- function(path) {
+  b <- read.csv("/srv/metrics.torproject.org/web/stats/bandwidth.csv",
+    stringsAsFactors = FALSE)
+  b <- b[b$isexit != '' & b$isguard != '' & !is.na(b$bwread) &
+         !is.na(b$bwwrite), ]
+  b <- data.frame(date = as.Date(b$date, "%Y-%m-%d"),
+                  isexit = b$isexit == 't', isguard = b$isguard == 't',
+                  read = b$bwread, written = b$bwwrite)
+  write.csv(b, path, quote = FALSE, row.names = FALSE)
+}
+
+export_dirbytes <- function(path) {
+  b <- read.csv("/srv/metrics.torproject.org/web/stats/bandwidth.csv",
+    stringsAsFactors = FALSE)
+  b <- b[b$isexit == '' & b$isguard == '' & !is.na(b$dirread) &
+         !is.na(b$dirwrite), ]
+  b <- data.frame(date = as.Date(b$date, "%Y-%m-%d"),
+                  dirread = b$dirread, dirwrite = b$dirwrite)
+  b <- b[order(b$date), ]
+  write.csv(b, path, quote = FALSE, row.names = FALSE)
+}
+
+export_relayflags <- function(path) {
+  s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv",
+    stringsAsFactors = FALSE)
+  s <- s[s$country == '' & s$version == '' & s$platform == '' &
+         s$ec2bridge == '', ]
+  s <- data.frame(date = as.Date(s$date, "%Y-%m-%d"),
+                  flag = ifelse(s$flag == '', 'running', tolower(s$flag)),
+                  relays = s$relays)
+  s <- cast(s, date ~ flag, value = "relays")
+  relayflags <- s[order(s$date), ]
+  write.csv(relayflags, path, quote = FALSE, row.names = FALSE)
+}
+
+export_torperf <- function(path) {
+  t <- read.csv("/srv/metrics.torproject.org/web/stats/torperf.csv",
+    stringsAsFactors = FALSE)
+  t <- data.frame(
+     source = paste(ifelse(t$source == '', 'all', t$source),
+                    ifelse(t$size == 50 * 1024, '50kb',
+                           ifelse(t$size == 1024 * 1024, '1mb', '5mb')),
+                    sep = '-'),
+     date = as.Date(t$date, "%Y-%m-%d"),
+     q1 = t$q1, md = t$md, q3 = t$q3)
+  torperf <- t[order(t$source, t$date), ]
+  write.csv(torperf, path, quote = FALSE, row.names = FALSE)
+}
+
+export_torperf_failures <- function(path) {
+  t <- read.csv("/srv/metrics.torproject.org/web/stats/torperf.csv",
+    stringsAsFactors = FALSE)
+  t <- data.frame(
+     source = paste(ifelse(t$source == '', 'all', t$source),
+                    ifelse(t$size == 50 * 1024, '50kb',
+                           ifelse(t$size == 1024 * 1024, '1mb', '5mb')),
+                    sep = '-'),
+     date = as.Date(t$date, "%Y-%m-%d"),
+     timeouts = t$timeouts, failures = t$failures, requests = t$requests)
+  torperf <- t[order(t$source, t$date), ]
+  write.csv(torperf, path, quote = FALSE, row.names = FALSE)
+}
+
+export_connbidirect <- function(path) {
+  c <- read.csv("/srv/metrics.torproject.org/web/stats/connbidirect.csv",
+    stringsAsFactors = FALSE)
+  write.csv(format(c, trim = TRUE, scientific = FALSE), path,
+      quote = FALSE, row.names = FALSE)
+}
+
+export_bandwidth_flags <- function(path) {
+  b <- read.csv("/srv/metrics.torproject.org/web/stats/bandwidth.csv",
+    stringsAsFactors = FALSE)
+  b <- b[b$isexit != '' & b$isguard != '', ]
+  b <- data.frame(date = as.Date(b$date, "%Y-%m-%d"),
+                  isexit = b$isexit == 't', isguard = b$isguard == 't',
+                  advbw = b$advbw,
+                  bwhist = floor((b$bwread + b$bwwrite) / 2))
+  b <- rbind(
+    data.frame(b[b$isguard == TRUE, ], flag = "guard"),
+    data.frame(b[b$isexit == TRUE, ], flag = "exit"))
+  b <- data.frame(date = b$date, advbw = b$advbw, bwhist = b$bwhist,
+                  flag = b$flag)
+  b <- aggregate(list(advbw = b$advbw, bwhist = b$bwhist),
+                 by = list(date = b$date, flag = b$flag), FUN = sum,
+                 na.rm = TRUE, na.action = NULL)
+  b <- melt(b, id.vars = c("date", "flag"))
+  b <- data.frame(date = b$date, type = b$variable, flag = b$flag,
+                  value = b$value)
+  b <- b[b$value > 0, ]
+  write.csv(b, path, quote = FALSE, row.names = FALSE)
+}
+
+export_userstats <- function(path) {
+  c <- read.csv("/srv/metrics.torproject.org/web/stats/clients.csv",
+    stringsAsFactors = FALSE)
+  c <- data.frame(date = c$date, node = c$node, country = c$country,
+                  transport = c$transport, version = c$version,
+                  frac = c$frac, users = c$clients)
+  write.csv(format(c, trim = TRUE, scientific = FALSE), path,
+      quote = FALSE, row.names = FALSE)
+}
+
+help_export_monthly_userstats <- function(path, aggr_fun) {
+  c <- read.csv("/srv/metrics.torproject.org/web/stats/clients.csv",
+    stringsAsFactors = FALSE)
+  c <- c[c$country != '' & c$transport == '' & c$version == '', ]
+  u <- data.frame(date = c$date, country = c$country, users = c$clients,
+                  stringsAsFactors = FALSE)
+  u <- aggregate(list(users = u$users),
+                      by = list(date = u$date, country = u$country), sum)
+  u <- aggregate(list(users = u$users),
+                      by = list(country = u$country,
+                                month = substr(u$date, 1, 7)), aggr_fun)
+  u <- rbind(u, data.frame(country = "zy",
+                aggregate(list(users = u$users),
+                          by = list(month = u$month), sum)))
+  u <- cast(u, country ~ month, value = "users")
+  u[u$country == "zy", "country"] <- "all"
+  u[, 2:length(u)] <- floor(u[, 2:length(u)])
+  write.csv(u, path, quote = FALSE, row.names = FALSE)
+}
+
+export_monthly_userstats_peak <- function(path) {
+  help_export_monthly_userstats(path, max)
+}
+
+export_monthly_userstats_average <- function(path) {
+  help_export_monthly_userstats(path, mean)
+}
+
+export_userstats_detector <- function(path) {
+  c <- read.csv("/srv/metrics.torproject.org/web/stats/clients.csv",
+    stringsAsFactors = FALSE)
+  c <- c[c$country != '' & c$transport == '' & c$version == '' &
+         c$node == 'relay', ]
+  u <- data.frame(country = c$country, date = c$date, users = c$clients,
+                  stringsAsFactors = FALSE)
+  u <- rbind(u, data.frame(country = "zy",
+                aggregate(list(users = u$users),
+                          by = list(date = u$date), sum)))
+  u <- data.frame(date = u$date, country = u$country,
+                  users = floor(u$users))
+  u <- cast(u, date ~ country, value = "users")
+  names(u)[names(u) == "zy"] <- "all"
+  write.csv(u, path, quote = FALSE, row.names = FALSE)
+}
+
diff --git a/website/rserve/graphs.R b/website/rserve/graphs.R
new file mode 100644
index 0000000..c4170e7
--- /dev/null
+++ b/website/rserve/graphs.R
@@ -0,0 +1,929 @@
+countrylist <- list(
+  "ad" = "Andorra",
+  "ae" = "the United Arab Emirates",
+  "af" = "Afghanistan",
+  "ag" = "Antigua and Barbuda",
+  "ai" = "Anguilla",
+  "al" = "Albania",
+  "am" = "Armenia",
+  "an" = "the Netherlands Antilles",
+  "ao" = "Angola",
+  "aq" = "Antarctica",
+  "ar" = "Argentina",
+  "as" = "American Samoa",
+  "at" = "Austria",
+  "au" = "Australia",
+  "aw" = "Aruba",
+  "ax" = "the Aland Islands",
+  "az" = "Azerbaijan",
+  "ba" = "Bosnia and Herzegovina",
+  "bb" = "Barbados",
+  "bd" = "Bangladesh",
+  "be" = "Belgium",
+  "bf" = "Burkina Faso",
+  "bg" = "Bulgaria",
+  "bh" = "Bahrain",
+  "bi" = "Burundi",
+  "bj" = "Benin",
+  "bl" = "Saint Bartelemey",
+  "bm" = "Bermuda",
+  "bn" = "Brunei",
+  "bo" = "Bolivia",
+  "br" = "Brazil",
+  "bs" = "the Bahamas",
+  "bt" = "Bhutan",
+  "bv" = "the Bouvet Island",
+  "bw" = "Botswana",
+  "by" = "Belarus",
+  "bz" = "Belize",
+  "ca" = "Canada",
+  "cc" = "the Cocos (Keeling) Islands",
+  "cd" = "the Democratic Republic of the Congo",
+  "cf" = "Central African Republic",
+  "cg" = "Congo",
+  "ch" = "Switzerland",
+  "ci" = "Côte d'Ivoire",
+  "ck" = "the Cook Islands",
+  "cl" = "Chile",
+  "cm" = "Cameroon",
+  "cn" = "China",
+  "co" = "Colombia",
+  "cr" = "Costa Rica",
+  "cu" = "Cuba",
+  "cv" = "Cape Verde",
+  "cx" = "the Christmas Island",
+  "cy" = "Cyprus",
+  "cz" = "the Czech Republic",
+  "de" = "Germany",
+  "dj" = "Djibouti",
+  "dk" = "Denmark",
+  "dm" = "Dominica",
+  "do" = "the Dominican Republic",
+  "dz" = "Algeria",
+  "ec" = "Ecuador",
+  "ee" = "Estonia",
+  "eg" = "Egypt",
+  "eh" = "the Western Sahara",
+  "er" = "Eritrea",
+  "es" = "Spain",
+  "et" = "Ethiopia",
+  "fi" = "Finland",
+  "fj" = "Fiji",
+  "fk" = "the Falkland Islands (Malvinas)",
+  "fm" = "the Federated States of Micronesia",
+  "fo" = "the Faroe Islands",
+  "fr" = "France",
+  "fx" = "Metropolitan France",
+  "ga" = "Gabon",
+  "gb" = "the United Kingdom",
+  "gd" = "Grenada",
+  "ge" = "Georgia",
+  "gf" = "French Guiana",
+  "gg" = "Guernsey",
+  "gh" = "Ghana",
+  "gi" = "Gibraltar",
+  "gl" = "Greenland",
+  "gm" = "Gambia",
+  "gn" = "Guinea",
+  "gp" = "Guadeloupe",
+  "gq" = "Equatorial Guinea",
+  "gr" = "Greece",
+  "gs" = "South Georgia and the South Sandwich Islands",
+  "gt" = "Guatemala",
+  "gu" = "Guam",
+  "gw" = "Guinea-Bissau",
+  "gy" = "Guyana",
+  "hk" = "Hong Kong",
+  "hm" = "Heard Island and McDonald Islands",
+  "hn" = "Honduras",
+  "hr" = "Croatia",
+  "ht" = "Haiti",
+  "hu" = "Hungary",
+  "id" = "Indonesia",
+  "ie" = "Ireland",
+  "il" = "Israel",
+  "im" = "the Isle of Man",
+  "in" = "India",
+  "io" = "the British Indian Ocean Territory",
+  "iq" = "Iraq",
+  "ir" = "Iran",
+  "is" = "Iceland",
+  "it" = "Italy",
+  "je" = "Jersey",
+  "jm" = "Jamaica",
+  "jo" = "Jordan",
+  "jp" = "Japan",
+  "ke" = "Kenya",
+  "kg" = "Kyrgyzstan",
+  "kh" = "Cambodia",
+  "ki" = "Kiribati",
+  "km" = "Comoros",
+  "kn" = "Saint Kitts and Nevis",
+  "kp" = "North Korea",
+  "kr" = "the Republic of Korea",
+  "kw" = "Kuwait",
+  "ky" = "the Cayman Islands",
+  "kz" = "Kazakhstan",
+  "la" = "Laos",
+  "lb" = "Lebanon",
+  "lc" = "Saint Lucia",
+  "li" = "Liechtenstein",
+  "lk" = "Sri Lanka",
+  "lr" = "Liberia",
+  "ls" = "Lesotho",
+  "lt" = "Lithuania",
+  "lu" = "Luxembourg",
+  "lv" = "Latvia",
+  "ly" = "Libya",
+  "ma" = "Morocco",
+  "mc" = "Monaco",
+  "md" = "the Republic of Moldova",
+  "me" = "Montenegro",
+  "mf" = "Saint Martin",
+  "mg" = "Madagascar",
+  "mh" = "the Marshall Islands",
+  "mk" = "Macedonia",
+  "ml" = "Mali",
+  "mm" = "Burma",
+  "mn" = "Mongolia",
+  "mo" = "Macau",
+  "mp" = "the Northern Mariana Islands",
+  "mq" = "Martinique",
+  "mr" = "Mauritania",
+  "ms" = "Montserrat",
+  "mt" = "Malta",
+  "mu" = "Mauritius",
+  "mv" = "the Maldives",
+  "mw" = "Malawi",
+  "mx" = "Mexico",
+  "my" = "Malaysia",
+  "mz" = "Mozambique",
+  "na" = "Namibia",
+  "nc" = "New Caledonia",
+  "ne" = "Niger",
+  "nf" = "Norfolk Island",
+  "ng" = "Nigeria",
+  "ni" = "Nicaragua",
+  "nl" = "the Netherlands",
+  "no" = "Norway",
+  "np" = "Nepal",
+  "nr" = "Nauru",
+  "nu" = "Niue",
+  "nz" = "New Zealand",
+  "om" = "Oman",
+  "pa" = "Panama",
+  "pe" = "Peru",
+  "pf" = "French Polynesia",
+  "pg" = "Papua New Guinea",
+  "ph" = "the Philippines",
+  "pk" = "Pakistan",
+  "pl" = "Poland",
+  "pm" = "Saint Pierre and Miquelon",
+  "pn" = "the Pitcairn Islands",
+  "pr" = "Puerto Rico",
+  "ps" = "the Palestinian Territory",
+  "pt" = "Portugal",
+  "pw" = "Palau",
+  "py" = "Paraguay",
+  "qa" = "Qatar",
+  "re" = "Reunion",
+  "ro" = "Romania",
+  "rs" = "Serbia",
+  "ru" = "Russia",
+  "rw" = "Rwanda",
+  "sa" = "Saudi Arabia",
+  "sb" = "the Solomon Islands",
+  "sc" = "the Seychelles",
+  "sd" = "Sudan",
+  "se" = "Sweden",
+  "sg" = "Singapore",
+  "sh" = "Saint Helena",
+  "si" = "Slovenia",
+  "sj" = "Svalbard and Jan Mayen",
+  "sk" = "Slovakia",
+  "sl" = "Sierra Leone",
+  "sm" = "San Marino",
+  "sn" = "Senegal",
+  "so" = "Somalia",
+  "sr" = "Suriname",
+  "ss" = "South Sudan",
+  "st" = "São Tomé and Príncipe",
+  "sv" = "El Salvador",
+  "sy" = "the Syrian Arab Republic",
+  "sz" = "Swaziland",
+  "tc" = "Turks and Caicos Islands",
+  "td" = "Chad",
+  "tf" = "the French Southern Territories",
+  "tg" = "Togo",
+  "th" = "Thailand",
+  "tj" = "Tajikistan",
+  "tk" = "Tokelau",
+  "tl" = "East Timor",
+  "tm" = "Turkmenistan",
+  "tn" = "Tunisia",
+  "to" = "Tonga",
+  "tr" = "Turkey",
+  "tt" = "Trinidad and Tobago",
+  "tv" = "Tuvalu",
+  "tw" = "Taiwan",
+  "tz" = "the United Republic of Tanzania",
+  "ua" = "Ukraine",
+  "ug" = "Uganda",
+  "um" = "the United States Minor Outlying Islands",
+  "us" = "the United States",
+  "uy" = "Uruguay",
+  "uz" = "Uzbekistan",
+  "va" = "Vatican City",
+  "vc" = "Saint Vincent and the Grenadines",
+  "ve" = "Venezuela",
+  "vg" = "the British Virgin Islands",
+  "vi" = "the United States Virgin Islands",
+  "vn" = "Vietnam",
+  "vu" = "Vanuatu",
+  "wf" = "Wallis and Futuna",
+  "ws" = "Samoa",
+  "ye" = "Yemen",
+  "yt" = "Mayotte",
+  "za" = "South Africa",
+  "zm" = "Zambia",
+  "zw" = "Zimbabwe")
+
+countryname <- function(country) {
+  res <- countrylist[[country]]
+  if (is.null(res))
+    res <- "no-man's-land"
+  res
+}
+
+date_breaks <- function(days) {
+  length <- cut(days, c(0, 7, 12, 56, 180, 600, 5000, Inf), labels=FALSE)
+  major <- c("days", "2 days", "weeks", "months", "3 months", "years",
+    "5 years")[length]
+  minor <- c("10 years", "days", "days", "weeks", "months", "months",
+    "years")[length]
+  format <- c("%d-%b", "%d-%b", "%d-%b", "%b-%Y", "%b-%Y", "%Y",
+    "%Y")[length]
+  list(major = major, minor = minor, format = format)
+}
+
+plot_networksize <- function(start, end, path) {
+  end <- min(end, as.character(Sys.Date() - 2))
+  s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv",
+    stringsAsFactors = FALSE)
+  s <- s[s$date >= start & s$date <= end & s$flag == '' &
+         s$country == '' & s$version == '' & s$platform == '' &
+         s$ec2bridge == '', ]
+  s <- data.frame(date = as.Date(s$date, "%Y-%m-%d"), relays = s$relays,
+                  bridges = s$bridges)
+  dates <- seq(from = as.Date(start, "%Y-%m-%d"),
+      to = as.Date(end, "%Y-%m-%d"), by="1 day")
+  missing <- setdiff(dates, as.Date(s$date, origin = "1970-01-01"))
+  if (length(missing) > 0)
+    s <- rbind(s,
+        data.frame(date = as.Date(missing, origin = "1970-01-01"),
+        relays = NA, bridges = NA))
+  networksize <- melt(s, id = "date")
+  date_breaks <- date_breaks(
+    as.numeric(max(as.Date(networksize$date, "%Y-%m-%d")) -
+    min(as.Date(networksize$date, "%Y-%m-%d"))))
+  ggplot(networksize, aes(x = as.Date(date, "%Y-%m-%d"), y = value,
+    colour = variable)) + geom_line(size = 1) +
+    scale_x_date(name = paste("\nThe Tor Project - ",
+        "https://metrics.torproject.org/", sep = ""),
+        format = date_breaks$format, major = date_breaks$major,
+        minor = date_breaks$minor) +
+    scale_y_continuous(name = "", limits = c(0, max(networksize$value,
+        na.rm = TRUE))) +
+    scale_colour_hue("", breaks = c("relays", "bridges"),
+        labels = c("Relays", "Bridges")) +
+    opts(title = "Number of relays\n")
+  ggsave(filename = path, width = 8, height = 5, dpi = 72)
+}
+
+plot_cloudbridges <- function(start, end, path) {
+  end <- min(end, as.character(Sys.Date() - 2))
+  s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv",
+    stringsAsFactors = FALSE)
+  s <- s[s$date >= start & s$date <= end & s$flag == '' &
+         s$country == '' & s$version == '' & s$platform == '' &
+         s$ec2bridge == 't', ]
+  s <- data.frame(date = as.Date(s$date, "%Y-%m-%d"), bridges = s$bridges)
+  dates <- seq(from = as.Date(start, "%Y-%m-%d"),
+      to = as.Date(end, "%Y-%m-%d"), by="1 day")
+  missing <- setdiff(dates, s$date)
+  if (length(missing) > 0)
+    s <- rbind(s,
+        data.frame(date = as.Date(missing, origin = "1970-01-01"),
+        bridges = NA))
+  date_breaks <- date_breaks(
+    as.numeric(max(as.Date(s$date, "%Y-%m-%d")) -
+    min(as.Date(s$date, "%Y-%m-%d"))))
+  ggplot(s, aes(x = as.Date(date, "%Y-%m-%d"), y = bridges)) +
+    geom_line(size = 1, colour = "green3") +
+    scale_x_date(name = paste("\nThe Tor Project - ",
+        "https://metrics.torproject.org/", sep = ""),
+        format = date_breaks$format, major = date_breaks$major,
+        minor = date_breaks$minor) +
+    scale_y_continuous(name = "", limits = c(0,
+        max(s$bridges, na.rm = TRUE))) +
+    opts(title = "Number of Tor Cloud bridges\n")
+  ggsave(filename = path, width = 8, height = 5, dpi = 72)
+}
+
+plot_relaycountries <- function(start, end, country, path) {
+  end <- min(end, as.character(Sys.Date() - 2))
+  s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv",
+    stringsAsFactors = FALSE)
+  s <- s[s$date >= start & s$date <= end & s$flag == '' &
+         s$country == ifelse(country == "all", '', country) &
+         s$version == '' & s$platform == '' & s$ec2bridge == '', ]
+  s <- data.frame(date = as.Date(s$date, "%Y-%m-%d"), relays = s$relays)
+  dates <- seq(from = as.Date(start, "%Y-%m-%d"),
+      to = as.Date(end, "%Y-%m-%d"), by="1 day")
+  missing <- setdiff(dates, s$date)
+  if (length(missing) > 0)
+    s <- rbind(s,
+        data.frame(date = as.Date(missing, origin = "1970-01-01"),
+        relays = NA))
+  title <- ifelse(country == "all",
+    "Number of relays in all countries\n",
+    paste("Number of relays in ", countryname(country), "\n", sep = ""))
+  formatter <- function(x, ...) { format(x, scientific = FALSE, ...) }
+  date_breaks <- date_breaks(
+    as.numeric(max(as.Date(s$date, "%Y-%m-%d")) -
+    min(as.Date(s$date, "%Y-%m-%d"))))
+  ggplot(s, aes(x = as.Date(date, "%Y-%m-%d"), y = relays)) +
+    geom_line(size = 1) +
+    scale_x_date(name = paste("\nThe Tor Project - ",
+        "https://metrics.torproject.org/", sep = ""),
+        format = date_breaks$format, major = date_breaks$major,
+        minor = date_breaks$minor) +
+    scale_y_continuous(name = "", limits = c(0, max(s$relays,
+        na.rm = TRUE)), formatter = formatter) +
+    opts(title = title)
+  ggsave(filename = path, width = 8, height = 5, dpi = 72)
+}
+
+plot_versions <- function(start, end, path) {
+  end <- min(end, as.character(Sys.Date() - 2))
+  s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv",
+    stringsAsFactors = FALSE)
+  s <- s[s$date >= start & s$date <= end & s$flag == '' &
+         s$country == '' & s$version != '' & s$platform == '' &
+         s$ec2bridge == '', ]
+  s <- data.frame(date = as.Date(s$date, "%Y-%m-%d"), version = s$version,
+                  relays = s$relays)
+  known_versions <- c("0.1.0", "0.1.1", "0.1.2", "0.2.0", "0.2.1",
+        "0.2.2", "0.2.3", "0.2.4", "0.2.5")
+  colours <- data.frame(breaks = known_versions,
+    values = brewer.pal(length(known_versions), "Paired"),
+    stringsAsFactors = FALSE)
+  versions <- s[s$version %in% known_versions, ]
+  visible_versions <- sort(unique(versions$version))
+  date_breaks <- date_breaks(
+    as.numeric(max(as.Date(versions$date, "%Y-%m-%d")) -
+    min(as.Date(versions$date, "%Y-%m-%d"))))
+  ggplot(versions, aes(x = as.Date(date, "%Y-%m-%d"), y = relays,
+      colour = version)) +
+    geom_line(size = 1) +
+    scale_x_date(name = paste("\nThe Tor Project - ",
+        "https://metrics.torproject.org/", sep = ""),
+        format = date_breaks$format, major = date_breaks$major,
+        minor = date_breaks$minor) +
+    scale_y_continuous(name = "",
+      limits = c(0, max(versions$relays, na.rm = TRUE))) +
+    scale_colour_manual(name = "Tor version",
+      values = colours[colours$breaks %in% visible_versions, 2],
+      breaks = visible_versions) +
+    opts(title = "Relay versions\n")
+  ggsave(filename = path, width = 8, height = 5, dpi = 72)
+}
+
+plot_platforms <- function(start, end, path) {
+  end <- min(end, as.character(Sys.Date() - 2))
+  s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv",
+    stringsAsFactors = FALSE)
+  s <- s[s$date >= start & s$date <= end & s$flag == '' &
+         s$country == '' & s$version == '' & s$platform != '' &
+         s$ec2bridge == '', ]
+  platforms <- data.frame(date = as.Date(s$date, "%Y-%m-%d"),
+                  variable = s$platform, value = s$relays)
+  date_breaks <- date_breaks(
+    as.numeric(max(as.Date(platforms$date, "%Y-%m-%d")) -
+    min(as.Date(platforms$date, "%Y-%m-%d"))))
+  ggplot(platforms, aes(x = as.Date(date, "%Y-%m-%d"), y = value,
+      colour = variable)) +
+    geom_line(size = 1) +
+    scale_x_date(name = paste("\nThe Tor Project - ",
+        "https://metrics.torproject.org/", sep = ""),
+        format = date_breaks$format, major = date_breaks$major,
+        minor = date_breaks$minor) +
+    scale_y_continuous(name = "",
+      limits = c(0, max(platforms$value, na.rm = TRUE))) +
+    scale_colour_manual(name = "Platform",
+      breaks = c("Linux", "Darwin", "FreeBSD", "Windows", "Other"),
+      values = c("#E69F00", "#56B4E9", "#009E73", "#0072B2", "#333333")) +
+    opts(title = "Relay platforms\n")
+  ggsave(filename = path, width = 8, height = 5, dpi = 72)
+}
+
+plot_bandwidth <- function(start, end, path) {
+  end <- min(end, as.character(Sys.Date() - 4))
+  b <- read.csv("/srv/metrics.torproject.org/web/stats/bandwidth.csv",
+    stringsAsFactors = FALSE)
+  b <- b[b$date >= start & b$date <= end & b$isexit == '' &
+         b$isguard == '', ]
+  b <- data.frame(date = as.Date(b$date, "%Y-%m-%d"),
+                  bwadv = b$advbw,
+                  bwhist = (b$bwread + b$bwwrite) / 2)
+  bandwidth <- melt(b, id = "date")
+  date_breaks <- date_breaks(
+    as.numeric(max(as.Date(bandwidth$date, "%Y-%m-%d")) -
+    min(as.Date(bandwidth$date, "%Y-%m-%d"))))
+  ggplot(bandwidth, aes(x = as.Date(date, "%Y-%m-%d"), y = value / 2^20,
+      colour = variable)) +
+    geom_line(size = 1) +
+    scale_x_date(name = paste("\nThe Tor Project - ",
+        "https://metrics.torproject.org/", sep = ""),
+        format = date_breaks$format, major = date_breaks$major,
+        minor = date_breaks$minor) +
+    scale_y_continuous(name="Bandwidth (MiB/s)",
+        limits = c(0, max(bandwidth$value, na.rm = TRUE) / 2^20)) +
+    scale_colour_hue(name = "", h.start = 90,
+        breaks = c("bwadv", "bwhist"),
+        labels = c("Advertised bandwidth", "Bandwidth history")) +
+    opts(title = "Total relay bandwidth", legend.position = "top")
+  ggsave(filename = path, width = 8, height = 5, dpi = 72)
+}
+
+plot_bwhist_flags <- function(start, end, path) {
+  end <- min(end, as.character(Sys.Date() - 4))
+  b <- read.csv("/srv/metrics.torproject.org/web/stats/bandwidth.csv",
+    stringsAsFactors = FALSE)
+  b <- b[b$date >= start & b$date <= end & b$isexit != '' &
+         b$isguard != '', ]
+  bw <- data.frame(date = as.Date(b$date, "%Y-%m-%d"),
+                  isexit = b$isexit == 't', isguard = b$isguard == 't',
+                  read = b$bwread, written = b$bwwrite)
+  dates <- seq(from = as.Date(start, "%Y-%m-%d"),
+      to = as.Date(end, "%Y-%m-%d"), by = "1 day")
+  missing <- setdiff(dates, as.Date(bw$date, origin = "1970-01-01"))
+  if (length(missing) > 0)
+    bw <- rbind(bw,
+        data.frame(date = as.Date(missing, origin = "1970-01-01"),
+        isexit = FALSE, isguard = FALSE, read = NA, written = NA),
+        data.frame(date = as.Date(missing, origin = "1970-01-01"),
+        isexit = FALSE, isguard = TRUE, read = NA, written = NA),
+        data.frame(date = as.Date(missing, origin = "1970-01-01"),
+        isexit = TRUE, isguard = FALSE, read = NA, written = NA),
+        data.frame(date = as.Date(missing, origin = "1970-01-01"),
+        isexit = TRUE, isguard = TRUE, read = NA, written = NA))
+  bw <- data.frame(date = bw$date, variable = ifelse(bw$isexit,
+        ifelse(bw$isguard, "Guard & Exit", "Exit only"),
+        ifelse(bw$isguard, "Guard only", "Middle only")),
+        value = (bw$read + bw$written) / 2)
+  date_breaks <- date_breaks(
+    as.numeric(max(as.Date(bw$date, "%Y-%m-%d")) -
+    min(as.Date(bw$date, "%Y-%m-%d"))))
+  ggplot(bw, aes(x = as.Date(date, "%Y-%m-%d"), y = value / 2^20,
+      colour = variable)) +
+    geom_line(size = 1) +
+    scale_x_date(name = paste("\nThe Tor Project - ",
+        "https://metrics.torproject.org/", sep = ""),
+        format = date_breaks$format, major = date_breaks$major,
+        minor = date_breaks$minor) +
+    scale_y_continuous(name="Bandwidth (MiB/s)",
+        limits = c(0, max(bw$value, na.rm = TRUE) / 2^20)) +
+    scale_colour_manual(name = "",
+        values = c("#E69F00", "#56B4E9", "#009E73", "#0072B2")) +
+    opts(title = "Bandwidth history by relay flags",
+        legend.position = "top")
+  ggsave(filename = path, width = 8, height = 5, dpi = 72)
+}
+
+plot_dirbytes <- function(start, end, path) {
+  end <- min(end, as.character(Sys.Date() - 4))
+  b <- read.csv("/srv/metrics.torproject.org/web/stats/bandwidth.csv",
+    stringsAsFactors = FALSE)
+  b <- b[b$date >= start & b$date <= end & b$isexit == '' &
+         b$isguard == '', ]
+  b <- data.frame(date = as.Date(b$date, "%Y-%m-%d"),
+                  dirread = b$dirread, dirwrite = b$dirwrite)
+  dir <- melt(b, id = "date")
+  date_breaks <- date_breaks(
+    as.numeric(max(as.Date(dir$date, "%Y-%m-%d")) -
+    min(as.Date(dir$date, "%Y-%m-%d"))))
+  ggplot(dir, aes(x = as.Date(date, "%Y-%m-%d"), y = value / 2^20,
+      colour = variable)) +
+    geom_line(size = 1) +
+    scale_x_date(name = paste("\nThe Tor Project - ",
+        "https://metrics.torproject.org/", sep = ""),
+        format = date_breaks$format, major = date_breaks$major,
+        minor = date_breaks$minor) +
+    scale_y_continuous(name="Bandwidth (MiB/s)",
+        limits = c(0, max(dir$value, na.rm = TRUE) / 2^20)) +
+    scale_colour_hue(name = "",
+        breaks = c("dirwrite", "dirread"),
+        labels = c("Written dir bytes", "Read dir bytes")) +
+    opts(title = "Number of bytes spent on answering directory requests",
+        legend.position = "top")
+  ggsave(filename = path, width = 8, height = 5, dpi = 72)
+}
+
+plot_relayflags <- function(start, end, flags, path) {
+  end <- min(end, as.character(Sys.Date() - 2))
+  s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv",
+    stringsAsFactors = FALSE)
+  s <- s[s$date >= start & s$date <= end & s$country == '' &
+         s$version == '' & s$platform == '' & s$ec2bridge == '', ]
+  s <- data.frame(date = as.Date(s$date, "%Y-%m-%d"),
+                  variable = ifelse(s$flag == '', 'Running', s$flag),
+                  value = s$relays)
+  networksize <- s[s$variable %in% flags, ]
+  networksize <- rbind(data.frame(
+    date = as.Date(end) + 1,
+    variable = c("Running", "Exit", "Guard", "Fast", "Stable", "HSDir"),
+    value = NA), networksize)
+  dates <- seq(from = as.Date(start, "%Y-%m-%d"),
+      to = as.Date(end, "%Y-%m-%d"), by="1 day")
+  missing <- setdiff(dates, networksize$date)
+  if (length(missing) > 0)
+    networksize <- rbind(data.frame(
+      date = as.Date(rep(missing, 6), origin = "1970-01-01"),
+      variable = c("Running", "Exit", "Guard", "Fast", "Stable", "HSDir"),
+      value = rep(NA, length(missing) * 6)), networksize)
+  date_breaks <- date_breaks(
+    as.numeric(max(as.Date(end, "%Y-%m-%d")) -
+    min(as.Date(networksize$date, "%Y-%m-%d"))))
+  ggplot(networksize, aes(x = as.Date(date, "%Y-%m-%d"), y = value,
+    colour = as.factor(variable))) + geom_line(size = 1) +
+    scale_x_date(name = paste("\nThe Tor Project - ",
+        "https://metrics.torproject.org/", sep = ""),
+          format = date_breaks$format, major = date_breaks$major,
+        minor = date_breaks$minor, limits = as.Date(c(start, end))) +
+    scale_y_continuous(name = "", limits = c(0, max(networksize$value,
+        na.rm = TRUE))) +
+    scale_colour_manual(name = "Relay flags", values = c("#E69F00",
+        "#56B4E9", "#009E73", "#EE6A50", "#000000", "#0072B2"),
+        breaks = flags, labels = flags) +
+    opts(title = "Number of relays with relay flags assigned\n")
+  ggsave(filename = path, width = 8, height = 5, dpi = 72)
+}
+
+plot_torperf <- function(start, end, source, filesize, path) {
+  end <- min(end, as.character(Sys.Date() - 2))
+  size <- ifelse(filesize == '50kb', 50 * 1024,
+          ifelse(filesize == '1mb', 1024 * 1024, 5 * 1024 * 1024))
+  t <- read.csv("/srv/metrics.torproject.org/web/stats/torperf.csv",
+    stringsAsFactors = FALSE)
+  t <- t[t$date >= start & t$date <= end & t$size == size &
+         t$source == ifelse(source == 'all', '', source), ]
+  torperf <- data.frame(date = as.Date(t$date, "%Y-%m-%d"),
+                        q1 = t$q1, md = t$md, q3 = t$q3)
+  dates <- seq(from = as.Date(start, "%Y-%m-%d"),
+      to = as.Date(end, "%Y-%m-%d"), by="1 day")
+  missing <- setdiff(dates, torperf$date)
+  if (length(missing) > 0)
+    torperf <- rbind(torperf,
+        data.frame(date = as.Date(missing, origin = "1970-01-01"),
+        q1 = NA, md = NA, q3 = NA))
+  colours <- data.frame(source = c("all", "siv", "moria", "torperf"),
+      colour = c("#FF8C00", "#0000EE", "#EE0000", "#00CD00"),
+      stringsAsFactors = FALSE)
+  colour <- colours[colours$source == source, "colour"]
+  filesizes <- data.frame(filesizes = c("5mb", "1mb", "50kb"),
+      label = c("5 MiB", "1 MiB", "50 KiB"), stringsAsFactors = FALSE)
+  filesizeStr <- filesizes[filesizes$filesize == filesize, "label"]
+  maxY <- max(torperf$q3, na.rm = TRUE)
+  date_breaks <- date_breaks(
+    as.numeric(max(as.Date(torperf$date, "%Y-%m-%d")) -
+    min(as.Date(torperf$date, "%Y-%m-%d"))))
+  ggplot(torperf, aes(x = as.Date(date, "%Y-%m-%d"), y = md/1e3,
+      fill = "line")) +
+    geom_line(colour = colour, size = 0.75) +
+    geom_ribbon(data = torperf, aes(x = date, ymin = q1/1e3,
+      ymax = q3/1e3, fill = "ribbon")) +
+    scale_x_date(name = paste("\nThe Tor Project - ",
+        "https://metrics.torproject.org/", sep = ""),
+        format = date_breaks$format, major = date_breaks$major,
+        minor = date_breaks$minor) +
+    scale_y_continuous(name = "", limits = c(0, maxY) / 1e3) +
+    scale_fill_manual(name = paste("Measured times on",
+        ifelse(source == "all", "all sources", source), "per day"),
+      breaks = c("line", "ribbon"),
+      labels = c("Median", "1st to 3rd quartile"),
+      values = paste(colour, c("", "66"), sep = "")) +
+    opts(title = paste("Time in seconds to complete", filesizeStr,
+        "request"), legend.position = "top")
+  ggsave(filename = path, width = 8, height = 5, dpi = 72)
+}
+
+plot_torperf_failures <- function(start, end, source, filesize, path) {
+  end <- min(end, as.character(Sys.Date() - 2))
+  size <- ifelse(filesize == '50kb', 50 * 1024,
+          ifelse(filesize == '1mb', 1024 * 1024, 5 * 1024 * 1024))
+  t <- read.csv("/srv/metrics.torproject.org/web/stats/torperf.csv",
+    stringsAsFactors = FALSE)
+  t <- t[t$date >= start & t$date <= end & t$size == size &
+         t$source == ifelse(source == 'all', '', source), ]
+  torperf <- data.frame(date = as.Date(t$date, "%Y-%m-%d"),
+                        timeouts = t$timeouts, failures = t$failures,
+                        requests = t$requests)
+  dates <- seq(from = as.Date(start, "%Y-%m-%d"),
+      to = as.Date(end, "%Y-%m-%d"), by="1 day")
+  missing <- setdiff(dates, torperf$date)
+  if (length(missing) > 0)
+    torperf <- rbind(torperf,
+        data.frame(date = as.Date(missing, origin = "1970-01-01"),
+        timeouts = NA, failures = NA, requests = NA))
+  colours <- data.frame(source = c("all", "siv", "moria", "torperf"),
+      colour = c("#FF8C00", "#0000EE", "#EE0000", "#00CD00"),
+      stringsAsFactors = FALSE)
+  colour <- colours[colours$source == source, "colour"]
+  filesizes <- data.frame(filesizes = c("5mb", "1mb", "50kb"),
+      label = c("5 MiB", "1 MiB", "50 KiB"), stringsAsFactors = FALSE)
+  filesizeStr <- filesizes[filesizes$filesize == filesize, "label"]
+  torperf <- rbind(data.frame(date = torperf$date,
+      value = ifelse(torperf$requests > 0,
+                     torperf$timeouts / torperf$requests, 0),
+      variable = "timeouts"),
+    data.frame(date = torperf$date,
+      value = ifelse(torperf$requests > 0,
+                     torperf$failures / torperf$requests, 0),
+      variable = "failures"))
+  date_breaks <- date_breaks(
+    as.numeric(max(as.Date(torperf$date, "%Y-%m-%d")) -
+    min(as.Date(torperf$date, "%Y-%m-%d"))))
+  ggplot(torperf, aes(x = as.Date(date, "%Y-%m-%d"), y = value,
+    colour = variable)) +
+    geom_point(size = 2) +
+    scale_x_date(name = paste("\nThe Tor Project - ",
+        "https://metrics.torproject.org/", sep = ""),
+        format = date_breaks$format, major = date_breaks$major,
+        minor = date_breaks$minor) +
+    scale_y_continuous(name = "", formatter = "percent") +
+    scale_colour_hue(name = paste("Problems encountered on",
+        ifelse(source == "all", "all sources", source)),
+        h.start = 45, breaks = c("timeouts", "failures"),
+        labels = c("Timeouts", "Failures")) +
+    opts(title = paste("Timeouts and failures of", filesizeStr,
+        "requests"), legend.position = "top")
+  ggsave(filename = path, width = 8, height = 5, dpi = 72)
+}
+
+plot_connbidirect <- function(start, end, path) {
+  end <- min(end, as.character(Sys.Date() - 2))
+  c <- read.csv("/srv/metrics.torproject.org/web/stats/connbidirect.csv",
+    stringsAsFactors = FALSE)
+  c <- c[c$date >= start & c$date <= end &
+         c$read + c$write + c$both > 0, ]
+  c <- data.frame(date = as.Date(c$date, "%Y-%m-%d"),
+                  both = c$both / (c$read + c$write + c$both),
+                  read = c$read / (c$read + c$write + c$both),
+                  write = c$write / (c$read + c$write + c$both))
+  c <- aggregate(list(both = c$both, read = c$read, write = c$write),
+                 by = list(date = c$date), quantile,
+                 probs = c(0.25, 0.5, 0.75))
+  c <- rbind(
+    data.frame(date = as.Date(c$date), data.frame(c$both),
+               variable = "both"),
+    data.frame(date = as.Date(c$date), data.frame(c$write),
+               variable = "write"),
+    data.frame(date = as.Date(c$date), data.frame(c$read),
+               variable = "read"))
+  date_breaks <- date_breaks(
+    as.numeric(max(as.Date(c$date, "%Y-%m-%d")) -
+    min(as.Date(c$date, "%Y-%m-%d"))))
+  ggplot(c, aes(x = date, y = X50., colour = variable)) +
+    geom_line(size = 0.75) +
+    geom_ribbon(aes(x = date, ymin = X25., ymax = X75., fill = variable),
+                alpha = 0.5, legend = FALSE) +
+    scale_x_date(name = paste("\nThe Tor Project - ",
+        "https://metrics.torproject.org/", sep = ""),
+        format = date_breaks$format, major = date_breaks$major,
+        minor = date_breaks$minor) +
+    scale_y_continuous(name = "", formatter = "percent") +
+    scale_colour_hue(name = "Medians and interquartile ranges",
+                     breaks = c("both", "write", "read"),
+        labels = c("Both reading and writing", "Mostly writing",
+                   "Mostly reading")) +
+    scale_fill_hue(name = "Medians and interquartile ranges",
+                   breaks = c("both", "write", "read"),
+        labels = c("Both reading and writing", "Mostly writing",
+                   "Mostly reading")) +
+    opts(title = "Fraction of connections used uni-/bidirectionally\n",
+      legend.position = "top")
+  ggsave(filename = path, width = 8, height = 5, dpi = 72)
+}
+
+plot_fast_exits <- function(start, end, path) {
+  f <- read.csv("/srv/metrics.torproject.org/web/stats/fast-exits.csv",
+    stringsAsFactors = FALSE)
+  f <- f[f$date >= start & f$date <= end, ]
+  f <- data.frame(date = as.Date(f$date, "%Y-%m-%d"),
+                  relays = f$fastnum, P_exit = f$fastprob)
+  r <- melt(f, id.vars = c("date"))
+  r <- data.frame(r, type = ifelse(r$variable == "P_exit",
+    "Total exit probability (in %)", "Number of relays"))
+  ggplot(r, aes(x = date, y = value)) +
+  geom_line(colour = "purple", size = 0.75) +
+  facet_grid(type ~ ., scales = "free_y") +
+  scale_x_date(name = "") +
+  scale_y_continuous(name = "") +
+  scale_colour_manual(values = c("purple", "orange")) +
+  opts(title = paste("Fast exits (95+ Mbit/s configured bandwidth ",
+    "rate,\n5000+ KB/s advertised bandwidth capacity,\n",
+    "exit to ports 80, 443, 554, and 1755,\n",
+    "at most 2 relays per /24 network)\n", sep = ""))
+  ggsave(filename = path, width = 8, height = 6, dpi = 72)
+}
+
+plot_almost_fast_exits <- function(start, end, path) {
+  f <- read.csv("/srv/metrics.torproject.org/web/stats/fast-exits.csv",
+    stringsAsFactors = FALSE)
+  f <- f[f$date >= start & f$date <= end, ]
+  f <- melt(f, id.vars = c("date"))
+  t <- data.frame(date = as.Date(f$date, "%Y-%m-%d"),
+       var = ifelse(f$variable == 'fastnum' | f$variable == 'almostnum',
+             "Number of relays", "Total exit probability (in %)"),
+       variable = ifelse(f$variable == 'fastnum' |
+                  f$variable == 'fastprob', "fast", "almost fast"),
+       value = floor(f$value))
+  t <- data.frame(t, type = ifelse(t$variable == "fast",
+    "fast exits (95+ Mbit/s, 5000+ KB/s, 80/443/554/1755, 2- per /24",
+    paste("almost fast exits (80+ Mbit/s, 2000+ KB/s, 80/443,",
+    "not in set of fast exits)")))
+  ggplot(t, aes(x = date, y = value, colour = type)) +
+  geom_line(size = 0.75) +
+  facet_grid(var ~ ., scales = "free_y") +
+  scale_x_date(name = "") +
+  scale_y_continuous(name = "") +
+  scale_colour_manual(name = "", values = c("orange", "purple")) +
+  opts(title = "Relays almost meeting the fast-exit requirements",
+    legend.position = "top")
+  ggsave(filename = path, width = 8, height = 6, dpi = 72)
+}
+
+plot_bandwidth_flags <- function(start, end, path) {
+  end <- min(end, as.character(Sys.Date() - 4))
+  b <- read.csv("/srv/metrics.torproject.org/web/stats/bandwidth.csv",
+    stringsAsFactors = FALSE)
+  b <- b[b$date >= start & b$date <= end & b$isexit != '' &
+         b$isguard != '', ]
+  b <- data.frame(date = as.Date(b$date, "%Y-%m-%d"),
+                  isexit = b$isexit == 't', isguard = b$isguard == 't',
+                  advbw = b$advbw,
+                  bwhist = floor((b$bwread + b$bwwrite) / 2))
+  b <- rbind(
+    data.frame(b[b$isguard == TRUE, ], flag = "Guard"),
+    data.frame(b[b$isexit == TRUE, ], flag = "Exit"))
+  b <- data.frame(date = b$date, advbw = b$advbw, bwhist = b$bwhist,
+                  flag = b$flag)
+  b <- aggregate(list(advbw = b$advbw, bwhist = b$bwhist),
+                 by = list(date = b$date, flag = b$flag), FUN = sum,
+                 na.rm = TRUE, na.action = NULL)
+  b <- melt(b, id.vars = c("date", "flag"))
+  b <- data.frame(date = b$date,
+      type = ifelse(b$variable == 'advbw', 'advertised bandwidth',
+                    'bandwidth history'),
+      flag = b$flag, value = b$value)
+  bandwidth <- b[b$value > 0, ]
+  date_breaks <- date_breaks(
+    as.numeric(max(as.Date(bandwidth$date, "%Y-%m-%d")) -
+    min(as.Date(bandwidth$date, "%Y-%m-%d"))))
+  dates <- seq(from = as.Date(start, "%Y-%m-%d"),
+      to = as.Date(end, "%Y-%m-%d"), by = "1 day")
+  missing <- setdiff(dates, as.Date(bandwidth$date,
+    origin = "1970-01-01"))
+  if (length(missing) > 0) {
+    bandwidth <- rbind(bandwidth,
+        data.frame(date = as.Date(missing, origin = "1970-01-01"),
+        type = "advertised bandwidth", flag = "Exit", value = NA),
+        data.frame(date = as.Date(missing, origin = "1970-01-01"),
+        type = "bandwidth history", flag = "Exit", value = NA),
+        data.frame(date = as.Date(missing, origin = "1970-01-01"),
+        type = "advertised bandwidth", flag = "Guard", value = NA),
+        data.frame(date = as.Date(missing, origin = "1970-01-01"),
+        type = "bandwidth history", flag = "Guard", value = NA))
+  }
+  bandwidth <- data.frame(date = bandwidth$date,
+    variable = as.factor(paste(bandwidth$flag, ", ", bandwidth$type,
+    sep = "")), value = bandwidth$value)
+  bandwidth$variable <- factor(bandwidth$variable,
+    levels = levels(bandwidth$variable)[c(3, 4, 1, 2)])
+  ggplot(bandwidth, aes(x = as.Date(date, "%Y-%m-%d"), y = value / 2^20,
+      colour = variable)) +
+    geom_line(size = 1) +
+    scale_x_date(name = paste("\nThe Tor Project - ",
+        "https://metrics.torproject.org/", sep = ""),
+        format = date_breaks$format, major = date_breaks$major,
+        minor = date_breaks$minor) +
+    scale_y_continuous(name="Bandwidth (MiB/s)",
+        limits = c(0, max(bandwidth$value, na.rm = TRUE) / 2^20)) +
+    scale_colour_manual(name = "",
+        values = c("#E69F00", "#D6C827", "#009E73", "#00C34F")) +
+    opts(title = paste("Advertised bandwidth and bandwidth history by",
+        "relay flags"), legend.position = "top")
+  ggsave(filename = path, width = 8, height = 5, dpi = 72)
+}
+
+plot_userstats <- function(start, end, node, variable, value, events,
+                           path) {
+  end <- min(end, as.character(Sys.Date() - 2))
+  c <- read.csv("/srv/metrics.torproject.org/web/stats/clients.csv",
+    stringsAsFactors = FALSE)
+  u <- c[c$date >= start & c$date <= end, ]
+  if (node == 'relay') {
+    if (value != 'all') {
+      u <- u[u$country == value & u$node == 'relay', ]
+      title <- paste("Directly connecting users from ",
+                     countryname(value), "\n", sep = "")
+    } else {
+      u <- u[u$country == '' & u$transport == '' & u$version == '' &
+             u$node == 'relay', ]
+      title <- "Directly connecting users\n"
+    }
+  } else if (variable == 'transport') {
+    u <- u[u$transport == value & u$node == 'bridge', ]
+    title <- paste("Bridge users using transport ", value, "\n",
+                   sep = "")
+  } else if (variable == 'version') {
+    u <- u[u$version== value & u$node == 'bridge', ]
+    title <- paste("Bridge users using IP", value, "\n", sep = "")
+  } else {
+    if (value != 'all') {
+      u <- u[u$country == value & u$node == 'bridge', ]
+      title <- paste("Bridge users from ", countryname(value),
+                     "\n", sep = "")
+    } else {
+      u <- u[u$country == '' & u$transport == '' & u$version == '' &
+             u$node == 'bridge', ]
+      title <- "Bridge users\n"
+    }
+  }
+  u <- data.frame(date = as.Date(u$date, "%Y-%m-%d"), users = u$clients,
+                  lower = u$lower, upper = u$upper)
+  dates <- seq(from = as.Date(start, "%Y-%m-%d"),
+      to = as.Date(end, "%Y-%m-%d"), by="1 day")
+  missing <- setdiff(dates, u$date)
+  if (length(missing) > 0) {
+    u <- rbind(u,
+        data.frame(date = as.Date(missing, origin = "1970-01-01"),
+        users = NA, lower = NA, upper = NA))
+  }
+  formatter <- function(x, ...) { format(x, scientific = FALSE, ...) }
+  date_breaks <- date_breaks(
+    as.numeric(max(u$date) - min(u$date)))
+  max_y <- ifelse(length(na.omit(u$users)) == 0, 0,
+      max(u$users, na.rm = TRUE))
+  plot <- ggplot(u, aes(x = date, y = users))
+  if (length(na.omit(u$users)) > 0 & events != "off" &
+      variable == 'country' & value != "all") {
+    upturns <- u[u$users > u$upper, c("date", "users")]
+    downturns <- u[u$users <= u$lower, c("date", "users")]
+    if (events == "on") {
+      if (length(u$upper) > 0)
+        max_y <- max(max_y, max(u$upper, na.rm = TRUE))
+      u[!is.na(u$lower) & u$lower < 0, "lower"] <- 0
+      plot <- plot +
+        geom_ribbon(aes(ymin = lower, ymax = upper), fill = "gray")
+    }
+    if (length(upturns$date) > 0)
+      plot <- plot +
+          geom_point(data = upturns, aes(x = date, y = users), size = 5,
+          colour = "dodgerblue2")
+    if (length(downturns$date) > 0)
+      plot <- plot +
+          geom_point(data = downturns, aes(x = date, y = users), size = 5,
+          colour = "firebrick2")
+  }
+  plot <- plot +
+    geom_line(size = 1) +
+    scale_x_date(name = paste("\nThe Tor Project - ",
+        "https://metrics.torproject.org/", sep = ""),
+        format = date_breaks$format, major = date_breaks$major,
+        minor = date_breaks$minor) +
+    scale_y_continuous(name = "", limits = c(0, max_y),
+        formatter = formatter) +
+    opts(title = title)
+  ggsave(filename = path, width = 8, height = 5, dpi = 72)
+}
+
+plot_userstats_relay_country <- function(start, end, country, events,
+    path) {
+  plot_userstats(start, end, 'relay', 'country', country, events, path)
+}
+
+plot_userstats_bridge_country <- function(start, end, country, path) {
+  plot_userstats(start, end, 'bridge', 'country', country, 'off', path)
+}
+
+plot_userstats_bridge_transport <- function(start, end, transport, path) {
+  plot_userstats(start, end, 'bridge', 'transport', transport, 'off',
+    path)
+}
+
+plot_userstats_bridge_version <- function(start, end, version, path) {
+  plot_userstats(start, end, 'bridge', 'version', version, 'off', path)
+}
+
diff --git a/website/rserve/rserve-init.R b/website/rserve/rserve-init.R
new file mode 100644
index 0000000..7a87b16
--- /dev/null
+++ b/website/rserve/rserve-init.R
@@ -0,0 +1,14 @@
+##Pre-loaded libraries and graphing functions to speed things up
+
+library("ggplot2")
+library("proto")
+library("grid")
+library("reshape")
+library("plyr")
+library("digest")
+library("RColorBrewer")
+
+source('graphs.R')
+source('csv.R')
+source('tables.R')
+
diff --git a/website/rserve/shutdown.sh b/website/rserve/shutdown.sh
new file mode 100755
index 0000000..dd5ecd8
--- /dev/null
+++ b/website/rserve/shutdown.sh
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+echo "library(Rserve)
+c <- RSconnect()
+RSshutdown(c)" | R --slave
diff --git a/website/rserve/start.sh b/website/rserve/start.sh
new file mode 100755
index 0000000..0979817
--- /dev/null
+++ b/website/rserve/start.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+dir=`pwd`
+R CMD /home/metrics/R/x86_64-pc-linux-gnu-library/2.11/Rserve/libs/Rserve-bin.so --no-save --RS-conf $dir/Rserv.conf >> rserve.log 2>&1
diff --git a/website/rserve/tables.R b/website/rserve/tables.R
new file mode 100644
index 0000000..091a4de
--- /dev/null
+++ b/website/rserve/tables.R
@@ -0,0 +1,55 @@
+countrynames <- function(countries) {
+  sapply(countries, countryname)
+}
+
+write_userstats <- function(start, end, node, path) {
+  end <- min(end, as.character(Sys.Date()))
+  c <- read.csv("/srv/metrics.torproject.org/web/stats/clients.csv",
+    stringsAsFactors = FALSE)
+  c <- c[c$date >= start & c$date <= end & c$country != '' &
+         c$transport == '' & c$version == '' & c$node == node, ]
+  u <- data.frame(country = c$country, users = c$clients,
+                  stringsAsFactors = FALSE)
+  u <- aggregate(list(users = u$users), by = list(country = u$country),
+                 mean)
+  total <- sum(u$users)
+  u <- u[!(u$country %in% c("zy", "??", "a1", "a2", "o1", "ap", "eu")), ]
+  u <- u[order(u$users, decreasing = TRUE), ]
+  u <- u[1:10, ]
+  u <- data.frame(
+    cc = as.character(u$country),
+    country = sub('the ', '', countrynames(as.character(u$country))),
+    abs = round(u$users),
+    rel = round(100 * u$users / total, 2))
+  write.csv(u, path, quote = FALSE, row.names = FALSE)
+}
+
+write_userstats_relay <- function(start, end, path) {
+  write_userstats(start, end, 'relay', path)
+}
+
+write_userstats_bridge <- function(start, end, path) {
+  write_userstats(start, end, 'bridge', path)
+}
+
+write_userstats_censorship_events <- function(start, end, path) {
+  end <- min(end, as.character(Sys.Date()))
+  c <- read.csv("/srv/metrics.torproject.org/web/stats/clients.csv",
+    stringsAsFactors = FALSE)
+  c <- c[c$date >= start & c$date <= end & c$country != '' &
+         c$transport == '' & c$version == '' & c$node == 'relay', ]
+  r <- data.frame(date = c$date, country = c$country,
+                  upturn = ifelse(c$clients > c$upper, 1, 0),
+                  downturn = ifelse(c$clients <= c$lower, 1, 0))
+  r <- aggregate(r[, c("upturn", "downturn")],
+    by = list(country = r$country), sum)
+  r <- r[!(r$country %in% c("zy", "??", "a1", "a2", "o1", "ap", "eu")), ]
+  r <- r[order(r$downturn, r$upturn, decreasing = TRUE), ]
+  r <- r[1:10, ]
+  r <- data.frame(cc = r$country,
+    country = sub('the ', '', countrynames(as.character(r$country))),
+    downturns = r$downturn,
+    upturns = r$upturn)
+  write.csv(r, path, quote = FALSE, row.names = FALSE)
+}
+
diff --git a/website/src/org/torproject/ernie/status/doctor/ConsensusHealthServlet.java b/website/src/org/torproject/ernie/status/doctor/ConsensusHealthServlet.java
new file mode 100644
index 0000000..330708f
--- /dev/null
+++ b/website/src/org/torproject/ernie/status/doctor/ConsensusHealthServlet.java
@@ -0,0 +1,57 @@
+/* Copyright 2011, 2012 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.ernie.status.doctor;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class ConsensusHealthServlet extends HttpServlet {
+
+  private static final long serialVersionUID = -5230032733057814869L;
+
+  public void doGet(HttpServletRequest request,
+      HttpServletResponse response) throws IOException,
+      ServletException {
+
+    /* Read file from disk and write it to response. */
+    BufferedInputStream input = null;
+    BufferedOutputStream output = null;
+    try {
+      File f = new File("/srv/metrics.torproject.org/ernie/website/"
+          + "consensus-health.html");
+      if (!f.exists()) {
+        response.sendError(HttpServletResponse.SC_NOT_FOUND);
+        return;
+      }
+      response.setContentType(this.getServletContext().getMimeType(f.getName()));
+      response.setHeader("Content-Length", String.valueOf(
+          f.length()));
+      response.setHeader("Content-Disposition",
+          "inline; filename=\"" + f.getName() + "\"");
+      input = new BufferedInputStream(new FileInputStream(f),
+          1024);
+      output = new BufferedOutputStream(response.getOutputStream(), 1024);
+      byte[] buffer = new byte[1024];
+      int length;
+      while ((length = input.read(buffer)) > 0) {
+          output.write(buffer, 0, length);
+      }
+    } finally {
+      if (output != null) {
+        output.close();
+      }
+      if (input != null) {
+        input.close();
+      }
+    }
+  }
+}
+
diff --git a/website/src/org/torproject/ernie/status/exonerator/ExoneraTorServlet.java b/website/src/org/torproject/ernie/status/exonerator/ExoneraTorServlet.java
new file mode 100644
index 0000000..d37b9a8
--- /dev/null
+++ b/website/src/org/torproject/ernie/status/exonerator/ExoneraTorServlet.java
@@ -0,0 +1,24 @@
+/* Copyright 2011, 2012 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.ernie.status.exonerator;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class ExoneraTorServlet extends HttpServlet {
+
+  private static final long serialVersionUID = -6227541092325776626L;
+
+  public void doGet(HttpServletRequest request,
+      HttpServletResponse response) throws IOException, ServletException {
+
+    /* Forward the request to the JSP that does all the hard work. */
+    request.getRequestDispatcher("WEB-INF/exonerator.jsp").forward(
+        request, response);
+  }
+}
+
diff --git a/website/src/org/torproject/ernie/status/relaysearch/RelaySearchServlet.java b/website/src/org/torproject/ernie/status/relaysearch/RelaySearchServlet.java
new file mode 100644
index 0000000..b7c8291
--- /dev/null
+++ b/website/src/org/torproject/ernie/status/relaysearch/RelaySearchServlet.java
@@ -0,0 +1,535 @@
+/* Copyright 2011, 2012 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.ernie.status.relaysearch;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.math.BigInteger;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.SortedSet;
+import java.util.TimeZone;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+
+import javax.naming.Context;
+import javax.naming.InitialContext;
+import javax.naming.NamingException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.sql.DataSource;
+
+import org.apache.commons.codec.binary.Base64;
+
+/**
+ * Web page that allows users to search for relays in the descriptor
+ * archives.
+ *
+ * Possible search terms for testing:
+ * - gabelmoo
+ * - gabelmoo 2010-09
+ * - gabelmoo 2010-09-18
+ * - gabelmoo $F2044413DAC2E02E3D6BCF4735A19BCA1DE97281
+ * - gabelmoo 80.190.246
+ * - gabelmoo $F2044413DAC2E02E3D6BCF4735A19BCA1DE97281 80.190.246
+ * - 5898549205 dc737cc9dca16af6 79.212.74.45
+ * - 5898549205 dc737cc9dca16af6
+ * - 80.190.246.100
+ * - $F2044413DAC2E02E3D6BCF4735A19BCA1DE97281
+ * - $F2044413DAC2E02E3D6BCF4735A19BCA1DE97281 80.190.246
+ * - 58985492
+ * - 58985492 79.212.74.45
+ */
+public class RelaySearchServlet extends HttpServlet {
+
+  private static final long serialVersionUID = -1772662230310611805L;
+
+  private Pattern alphaNumDotDashDollarSpacePattern =
+      Pattern.compile("[A-Za-z0-9\\.\\-$ ]+");
+
+  private Pattern numPattern = Pattern.compile("[0-9]+");
+
+  private Pattern hexPattern = Pattern.compile("[A-Fa-f0-9]+");
+
+  private Pattern alphaNumPattern = Pattern.compile("[A-Za-z0-9]+");
+
+  private SimpleDateFormat dayFormat = new SimpleDateFormat("yyyy-MM-dd");
+
+  private SimpleDateFormat monthFormat = new SimpleDateFormat("yyyy-MM");
+
+  private SimpleDateFormat dateTimeFormat =
+      new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+
+  private long minValidAfterMillis;
+
+  private DataSource ds;
+
+  private Logger logger;
+
+  public void init() {
+
+    /* Initialize logger. */
+    this.logger = Logger.getLogger(RelaySearchServlet.class.toString());
+
+    /* Initialize date format parsers. */
+    dayFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+    monthFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+    dateTimeFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+
+    /* Look up data source. */
+    try {
+      Context cxt = new InitialContext();
+      this.ds = (DataSource) cxt.lookup("java:comp/env/jdbc/tordir");
+      this.logger.info("Successfully looked up data source.");
+    } catch (NamingException e) {
+      this.logger.log(Level.WARNING, "Could not look up data source", e);
+    }
+
+    /* Look up first consensus in the database. */
+    try {
+      long requestedConnection = System.currentTimeMillis();
+      Connection conn = this.ds.getConnection();
+      String query = "SELECT MIN(validafter) AS first FROM consensus";
+      Statement statement = conn.createStatement();
+      ResultSet rs = statement.executeQuery(query);
+      if (rs.next()) {
+        this.minValidAfterMillis = rs.getTimestamp(1).getTime();
+      }
+      rs.close();
+      statement.close();
+      conn.close();
+      this.logger.info("Returned a database connection to the pool "
+          + "after " + (System.currentTimeMillis()
+          - requestedConnection) + " millis.");
+    } catch (SQLException e) {
+      this.logger.log(Level.WARNING, "Could not look up first consensus "
+          + "valid-after time in the database.", e);
+    }
+  }
+
+  public void doGet(HttpServletRequest request,
+      HttpServletResponse response) throws IOException,
+      ServletException {
+
+    /* Read search parameter. If we don't have a search parameter, we're
+     * done here. */
+    String searchParameter = request.getParameter("search");
+    if (searchParameter == null || searchParameter.length() == 0) {
+      request.getRequestDispatcher("WEB-INF/relay-search.jsp").forward(
+          request, response);
+      return;
+    }
+
+    /* Parse search parameter to identify what nickname, fingerprint,
+     * and/or IP address to search for. A valid query contains no more
+     * than one identifier for each of the fields. As a special case,
+     * there are search terms consisting of 8 to 19 hex characters that
+     * can be either a nickname or a fingerprint. */
+    String searchNickname = "";
+    String searchFingerprint = "";
+    String searchIPAddress = "";
+    SortedSet<String> searchDays = new TreeSet<String>();
+    SortedSet<String> searchMonths = new TreeSet<String>();
+    SortedSet<Long> searchDayTimestamps = new TreeSet<Long>();
+    SortedSet<Long> searchMonthTimestamps = new TreeSet<Long>();
+    boolean validQuery = false;
+
+    /* Only parse search parameter if it contains nothing else than
+     * alphanumeric characters, dots, and spaces. */
+    if (alphaNumDotDashDollarSpacePattern.matcher(searchParameter).
+        matches()) {
+      SortedSet<String> searchTerms = new TreeSet<String>();
+      if (searchParameter.trim().contains(" ")) {
+        String[] split = searchParameter.trim().split(" ");
+        for (int i = 0; i < split.length; i++) {
+          if (split[i].length() > 0) {
+            searchTerms.add(split[i]);
+          }
+        }
+      } else {
+        searchTerms.add(searchParameter.trim());
+      }
+
+      /* Parse each search term separately. */
+      for (String searchTerm : searchTerms) {
+
+        /* If the search term contains a dot, it can only be an IP
+         * address. */
+        if (searchTerm.contains(".") && !searchTerm.startsWith(".")) {
+          String[] octets = searchTerm.split("\\.");
+          if (searchIPAddress.length() > 0 || octets.length < 2 ||
+              octets.length > 4) {
+            validQuery = false;
+            break;
+          }
+          boolean invalidOctet = false;
+          StringBuilder sb = new StringBuilder();
+          for (int i = 0; i < octets.length; i++) {
+            if (!numPattern.matcher(octets[i]).matches() ||
+                octets[i].length() > 3 ||
+                Integer.parseInt(octets[i]) > 255) {
+              invalidOctet = true;
+              break;
+            } else {
+              sb.append("." + Integer.parseInt(octets[i]));
+            }
+          }
+          if (invalidOctet) {
+            validQuery = false;
+            break;
+          }
+          if (octets.length < 4) {
+            sb.append(".");
+          }
+          searchIPAddress = sb.toString().substring(1);
+          validQuery = true;
+        }
+
+        /* If the search term contains hyphens, it must be a month or a
+         * day. */
+        else if (searchTerm.contains("-") &&
+            searchTerm.startsWith("20")) {
+          try {
+            if (searchTerm.length() == 10) {
+              searchDayTimestamps.add(dayFormat.parse(searchTerm).
+                  getTime());
+              searchDays.add(searchTerm);
+            } else if (searchTerm.length() == 7) {
+              searchMonthTimestamps.add(monthFormat.parse(searchTerm).
+                  getTime());
+              searchMonths.add(searchTerm);
+            } else {
+              validQuery = false;
+              break;
+            }
+          } catch (ParseException e) {
+            validQuery = false;
+            break;
+          }
+        }
+
+        /* If the search term starts with a $ followed by 8 to 40 hex
+         * characters, it must be a fingerprint. */
+        else if ((searchTerm.length() >= 9 && searchTerm.length() <= 41 &&
+            searchTerm.startsWith("$") &&
+            hexPattern.matcher(searchTerm.substring(1)).matches()) ||
+            (searchTerm.length() > 19 && searchTerm.length() <= 40 &&
+            !searchTerm.startsWith("$") &&
+            hexPattern.matcher(searchTerm).matches())) {
+          if (searchFingerprint.length() > 0) {
+            validQuery = false;
+            break;
+          }
+          searchFingerprint = searchTerm.substring(
+              (searchTerm.startsWith("$") ? 1 : 0));
+          validQuery = true;
+        }
+
+        /* If the search term contains up to 19 alphanumerical characters,
+         * it must be a nickname. */
+        else if (searchTerm.length() <= 19 &&
+            alphaNumPattern.matcher(searchTerm).matches()) {
+          if (searchNickname.length() > 0) {
+            validQuery = false;
+            break;
+          }
+          searchNickname = searchTerm;
+          validQuery = true;
+        }
+
+        /* We didn't recognize this search term. */
+        else {
+          validQuery = false;
+          break;
+        }
+      }
+    }
+
+    /* We only accept at most one month or three days, but not both, or
+     * people could accidentally keep the database busy. */
+    if (searchDays.size() > 3 || searchMonths.size() > 1 ||
+        (searchMonths.size() == 1 && searchDays.size() > 0)) {
+      validQuery = false;
+    }
+
+    /* If the query is invalid, stop here. */
+    if (!validQuery) {
+      request.setAttribute("invalidQuery", "Query is invalid.");
+      request.getRequestDispatcher("WEB-INF/relay-search.jsp").
+          forward(request, response);
+      return;
+    }
+
+    /* Look up last consensus in the database. */
+    long maxValidAfterMillis = -1L;
+    try {
+      long requestedConnection = System.currentTimeMillis();
+      Connection conn = this.ds.getConnection();
+      String query = "SELECT MAX(validafter) AS last FROM consensus";
+      Statement statement = conn.createStatement();
+      ResultSet rs = statement.executeQuery(query);
+      if (rs.next()) {
+        maxValidAfterMillis = rs.getTimestamp(1).getTime();
+      }
+      rs.close();
+      statement.close();
+      conn.close();
+      this.logger.info("Returned a database connection to the pool "
+          + "after " + (System.currentTimeMillis()
+          - requestedConnection) + " millis.");
+    } catch (SQLException e) {
+      this.logger.log(Level.WARNING, "Could not look up last consensus "
+          + "valid-after time in the database.", e);
+    }
+
+    /* Prepare a string that says what we're searching for. */
+    List<String> recognizedSearchTerms = new ArrayList<String>();
+    if (searchNickname.length() > 0) {
+      recognizedSearchTerms.add("nickname <b>" + searchNickname + "</b>");
+    }
+    if (searchFingerprint.length() > 0) {
+      recognizedSearchTerms.add("fingerprint <b>" + searchFingerprint
+          + "</b>");
+    }
+    if (searchIPAddress.length() > 0) {
+      recognizedSearchTerms.add("IP address <b>" + searchIPAddress
+          + "</b>");
+    }
+    List<String> recognizedIntervals = new ArrayList<String>();
+    for (String searchTerm : searchMonths) {
+      recognizedIntervals.add("in <b>" + searchTerm + "</b>");
+    }
+    for (String searchTerm : searchDays) {
+      recognizedIntervals.add("on <b>" + searchTerm + "</b>");
+    }
+    StringBuilder searchNoticeBuilder = new StringBuilder();
+    if (maxValidAfterMillis > 0L) {
+      searchNoticeBuilder.append("Most recent consensus in database is "
+          + "from " + dateTimeFormat.format(maxValidAfterMillis)
+          + ".</p><p>");
+    }
+    searchNoticeBuilder.append("Searching for relays with ");
+    if (recognizedSearchTerms.size() == 1) {
+      searchNoticeBuilder.append(recognizedSearchTerms.get(0));
+    } else if (recognizedSearchTerms.size() == 2) {
+      searchNoticeBuilder.append(recognizedSearchTerms.get(0) + " and "
+          + recognizedSearchTerms.get(1));
+    } else {
+      for (int i = 0; i < recognizedSearchTerms.size() - 1; i++) {
+        searchNoticeBuilder.append(recognizedSearchTerms.get(i) + ", ");
+      }
+      searchNoticeBuilder.append("and " + recognizedSearchTerms.get(
+          recognizedSearchTerms.size() - 1));
+    }
+    if (recognizedIntervals.size() == 1) {
+      searchNoticeBuilder.append(" running "
+          + recognizedIntervals.get(0));
+    } else if (recognizedIntervals.size() == 2) {
+      searchNoticeBuilder.append(" running " + recognizedIntervals.get(0)
+          + " and/or " + recognizedIntervals.get(1));
+    } else if (recognizedIntervals.size() > 2) {
+      searchNoticeBuilder.append(" running ");
+      for (int i = 0; i < recognizedIntervals.size() - 1; i++) {
+      searchNoticeBuilder.append(recognizedIntervals.get(i) + ", ");
+      }
+      searchNoticeBuilder.append("and/or " + recognizedIntervals.get(
+          recognizedIntervals.size() - 1));
+    }
+    searchNoticeBuilder.append(" ...");
+    String searchNotice = searchNoticeBuilder.toString();
+    request.setAttribute("searchNotice", searchNotice);
+
+    /* Prepare the query string. */
+    StringBuilder conditionBuilder = new StringBuilder();
+    boolean addAnd = false;
+    if (searchNickname.length() > 0) {
+      conditionBuilder.append((addAnd ? "AND " : "")
+          + "LOWER(nickname) LIKE '" + searchNickname.toLowerCase()
+          + "%' ");
+      addAnd = true;
+    }
+    if (searchFingerprint.length() > 0) {
+      conditionBuilder.append((addAnd ? "AND " : "")
+          + "fingerprint LIKE '" + searchFingerprint.toLowerCase()
+          + "%' ");
+      addAnd = true;
+    }
+    if (searchIPAddress.length() > 0) {
+      conditionBuilder.append((addAnd ? "AND " : "")
+          + "address LIKE '" + searchIPAddress + "%' ");
+      addAnd = true;
+    }
+    List<String> timeIntervals = new ArrayList<String>();
+    if (searchDayTimestamps.size() > 0 ||
+        searchMonthTimestamps.size() > 0) {
+      StringBuilder timeIntervalBuilder = new StringBuilder();
+      boolean addOr = false;
+      timeIntervalBuilder.append("AND (");
+      for (long searchTimestamp : searchDayTimestamps) {
+        if (searchTimestamp < this.minValidAfterMillis) {
+          request.setAttribute("outsideInterval", "Returned search "
+              + "results may be incomplete, as our data only dates back "
+              + "to " + dateTimeFormat.format(this.minValidAfterMillis)
+              + ".  Older archives are not available.");
+        }
+        timeIntervalBuilder.append((addOr ? "OR " : "")
+            + "(validafter >= '"
+            + dateTimeFormat.format(searchTimestamp) + "' AND "
+            + "validafter < '" + dateTimeFormat.format(searchTimestamp
+            + 24L * 60L * 60L * 1000L) + "') ");
+        addOr = true;
+      }
+      for (long searchTimestamp : searchMonthTimestamps) {
+        if (searchTimestamp < this.minValidAfterMillis) {
+          request.setAttribute("outsideInterval", "Returned search "
+              + "results may be incomplete, as our data only dates back "
+              + "to " + dateTimeFormat.format(this.minValidAfterMillis)
+              + ".  Older archives are not available.");
+        }
+        Calendar firstOfNextMonth = Calendar.getInstance(
+            TimeZone.getTimeZone("UTC"));
+        firstOfNextMonth.setTimeInMillis(searchTimestamp);
+        firstOfNextMonth.add(Calendar.MONTH, 1);
+        timeIntervalBuilder.append((addOr ? "OR " : "")
+            + "(validafter >= '"
+            + dateTimeFormat.format(searchTimestamp) + "' AND "
+            + "validafter < '" + dateTimeFormat.format(
+            firstOfNextMonth.getTimeInMillis()) + "') ");
+        addOr = true;
+      }
+      timeIntervalBuilder.append(") ");
+      timeIntervals.add(timeIntervalBuilder.toString());
+    } else {
+      timeIntervals.add("AND validafter >= '"
+          + dateTimeFormat.format(System.currentTimeMillis()
+          - 4L * 24L * 60L * 60L * 1000L) + "' ");
+      timeIntervals.add("AND validafter >= '"
+          + dateTimeFormat.format(System.currentTimeMillis()
+          - 30L * 24L * 60L * 60L * 1000L) + "' ");
+    }
+    List<String> queries = new ArrayList<String>();
+    for (String timeInterval : timeIntervals) {
+      StringBuilder queryBuilder = new StringBuilder();
+      queryBuilder.append("SELECT validafter, fingerprint, descriptor, "
+          + "rawdesc FROM statusentry WHERE validafter IN (SELECT "
+          + "validafter FROM statusentry WHERE ");
+      queryBuilder.append(conditionBuilder.toString());
+      queryBuilder.append(timeInterval);
+      queryBuilder.append("ORDER BY validafter DESC LIMIT 31) AND ");
+      queryBuilder.append(conditionBuilder.toString());
+      queryBuilder.append(timeInterval);
+      queries.add(queryBuilder.toString());
+    }
+
+    /* Actually execute the query. */
+    long startedQuery = System.currentTimeMillis();
+    SortedMap<String, SortedSet<String>> foundDescriptors =
+        new TreeMap<String, SortedSet<String>>(
+        Collections.reverseOrder());
+    Map<String, String> rawValidAfterLines =
+        new HashMap<String, String>();
+    Map<String, String> rawStatusEntries = new HashMap<String, String>();
+    String query = null;
+    int matches = 0;
+    try {
+      long requestedConnection = System.currentTimeMillis();
+      Connection conn = this.ds.getConnection();
+      while (!queries.isEmpty()) {
+        query = queries.remove(0);
+        this.logger.info("Running query '" + query + "'.");
+        Statement statement = conn.createStatement();
+        ResultSet rs = statement.executeQuery(query);
+        while (rs.next()) {
+          matches++;
+          String validAfter = rs.getTimestamp(1).toString().
+              substring(0, 19);
+          String fingerprint = rs.getString(2);
+          if (!foundDescriptors.containsKey(validAfter)) {
+            foundDescriptors.put(validAfter, new TreeSet<String>());
+          }
+          foundDescriptors.get(validAfter).add(validAfter + " "
+              + fingerprint);
+          if (!rawValidAfterLines.containsKey(validAfter)) {
+            rawValidAfterLines.put(validAfter, "<tt>valid-after "
+                + "<a href=\"https://exonerator.torproject.org/"
+                + "consensus?valid-after="
+                + validAfter.replaceAll(":", "-").replaceAll(" ", "-")
+                + "\" target=\"_blank\">" + validAfter + "</a></tt><br>");
+          }
+          byte[] rawStatusEntry = rs.getBytes(4);
+          String statusEntryLines = null;
+          try {
+            statusEntryLines = new String(rawStatusEntry, "US-ASCII");
+          } catch (UnsupportedEncodingException e) {
+            /* This shouldn't happen, because we know that ASCII is
+             * supported. */
+          }
+          StringBuilder rawStatusEntryBuilder = new StringBuilder();
+          String[] lines = statusEntryLines.split("\n");
+          for (String line : lines) {
+            if (line.startsWith("r ")) {
+              String[] parts = line.split(" ");
+              String descriptorBase64 = String.format("%040x",
+                  new BigInteger(1, Base64.decodeBase64(parts[3]
+                  + "==")));
+              rawStatusEntryBuilder.append("<tt>r " + parts[1] + " "
+                  + parts[2] + " <a href=\""
+                  + "https://exonerator.torproject.org/"
+                  + "serverdesc?desc-id="
+                  + descriptorBase64 + "\" target=\"_blank\">" + parts[3]
+                  + "</a> " + parts[4] + " " + parts[5] + " " + parts[6]
+                  + " " + parts[7] + " " + parts[8] + "</tt><br>");
+            } else {
+              rawStatusEntryBuilder.append("<tt>" + line + "</tt><br>");
+            }
+          }
+          rawStatusEntries.put(validAfter + " " + fingerprint,
+                rawStatusEntryBuilder.toString());
+        }
+        rs.close();
+        statement.close();
+        if (matches >= 31) {
+          queries.clear();
+        }
+      }
+      conn.close();
+      this.logger.info("Returned a database connection to the pool "
+          + "after " + (System.currentTimeMillis()
+          - requestedConnection) + " millis.");
+    } catch (SQLException e) {
+
+      /* Tell the user we have a database problem. */
+      response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+          "Database problem");
+      return;
+    }
+    request.setAttribute("query", query);
+    request.setAttribute("queryTime", System.currentTimeMillis()
+        - startedQuery);
+    request.setAttribute("foundDescriptors", foundDescriptors);
+    request.setAttribute("rawValidAfterLines", rawValidAfterLines);
+    request.setAttribute("rawStatusEntries", rawStatusEntries);
+    request.setAttribute("matches", matches);
+
+    /* We're done. Let the JSP do the rest. */
+    request.getRequestDispatcher("WEB-INF/relay-search.jsp").forward(
+        request, response);
+  }
+}
+
diff --git a/website/src/org/torproject/ernie/web/GraphsServlet.java b/website/src/org/torproject/ernie/web/GraphsServlet.java
new file mode 100644
index 0000000..111aa0f
--- /dev/null
+++ b/website/src/org/torproject/ernie/web/GraphsServlet.java
@@ -0,0 +1,24 @@
+/* Copyright 2011, 2012 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.ernie.web;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class GraphsServlet extends HttpServlet {
+
+  private static final long serialVersionUID = 7615715032362498151L;
+
+  public void doGet(HttpServletRequest request,
+      HttpServletResponse response) throws IOException, ServletException {
+
+    /* Forward the request to the JSP that does all the hard work. */
+    request.getRequestDispatcher("WEB-INF/graphs.jsp").forward(request,
+        response);
+  }
+}
+
diff --git a/website/src/org/torproject/ernie/web/IndexServlet.java b/website/src/org/torproject/ernie/web/IndexServlet.java
new file mode 100644
index 0000000..11aff7c
--- /dev/null
+++ b/website/src/org/torproject/ernie/web/IndexServlet.java
@@ -0,0 +1,24 @@
+/* Copyright 2011, 2012 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.ernie.web;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class IndexServlet extends HttpServlet {
+
+  private static final long serialVersionUID = 7871368999788994664L;
+
+  public void doGet(HttpServletRequest request,
+      HttpServletResponse response) throws IOException, ServletException {
+
+    /* Forward the request to the JSP that does all the hard work. */
+    request.getRequestDispatcher("WEB-INF/index.jsp").forward(request,
+        response);
+  }
+}
+
diff --git a/website/src/org/torproject/ernie/web/ResearchServlet.java b/website/src/org/torproject/ernie/web/ResearchServlet.java
new file mode 100644
index 0000000..c78be69
--- /dev/null
+++ b/website/src/org/torproject/ernie/web/ResearchServlet.java
@@ -0,0 +1,24 @@
+/* Copyright 2011, 2012 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.ernie.web;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class ResearchServlet extends HttpServlet {
+
+  private static final long serialVersionUID = -9151727188925700665L;
+
+  public void doGet(HttpServletRequest request,
+      HttpServletResponse response) throws IOException, ServletException {
+
+    /* Forward the request to the JSP that does all the hard work. */
+    request.getRequestDispatcher("WEB-INF/research.jsp").forward(request,
+        response);
+  }
+}
+
diff --git a/website/src/org/torproject/ernie/web/StatusServlet.java b/website/src/org/torproject/ernie/web/StatusServlet.java
new file mode 100644
index 0000000..07790ec
--- /dev/null
+++ b/website/src/org/torproject/ernie/web/StatusServlet.java
@@ -0,0 +1,24 @@
+/* Copyright 2011, 2012 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.ernie.web;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class StatusServlet extends HttpServlet {
+
+  private static final long serialVersionUID = -7249872082399236981L;
+
+  public void doGet(HttpServletRequest request,
+      HttpServletResponse response) throws IOException, ServletException {
+
+    /* Forward the request to the JSP that does all the hard work. */
+    request.getRequestDispatcher("WEB-INF/status.jsp").forward(request,
+        response);
+  }
+}
+
diff --git a/website/src/org/torproject/ernie/web/graphs/BubblesServlet.java b/website/src/org/torproject/ernie/web/graphs/BubblesServlet.java
new file mode 100644
index 0000000..6f66413
--- /dev/null
+++ b/website/src/org/torproject/ernie/web/graphs/BubblesServlet.java
@@ -0,0 +1,24 @@
+/* Copyright 2013 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.ernie.web.graphs;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class BubblesServlet extends HttpServlet {
+
+  private static final long serialVersionUID = -6011833075497881033L;
+
+  public void doGet(HttpServletRequest request,
+      HttpServletResponse response) throws IOException, ServletException {
+
+    /* Forward the request to the JSP that does all the hard work. */
+    request.getRequestDispatcher("WEB-INF/bubbles.jsp").forward(request,
+        response);
+  }
+}
+
diff --git a/website/src/org/torproject/ernie/web/graphs/Countries.java b/website/src/org/torproject/ernie/web/graphs/Countries.java
new file mode 100644
index 0000000..93dac4c
--- /dev/null
+++ b/website/src/org/torproject/ernie/web/graphs/Countries.java
@@ -0,0 +1,285 @@
+/* Copyright 2011, 2012 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.ernie.web.graphs;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class Countries {
+
+  private static Countries instance = new Countries();
+
+  public static Countries getInstance() {
+    return Countries.instance;
+  }
+
+  /* List of arrays of length 2, containing country codes at [0] and
+   * country names at [1], alphabetically ordered by country names. */
+  private List<String[]> knownCountries;
+
+  private Countries() {
+    this.knownCountries = new ArrayList<String[]>();
+    this.knownCountries.add("af;Afghanistan".split(";"));
+    this.knownCountries.add("ax;Aland Islands".split(";"));
+    this.knownCountries.add("al;Albania".split(";"));
+    this.knownCountries.add("dz;Algeria".split(";"));
+    this.knownCountries.add("as;American Samoa".split(";"));
+    this.knownCountries.add("ad;Andorra".split(";"));
+    this.knownCountries.add("ao;Angola".split(";"));
+    this.knownCountries.add("ai;Anguilla".split(";"));
+    this.knownCountries.add("aq;Antarctica".split(";"));
+    this.knownCountries.add("ag;Antigua and Barbuda".split(";"));
+    this.knownCountries.add("ar;Argentina".split(";"));
+    this.knownCountries.add("am;Armenia".split(";"));
+    this.knownCountries.add("aw;Aruba".split(";"));
+    this.knownCountries.add("au;Australia".split(";"));
+    this.knownCountries.add("at;Austria".split(";"));
+    this.knownCountries.add("az;Azerbaijan".split(";"));
+    this.knownCountries.add("bs;Bahamas".split(";"));
+    this.knownCountries.add("bh;Bahrain".split(";"));
+    this.knownCountries.add("bd;Bangladesh".split(";"));
+    this.knownCountries.add("bb;Barbados".split(";"));
+    this.knownCountries.add("by;Belarus".split(";"));
+    this.knownCountries.add("be;Belgium".split(";"));
+    this.knownCountries.add("bz;Belize".split(";"));
+    this.knownCountries.add("bj;Benin".split(";"));
+    this.knownCountries.add("bm;Bermuda".split(";"));
+    this.knownCountries.add("bt;Bhutan".split(";"));
+    this.knownCountries.add("bo;Bolivia".split(";"));
+    this.knownCountries.add("ba;Bosnia and Herzegovina".split(";"));
+    this.knownCountries.add("bw;Botswana".split(";"));
+    this.knownCountries.add("bv;Bouvet Island".split(";"));
+    this.knownCountries.add("br;Brazil".split(";"));
+    this.knownCountries.add("io;British Indian Ocean Territory".
+        split(";"));
+    this.knownCountries.add("bn;Brunei".split(";"));
+    this.knownCountries.add("bg;Bulgaria".split(";"));
+    this.knownCountries.add("bf;Burkina Faso".split(";"));
+    this.knownCountries.add("mm;Burma".split(";"));
+    this.knownCountries.add("bi;Burundi".split(";"));
+    this.knownCountries.add("kh;Cambodia".split(";"));
+    this.knownCountries.add("cm;Cameroon".split(";"));
+    this.knownCountries.add("ca;Canada".split(";"));
+    this.knownCountries.add("cv;Cape Verde".split(";"));
+    this.knownCountries.add("ky;Cayman Islands".split(";"));
+    this.knownCountries.add("cf;Central African Republic".split(";"));
+    this.knownCountries.add("td;Chad".split(";"));
+    this.knownCountries.add("cl;Chile".split(";"));
+    this.knownCountries.add("cn;China".split(";"));
+    this.knownCountries.add("cx;Christmas Island".split(";"));
+    this.knownCountries.add("cc;Cocos (Keeling) Islands".split(";"));
+    this.knownCountries.add("co;Colombia".split(";"));
+    this.knownCountries.add("km;Comoros".split(";"));
+    this.knownCountries.add("cd;Congo, The Democratic Republic of the".
+        split(";"));
+    this.knownCountries.add("cg;Congo".split(";"));
+    this.knownCountries.add("ck;Cook Islands".split(";"));
+    this.knownCountries.add("cr;Costa Rica".split(";"));
+    this.knownCountries.add("ci:Côte d'Ivoire".split(":"));
+    this.knownCountries.add("hr;Croatia".split(";"));
+    this.knownCountries.add("cu;Cuba".split(";"));
+    this.knownCountries.add("cy;Cyprus".split(";"));
+    this.knownCountries.add("cz;Czech Republic".split(";"));
+    this.knownCountries.add("dk;Denmark".split(";"));
+    this.knownCountries.add("dj;Djibouti".split(";"));
+    this.knownCountries.add("dm;Dominica".split(";"));
+    this.knownCountries.add("do;Dominican Republic".split(";"));
+    this.knownCountries.add("ec;Ecuador".split(";"));
+    this.knownCountries.add("eg;Egypt".split(";"));
+    this.knownCountries.add("sv;El Salvador".split(";"));
+    this.knownCountries.add("gq;Equatorial Guinea".split(";"));
+    this.knownCountries.add("er;Eritrea".split(";"));
+    this.knownCountries.add("ee;Estonia".split(";"));
+    this.knownCountries.add("et;Ethiopia".split(";"));
+    this.knownCountries.add("fk;Falkland Islands (Malvinas)".split(";"));
+    this.knownCountries.add("fo;Faroe Islands".split(";"));
+    this.knownCountries.add("fj;Fiji".split(";"));
+    this.knownCountries.add("fi;Finland".split(";"));
+    this.knownCountries.add("fx;France, Metropolitan".split(";"));
+    this.knownCountries.add("fr;France".split(";"));
+    this.knownCountries.add("gf;French Guiana".split(";"));
+    this.knownCountries.add("pf;French Polynesia".split(";"));
+    this.knownCountries.add("tf;French Southern Territories".split(";"));
+    this.knownCountries.add("ga;Gabon".split(";"));
+    this.knownCountries.add("gm;Gambia".split(";"));
+    this.knownCountries.add("ge;Georgia".split(";"));
+    this.knownCountries.add("de;Germany".split(";"));
+    this.knownCountries.add("gh;Ghana".split(";"));
+    this.knownCountries.add("gi;Gibraltar".split(";"));
+    this.knownCountries.add("gr;Greece".split(";"));
+    this.knownCountries.add("gl;Greenland".split(";"));
+    this.knownCountries.add("gd;Grenada".split(";"));
+    this.knownCountries.add("gp;Guadeloupe".split(";"));
+    this.knownCountries.add("gu;Guam".split(";"));
+    this.knownCountries.add("gt;Guatemala".split(";"));
+    this.knownCountries.add("gg;Guernsey".split(";"));
+    this.knownCountries.add("gn;Guinea".split(";"));
+    this.knownCountries.add("gw;Guinea-Bissau".split(";"));
+    this.knownCountries.add("gy;Guyana".split(";"));
+    this.knownCountries.add("ht;Haiti".split(";"));
+    this.knownCountries.add("hm;Heard Island and McDonald Islands".
+        split(";"));
+    this.knownCountries.add("va;Vatican City".split(";"));
+    this.knownCountries.add("hn;Honduras".split(";"));
+    this.knownCountries.add("hk;Hong Kong".split(";"));
+    this.knownCountries.add("hu;Hungary".split(";"));
+    this.knownCountries.add("is;Iceland".split(";"));
+    this.knownCountries.add("in;India".split(";"));
+    this.knownCountries.add("id;Indonesia".split(";"));
+    this.knownCountries.add("ir;Iran".split(";"));
+    this.knownCountries.add("iq;Iraq".split(";"));
+    this.knownCountries.add("ie;Ireland".split(";"));
+    this.knownCountries.add("im;Isle of Man".split(";"));
+    this.knownCountries.add("il;Israel".split(";"));
+    this.knownCountries.add("it;Italy".split(";"));
+    this.knownCountries.add("jm;Jamaica".split(";"));
+    this.knownCountries.add("jp;Japan".split(";"));
+    this.knownCountries.add("je;Jersey".split(";"));
+    this.knownCountries.add("jo;Jordan".split(";"));
+    this.knownCountries.add("kz;Kazakhstan".split(";"));
+    this.knownCountries.add("ke;Kenya".split(";"));
+    this.knownCountries.add("ki;Kiribati".split(";"));
+    this.knownCountries.add("kp;North Korea".split(";"));
+    this.knownCountries.add("kr;Korea, Republic of".split(";"));
+    this.knownCountries.add("kw;Kuwait".split(";"));
+    this.knownCountries.add("kg;Kyrgyzstan".split(";"));
+    this.knownCountries.add("la;Laos".split(";"));
+    this.knownCountries.add("lv;Latvia".split(";"));
+    this.knownCountries.add("lb;Lebanon".split(";"));
+    this.knownCountries.add("ls;Lesotho".split(";"));
+    this.knownCountries.add("lr;Liberia".split(";"));
+    this.knownCountries.add("ly;Libya".split(";"));
+    this.knownCountries.add("li;Liechtenstein".split(";"));
+    this.knownCountries.add("lt;Lithuania".split(";"));
+    this.knownCountries.add("lu;Luxembourg".split(";"));
+    this.knownCountries.add("mo;Macau".split(";"));
+    this.knownCountries.add("mk;Macedonia".split(";"));
+    this.knownCountries.add("mg;Madagascar".split(";"));
+    this.knownCountries.add("mw;Malawi".split(";"));
+    this.knownCountries.add("my;Malaysia".split(";"));
+    this.knownCountries.add("mv;Maldives".split(";"));
+    this.knownCountries.add("ml;Mali".split(";"));
+    this.knownCountries.add("mt;Malta".split(";"));
+    this.knownCountries.add("mh;Marshall Islands".split(";"));
+    this.knownCountries.add("mq;Martinique".split(";"));
+    this.knownCountries.add("mr;Mauritania".split(";"));
+    this.knownCountries.add("mu;Mauritius".split(";"));
+    this.knownCountries.add("yt;Mayotte".split(";"));
+    this.knownCountries.add("mx;Mexico".split(";"));
+    this.knownCountries.add("fm;Micronesia, Federated States of".
+        split(";"));
+    this.knownCountries.add("md;Moldova, Republic of".split(";"));
+    this.knownCountries.add("mc;Monaco".split(";"));
+    this.knownCountries.add("mn;Mongolia".split(";"));
+    this.knownCountries.add("me;Montenegro".split(";"));
+    this.knownCountries.add("ms;Montserrat".split(";"));
+    this.knownCountries.add("ma;Morocco".split(";"));
+    this.knownCountries.add("mz;Mozambique".split(";"));
+    this.knownCountries.add("mm;Burma".split(";"));
+    this.knownCountries.add("na;Namibia".split(";"));
+    this.knownCountries.add("nr;Nauru".split(";"));
+    this.knownCountries.add("np;Nepal".split(";"));
+    this.knownCountries.add("an;Netherlands Antilles".split(";"));
+    this.knownCountries.add("nl;Netherlands".split(";"));
+    this.knownCountries.add("nc;New Caledonia".split(";"));
+    this.knownCountries.add("nz;New Zealand".split(";"));
+    this.knownCountries.add("ni;Nicaragua".split(";"));
+    this.knownCountries.add("ne;Niger".split(";"));
+    this.knownCountries.add("ng;Nigeria".split(";"));
+    this.knownCountries.add("nu;Niue".split(";"));
+    this.knownCountries.add("nf;Norfolk Island".split(";"));
+    this.knownCountries.add("mp;Northern Mariana Islands".split(";"));
+    this.knownCountries.add("no;Norway".split(";"));
+    this.knownCountries.add("om;Oman".split(";"));
+    this.knownCountries.add("pk;Pakistan".split(";"));
+    this.knownCountries.add("pw;Palau".split(";"));
+    this.knownCountries.add("ps;Palestinian Territory".split(";"));
+    this.knownCountries.add("pa;Panama".split(";"));
+    this.knownCountries.add("pg;Papua New Guinea".split(";"));
+    this.knownCountries.add("py;Paraguay".split(";"));
+    this.knownCountries.add("pe;Peru".split(";"));
+    this.knownCountries.add("ph;Philippines".split(";"));
+    this.knownCountries.add("pn;Pitcairn Islands".split(";"));
+    this.knownCountries.add("pl;Poland".split(";"));
+    this.knownCountries.add("pt;Portugal".split(";"));
+    this.knownCountries.add("pr;Puerto Rico".split(";"));
+    this.knownCountries.add("qa;Qatar".split(";"));
+    this.knownCountries.add("re;Reunion".split(";"));
+    this.knownCountries.add("ro;Romania".split(";"));
+    this.knownCountries.add("ru;Russia".split(";"));
+    this.knownCountries.add("rw;Rwanda".split(";"));
+    this.knownCountries.add("bl;Saint Bartelemey".split(";"));
+    this.knownCountries.add("sh;Saint Helena".split(";"));
+    this.knownCountries.add("kn;Saint Kitts and Nevis".split(";"));
+    this.knownCountries.add("lc;Saint Lucia".split(";"));
+    this.knownCountries.add("mf;Saint Martin".split(";"));
+    this.knownCountries.add("pm;Saint Pierre and Miquelon".split(";"));
+    this.knownCountries.add("vc;Saint Vincent and the Grenadines".
+        split(";"));
+    this.knownCountries.add("ws;Samoa".split(";"));
+    this.knownCountries.add("sm;San Marino".split(";"));
+    this.knownCountries.add("st:São Tomé and Príncipe".
+        split(":"));
+    this.knownCountries.add("sa;Saudi Arabia".split(";"));
+    this.knownCountries.add("sn;Senegal".split(";"));
+    this.knownCountries.add("rs;Serbia".split(";"));
+    this.knownCountries.add("sc;Seychelles".split(";"));
+    this.knownCountries.add("sl;Sierra Leone".split(";"));
+    this.knownCountries.add("sg;Singapore".split(";"));
+    this.knownCountries.add("sk;Slovakia".split(";"));
+    this.knownCountries.add("si;Slovenia".split(";"));
+    this.knownCountries.add("sb;Solomon Islands".split(";"));
+    this.knownCountries.add("so;Somalia".split(";"));
+    this.knownCountries.add("za;South Africa".split(";"));
+    this.knownCountries.add(("gs;South Georgia and the South Sandwich "
+        + "Islands").split(";"));
+    this.knownCountries.add("ss;South Sudan".split(";"));
+    this.knownCountries.add("es;Spain".split(";"));
+    this.knownCountries.add("lk;Sri Lanka".split(";"));
+    this.knownCountries.add("sd;Sudan".split(";"));
+    this.knownCountries.add("sr;Suriname".split(";"));
+    this.knownCountries.add("sj;Svalbard and Jan Mayen".split(";"));
+    this.knownCountries.add("sz;Swaziland".split(";"));
+    this.knownCountries.add("se;Sweden".split(";"));
+    this.knownCountries.add("ch;Switzerland".split(";"));
+    this.knownCountries.add("sy;Syrian Arab Republic".split(";"));
+    this.knownCountries.add("tw;Taiwan".split(";"));
+    this.knownCountries.add("tj;Tajikistan".split(";"));
+    this.knownCountries.add("tz;Tanzania, United Republic of".split(";"));
+    this.knownCountries.add("th;Thailand".split(";"));
+    this.knownCountries.add("tl;East Timor".split(";"));
+    this.knownCountries.add("tg;Togo".split(";"));
+    this.knownCountries.add("tk;Tokelau".split(";"));
+    this.knownCountries.add("to;Tonga".split(";"));
+    this.knownCountries.add("tt;Trinidad and Tobago".split(";"));
+    this.knownCountries.add("tn;Tunisia".split(";"));
+    this.knownCountries.add("tr;Turkey".split(";"));
+    this.knownCountries.add("tm;Turkmenistan".split(";"));
+    this.knownCountries.add("tc;Turks and Caicos Islands".split(";"));
+    this.knownCountries.add("tv;Tuvalu".split(";"));
+    this.knownCountries.add("ug;Uganda".split(";"));
+    this.knownCountries.add("ua;Ukraine".split(";"));
+    this.knownCountries.add("ae;United Arab Emirates".split(";"));
+    this.knownCountries.add("gb;United Kingdom".split(";"));
+    this.knownCountries.add("um;United States Minor Outlying Islands".
+        split(";"));
+    this.knownCountries.add("us;United States".split(";"));
+    this.knownCountries.add("uy;Uruguay".split(";"));
+    this.knownCountries.add("uz;Uzbekistan".split(";"));
+    this.knownCountries.add("vu;Vanuatu".split(";"));
+    this.knownCountries.add("ve;Venezuela".split(";"));
+    this.knownCountries.add("vn;Vietnam".split(";"));
+    this.knownCountries.add("vg;Virgin Islands, British".split(";"));
+    this.knownCountries.add("vi;Virgin Islands, U.S.".split(";"));
+    this.knownCountries.add("wf;Wallis and Futuna".split(";"));
+    this.knownCountries.add("eh;Western Sahara".split(";"));
+    this.knownCountries.add("ye;Yemen".split(";"));
+    this.knownCountries.add("zm;Zambia".split(";"));
+    this.knownCountries.add("zw;Zimbabwe".split(";"));
+  }
+
+  public List<String[]> getCountryList() {
+    return this.knownCountries;
+  }
+}
+
diff --git a/website/src/org/torproject/ernie/web/graphs/CsvServlet.java b/website/src/org/torproject/ernie/web/graphs/CsvServlet.java
new file mode 100644
index 0000000..40e3bea
--- /dev/null
+++ b/website/src/org/torproject/ernie/web/graphs/CsvServlet.java
@@ -0,0 +1,97 @@
+/* Copyright 2011, 2012 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.ernie.web.graphs;
+
+import java.io.IOException;
+import java.util.SortedSet;
+import java.util.logging.Logger;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Servlet that reads an HTTP request for a comma-separated value file,
+ * asks the GraphGenerator to generate this file, and returns it to the
+ * client.
+ */
+public class CsvServlet extends HttpServlet {
+
+  private static final long serialVersionUID = 7501442926823719958L;
+
+  private RObjectGenerator rObjectGenerator;
+
+  /* Available CSV files. */
+  private SortedSet<String> availableCsvFiles;
+
+  private Logger logger;
+
+  public void init() {
+
+    /* Initialize logger. */
+    this.logger = Logger.getLogger(CsvServlet.class.toString());
+
+    /* Get a reference to the R object generator that we need to generate
+     * CSV files. */
+    this.rObjectGenerator = (RObjectGenerator) getServletContext().
+        getAttribute("RObjectGenerator");
+    this.availableCsvFiles = rObjectGenerator.getAvailableCsvFiles();
+  }
+
+  public void doGet(HttpServletRequest request,
+      HttpServletResponse response) throws IOException,
+      ServletException {
+
+    /* Check if the directory listing was requested. */
+    String requestURI = request.getRequestURI();
+    if (requestURI.equals("/ernie/csv/")) {
+      request.setAttribute("directory", "/csv");
+      request.setAttribute("extension", ".csv");
+      request.setAttribute("files", this.availableCsvFiles);
+      request.getRequestDispatcher("/WEB-INF/dir.jsp").forward(request,
+          response);
+      return;
+    }
+
+    /* Find out which CSV file was requested and make sure we know this
+     * CSV file type. */
+    String requestedCsvFile = requestURI;
+    if (requestedCsvFile.endsWith(".csv")) {
+      requestedCsvFile = requestedCsvFile.substring(0,
+          requestedCsvFile.length() - ".csv".length());
+    }
+    if (requestedCsvFile.contains("/")) {
+      requestedCsvFile = requestedCsvFile.substring(requestedCsvFile.
+          lastIndexOf("/") + 1);
+    }
+    if (!availableCsvFiles.contains(requestedCsvFile)) {
+      logger.info("Did not recognize requested .csv file from request "
+          + "URI: '" + requestURI + "'. Responding with 404 Not Found.");
+      response.sendError(HttpServletResponse.SC_NOT_FOUND);
+      return;
+    }
+    logger.fine("CSV file '" + requestedCsvFile + ".csv' requested.");
+
+    /* Request CSV file from R object generator, which asks Rserve to
+     * generate it. */
+    RObject csvFile = this.rObjectGenerator.generateCsv(
+        requestedCsvFile, true);
+
+    /* Make sure that we have a .csv file to return. */
+    if (csvFile == null) {
+      response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+      return;
+    }
+
+    /* Write CSV file to response. */
+    String csvFileContent = new String(csvFile.getBytes());
+    response.setContentType("text/csv");
+    response.setHeader("Content-Length", String.valueOf(
+        csvFileContent.length()));
+    response.setHeader("Content-Disposition",
+        "inline; filename=\"" + requestedCsvFile + ".csv\"");
+    response.getWriter().print(csvFileContent);
+  }
+}
+
diff --git a/website/src/org/torproject/ernie/web/graphs/GraphImageServlet.java b/website/src/org/torproject/ernie/web/graphs/GraphImageServlet.java
new file mode 100644
index 0000000..b7d0b17
--- /dev/null
+++ b/website/src/org/torproject/ernie/web/graphs/GraphImageServlet.java
@@ -0,0 +1,76 @@
+/* Copyright 2011, 2012 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.ernie.web.graphs;
+
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Servlet that reads an HTTP request for a graph image, asks the
+ * RObjectGenerator to generate this graph if it's not in the cache, and
+ * returns the image bytes to the client.
+ */
+public class GraphImageServlet extends HttpServlet {
+
+  private static final long serialVersionUID = -7356818641689744288L;
+
+  private RObjectGenerator rObjectGenerator;
+
+  public void init() {
+
+    /* Get a reference to the R object generator that we need to generate
+     * graph images. */
+    this.rObjectGenerator = (RObjectGenerator) getServletContext().
+        getAttribute("RObjectGenerator");
+  }
+
+  public void doGet(HttpServletRequest request,
+      HttpServletResponse response) throws IOException,
+      ServletException {
+
+    /* Find out which graph type was requested and make sure we know this
+     * graph type and file type. */
+    String requestedGraph = request.getRequestURI();
+    String fileType = null;
+    if (requestedGraph.endsWith(".png") ||
+        requestedGraph.endsWith(".pdf") ||
+        requestedGraph.endsWith(".svg")) {
+      fileType = requestedGraph.substring(requestedGraph.length() - 3);
+      requestedGraph = requestedGraph.substring(0, requestedGraph.length()
+          - 4);
+    }
+    if (requestedGraph.contains("/")) {
+      requestedGraph = requestedGraph.substring(requestedGraph.
+          lastIndexOf("/") + 1);
+    }
+
+    /* Request graph from R object generator, which either returns it from
+     * its cache or asks Rserve to generate it. */
+    RObject graph = rObjectGenerator.generateGraph(requestedGraph,
+        fileType, request.getParameterMap(), true);
+
+    /* Make sure that we have a graph to return. */
+    if (graph == null || graph.getBytes() == null || fileType == null) {
+      response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+      return;
+    }
+
+    /* Write graph bytes to response. */
+    BufferedOutputStream output = null;
+    response.setContentType("image/" + fileType);
+    response.setHeader("Content-Length",
+        String.valueOf(graph.getBytes().length));
+    response.setHeader("Content-Disposition",
+        "inline; filename=\"" + graph.getFileName() + "\"");
+    output = new BufferedOutputStream(response.getOutputStream(), 1024);
+    output.write(graph.getBytes(), 0, graph.getBytes().length);
+    output.flush();
+    output.close();
+  }
+}
+
diff --git a/website/src/org/torproject/ernie/web/graphs/GraphParameterChecker.java b/website/src/org/torproject/ernie/web/graphs/GraphParameterChecker.java
new file mode 100644
index 0000000..74ca6f9
--- /dev/null
+++ b/website/src/org/torproject/ernie/web/graphs/GraphParameterChecker.java
@@ -0,0 +1,280 @@
+/* Copyright 2011, 2012 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.ernie.web.graphs;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TimeZone;
+import java.util.regex.Pattern;
+
+/**
+ * Checks request parameters passed to graph-generating servlets.
+ */
+public class GraphParameterChecker {
+
+  /**
+   * Singleton instance of this class.
+   */
+  private static GraphParameterChecker instance =
+      new GraphParameterChecker();
+
+  /**
+   * Returns the singleton instance of this class.
+   */
+  public static GraphParameterChecker getInstance() {
+    return instance;
+  }
+
+  /* Date format for parsing start and end dates. */
+  private SimpleDateFormat dateFormat;
+
+  /* Available graphs with corresponding parameter lists. */
+  private Map<String, String> availableGraphs;
+
+  /* Known parameters and parameter values. */
+  private Map<String, String> knownParameterValues;
+
+  /**
+   * Initializes map with valid parameters for each of the graphs.
+   */
+  public GraphParameterChecker() {
+    this.dateFormat = new SimpleDateFormat("yyyy-MM-dd");
+    this.dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+
+    this.knownParameterValues = new HashMap<String, String>();
+    this.knownParameterValues.put("flag",
+        "Running,Exit,Guard,Fast,Stable,HSDir");
+    StringBuilder sb = new StringBuilder("all");
+    for (String[] country : Countries.getInstance().getCountryList()) {
+      sb.append("," + country[0]);
+    }
+    this.knownParameterValues.put("country", sb.toString());
+    this.knownParameterValues.put("events", "on,off,points");
+    this.knownParameterValues.put("source", "all,siv,moria,torperf");
+    this.knownParameterValues.put("filesize", "50kb,1mb,5mb");
+    this.knownParameterValues.put("transport",
+        "obfs2,obfs3,websocket,<OR>,<??>");
+    this.knownParameterValues.put("version", "v4,v6");
+  }
+
+  public void setAvailableGraphs(Map<String, String> availableGraphs) {
+    this.availableGraphs = availableGraphs;
+  }
+
+  /**
+   * Checks request parameters for the given graph type and returns a map
+   * of recognized parameters, or null if the graph type doesn't exist or
+   * the parameters are invalid.
+   */
+  public Map<String, String[]> checkParameters(String graphType,
+      Map requestParameters) {
+
+    /* Check if the graph type exists. */
+    if (graphType == null ||
+        !this.availableGraphs.containsKey(graphType)) {
+      return null;
+    }
+
+    /* Find out which other parameters are supported by this graph type
+     * and parse them if they are given. */
+    Set<String> supportedGraphParameters = new HashSet<String>(Arrays.
+        asList(this.availableGraphs.get(graphType).split(",")));
+    Map<String, String[]> recognizedGraphParameters =
+        new HashMap<String, String[]>();
+
+    /* Parse start and end dates if supported by the graph type. If no end
+     * date is provided, set it to today. If no start date is provided,
+     * set it to 90 days before the end date. Make sure that start date
+     * precedes end date. */
+    if (supportedGraphParameters.contains("start") ||
+        supportedGraphParameters.contains("end")) {
+      String[] startParameter = (String[]) requestParameters.get("start");
+      String[] endParameter = (String[]) requestParameters.get("end");
+      long endTimestamp = System.currentTimeMillis();
+      if (endParameter != null && endParameter.length > 0 &&
+          endParameter[0].length() > 0) {
+        try {
+          endTimestamp = dateFormat.parse(endParameter[0]).getTime();
+        } catch (ParseException e)  {
+          return null;
+        }
+        if (!endParameter[0].startsWith("20")) {
+          return null;
+        }
+      }
+      endParameter = new String[] { dateFormat.format(endTimestamp) };
+      long startTimestamp = endTimestamp - 90L * 24L * 60L * 60L * 1000L;
+      if (startParameter != null && startParameter.length > 0 &&
+          startParameter[0].length() > 0) {
+        try {
+          startTimestamp = dateFormat.parse(startParameter[0]).getTime();
+        } catch (ParseException e)  {
+          return null;
+        }
+        if (!startParameter[0].startsWith("20")) {
+          return null;
+        }
+      }
+      startParameter = new String[] { dateFormat.format(startTimestamp) };
+      if (startTimestamp > endTimestamp) {
+       return null;
+      }
+      recognizedGraphParameters.put("start", startParameter);
+      recognizedGraphParameters.put("end", endParameter);
+    }
+
+    /* Parse relay flags if supported by the graph type. If no relay flags
+     * are passed or none of them have been recognized, use the set of all
+     * known flags as default. */
+    if (supportedGraphParameters.contains("flag")) {
+      String[] flagParameters = (String[]) requestParameters.get("flag");
+      List<String> knownFlags = Arrays.asList(
+          this.knownParameterValues.get("flag").split(","));
+      if (flagParameters != null) {
+        for (String flag : flagParameters) {
+          if (flag == null || flag.length() == 0 ||
+              !knownFlags.contains(flag)) {
+            return null;
+          }
+        }
+      } else {
+        flagParameters = "Running,Exit,Guard,Fast,Stable".split(",");
+      }
+      recognizedGraphParameters.put("flag", flagParameters);
+    }
+
+    /* Parse country codes if supported by the graph type. If no countries
+     * are passed, use country code "all" (all countries) as default. */
+    if (supportedGraphParameters.contains("country")) {
+      String[] countryParameters = (String[]) requestParameters.get(
+          "country");
+      List<String> knownCountries = Arrays.asList(
+          this.knownParameterValues.get("country").split(","));
+      if (countryParameters != null) {
+        for (String country : countryParameters) {
+          if (country == null || country.length() == 0 ||
+              !knownCountries.contains(country)) {
+            return null;
+          }
+        }
+      } else {
+        countryParameters = new String[] { "all" };
+      }
+      recognizedGraphParameters.put("country", countryParameters);
+    }
+
+    /* Parse whether the estimated min/max range shall be displayed if
+     * supported by the graph type. This parameter can either be "on" or
+     * "off," where "off" is the default. */
+    if (supportedGraphParameters.contains("events")) {
+      String[] eventsParameter = (String[]) requestParameters.get(
+          "events");
+      List<String> knownRanges = Arrays.asList(
+          this.knownParameterValues.get("events").split(","));
+      if (eventsParameter != null) {
+        if (eventsParameter.length != 1 ||
+            eventsParameter[0].length() == 0 ||
+            !knownRanges.contains(eventsParameter[0])) {
+          return null;
+        }
+      } else {
+        eventsParameter = new String[] { "off" };
+      }
+      recognizedGraphParameters.put("events", eventsParameter);
+    }
+
+    /* Parse torperf data source if supported by the graph type. Only a
+     * single source can be passed. If no source is passed, use "torperf"
+     * as default. */
+    if (supportedGraphParameters.contains("source")) {
+      String[] sourceParameter = (String[]) requestParameters.get(
+          "source");
+      List<String> knownSources = Arrays.asList(
+          this.knownParameterValues.get("source").split(","));
+      if (sourceParameter != null) {
+        if (sourceParameter.length != 1) {
+          return null;
+        }
+        if (sourceParameter[0].length() == 0 ||
+            !knownSources.contains(sourceParameter[0])) {
+          return null;
+        }
+      } else {
+        sourceParameter = new String[] { "all" };
+      }
+      recognizedGraphParameters.put("source", sourceParameter);
+    }
+
+    /* Parse torperf file size if supported by the graph type. Only a
+     * single file size can be passed. If no file size is passed, use
+     * "50kb" as default. */
+    if (supportedGraphParameters.contains("filesize")) {
+      String[] filesizeParameter = (String[]) requestParameters.get(
+          "filesize");
+      List<String> knownFilesizes = Arrays.asList(
+          this.knownParameterValues.get("filesize").split(","));
+      if (filesizeParameter != null) {
+        if (filesizeParameter.length != 1) {
+          return null;
+        }
+        if (filesizeParameter[0].length() == 0 ||
+            !knownFilesizes.contains(filesizeParameter[0])) {
+          return null;
+        }
+      } else {
+        filesizeParameter = new String[] { "50kb" };
+      }
+      recognizedGraphParameters.put("filesize", filesizeParameter);
+    }
+
+    /* Parse transports if supported by the graph type. If no transports
+     * are passed, use "<OR>" as default. */
+    if (supportedGraphParameters.contains("transport")) {
+      String[] transportParameters = (String[]) requestParameters.get(
+          "transport");
+      List<String> knownTransports = Arrays.asList(
+          this.knownParameterValues.get("transport").split(","));
+      if (transportParameters != null) {
+        for (String transport : transportParameters) {
+          if (transport == null || transport.length() == 0 ||
+              !knownTransports.contains(transport)) {
+            return null;
+          }
+        }
+      } else {
+        transportParameters = new String[] { "<OR>" };
+      }
+      recognizedGraphParameters.put("transport", transportParameters);
+    }
+
+    /* Parse versions if supported by the graph type. If no versions
+     * are passed, use "v4" as default. */
+    if (supportedGraphParameters.contains("version")) {
+      String[] versionParameters = (String[]) requestParameters.get(
+          "version");
+      List<String> knownVersions = Arrays.asList(
+          this.knownParameterValues.get("version").split(","));
+      if (versionParameters != null) {
+        for (String version : versionParameters) {
+          if (version == null || version.length() == 0 ||
+              !knownVersions.contains(version)) {
+            return null;
+          }
+        }
+      } else {
+        versionParameters = new String[] { "v4" };
+      }
+      recognizedGraphParameters.put("version", versionParameters);
+    }
+
+    /* We now have a map with all required graph parameters. Return it. */
+    return recognizedGraphParameters;
+  }
+}
+
diff --git a/website/src/org/torproject/ernie/web/graphs/GraphsSubpagesServlet.java b/website/src/org/torproject/ernie/web/graphs/GraphsSubpagesServlet.java
new file mode 100644
index 0000000..94d7340
--- /dev/null
+++ b/website/src/org/torproject/ernie/web/graphs/GraphsSubpagesServlet.java
@@ -0,0 +1,162 @@
+/* Copyright 2011, 2012 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.ernie.web.graphs;
+
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TimeZone;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class GraphsSubpagesServlet extends HttpServlet {
+
+  private static final long serialVersionUID = -5959829347747628403L;
+
+  /* Available graphs subpages with corresponding JSP to which requests
+   * are forwarded. */
+  private Map<String, String> availableGraphsSubpages;
+
+  /* Available tables on graphs subpages. */
+  private Map<String, Set<String>> availableGraphsSubpageTables;
+
+  /* Country codes and names for per-country graphs. */
+  private List<String[]> knownCountries;
+
+  /* R object generator for generating table data. */
+  private RObjectGenerator rObjectGenerator;
+
+  public GraphsSubpagesServlet() {
+    this.availableGraphsSubpages = new HashMap<String, String>();
+    this.availableGraphsSubpages.put("network.html",
+        "WEB-INF/network.jsp");
+    this.availableGraphsSubpages.put("fast-exits.html",
+        "WEB-INF/fast-exits.jsp");
+    this.availableGraphsSubpages.put("users.html", "WEB-INF/users.jsp");
+    this.availableGraphsSubpages.put("performance.html",
+        "WEB-INF/performance.jsp");
+
+    this.availableGraphsSubpageTables =
+        new HashMap<String, Set<String>>();
+    this.availableGraphsSubpageTables.put("users.html",
+        new HashSet<String>(Arrays.asList((
+        "direct-users,censorship-events,bridge-users,userstats-relay,"
+        + "userstats-censorship-events,userstats-bridge").split(","))));
+
+    this.knownCountries = Countries.getInstance().getCountryList();
+  }
+
+  public void init() {
+    /* Get a reference to the R object generator that we need to generate
+     * table data. */
+    this.rObjectGenerator = (RObjectGenerator) getServletContext().
+        getAttribute("RObjectGenerator");
+  }
+
+  public void doGet(HttpServletRequest request,
+      HttpServletResponse response) throws IOException, ServletException {
+
+    /* Find out which graph subpage was requested and look up which JSP
+     * handles this subpage. */
+    String requestedPage = request.getRequestURI();
+    if (requestedPage == null) {
+      response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+      return;
+    }
+    if (requestedPage.contains("/")) {
+      requestedPage = requestedPage.substring(requestedPage.
+          lastIndexOf("/") + 1);
+    }
+    if (!availableGraphsSubpages.containsKey(requestedPage)) {
+      response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+      return;
+    }
+    String jsp = availableGraphsSubpages.get(requestedPage);
+
+    /* Find out which graph or table type was requested, if any. */
+    String requestedGraph = request.getParameter("graph");
+    String requestedTable = request.getParameter("table");
+    if (requestedGraph != null) {
+
+      /* Check if the passed parameters are valid. */
+      Map<String, String[]> checkedParameters = GraphParameterChecker.
+          getInstance().checkParameters(requestedGraph,
+          request.getParameterMap());
+      if (checkedParameters != null) {
+
+        /* Set the graph's attributes to the appropriate values, so that
+         * we can display the correct graph and prepopulate the form. */
+        StringBuilder urlBuilder = new StringBuilder();
+        for (Map.Entry<String, String[]> param :
+            checkedParameters.entrySet()) {
+          request.setAttribute(requestedGraph.replaceAll("-", "_") + "_"
+              + param.getKey(), param.getValue());
+          for (String paramValue : param.getValue()) {
+            urlBuilder.append("&" + param.getKey() + "=" + paramValue);
+          }
+        }
+        String url = "?" + urlBuilder.toString().substring(1);
+        request.setAttribute(requestedGraph.replaceAll("-", "_") + "_url",
+            url);
+      }
+    }
+    if (requestedTable != null) {
+
+      /* Check if the passed parameters are valid. */
+      Map<String, String[]> checkedParameters = TableParameterChecker.
+          getInstance().checkParameters(requestedTable,
+          request.getParameterMap());
+      if (checkedParameters != null) {
+
+        /* Set the table's attributes to the appropriate values, so that
+         * we can prepopulate the form. */
+        for (Map.Entry<String, String[]> param :
+            checkedParameters.entrySet()) {
+          request.setAttribute(requestedTable.replaceAll("-", "_") + "_"
+              + param.getKey(), param.getValue());
+        }
+      }
+    }
+
+    /* Generate table data if the graphs subpage has any tables,
+     * regardless of whether a table update was requested, and add the
+     * table data as request attribute. */
+    if (this.availableGraphsSubpageTables.containsKey(requestedPage)) {
+      for (String tableName :
+          this.availableGraphsSubpageTables.get(requestedPage)) {
+        List<Map<String, String>> tableData = rObjectGenerator.
+            generateTable(tableName, requestedTable,
+            request.getParameterMap(), true);
+        request.setAttribute(tableName.replaceAll("-", "_")
+              + "_tabledata", tableData);
+      }
+    }
+
+    /* Pass list of known countries in case we want to display them. */
+    request.setAttribute("countries", this.knownCountries);
+
+    /* Pass the default start and end dates. */
+    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
+    dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+    Date defaultEndDate = new Date();
+    Date defaultStartDate = new Date(defaultEndDate.getTime()
+        - 90L * 24L * 60L * 60L * 1000L);
+    request.setAttribute("default_start_date",
+        dateFormat.format(defaultStartDate));
+    request.setAttribute("default_end_date",
+        dateFormat.format(defaultEndDate));
+
+    /* Forward the request to the JSP that does all the hard work. */
+    request.getRequestDispatcher(jsp).forward(request, response);
+  }
+}
+
diff --git a/website/src/org/torproject/ernie/web/graphs/RObject.java b/website/src/org/torproject/ernie/web/graphs/RObject.java
new file mode 100644
index 0000000..cfab819
--- /dev/null
+++ b/website/src/org/torproject/ernie/web/graphs/RObject.java
@@ -0,0 +1,23 @@
+/* Copyright 2011, 2012 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.ernie.web.graphs;
+
+public class RObject {
+  private byte[] bytes;
+  private String fileName;
+  private long lastModified;
+  public RObject(byte[] bytes, String fileName, long lastModified) {
+    this.bytes = bytes;
+    this.fileName = fileName;
+    this.lastModified = lastModified;
+  }
+  public String getFileName() {
+    return this.fileName;
+  }
+  public byte[] getBytes() {
+    return this.bytes;
+  }
+  public long getLastModified() {
+    return this.lastModified;
+  }
+}
diff --git a/website/src/org/torproject/ernie/web/graphs/RObjectGenerator.java b/website/src/org/torproject/ernie/web/graphs/RObjectGenerator.java
new file mode 100644
index 0000000..927b5c4
--- /dev/null
+++ b/website/src/org/torproject/ernie/web/graphs/RObjectGenerator.java
@@ -0,0 +1,398 @@
+/* Copyright 2011, 2012 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.ernie.web.graphs;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import javax.servlet.ServletContext;
+import javax.servlet.ServletContextEvent;
+import javax.servlet.ServletContextListener;
+
+import org.rosuda.REngine.Rserve.RConnection;
+import org.rosuda.REngine.Rserve.RserveException;
+
+public class RObjectGenerator implements ServletContextListener {
+
+  /* Host and port where Rserve is listening. */
+  private String rserveHost;
+  private int rservePort;
+
+  /* Some parameters for our cache of graph images. */
+  private String cachedGraphsDirectory;
+  private long maxCacheAge;
+
+  private SortedSet<String> availableCsvFiles;
+  private Map<String, String> availableTables;
+  private Map<String, String> availableGraphs;
+  private Set<String> availableGraphFileTypes;
+
+  public void contextInitialized(ServletContextEvent event) {
+
+    /* Initialize using context parameters. */
+    ServletContext servletContext = event.getServletContext();
+    this.rserveHost = servletContext.getInitParameter("rserveHost");
+    this.rservePort = Integer.parseInt(servletContext.getInitParameter(
+        "rservePort"));
+    this.maxCacheAge = Long.parseLong(servletContext.getInitParameter(
+        "maxCacheAge"));
+    this.cachedGraphsDirectory = servletContext.getInitParameter(
+        "cachedGraphsDir");
+
+    /* Initialize map of available CSV files. */
+    this.availableCsvFiles = new TreeSet<String>();
+    this.availableCsvFiles.add("bandwidth");
+    this.availableCsvFiles.add("bandwidth-flags");
+    this.availableCsvFiles.add("bwhist-flags");
+    this.availableCsvFiles.add("connbidirect");
+    this.availableCsvFiles.add("cloudbridges");
+    this.availableCsvFiles.add("dirbytes");
+    this.availableCsvFiles.add("monthly-userstats-average");
+    this.availableCsvFiles.add("monthly-userstats-peak");
+    this.availableCsvFiles.add("networksize");
+    this.availableCsvFiles.add("platforms");
+    this.availableCsvFiles.add("relaycountries");
+    this.availableCsvFiles.add("relayflags");
+    this.availableCsvFiles.add("torperf");
+    this.availableCsvFiles.add("torperf-failures");
+    this.availableCsvFiles.add("userstats");
+    this.availableCsvFiles.add("userstats-detector");
+    this.availableCsvFiles.add("versions");
+
+    this.availableTables = new HashMap<String, String>();
+    this.availableTables.put("userstats-relay", "start,end,filename");
+    this.availableTables.put("userstats-bridge", "start,end,filename");
+    this.availableTables.put("userstats-censorship-events",
+        "start,end,filename");
+    TableParameterChecker.getInstance().setAvailableTables(
+        availableTables);
+
+    this.availableGraphs = new HashMap<String, String>();
+    this.availableGraphs.put("networksize", "start,end,filename");
+    this.availableGraphs.put("cloudbridges", "start,end,filename");
+    this.availableGraphs.put("relaycountries",
+        "start,end,country,filename");
+    this.availableGraphs.put("relayflags", "start,end,flag,filename");
+    this.availableGraphs.put("versions", "start,end,filename");
+    this.availableGraphs.put("platforms", "start,end,filename");
+    this.availableGraphs.put("bandwidth", "start,end,filename");
+    this.availableGraphs.put("bandwidth-flags", "start,end,filename");
+    this.availableGraphs.put("bwhist-flags", "start,end,filename");
+    this.availableGraphs.put("dirbytes", "start,end,filename");
+    this.availableGraphs.put("torperf",
+         "start,end,source,filesize,filename");
+    this.availableGraphs.put("torperf-failures",
+         "start,end,source,filesize,filename");
+    this.availableGraphs.put("connbidirect", "start,end,filename");
+    this.availableGraphs.put("fast-exits", "start,end,filename");
+    this.availableGraphs.put("almost-fast-exits", "start,end,filename");
+    this.availableGraphs.put("userstats-relay-country",
+        "start,end,country,events,filename");
+    this.availableGraphs.put("userstats-bridge-country",
+        "start,end,country,filename");
+    this.availableGraphs.put("userstats-bridge-transport",
+        "start,end,transport,filename");
+    this.availableGraphs.put("userstats-bridge-version",
+        "start,end,version,filename");
+    this.availableGraphFileTypes = new HashSet<String>(Arrays.asList(
+        "png,pdf,svg".split(",")));
+    GraphParameterChecker.getInstance().setAvailableGraphs(
+        availableGraphs);
+
+    /* Register ourself, so that servlets can use us. */
+    servletContext.setAttribute("RObjectGenerator", this);
+
+    /* Periodically generate R objects with default parameters. */
+    new Thread() {
+      public void run() {
+        long lastUpdated = 0L, sleep;
+        while (true) {
+          while ((sleep = maxCacheAge * 1000L / 2L + lastUpdated
+              - System.currentTimeMillis()) > 0L) {
+            try {
+              Thread.sleep(sleep);
+            } catch (InterruptedException e) {
+            }
+          }
+          for (String csvFile : availableCsvFiles) {
+            generateCsv(csvFile, false);
+          }
+          for (String tableName : availableTables.keySet()) {
+            generateTable(tableName, tableName, new HashMap(), false);
+          }
+          for (String graphName : availableGraphs.keySet()) {
+            for (String fileType : availableGraphFileTypes) {
+              generateGraph(graphName, fileType, new HashMap(), false);
+            }
+          }
+          lastUpdated = System.currentTimeMillis();
+        }
+      };
+    }.start();
+  }
+
+  public void contextDestroyed(ServletContextEvent event) {
+    /* Nothing to do. */
+  }
+
+  public RObject generateGraph(String requestedGraph, String fileType,
+      Map parameterMap, boolean checkCache) {
+    Map<String, String[]> checkedParameters = GraphParameterChecker.
+        getInstance().checkParameters(requestedGraph, parameterMap);
+    if (checkedParameters == null) {
+      /* TODO We're going to take the blame by sending an internal server
+       * error to the client, but really the user is to blame. */
+      return null;
+    }
+    StringBuilder rQueryBuilder = new StringBuilder("plot_"
+        + requestedGraph.replaceAll("-", "_") + "("),
+        imageFilenameBuilder = new StringBuilder(requestedGraph);
+    for (Map.Entry<String, String[]> parameter :
+        checkedParameters.entrySet()) {
+      String parameterName = parameter.getKey();
+      String[] parameterValues = parameter.getValue();
+      for (String param : parameterValues) {
+        imageFilenameBuilder.append("-" + param);
+      }
+      if (parameterValues.length < 2) {
+        rQueryBuilder.append(parameterName + " = '" + parameterValues[0]
+            + "', ");
+      } else {
+        rQueryBuilder.append(parameterName + " = c(");
+        for (int i = 0; i < parameterValues.length - 1; i++) {
+          rQueryBuilder.append("'" + parameterValues[i] + "', ");
+        }
+        rQueryBuilder.append("'" + parameterValues[
+            parameterValues.length - 1] + "'), ");
+      }
+    }
+    imageFilenameBuilder.append("." + fileType);
+    String imageFilename = imageFilenameBuilder.toString();
+    rQueryBuilder.append("path = '%s')");
+    String rQuery = rQueryBuilder.toString();
+    File imageFile = new File(this.cachedGraphsDirectory + "/"
+        + imageFilename);
+    return this.generateRObject(rQuery, imageFile, imageFilename,
+        checkCache);
+  }
+
+  public SortedSet<String> getAvailableCsvFiles() {
+    return this.availableCsvFiles;
+  }
+
+  public RObject generateCsv(String requestedCsvFile,
+      boolean checkCache) {
+    /* Prepare filename and R query string. */
+    String rQuery = "export_" + requestedCsvFile.replaceAll("-", "_")
+        + "(path = '%s')";
+    String csvFilename = requestedCsvFile + ".csv";
+
+    /* See if we need to generate this .csv file. */
+    File csvFile = new File(this.cachedGraphsDirectory + "/"
+        + csvFilename);
+    return this.generateRObject(rQuery, csvFile, csvFilename, checkCache);
+  }
+
+  public List<Map<String, String>> generateTable(String tableName,
+      String requestedTable, Map parameterMap, boolean checkCache) {
+
+    Map<String, String[]> checkedParameters = null;
+    if (tableName.equals(requestedTable)) {
+      checkedParameters = TableParameterChecker.
+          getInstance().checkParameters(requestedTable,
+          parameterMap);
+    } else {
+      checkedParameters = TableParameterChecker.
+          getInstance().checkParameters(tableName, null);
+    }
+    if (checkedParameters == null) {
+      /* TODO We're going to take the blame by sending an internal server
+       * error to the client, but really the user is to blame. */
+      return null;
+    }
+    StringBuilder rQueryBuilder = new StringBuilder("write_"
+        + tableName.replaceAll("-", "_") + "("),
+        tableFilenameBuilder = new StringBuilder(tableName);
+
+    for (Map.Entry<String, String[]> parameter :
+        checkedParameters.entrySet()) {
+      String parameterName = parameter.getKey();
+      String[] parameterValues = parameter.getValue();
+      for (String param : parameterValues) {
+        tableFilenameBuilder.append("-" + param);
+      }
+      if (parameterValues.length < 2) {
+        rQueryBuilder.append(parameterName + " = '"
+            + parameterValues[0] + "', ");
+      } else {
+        rQueryBuilder.append(parameterName + " = c(");
+        for (int i = 0; i < parameterValues.length - 1; i++) {
+          rQueryBuilder.append("'" + parameterValues[i] + "', ");
+        }
+        rQueryBuilder.append("'" + parameterValues[
+            parameterValues.length - 1] + "'), ");
+      }
+    }
+    tableFilenameBuilder.append(".tbl");
+    String tableFilename = tableFilenameBuilder.toString();
+    rQueryBuilder.append("path = '%s')");
+    String rQuery = rQueryBuilder.toString();
+    return this.generateTable(rQuery, tableFilename, checkCache);
+  }
+
+  /* Generate table data using the given R query and filename or read
+   * previously generated table data from disk if it's not too old and
+   * return table data. */
+  private List<Map<String, String>> generateTable(String rQuery,
+      String tableFilename, boolean checkCache) {
+
+    /* See if we need to generate this table. */
+    File tableFile = new File(this.cachedGraphsDirectory + "/"
+        + tableFilename);
+    byte[] tableBytes = this.generateRObject(rQuery, tableFile,
+        tableFilename, checkCache).getBytes();
+
+    /* Write the table content to a map. */
+    List<Map<String, String>> result = null;
+    try {
+      result = new ArrayList<Map<String, String>>();
+      BufferedReader br = new BufferedReader(new InputStreamReader(
+          new ByteArrayInputStream(tableBytes)));
+      String line = br.readLine();
+      if (line != null) {
+        List<String> headers = new ArrayList<String>(Arrays.asList(
+            line.split(",")));
+        while ((line = br.readLine()) != null) {
+          String[] parts = line.split(",");
+          if (headers.size() != parts.length) {
+            return null;
+          }
+          Map<String, String> row = new HashMap<String, String>();
+          for (int i = 0; i < headers.size(); i++) {
+            row.put(headers.get(i), parts[i]);
+          }
+          result.add(row);
+        }
+      }
+    } catch (IOException e) {
+      return null;
+    }
+
+    /* Return table values. */
+    return result;
+  }
+
+  /* Generate an R object in a separate worker thread, or wait for an
+   * already running worker thread to finish and get its result. */
+  private RObject generateRObject(String rQuery, File rObjectFile,
+      String fileName, boolean checkCache) {
+    RObjectGeneratorWorker worker = null;
+    synchronized (this.rObjectGeneratorThreads) {
+      if (this.rObjectGeneratorThreads.containsKey(rQuery)) {
+        worker = this.rObjectGeneratorThreads.get(rQuery);
+      } else {
+        worker = new RObjectGeneratorWorker(rQuery, rObjectFile,
+            fileName, checkCache);
+        this.rObjectGeneratorThreads.put(rQuery, worker);
+        worker.start();
+      }
+    }
+    try {
+      worker.join();
+    } catch (InterruptedException e) {
+    }
+    synchronized (this.rObjectGeneratorThreads) {
+      if (this.rObjectGeneratorThreads.containsKey(rQuery) &&
+          this.rObjectGeneratorThreads.get(rQuery) == worker) {
+        this.rObjectGeneratorThreads.remove(rQuery);
+      }
+    }
+    return worker.getRObject();
+  }
+
+  private Map<String, RObjectGeneratorWorker> rObjectGeneratorThreads =
+      new HashMap<String, RObjectGeneratorWorker>();
+
+  private class RObjectGeneratorWorker extends Thread {
+
+    private String rQuery;
+    private File rObjectFile;
+    private String fileName;
+    private boolean checkCache;
+    private RObject result = null;
+
+    public RObjectGeneratorWorker(String rQuery, File rObjectFile,
+        String fileName, boolean checkCache) {
+      this.rQuery = rQuery;
+      this.rObjectFile = rObjectFile;
+      this.fileName = fileName;
+      this.checkCache = checkCache;
+    }
+
+    public void run() {
+
+      /* See if we need to generate this R object. */
+      long now = System.currentTimeMillis();
+      if (!this.checkCache || !this.rObjectFile.exists() ||
+          this.rObjectFile.lastModified() < now - maxCacheAge * 1000L) {
+
+        /* We do. Update the R query to contain the absolute path to the
+         * file to be generated, create a connection to Rserve, run the R
+         * query, and close the connection. The generated object will be
+         * on disk. */
+        this.rQuery = String.format(this.rQuery,
+            this.rObjectFile.getAbsolutePath());
+        try {
+          RConnection rc = new RConnection(rserveHost, rservePort);
+          rc.eval(this.rQuery);
+          rc.close();
+        } catch (RserveException e) {
+          return;
+        }
+
+        /* Check that we really just generated the R object. */
+        if (!this.rObjectFile.exists() || this.rObjectFile.lastModified()
+            < now - maxCacheAge * 1000L) {
+          return;
+        }
+      }
+
+      /* Read the R object from disk and write it to a byte array. */
+      long lastModified = this.rObjectFile.lastModified();
+      try {
+        BufferedInputStream bis = new BufferedInputStream(
+            new FileInputStream(this.rObjectFile), 1024);
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        byte[] buffer = new byte[1024];
+        int length;
+        while ((length = bis.read(buffer)) > 0) {
+          baos.write(buffer, 0, length);
+        }
+        bis.close();
+        this.result = new RObject(baos.toByteArray(), this.fileName,
+            lastModified);
+      } catch (IOException e) {
+        return;
+      }
+    }
+
+    public RObject getRObject() {
+      return this.result;
+    }
+  }
+}
diff --git a/website/src/org/torproject/ernie/web/graphs/TableParameterChecker.java b/website/src/org/torproject/ernie/web/graphs/TableParameterChecker.java
new file mode 100644
index 0000000..02a0c85
--- /dev/null
+++ b/website/src/org/torproject/ernie/web/graphs/TableParameterChecker.java
@@ -0,0 +1,120 @@
+/* Copyright 2011, 2012 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.ernie.web.graphs;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.TimeZone;
+
+/**
+ * Checks request parameters passed to generate tables.
+ */
+public class TableParameterChecker {
+
+  /**
+   * Singleton instance of this class.
+   */
+  private static TableParameterChecker instance =
+      new TableParameterChecker();
+
+  /**
+   * Returns the singleton instance of this class.
+   */
+  public static TableParameterChecker getInstance() {
+    return instance;
+  }
+
+  /* Date format for parsing start and end dates. */
+  private SimpleDateFormat dateFormat;
+
+  /* Available tables with corresponding parameter lists. */
+  private Map<String, String> availableTables;
+
+  /**
+   * Initializes map with valid parameters for each of the graphs.
+   */
+  public TableParameterChecker() {
+    this.dateFormat = new SimpleDateFormat("yyyy-MM-dd");
+    this.dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+  }
+
+  public void setAvailableTables(Map<String, String> availableTables) {
+    this.availableTables = availableTables;
+  }
+
+  /**
+   * Checks request parameters for the given table type and returns a map
+   * of recognized parameters, or null if the table type doesn't exist or
+   * the parameters are invalid.
+   */
+  public Map<String, String[]> checkParameters(String tableType,
+      Map requestParameters) {
+
+    /* Check if the table type exists. */
+    if (tableType == null ||
+        !this.availableTables.containsKey(tableType)) {
+      return null;
+    }
+
+    /* Find out which other parameters are supported by this table type
+     * and parse them if they are given. */
+    Set<String> supportedTableParameters = new HashSet<String>(Arrays.
+        asList(this.availableTables.get(tableType).split(",")));
+    Map<String, String[]> recognizedTableParameters =
+        new HashMap<String, String[]>();
+
+    /* Parse start and end dates if supported by the table type. If no end
+     * date is provided, set it to today. If no start date is provided,
+     * set it to 90 days before the end date. Make sure that start date
+     * precedes end date. */
+    if (supportedTableParameters.contains("start") ||
+        supportedTableParameters.contains("end")) {
+      String[] startParameter = null;
+      String[] endParameter = null;
+      if (requestParameters != null) {
+        startParameter = (String[]) requestParameters.get("start");
+        endParameter = (String[]) requestParameters.get("end");
+      }
+      long endTimestamp = System.currentTimeMillis();
+      if (endParameter != null && endParameter.length > 0 &&
+          endParameter[0].length() > 0) {
+        try {
+          endTimestamp = dateFormat.parse(endParameter[0]).getTime();
+        } catch (ParseException e)  {
+          return null;
+        }
+        if (!endParameter[0].startsWith("20")) {
+          return null;
+        }
+      }
+      endParameter = new String[] { dateFormat.format(endTimestamp) };
+      long startTimestamp = endTimestamp - 90L * 24L * 60L * 60L * 1000L;
+      if (startParameter != null && startParameter.length > 0 &&
+          startParameter[0].length() > 0) {
+        try {
+          startTimestamp = dateFormat.parse(startParameter[0]).getTime();
+        } catch (ParseException e)  {
+          return null;
+        }
+        if (!startParameter[0].startsWith("20")) {
+          return null;
+        }
+      }
+      startParameter = new String[] { dateFormat.format(startTimestamp) };
+      if (startTimestamp > endTimestamp) {
+       return null;
+      }
+      recognizedTableParameters.put("start", startParameter);
+      recognizedTableParameters.put("end", endParameter);
+    }
+
+    /* We now have a map with all required table parameters. Return it. */
+    return recognizedTableParameters;
+  }
+}
+
diff --git a/website/src/org/torproject/ernie/web/research/ResearchDataServlet.java b/website/src/org/torproject/ernie/web/research/ResearchDataServlet.java
new file mode 100644
index 0000000..6c24e0d
--- /dev/null
+++ b/website/src/org/torproject/ernie/web/research/ResearchDataServlet.java
@@ -0,0 +1,260 @@
+/* Copyright 2011, 2012 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.ernie.web.research;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Controller servlet for the Data page. Prepares the various lists of
+ * downloadable metrics data files by parsing a file with URLs on other
+ * servers and looking at a local directory with files served by local
+ * Apache HTTP server. The file with URLs on other servers may contain
+ * comment lines starting with #. Recognizes metrics data file types from
+ * the file names.
+ */
+public class ResearchDataServlet extends HttpServlet {
+
+  private static final long serialVersionUID = -5168280373350515577L;
+
+  public void doGet(HttpServletRequest request,
+      HttpServletResponse response) throws IOException, ServletException {
+
+    /* Read local directory with files served by the local Apache HTTP
+     * server and add the URLs to the list. */
+    List<String> dataFileUrls = new ArrayList<String>();
+    String localDataDir = getServletConfig().getInitParameter(
+        "localDataDir");
+    if (localDataDir != null) {
+      try {
+        File localDataDirFile = new File(localDataDir);
+        if (localDataDirFile.exists() && localDataDirFile.isDirectory()) {
+          for (File localDataFile : localDataDirFile.listFiles()) {
+            if (!localDataFile.isDirectory()) {
+              dataFileUrls.add("/data/" + localDataFile.getName());
+            }
+          }
+        }
+      } catch (SecurityException e) {
+        /* We're not permitted to read the directory with metrics data
+         * files. Ignore. */
+      }
+    }
+
+    /* Prepare data structures that we're going to pass to the JSP. All
+     * data structures are (nested) maps with the map keys being used for
+     * displaying the files in tables and map values being 2-element
+     * arrays containing the file url and optional signature file. */
+    SortedMap<Date, Map<String, String[]>> relayDescriptors =
+        new TreeMap<Date, Map<String, String[]>>(
+        java.util.Collections.reverseOrder());
+    String[] certs = new String[2];
+    SortedMap<Date, String[]> bridgeDescriptors =
+        new TreeMap<Date, String[]>(java.util.Collections.reverseOrder());
+    String[] relayStatistics = new String[2];
+    SortedMap<Date, String[]> torperfTarballs =
+        new TreeMap<Date, String[]>(java.util.Collections.reverseOrder());
+    SortedMap<String, Map<String, String[]>> torperfData =
+        new TreeMap<String, Map<String, String[]>>();
+    SortedMap<Date, String[]> exitLists =
+        new TreeMap<Date, String[]>(java.util.Collections.reverseOrder());
+    SortedMap<Date, String[]> torperfExperiments =
+        new TreeMap<Date, String[]>();
+    SortedMap<Date, String[]> bridgePoolAssignments =
+        new TreeMap<Date, String[]>(java.util.Collections.reverseOrder());
+
+    /* Prepare rewriting Torperf sources. */
+    Map<String, String> torperfSources = new HashMap<String, String>();
+    torperfSources.put("torperffast", "torperf, fastest");
+    torperfSources.put("torperffastratio", "torperf, best ratio");
+    torperfSources.put("torperfslow", "torperf, slowest");
+    torperfSources.put("torperfslowratio", "torperf, worst ratio");
+
+    /* Go through the file list, decide for each file what metrics data
+     * type it is, and put it in the appropriate map. */
+    SimpleDateFormat monthFormat = new SimpleDateFormat("yyyy-MM");
+    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
+    List<String> torperfFilesizes = Arrays.asList("50kb,1mb,5mb".
+        split(","));
+    for (String url : dataFileUrls) {
+      if (!url.contains("/")) {
+        continue;
+      }
+      String filename = url.substring(url.lastIndexOf("/") + 1);
+
+      /* URL contains relay descriptors. */
+      if (filename.startsWith("tor-20") ||
+          filename.startsWith("statuses-20") ||
+          filename.startsWith("server-descriptors-20") ||
+          filename.startsWith("extra-infos-20") ||
+          filename.startsWith("votes-20") ||
+          filename.startsWith("consensuses-20")) {
+        String type = filename.substring(0, filename.indexOf("-20"));
+        String yearMonth = filename.substring(filename.indexOf("20"));
+        yearMonth = yearMonth.substring(0, 7);
+        Date month = null;
+        try {
+          month = monthFormat.parse(yearMonth);
+        } catch (ParseException e) {
+          /* Ignore this URL. */
+          continue;
+        }
+        int index = filename.endsWith(".asc") ? 1 : 0;
+        if (!relayDescriptors.containsKey(month)) {
+          relayDescriptors.put(month, new HashMap<String, String[]>());
+        }
+        if (!relayDescriptors.get(month).containsKey(type)) {
+          relayDescriptors.get(month).put(type, new String[2]);
+        }
+        relayDescriptors.get(month).get(type)[index] = url;
+
+      /* URL contains v3 certificates. */
+      } else if (filename.startsWith("certs.tar")) {
+        int index = filename.endsWith(".asc") ? 1 : 0;
+        certs[index] = url;
+
+      /* URL contains bridge descriptors. */
+      } else if (filename.startsWith("bridge-descriptors-20")) {
+        String yearMonth = filename.substring(filename.indexOf("20"));
+        yearMonth = yearMonth.substring(0, 7);
+        Date month = null;
+        try {
+          month = monthFormat.parse(yearMonth);
+        } catch (ParseException e) {
+          /* Ignore this URL. */
+          continue;
+        }
+        int index = filename.endsWith(".asc") ? 1 : 0;
+        if (!bridgeDescriptors.containsKey(month)) {
+          bridgeDescriptors.put(month, new String[2]);
+        }
+        bridgeDescriptors.get(month)[index] = url;
+
+      /* URL contains relay statistics. */
+      } else if (filename.startsWith("relay-statistics.tar.bz2")) {
+        int index = filename.endsWith(".asc") ? 1 : 0;
+        relayStatistics[index] = url;
+
+      /* URL contains Torperf tarball. */
+      } else if (filename.startsWith("torperf-20")) {
+        String yearMonth = filename.substring(filename.indexOf("20"));
+        yearMonth = yearMonth.substring(0, 7);
+        Date month = null;
+        try {
+          month = monthFormat.parse(yearMonth);
+        } catch (ParseException e) {
+          /* Ignore this URL. */
+          continue;
+        }
+        if (!torperfTarballs.containsKey(month)) {
+          torperfTarballs.put(month, new String[2]);
+        }
+        torperfTarballs.get(month)[0] = url;
+
+      /* URL contains Torperf data file. */
+      } else if (filename.endsWith("b.data") ||
+          filename.endsWith("b.extradata")) {
+        boolean isExtraData = filename.endsWith("b.extradata");
+        String[] parts = filename.split("-");
+        if (parts.length != 2) {
+          continue;
+        }
+        String source = parts[0];
+        if (torperfSources.containsKey(source)) {
+          source = torperfSources.get(source);
+        }
+        String filesize = parts[1];
+        filesize = filesize.substring(0, filesize.length()
+            - (isExtraData ? 10 : 5));
+        if (!torperfFilesizes.contains(filesize)) {
+          continue;
+        }
+        if (!torperfData.containsKey(source)) {
+          torperfData.put(source, new HashMap<String, String[]>());
+        }
+        if (!torperfData.get(source).containsKey(filesize)) {
+          torperfData.get(source).put(filesize, new String[2]);
+        }
+        torperfData.get(source).get(filesize)[isExtraData ? 1 : 0] = url;
+
+      /* URL contains Torperf experiment tarball. */
+      } else if (filename.startsWith("torperf-experiment-20")) {
+        String dateString = filename.substring(filename.indexOf("20"));
+        dateString = dateString.substring(0, 10);
+        Date date = null;
+        try {
+          date = dateFormat.parse(dateString);
+        } catch (ParseException e) {
+          /* Ignore this URL. */
+          continue;
+        }
+        if (!torperfExperiments.containsKey(date)) {
+          torperfExperiments.put(date, new String[2]);
+        }
+        torperfExperiments.get(date)[0] = url;
+
+      /* URL contains exit list. */
+      } else if (filename.startsWith("exit-list-20")) {
+        String yearMonth = filename.substring(filename.indexOf("20"));
+        yearMonth = yearMonth.substring(0, 7);
+        Date month = null;
+        try {
+          month = monthFormat.parse(yearMonth);
+        } catch (ParseException e) {
+          /* Ignore this URL. */
+          continue;
+        }
+        if (!exitLists.containsKey(month)) {
+          exitLists.put(month, new String[2]);
+        }
+        exitLists.get(month)[0] = url;
+
+      /* URL contains bridge pool assignments. */
+      } else if (filename.startsWith("bridge-pool-assignments-20")) {
+        String yearMonth = filename.substring(filename.indexOf("20"));
+        yearMonth = yearMonth.substring(0, 7);
+        Date month = null;
+        try {
+          month = monthFormat.parse(yearMonth);
+        } catch (ParseException e) {
+          /* Ignore this URL. */
+          continue;
+        }
+        if (!bridgePoolAssignments.containsKey(month)) {
+          bridgePoolAssignments.put(month, new String[2]);
+        }
+        bridgePoolAssignments.get(month)[0] = url;
+      }
+    }
+
+    /* Add the maps to the request and forward it to the JSP to display
+     * the page. */
+    request.setAttribute("relayDescriptors", relayDescriptors);
+    request.setAttribute("certs", certs);
+    request.setAttribute("bridgeDescriptors", bridgeDescriptors);
+    request.setAttribute("relayStatistics", relayStatistics);
+    request.setAttribute("torperfData", torperfData);
+    request.setAttribute("exitLists", exitLists);
+    request.setAttribute("torperfTarballs", torperfTarballs);
+    request.setAttribute("torperfExperiments", torperfExperiments);
+    request.setAttribute("bridgePoolAssignments", bridgePoolAssignments);
+    request.getRequestDispatcher("WEB-INF/data.jsp").forward(request,
+        response);
+  }
+}
+
diff --git a/website/src/org/torproject/ernie/web/research/ResearchFormatsServlet.java b/website/src/org/torproject/ernie/web/research/ResearchFormatsServlet.java
new file mode 100644
index 0000000..3b70ca3
--- /dev/null
+++ b/website/src/org/torproject/ernie/web/research/ResearchFormatsServlet.java
@@ -0,0 +1,24 @@
+/* Copyright 2011, 2012 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.ernie.web.research;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class ResearchFormatsServlet extends HttpServlet {
+
+  private static final long serialVersionUID = 5666493868675314116L;
+
+  public void doGet(HttpServletRequest request,
+      HttpServletResponse response) throws IOException, ServletException {
+
+    /* Forward the request to the JSP that does all the hard work. */
+    request.getRequestDispatcher("WEB-INF/formats.jsp").forward(request,
+        response);
+  }
+}
+
diff --git a/website/src/org/torproject/ernie/web/research/ResearchPapersServlet.java b/website/src/org/torproject/ernie/web/research/ResearchPapersServlet.java
new file mode 100644
index 0000000..a63eef0
--- /dev/null
+++ b/website/src/org/torproject/ernie/web/research/ResearchPapersServlet.java
@@ -0,0 +1,24 @@
+/* Copyright 2011, 2012 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.ernie.web.research;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class ResearchPapersServlet extends HttpServlet {
+
+  private static final long serialVersionUID = -8135459207158536268L;
+
+  public void doGet(HttpServletRequest request,
+      HttpServletResponse response) throws IOException, ServletException {
+
+    /* Forward the request to the JSP that does all the hard work. */
+    request.getRequestDispatcher("WEB-INF/papers.jsp").forward(request,
+        response);
+  }
+}
+
diff --git a/website/src/org/torproject/ernie/web/research/ResearchStatsServlet.java b/website/src/org/torproject/ernie/web/research/ResearchStatsServlet.java
new file mode 100644
index 0000000..2ca93a3
--- /dev/null
+++ b/website/src/org/torproject/ernie/web/research/ResearchStatsServlet.java
@@ -0,0 +1,132 @@
+/* Copyright 2013 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.ernie.web.research;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class ResearchStatsServlet extends HttpServlet {
+
+  private static final long serialVersionUID = 3346710354297653810L;
+
+  private File statsDir;
+
+  private SortedSet<String> availableStatisticsFiles;
+
+  public void init(ServletConfig config) throws ServletException {
+    super.init(config);
+    this.statsDir = new File(config.getInitParameter("statsDir"));
+    this.availableStatisticsFiles = new TreeSet<String>();
+    this.availableStatisticsFiles.add("servers");
+    this.availableStatisticsFiles.add("bandwidth");
+    this.availableStatisticsFiles.add("fast-exits");
+    this.availableStatisticsFiles.add("clients");
+    this.availableStatisticsFiles.add("torperf");
+    this.availableStatisticsFiles.add("connbidirect");
+  }
+
+  public long getLastModified(HttpServletRequest request) {
+    File statsFile = this.determineStatsFile(request);
+    if (statsFile == null || !statsFile.exists()) {
+      return -1L;
+    } else {
+      return statsFile.lastModified();
+    }
+  }
+
+  public void doGet(HttpServletRequest request,
+      HttpServletResponse response) throws IOException, ServletException {
+    String requestURI = request.getRequestURI();
+    if (requestURI.equals("/ernie/stats/")) {
+      this.writeDirectoryListing(request, response);
+    } else if (requestURI.equals("/ernie/stats.html")) {
+      this.writeStatisticsPage(request, response);
+    } else {
+      File statsFile = this.determineStatsFile(request);
+      if (statsFile == null) {
+        response.sendError(HttpServletResponse.SC_NOT_FOUND);
+        return;
+      } else if (!this.writeStatsFile(statsFile, response)) {
+        response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+      }
+    }
+  }
+
+  private void writeDirectoryListing(HttpServletRequest request,
+      HttpServletResponse response) throws IOException, ServletException {
+    request.setAttribute("directory", "/stats");
+    request.setAttribute("extension", ".csv");
+    request.setAttribute("files", this.availableStatisticsFiles);
+    request.getRequestDispatcher("/WEB-INF/dir.jsp").forward(request,
+        response);
+  }
+
+  private void writeStatisticsPage(HttpServletRequest request,
+      HttpServletResponse response) throws IOException, ServletException {
+    request.getRequestDispatcher("/WEB-INF/stats.jsp").forward(request,
+        response);
+  }
+
+  private File determineStatsFile(HttpServletRequest request) {
+    String requestedStatsFile = request.getRequestURI();
+    if (requestedStatsFile.equals("/ernie/stats/") ||
+        requestedStatsFile.equals("/ernie/stats.html")) {
+      return null;
+    }
+    if (requestedStatsFile.endsWith(".csv")) {
+      requestedStatsFile = requestedStatsFile.substring(0,
+          requestedStatsFile.length() - ".csv".length());
+    }
+    if (requestedStatsFile.contains("/")) {
+      requestedStatsFile = requestedStatsFile.substring(
+          requestedStatsFile.lastIndexOf("/") + 1);
+    }
+    if (!availableStatisticsFiles.contains(requestedStatsFile)) {
+      return null;
+    } else {
+      return new File(this.statsDir, requestedStatsFile + ".csv");
+    }
+  }
+
+  private boolean writeStatsFile(File statsFile,
+      HttpServletResponse response) throws IOException, ServletException {
+    if (!statsFile.exists()) {
+      return false;
+    }
+    byte[] statsFileBytes;
+    try {
+      BufferedInputStream bis = new BufferedInputStream(
+          new FileInputStream(statsFile), 1024);
+      ByteArrayOutputStream baos = new ByteArrayOutputStream();
+      byte[] buffer = new byte[1024];
+      int length;
+      while ((length = bis.read(buffer)) > 0) {
+        baos.write(buffer, 0, length);
+      }
+      bis.close();
+      statsFileBytes = baos.toByteArray();
+    } catch (IOException e) {
+      return false;
+    }
+    String statsFileContent = new String(statsFileBytes);
+    response.setContentType("text/csv");
+    response.setHeader("Content-Length", String.valueOf(
+        statsFileContent.length()));
+    response.setHeader("Content-Disposition",
+        "inline; filename=\"" + statsFile.getName() + "\"");
+    response.getWriter().print(statsFileContent);
+    return true;
+  }
+}
+
diff --git a/website/src/org/torproject/ernie/web/research/ResearchToolsServlet.java b/website/src/org/torproject/ernie/web/research/ResearchToolsServlet.java
new file mode 100644
index 0000000..173a1da
--- /dev/null
+++ b/website/src/org/torproject/ernie/web/research/ResearchToolsServlet.java
@@ -0,0 +1,24 @@
+/* Copyright 2011, 2012 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.ernie.web.research;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class ResearchToolsServlet extends HttpServlet {
+
+  private static final long serialVersionUID = -3344204426180358872L;
+
+  public void doGet(HttpServletRequest request,
+      HttpServletResponse response) throws IOException, ServletException {
+
+    /* Forward the request to the JSP that does all the hard work. */
+    request.getRequestDispatcher("WEB-INF/tools.jsp").forward(request,
+        response);
+  }
+}
+
diff --git a/website/web/WEB-INF/banner.jsp b/website/web/WEB-INF/banner.jsp
new file mode 100644
index 0000000..6bff272
--- /dev/null
+++ b/website/web/WEB-INF/banner.jsp
@@ -0,0 +1,81 @@
+<table class="banner" border="0" cellpadding="0" cellspacing="0" summary="">
+<tr>
+  <td class="banner-left">
+    <a href="/index.html">
+      <img src="/images/top-left.png" alt="Click to go to home page"
+           width="193" height="79"></a></td>
+  <td class="banner-middle">
+    <% String currentPage = request.getRequestURI(); %>
+    <a <% if (currentPage.endsWith("index.jsp")) {
+        %>class="current"<%} else {%>href="/index.html"<%}%>>Home</a>
+    <a <% if (currentPage.endsWith("graphs.jsp")) {
+        %>class="current"<%} else {%>href="/graphs.html"<%}%>>Graphs</a>
+    <a <% if (currentPage.endsWith("research.jsp")) {
+        %>class="current"<%} else {%>href="/research.html"<%}%>>Research</a>
+    <a <% if (currentPage.endsWith("status.jsp")) {
+        %>class="current"<%} else {%>href="/status.html"<%}%>>Status</a>
+    <%if (currentPage.endsWith("graphs.jsp") ||
+         currentPage.endsWith("network.jsp") ||
+         currentPage.endsWith("bubbles.jsp") ||
+         currentPage.endsWith("fast-exits.jsp") ||
+         currentPage.endsWith("users.jsp") ||
+         currentPage.endsWith("performance.jsp")) {
+     %><br>
+      <font size="2">
+        <a <%if (currentPage.endsWith("network.jsp")){
+            %>class="current"<%} else {%>href="/network.html"<%}
+            %>>Network</a>
+        <a <%if (currentPage.endsWith("fast-exits.jsp")){
+            %>class="current"<%} else {%>href="/fast-exits.html"<%}
+            %>>Fast Exits</a>
+        <a <%if (currentPage.endsWith("bubbles.jsp")){
+            %>class="current"<%} else {%>href="/bubbles.html"<%}
+            %>>Bubbles</a>
+        <a <%if (currentPage.endsWith("users.jsp")) {
+            %>class="current"<%} else {%>href="/users.html"<%}
+            %>>Users</a>
+        <a <%if (currentPage.endsWith("performance.jsp")) {
+            %>class="current"<%} else {%>href="/performance.html"<%}
+            %>>Performance</a>
+      </font>
+    <%} else if (currentPage.endsWith("status.jsp") ||
+                 currentPage.endsWith("exonerator.jsp") ||
+                 currentPage.endsWith("relay-search.jsp") ||
+                 currentPage.endsWith("consensus-health.jsp")) {
+     %><br>
+      <font size="2">
+        <a <%if (currentPage.endsWith("exonerator.jsp")){
+            %>class="current"<%} else {%>href="/exonerator.html"<%}
+            %>>ExoneraTor</a>
+        <a <%if (currentPage.endsWith("relay-search.jsp")){
+            %>class="current"<%} else {%>href="/relay-search.html"<%}
+            %>>Relay Search</a>
+        <a <%if (currentPage.endsWith("consensus-health.jsp")){
+            %>class="current"<%} else {%>href="/consensus-health.html"<%}
+            %>>Consensus Health</a>
+      </font>
+    <%} else if (currentPage.endsWith("research.jsp") ||
+                 currentPage.endsWith("data.jsp") ||
+                 currentPage.endsWith("formats.jsp") ||
+                 currentPage.endsWith("tools.jsp") ||
+                 currentPage.endsWith("stats.jsp")) {
+     %><br>
+      <font size="2">
+        <a <%if (currentPage.endsWith("data.jsp")) {
+            %>class="current"<%} else {%> href="/data.html"<%}
+            %>>Data</a>
+        <a <%if (currentPage.endsWith("formats.jsp")) {
+            %>class="current"<%} else {%> href="/formats.html"<%}
+            %>>Formats</a>
+        <a <%if (currentPage.endsWith("tools.jsp")) {
+            %>class="current"<%} else {%> href="/tools.html"<%}
+            %>>Tools</a>
+        <a <%if (currentPage.endsWith("stats.jsp")) {
+            %>class="current"<%} else {%> href="/stats.html"<%}
+            %>>Statistics</a>
+      </font>
+    <%}%>
+  </td>
+  <td class="banner-right"></td>
+</tr>
+</table>
diff --git a/website/web/WEB-INF/bubbles.jsp b/website/web/WEB-INF/bubbles.jsp
new file mode 100644
index 0000000..59b3025
--- /dev/null
+++ b/website/web/WEB-INF/bubbles.jsp
@@ -0,0 +1,40 @@
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<html>
+<head>
+  <title>Tor Metrics Portal: Network bubble graphs</title>
+  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
+  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
+  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
+  <script src="/js/d3.min.js"></script>
+  <script src="/js/bubbles.js"></script>
+</head>
+<body>
+  <div class="center">
+    <%@ include file="banner.jsp"%>
+    <div class="main-column">
+      <p>
+        All relays:
+        <a href="#no-group" onclick="make_bubble_graph('no-group');">No group</a> |
+        <a href="#as" onclick="make_bubble_graph('as');">Autonomous Systems</a> |
+        <a href="#contact" onclick="make_bubble_graph('contact');">Contact</a>  |
+        <a href="#country" onclick="make_bubble_graph('country');">Country</a> |
+        <a href="#network-family" onclick="make_bubble_graph('network-family');">Network family (/16)</a>
+      </p>
+      <p>
+        Exits only:
+        <a href="#no-group-exits-only" onclick="make_bubble_graph('no-group-exits-only');">No group</a> |
+        <a href="#as-exits-only" onclick="make_bubble_graph('as-exits-only');">Autonomous Systems</a> |
+        <a href="#contact-exits-only" onclick="make_bubble_graph('contact-exits-only');">Contact</a>  |
+        <a href="#country-exits-only" onclick="make_bubble_graph('country-exits-only');">Country</a> |
+        <a href="#network-family-exits-only" onclick="make_bubble_graph('network-family-exits-only');">Network family (/16)</a>
+      </p>
+      <script>make_bubble_graph();</script>
+      <noscript>Sorry, you need to turn on JavaScript.</script>
+    </div>
+  </div>
+  <div class="bottom" id="bottom">
+    <%@ include file="footer.jsp"%>
+  </div>
+</body>
+</html>
diff --git a/website/web/WEB-INF/data.jsp b/website/web/WEB-INF/data.jsp
new file mode 100644
index 0000000..06176db
--- /dev/null
+++ b/website/web/WEB-INF/data.jsp
@@ -0,0 +1,286 @@
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
+<fmt:setLocale value="en_US"/>
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<html>
+<head>
+  <title>Tor Metrics Portal: Data</title>
+  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
+  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
+  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
+</head>
+<body>
+  <div class="center">
+    <%@ include file="banner.jsp"%>
+    <div class="main-column">
+        <h2>Tor Metrics Portal: Data</h2>
+        <br>
+        <p>One of the main goals of the Tor Metrics Project is to make all
+        gathered data available to the public. This approach enables
+        privacy researchers to perform their own analyses using real data
+        on the Tor network, and it acts as a safeguard to not gather data
+        that are too sensitive to publish. The following data are
+        available (see the <a href="tools.html">Tools</a> section for
+        details on processing the files):</p>
+        <ul>
+          <li><a href="#relaydesc">Relay descriptor archives</a></li>
+          <li><a href="#bridgedesc">Bridge descriptor archives</a></li>
+          <li><a href="#bridgeassignments">Bridge pool assignments</a></li>
+          <li><a href="#performance">Performance data</a></li>
+          <li><a href="#exitlist">Exit lists</a></li>
+        </ul>
+        <p>The tarballs listed on this page and the raw files that were
+        published on the last three days are also available via
+        "rsync metrics.torproject.org::".</p>
+        <br>
+        <a name="relaydesc"></a>
+        <h3><a href="#relaydesc" class="anchor">Relay descriptor
+        archives</a></h3>
+        <br>
+        <p>The relay descriptor archives contain all documents that the
+        directory authorities make available about the network of relays.
+        These documents include network statuses, server (relay)
+        descriptors, and extra-info descriptors.
+        The data formats are described <a href="formats.html">here</a>.</p>
+        <table width="100%" border="0" cellpadding="5" cellspacing="0" summary="">
+          <c:forEach var="item" items="${relayDescriptors}" >
+            <fmt:formatDate var="longDate" pattern="MMMM yyyy"
+                            value="${item.key}"/>
+            <tr>
+              <td>${longDate}</td>
+              <td>
+                <c:if test="${item.value['tor'] ne null}" >
+                  <a href="${item.value['tor'][0]}">v1 directories</a>
+                  <c:if test="${item.value['tor'][1] ne null}">
+                    (<a href="${item.value['tor'][1]}">sig</a>)
+                  </c:if>
+                </c:if>
+              </td>
+              <td>
+                <c:if test="${item.value['statuses'] ne null}" >
+                  <a href="${item.value['statuses'][0]}">v2 statuses</a>
+                  <c:if test="${item.value['statuses'][1] ne null}">
+                    (<a href="${item.value['statuses'][1]}">sig</a>)
+                  </c:if>
+                </c:if>
+              </td>
+              <td>
+                <c:if test="${item.value['server-descriptors'] ne null}" >
+                  <a href="${item.value['server-descriptors'][0]}">server descriptors</a>
+                  <c:if test="${item.value['server-descriptors'][1] ne null}">
+                    (<a href="${item.value['server-descriptors'][1]}">sig</a>)
+                  </c:if>
+                </c:if>
+              </td>
+              <td>
+                <c:if test="${item.value['extra-infos'] ne null}" >
+                  <a href="${item.value['extra-infos'][0]}">extra-infos</a>
+                  <c:if test="${item.value['extra-infos'][1] ne null}">
+                    (<a href="${item.value['extra-infos'][1]}">sig</a>)
+                  </c:if>
+                </c:if>
+              </td>
+              <td>
+                <c:if test="${item.value['votes'] ne null}" >
+                  <a href="${item.value['votes'][0]}">v3 votes</a>
+                  <c:if test="${item.value['votes'][1] ne null}">
+                    (<a href="${item.value['votes'][1]}">sig</a>)
+                  </c:if>
+                </c:if>
+              </td>
+              <td>
+                <c:if test="${item.value['consensuses'] ne null}" >
+                  <a href="${item.value['consensuses'][0]}">v3 consensuses</a>
+                  <c:if test="${item.value['consensuses'][1] ne null}">
+                    (<a href="${item.value['consensuses'][1]}">sig</a>)
+                  </c:if>
+                </c:if>
+              </td>
+            </tr>
+          </c:forEach>
+        </table>
+        <c:if test="${certs[0] ne null}">
+          <br>
+          <p>In order to verify the v3 votes and v3 consensuses, download
+          the tarball of <a href="${certs[0]}">v3 certificates</a>
+          <c:if test="${certs[1] ne null}">
+            (<a href="${certs[1]}">sig</a>)
+          </c:if>
+          which is updated whenever new v3 certificates become available.</p>
+        </c:if>
+        <c:if test="${relayStatistics[0] ne null}">
+          <br>
+          <p>Some of the relays are configured to gather statistics on the
+          number of requests or connecting clients, the number of
+          processed cells per queue, or the number of exiting bytes per
+          port. Relays running version 0.2.2.4-alpha or higher can include
+          these statistics in extra-info descriptors, so that they are
+          included in the relay descriptor archives. This
+          <a href="${relayStatistics[0]}">archive</a>
+          <c:if test="${relayStatistics[1] ne null}">
+            (<a href="${relayStatistics[1]}">sig</a>)
+          </c:if>
+          contains the statistics produced by relays running earlier
+          versions.</p>
+        </c:if>
+        <br>
+        <a name="bridgedesc"></a>
+        <h3><a href="#bridgedesc" class="anchor">Bridge descriptor
+        archives</a></h3>
+        <br>
+        <p>The bridge descriptor archives contain similar documents as the
+        relay descriptor archives, but for the non-public bridges. The
+        descriptors have been sanitized before publication to remove all
+        information that could otherwise be used to locate bridges. The
+        files below contain all documents of a given month, including
+        bridge network statuses, bridge server descriptors, and bridge
+        extra-info descriptors. The sanitizing process is described
+        <a href="formats.html#bridgedesc">here</a>.</p>
+        <table width="100%" border="0" cellpadding="5" cellspacing="0" summary="">
+          <c:forEach var="item" items="${bridgeDescriptors}" >
+            <fmt:formatDate var="longDate" pattern="MMMM yyyy"
+                            value="${item.key}"/>
+            <tr>
+              <td>
+                <a href="${item.value[0]}">${longDate}</a>
+                <c:if test="${item.value[1] ne null}">
+                    (<a href="${item.value[1]}">sig</a>)
+                </c:if>
+              </td>
+            </tr>
+          </c:forEach>
+        </table>
+        <p></p>
+        <br>
+        <a name="bridgeassignments"></a>
+        <h3><a href="#bridgeassignments" class="anchor">Bridge pool
+        assignments</a></h3>
+        <br>
+        <p>BridgeDB periodically dumps the list of running bridges with
+        information about the rings, subrings, and file buckets to which
+        they are assigned to a local file.  We are archiving sanitized
+        versions of these files here to analyze how the pool assignment
+        affects a bridge's usage.</p>
+        The data format and sanitizing process is described
+        <a href="formats.html#bridgepool">here</a>.</p>
+        <table width="100%" border="0" cellpadding="5" cellspacing="0" summary="">
+          <c:forEach var="item" items="${bridgePoolAssignments}" >
+            <fmt:formatDate var="longDate" pattern="MMMM yyyy"
+                            value="${item.key}"/>
+            <tr>
+              <td>
+                <a href="${item.value[0]}">${longDate}</a>
+              </td>
+            </tr>
+          </c:forEach>
+        </table>
+        <br>
+        <a name="performance"></a>
+        <h3><a href="#performance" class="anchor">Performance
+        data</a></h3>
+        <br>
+        <p>We are continuously measuring the performance of the Tor
+        network by periodically requesting files of different sizes and
+        recording the time needed to do so. These measurements take place
+        on moria, siv, and torperf and use an unmodified Tor client.
+        The files below contain the output of the torperf application.
+        The data format is described
+        <a href="formats.html#torperf">here</a>.</p>
+        <table width="100%" border="0" cellpadding="5" cellspacing="0" summary="">
+          <c:forEach var="item" items="${torperfTarballs}" >
+            <fmt:formatDate var="longDate" pattern="MMMM yyyy"
+                            value="${item.key}"/>
+            <tr>
+              <td>
+                <a href="${item.value[0]}">${longDate}</a>
+              </td>
+            </tr>
+          </c:forEach>
+        </table>
+        <br>
+        <p>The output above is the result of combining torperf request
+        data with information about used paths.
+        The raw files are also available below.</p>
+        <table width="100%" border="0" cellpadding="5" cellspacing="0" summary="">
+          <c:forEach var="item" items="${torperfData}" >
+            <tr>
+              <td>${item.key}</td>
+              <td>
+                <c:if test="${item.value['50kb'] ne null}" >
+                  <c:if test="${item.value['50kb'][0] ne null}" >
+                    <a href="${item.value['50kb'][0]}">50 KiB requests</a>
+                  </c:if>
+                  <c:if test="${item.value['50kb'][1] ne null}" >
+                    <a href="${item.value['50kb'][1]}">50 KiB path info</a>
+                  </c:if>
+                </c:if>
+              </td>
+              <td>
+                <c:if test="${item.value['1mb'] ne null}" >
+                  <c:if test="${item.value['1mb'][0] ne null}" >
+                    <a href="${item.value['1mb'][0]}">1 MiB requests</a>
+                  </c:if>
+                  <c:if test="${item.value['1mb'][1] ne null}" >
+                    <a href="${item.value['1mb'][1]}">1 MiB path info</a>
+                  </c:if>
+                </c:if>
+              </td>
+              <td>
+                <c:if test="${item.value['5mb'] ne null}" >
+                  <c:if test="${item.value['5mb'][0] ne null}" >
+                    <a href="${item.value['5mb'][0]}">5 MiB requests</a>
+                  </c:if>
+                  <c:if test="${item.value['5mb'][1] ne null}" >
+                    <a href="${item.value['5mb'][1]}">5 MiB path info</a>
+                  </c:if>
+                </c:if>
+              </td>
+            </tr>
+          </c:forEach>
+        </table>
+        <br>
+        <p>We further conducted additional experiments with Torperf in the
+        past by modifying the guard node selection strategies or circuit
+        build timeouts.  The modified guard node selection strategies are
+        to pick guard nodes from sets of the a) absolute fastest, b)
+        absolute slowest, c) best rated vs. advertised ratio or d) worst
+        rated vs. advertised ratio nodes. The ratio mechanisms provide a
+        way to select the nodes that the bandwidth authorities think stand
+        out in their measurement.  Experiments are listed by the date when
+        they ended.  Details about the experiment setup are contained in a
+        README file in the tarballs.</p>
+        <table width="100%" border="0" cellpadding="5" cellspacing="0" summary="">
+          <c:forEach var="item" items="${torperfExperiments}" >
+            <fmt:formatDate var="endDate" pattern="MMMM dd, yyyy"
+                value="${item.key}"/>
+            <tr><td><a href="${item.value[0]}">${endDate}</a></td></tr>
+          </c:forEach>
+        </table>
+        <br>
+        <a name="exitlist"></a>
+        <h3><a href="#exitlist" class="anchor">Exit lists</a></h3>
+        <br>
+        <p>We are archiving the bulk exit lists used by
+        <a href="https://check.torproject.org/">Tor Check</a>
+        containing the IP addresses that exit relays exit from.
+        The data format is described in
+        <a href="https://www.torproject.org/tordnsel/exitlist-spec.txt">exitlist-spec.txt</a>
+        and <a href="formats.html#exitlist">here</a>.</p>
+        <table width="100%" border="0" cellpadding="5" cellspacing="0" summary="">
+          <c:forEach var="item" items="${exitLists}" >
+            <fmt:formatDate var="longDate" pattern="MMMM yyyy"
+                            value="${item.key}"/>
+            <tr>
+              <td>
+                <a href="${item.value[0]}">${longDate}</a>
+              </td>
+            </tr>
+          </c:forEach>
+        </table>
+    </div>
+  </div>
+  <div class="bottom" id="bottom">
+    <%@ include file="footer.jsp"%>
+  </div>
+</body>
+</html>
diff --git a/website/web/WEB-INF/dir.jsp b/website/web/WEB-INF/dir.jsp
new file mode 100644
index 0000000..1ac1819
--- /dev/null
+++ b/website/web/WEB-INF/dir.jsp
@@ -0,0 +1,25 @@
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<html>
+<head>
+  <title>Tor Metrics Portal: Index of ${directory}</title>
+  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
+  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
+  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
+</head>
+<body>
+  <div class="center">
+    <%@ include file="banner.jsp"%>
+    <div class="main-column">
+        <h2>Tor Metrics Portal: Index of ${directory}</h2>
+        <br>
+        <c:forEach var="file" items="${files}" >
+          <a href="${directory}/${file}${extension}">${file}${extension}</a><br>
+        </c:forEach>
+    </div>
+  </div>
+  <div class="bottom" id="bottom">
+    <%@ include file="footer.jsp"%>
+  </div>
+</body>
+</html>
diff --git a/website/web/WEB-INF/error.jsp b/website/web/WEB-INF/error.jsp
new file mode 100644
index 0000000..9c5150e
--- /dev/null
+++ b/website/web/WEB-INF/error.jsp
@@ -0,0 +1,77 @@
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<%@ page isErrorPage="true" %>
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<html>
+<head>
+  <title>Tor Metrics Portal: Error</title>
+  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
+  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
+  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
+</head>
+<body>
+  <div class="center">
+    <%@ include file="banner.jsp"%>
+    <div class="main-column">
+<h2>Tor Metrics Portal: Error</h2>
+<br>
+<p>
+Oops! Something went wrong here! We encountered a
+<b>
+<c:choose>
+<c:when test="${pageContext.errorData.statusCode eq 400}">
+400 Bad Request
+</c:when>
+<c:when test="${pageContext.errorData.statusCode eq 404}">
+404 Not Found
+</c:when>
+<c:when test="${pageContext.errorData.statusCode eq 500}">
+500 Internal Server Error
+</c:when>
+<c:when test="${not empty pageContext.errorData.throwable}">
+${pageContext.exception}
+</c:when>
+<c:otherwise>
+Unknown Error
+</c:otherwise>
+</c:choose>
+</b>
+when processing your request!</p>
+
+<p>
+Maybe you find what you're looking for on our sitemap:
+<ul>
+<li><a href="index.html">Home</a></li>
+<li><a href="graphs.html">Graphs</a>
+<ul>
+<li><a href="network.html">Network</a></li>
+<li><a href="fast-exits.html">Fast Exits</a></li>
+<li><a href="bubbles.html">Bubbles</a></li>
+<li><a href="users.html">Users</a></li>
+<li><a href="performance.html">Performance</a></li>
+</ul></li>
+<li><a href="research.html">Research</a>
+<ul>
+<li><a href="data.html">Data</a></li>
+<li><a href="formats.html">Formats</a></li>
+<li><a href="tools.html">Tools</a></li>
+<li><a href="stats.html">Statistics</a></li>
+</ul></li>
+<li><a href="status.html">Status</a>
+<ul>
+<li><a href="exonerator.html">ExoneraTor</a></li>
+<li><a href="relay-search.html">Relay Search</a></li>
+<li><a href="consensus-health.html">Consensus Health</a></li>
+</ul></li>
+</ul>
+</p>
+
+<p>If this problem persists, please
+<a href="mailto:tor-assistants at torproject.org">let us know</a>!</p>
+
+    </div>
+  </div>
+  <div class="bottom" id="bottom">
+    <%@ include file="footer.jsp"%>
+  </div>
+</body>
+</html>
diff --git a/website/web/WEB-INF/exonerator.jsp b/website/web/WEB-INF/exonerator.jsp
new file mode 100644
index 0000000..0eefe99
--- /dev/null
+++ b/website/web/WEB-INF/exonerator.jsp
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<html>
+<head>
+  <title>Tor Metrics Portal: ExoneraTor</title>
+  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
+  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
+  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
+</head>
+<body>
+  <div class="center">
+    <%@ include file="banner.jsp"%>
+  <div class="main-column" style="margin:5; Padding:0;">
+    <h2>ExoneraTor</h2>
+    <h3>or: a website that tells you whether a given IP address was a Tor
+    relay</h3>
+    <br>
+    <p>Just because you see an Internet connection from a particular IP
+    address does not mean you know <i>who</i> originated the traffic. Tor
+    anonymizes Internet traffic by "<a href="https://www.torproject.org/about/overview#thesolution">onion
+    routing</a>," sending packets through a series of encrypted hops
+    before they reach their destination. Therefore, if you see traffic
+    from a Tor node, you may be seeing traffic that originated from
+    someone using Tor, rather than from the node operator itself. The Tor
+    Project and Tor node operators have no records of the traffic that
+    passes over the network, but we do maintain current and historical
+    records of which IP addresses are part of the Tor network.</p>
+    <br>
+    <p>ExoneraTor tells you whether there was a Tor relay running on a
+    given IP address at a given time. ExoneraTor can further indicate
+    whether this relay permitted exiting to a given server and/or TCP
+    port. ExoneraTor learns these facts by parsing the public relay lists
+    and relay descriptors that are collected from the Tor directory
+    authorities and the exit lists collected by TorDNSEL. By inputting an
+    IP address and time, you can determine whether that IP was then a part
+    of the Tor network.</p>
+    <br>
+    <p><font color="red"><b>Notice:</b> This service has moved to:
+    <a href="https://exonerator.torproject.org/">https://exonerator.torproject.org/</a></font></p>
+  </div>
+  </div>
+  <div class="bottom" id="bottom">
+    <%@ include file="footer.jsp"%>
+  </div>
+</body>
+</html>
diff --git a/website/web/WEB-INF/fast-exits.jsp b/website/web/WEB-INF/fast-exits.jsp
new file mode 100644
index 0000000..903c88c
--- /dev/null
+++ b/website/web/WEB-INF/fast-exits.jsp
@@ -0,0 +1,81 @@
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
+<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<html>
+<head>
+  <title>Tor Metrics Portal: Fast Exits</title>
+  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
+  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
+  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
+</head>
+<body>
+  <div class="center">
+    <%@ include file="banner.jsp"%>
+    <div class="main-column">
+<h2>Tor Metrics Portal: Fast Exits</h2>
+<br>
+<p>This page summarizes progress in operating 125 fast exits, which is a
+deliverable for
+<a href="https://trac.torproject.org/projects/tor/wiki/org/sponsors/SponsorJ">SponsorJ</a>.</p>
+<br>
+
+<a name="fastexits"></a>
+<h3><a href="#fastexits" class="anchor">Relays meeting the fast-exit
+requirements</a></h3>
+<br>
+<img src="fast-exits.png${fast_exits_url}"
+     width="576" height="432" alt="Fast exits graph">
+<form action="fast-exits.html#fastexits">
+  <div class="formrow">
+    <input type="hidden" name="graph" value="fast-exits">
+    <p>
+    <label>Start date (yyyy-mm-dd):</label>
+      <input type="text" name="start" size="10"
+             value="<c:choose><c:when test="${fn:length(fast_exits_start) == 0}">${default_start_date}</c:when><c:otherwise>${fast_exits_start[0]}</c:otherwise></c:choose>">
+    <label>End date (yyyy-mm-dd):</label>
+      <input type="text" name="end" size="10"
+             value="<c:choose><c:when test="${fn:length(fast_exits_end) == 0}">${default_end_date}</c:when><c:otherwise>${fast_exits_end[0]}</c:otherwise></c:choose>">
+    </p><p>
+    <input class="submit" type="submit" value="Update graph">
+    </p>
+  </div>
+</form>
+<p>Download graph as
+<a href="fast-exits.pdf${fast_exits_url}">PDF</a> or
+<a href="fast-exits.svg${fast_exits_url}">SVG</a>.</p>
+<br>
+
+<a name="almostfastexits"></a>
+<h3><a href="#almostfastexits" class="anchor">Relays almost meeting the
+fast-exit requirements</a></h3>
+<br>
+<img src="almost-fast-exits.png${almost_fast_exits_url}"
+     width="576" height="432" alt="Almost fast exits graph">
+<form action="fast-exits.html#almostfastexits">
+  <div class="formrow">
+    <input type="hidden" name="graph" value="almost-fast-exits">
+    <p>
+    <label>Start date (yyyy-mm-dd):</label>
+      <input type="text" name="start" size="10"
+             value="<c:choose><c:when test="${fn:length(almost_fast_exits_start) == 0}">${default_start_date}</c:when><c:otherwise>${almost_fast_exits_start[0]}</c:otherwise></c:choose>">
+    <label>End date (yyyy-mm-dd):</label>
+      <input type="text" name="end" size="10"
+             value="<c:choose><c:when test="${fn:length(almost_fast_exits_end) == 0}">${default_end_date}</c:when><c:otherwise>${almost_fast_exits_end[0]}</c:otherwise></c:choose>">
+    </p><p>
+    <input class="submit" type="submit" value="Update graph">
+    </p>
+  </div>
+</form>
+<p>Download graph as
+<a href="almost-fast-exits.pdf${almost_fast_exits_url}">PDF</a> or
+<a href="almost-fast-exits.svg${almost_fast_exits_url}">SVG</a>.</p>
+<br>
+
+    </div>
+  </div>
+  <div class="bottom" id="bottom">
+    <%@ include file="footer.jsp"%>
+  </div>
+</body>
+</html>
diff --git a/website/web/WEB-INF/footer.jsp b/website/web/WEB-INF/footer.jsp
new file mode 100644
index 0000000..cab9e7f
--- /dev/null
+++ b/website/web/WEB-INF/footer.jsp
@@ -0,0 +1,13 @@
+      <p>This material is supported in part by the National Science
+      Foundation under Grant No. CNS-0959138. Any opinions,
+      finding, and conclusions or recommendations expressed in this
+      material are those of the author(s) and do not necessarily reflect
+      the views of the National Science Foundation.</p>
+      <p>"Tor" and the "Onion Logo" are <a href="https://www.torproject.org/docs/trademark-faq.html.en">registered trademarks</a> of The Tor Project, Inc.</p>
+      <p>Data on this site is freely available under a
+      <a href="http://creativecommons.org/publicdomain/zero/1.0/">CC0 no
+      copyright declaration</a>: To the extent possible under law, the Tor
+      Project has waived all copyright and related or neighboring rights
+      in the data. Graphs are licensed under a
+      <a href="http://creativecommons.org/licenses/by/3.0/us/">Creative
+      Commons Attribution 3.0 United States License</a>.</p>
diff --git a/website/web/WEB-INF/formats.jsp b/website/web/WEB-INF/formats.jsp
new file mode 100644
index 0000000..4d297b2
--- /dev/null
+++ b/website/web/WEB-INF/formats.jsp
@@ -0,0 +1,986 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<html>
+<head>
+  <title>Tor Metrics Portal: Data Formats</title>
+  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
+  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
+  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
+</head>
+<body>
+<div class="center">
+<%@ include file="banner.jsp"%>
+<div class="main-column">
+<h2>Tor Metrics Portal: Data Formats</h2>
+<br>
+
+<p>
+Statistical analysis in the Tor network can be performed using various
+kinds of data.
+This page gives an overview of three major data sources for
+statistics in the Tor network:</p>
+
+<ol>
+<li>First, we recap measuring the Tor network from public directory
+information (<a href="https://research.torproject.org/techreports/metrics-2009-08-07.pdf">PDF</a>)
+by describing the data format of
+<a href="#serverdesc">server descriptors and network statuses</a>,
+and we explain the sanitzation process of (non-public)
+<a href="#bridgedesc">bridge directory information</a>.</li>
+<li>Second, we describe the numerous aggregate statistics that relays
+publish about their usage (<a href="http://freehaven.net/anonbib/cache/wecsr10measuring-tor.pdf">PDF</a>),
+including
+<a href="#bytehist">byte histories</a>,
+<a href="#dirreqstats">directory request statistics</a>,
+<a href="#entrystats">connecting client statistics</a>,
+<a href="#bridgestats">bridge user statistics</a>,
+<a href="#cellstats">cell-queue statistics</a>,
+<a href="#exitstats">exit-port statistics</a>, and
+<a href="#bidistats">bidirectional connection use</a>.</li>
+<li>Third, we delineate the output of various Tor services like
+<a href="#bridgepool">BridgeDB</a>, or
+<a href="#exitlist">Tor Check</a> as well as specific measurement tools like
+<a href="#torperf">Torperf</a>.</li>
+</ol>
+
+<p>
+All data described on this page are available for download on the
+<a href="data.html">data</a> page.
+This page is based on a technical report
+(<a href="https://research.torproject.org/techreports/data-2011-03-14.pdf">PDF</a>)
+and is very likely more recent than the report.
+</p>
+<hr>
+<br>
+
+<a name="descriptortypes"></a>
+<h3><a href="#descriptortypes" class="anchor">Descriptor types</a></h3>
+<br>
+<p>
+Any file containing descriptors described on this page may contain meta
+data in its first text line using the format
+<tt>@type $descriptortype $major.$minor</tt>.
+Any tool that processes these descriptors may parse files without meta
+data or with an unknown descriptor type at its own risk, can safely parse
+files with known descriptor type and same major version number, and should
+not parse files with known descriptor type and higher major version
+number.
+</p>
+
+<p>
+The following descriptor types and versions are known.
+Gray entries are deprecated, black entries are recent:
+</p>
+
+<ul>
+<li><tt>@type server-descriptor 1.0</tt></li>
+<li><tt>@type microdescriptor 1.0</tt></li>
+<li><tt>@type extra-info 1.0</tt></li>
+<li><tt>@type directory 1.0</tt></li>
+<li><tt>@type network-status-2 1.0</tt></li>
+<li><tt>@type dir-key-certificate-3 1.0</tt></li>
+<li><tt>@type network-status-consensus-3 1.0</tt></li>
+<li><tt>@type network-status-vote-3 1.0</tt></li>
+<li><tt>@type network-status-microdesc-consensus-3 1.0</tt></li>
+<li><tt>@type bridge-network-status 1.0</tt></li>
+<li><tt>@type bridge-server-descriptor 1.0</tt></li>
+<li><font color="gray"><tt>@type bridge-extra-info 1.0</tt></font></li>
+<li><font color="gray"><tt>@type bridge-extra-info 1.1</tt> contain
+  sanitized <tt>transport</tt> lines</font></li>
+<li><tt>@type bridge-extra-info 1.2</tt> contain <tt>ntor-onion-key</tt>
+  lines</li>
+<li><tt>@type torperf 1.0</tt></li>
+<li><tt>@type bridge-pool-assignment 1.0</tt></li>
+<li><tt>@type tordnsel 1.0</tt></li>
+</ul>
+
+<hr>
+<br>
+
+<a name="serverdesc"></a>
+<h3><a href="#serverdesc" class="anchor">Server descriptors and network
+statuses</a></h3>
+<br>
+<p>
+Relays in the Tor network report their capabilities by publishing server
+descriptors to the directory authorities.
+The directory authorities confirm reachability of relays and assign flags
+to help clients make good path selections.
+Every hour, the directory authorities publish a network status consensus
+with all known running relays at the time.
+Both server descriptors and network statuses constitute a solid data basis
+for statistical analysis in the Tor network.
+We described the approach to measure the Tor network from public directory
+information in the HotPETS 2009 paper
+(<a href="https://research.torproject.org/techreports/metrics-2009-08-07.pdf">PDF</a>)
+and provide interactive
+<a href="graphs.html">graphs</a> on the metrics
+website.
+We briefly describe the most interesting pieces of the
+two descriptor formats that can be used for statistics.
+</p>
+
+<p>
+The server descriptors published by relays at least once every 18 hours
+contain the necessary information for clients to build circuits using a
+given relay.
+These server descriptors can also be useful for statistical analysis of
+the Tor network infrastructure.
+</p>
+
+<p>
+We assume that the majority of server descriptors are correct.
+But when performing statistical analysis on server descriptors, one has to
+keep in mind that only a small subset of the information written to server
+descriptors is confirmed by the trusted directory authorities.
+In theory, relays can provide false information in their server
+descriptors, even though the incentive to do so is probably low.
+</p>
+
+<blockquote>
+<p>
+<i>Server descriptor published by relay <tt>blutmagie</tt> (without
+cryptographic keys and hashes):</i>
+</p>
+<p>
+<tt>router blutmagie 192.251.226.206 443 0 80</tt><br>
+<tt>platform Tor 0.2.2.20-alpha on Linux x86_64</tt><br>
+<tt>opt protocols Link 1 2 Circuit 1</tt><br>
+<tt>published 2010-12-27 14:35:27</tt><br>
+<tt>opt fingerprint 6297 B13A 687B 521A 59C6 BD79 188A 2501 EC03 A065</tt><br>
+<tt>uptime 445412</tt><br>
+<tt>bandwidth 14336000 18432000 15905178</tt><br>
+<tt>opt extra-info-digest 5C1D5D6F8B243304079BC15CD96C7FCCB88322D4</tt><br>
+<tt>opt caches-extra-info</tt><br>
+<tt>onion-key</tt><br>
+<tt>[...]</tt><br>
+<tt>signing-key</tt><br>
+<tt>[...]</tt><br>
+<tt>family $66CA87E164F1CFCE8C3BB5C095217A28578B8BAF
+  $67EC84376D9C4C467DCE8621AACA109160B5264E
+  $7B698D327F1695590408FED95CDEE1565774D136</tt><br>
+<tt>opt hidden-service-dir</tt><br>
+<tt>contact abuse at blutmagie.de</tt><br>
+<tt>reject 0.0.0.0/8:*</tt><br>
+<tt>reject 169.254.0.0/16:*</tt><br>
+<tt>reject 127.0.0.0/8:*</tt><br>
+<tt>reject 192.168.0.0/16:*</tt><br>
+<tt>reject 10.0.0.0/8:*</tt><br>
+<tt>reject 172.16.0.0/12:*</tt><br>
+<tt>reject 192.251.226.206:*</tt><br>
+<tt>reject *:25</tt><br>
+<tt>reject *:119</tt><br>
+<tt>reject *:135-139</tt><br>
+<tt>reject *:445</tt><br>
+<tt>reject *:465</tt><br>
+<tt>reject *:563</tt><br>
+<tt>reject *:587</tt><br>
+<tt>reject *:1214</tt><br>
+<tt>reject *:4661-4666</tt><br>
+<tt>reject *:6346-6429</tt><br>
+<tt>reject *:6660-6999</tt><br>
+<tt>accept *:*</tt><br>
+<tt>router-signature</tt><br>
+<tt>[...]</tt><br>
+</p>
+</blockquote>
+
+<p>
+The document above shows an example server descriptor.
+The following data fields in server descriptors may be relevant to
+statistical analysis:
+</p>
+
+<ul>
+<li><b>IP address and ports:</b> Relays provide their IP address
+and ports where they accept requests to build circuits and directory
+requests.
+These data fields are contained in the first line of a server descriptor
+starting with <tt>router</tt>.
+Note that in rare cases, the IP address provided here can be different
+from the IP address used for exiting to the Internet.
+The latter can be found in the exit lists produced by Tor Check as
+described in the <a href="#exitlist">Tor Check exit lists</a> section below.
+</li>
+<li><b>Operating system and Tor software version:</b> Relays include
+their operating system and Tor software version in their server
+descriptors in the <tt>platform</tt> line.
+While this information is very likely correct in most cases, a few relay
+operators may try to impede hacking attempts by providing false platform
+strings.</li>
+<li><b>Uptime:</b> Relays include the number of seconds since the
+last restart in their server descriptor in the <tt>uptime</tt> line.</li>
+<li><b>Own measured bandwidth:</b> Relays report the bandwidth that
+they are willing to provide on average and for short periods of time.
+Relays also perform periodic bandwidth self-tests and report their actual
+available bandwidth.
+The latter was used by clients to weight relays in the path selection
+algorithm and was sometimes subject to manipulation by malicious relays.
+All three bandwidth values can be found in a server descriptor's
+<tt>bandwidth</tt> line.
+With the introduction of
+<a href="http://gitweb.torproject.org/torflow.git/">bandwidth scanners</a>, the self-reported relay
+bandwidth in server descriptors has become less
+relevant.</li>
+<li><b>Relay family:</b> Some relay operators who run more than one
+relay organize their relays in relay families, so that clients don't pick
+more than one of these relays for a single circuit.
+Each relay belonging to a relay family lists the members of that family
+either by nickname or fingerprint in its server descriptor in the
+<tt>family</tt> line.</li>
+<li><b>Exit policy:</b> Relays define their exit policy by including
+firewall-like rules which outgoing connections they reject or accept in
+the <tt>reject</tt> and <tt>accept</tt> lines.</li>
+</ul>
+
+<p>
+These are just a subset of the fields in a server descriptor that seem
+relevant for statistical analysis.
+For a complete list of fields in server descriptors, see the <a href="https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt">directory
+protocol specification</a>.
+</p>
+
+<p>
+Every hour, the directory authorities publish a new network status that
+contains a list of all running relays.
+The directory authorities confirm reachability of the contained relays and
+assign flags based on the relays' characteristics.
+The entries in a network status reference the last published server
+descriptor of a relay.
+</p>
+
+<p>
+The network statuses are relevant for statistical analysis, because they
+constitute trusted snapshots of the Tor network.
+Anyone can publish as many server descriptors as they want, but only the
+directory authorities can confirm that a relay was running at a given
+time.
+Most statistics on the Tor network infrastructure rely on network statuses
+and possibly combine them with the referenced server descriptors.
+The document below shows the network status entry referencing
+the server descriptor above.
+In addition to the reachability information, network statuses contain the
+following fields that may be relevant for statistical analysis:
+</p>
+
+<blockquote>
+<p>
+<i>Network status entry of relay <tt>blutmagie</tt>:</i>
+</p>
+<p>
+<tt>r blutmagie YpexOmh7UhpZxr15GIolAewDoGU
+  lFY7WmD/yvVFp9drmZzNeTxZ6dw 2010-12-27 14:35:27 192.251.226.206
+  443 80</tt><br>
+<tt>s Exit Fast Guard HSDir Named Running Stable V2Dir Valid</tt><br>
+<tt>v Tor 0.2.2.20-alpha</tt><br>
+<tt>w Bandwidth=30800</tt><br>
+<tt>p reject 25,119,135-139,445,465,563,587,1214,4661-4666,6346-6429,6660-6999</tt><br>
+</p>
+</blockquote>
+
+<ul>
+<li><b>Relay flags:</b> The directory authorities assign flags to
+relays based on their characteristics to the line starting with <tt>s</tt>.
+Examples are the <tt>Exit</tt> flag if a relay permits exiting to the
+Internet and the <tt>Guard</tt> flag if a relay is stable enough to be
+picked as guard node</li>
+<li><b>Relay version:</b> The directory authorities include the
+version part of the platform string written to server descriptors in the
+network status in the line starting with <tt>v</tt>.</li>
+<li><b>Bandwidth weights:</b> The network status contains a bandwidth
+weight for every relay in the lines with <tt>w</tt> that clients shall use
+for weighting relays in their path selection algorithm.
+This bandwidth weight is either the self-reported bandwidth of the relay
+or the bandwidth measured by the bandwidth scanners.</li>
+<li><b>Exit policy summary:</b> Every entry in a network status
+contains a summary version of a relay's exit policy in the line starting
+with <tt>p</tt>.
+This summary is a list of accepted or rejected ports for exit to most IP
+addresses.</li>
+</ul>
+<hr>
+<br>
+
+<a name="bridgedesc"></a>
+<h3><a href="#bridgedesc" class="anchor">Sanitized bridge
+descriptors</a></h3>
+<br>
+Bridges in the Tor network publish server descriptors to the bridge
+authority which in turn generates a bridge network status.
+We cannot, however, make the bridge server descriptors and bridge network
+statuses available for statistical analysis as we do with the relay server
+descriptors and relay network statuses.
+The problem is that bridge server descriptors and network statuses contain
+bridge IP addresses and other sensitive information that shall not be made
+publicly available.
+We therefore sanitize bridge descriptors by removing all potentially
+identifying information and publish sanitized versions of the descriptors.
+The processing steps for sanitizing bridge descriptors are as follows:
+
+<ol>
+<li><b>Replace the bridge identity with its SHA1 value:</b> Clients
+can request a bridge's current descriptor by sending its identity string
+to the bridge authority.
+This is a feature to make bridges on dynamic IP addresses useful.
+Therefore, the original identities (and anything that could be used to
+derive them) need to be removed from the descriptors.
+The bridge identity is replaced with its SHA1 hash value.
+The idea is to have a consistent replacement that remains stable over
+months or even years (without keeping a secret for a keyed hash function).</li>
+<li><b>Remove all cryptographic keys and signatures:</b> It would be
+straightforward to learn about the bridge identity from the bridge's
+public key.
+Replacing keys by newly generated ones seemed to be unnecessary (and would
+involve keeping a state over months/years), so that all cryptographic
+objects have simply been removed.</li>
+<li><b>Replace IP address with IP address hash:</b> Of course, IP
+addresses need to be sanitized, too.
+<ul><li>IPv4 addresses are replaced with <tt>10.x.x.x</tt> with
+<tt>x.x.x</tt> being the 3 byte output of
+<tt>H(IP address | bridge identity | secret)[:3]</tt>.
+The input <tt>IP address</tt> is the 4-byte long binary representation of
+the bridge's current IP address.
+The <tt>bridge identity</tt> is the 20-byte long binary representation of
+the bridge's long-term identity fingerprint.
+The <tt>secret</tt> is a 31-byte long secure random string that changes once
+per month for all descriptors and statuses published in that month.
+<tt>H()</tt> is SHA-256.
+The <tt>[:3]</tt> operator means that we pick the 3 most significant bytes
+of the result.</li>
+<li>IPv6 addresses are replaced with <tt>[fd9f:2e19:3bcf::xx:xxxx]</tt>
+with <tt>xx:xxxx</tt> being the hex-formatted 3 byte output of a similar
+hash function as described for IPv4 addresses.
+The only differences are that the input <tt>IP address</tt> is 16 bytes
+long and the <tt>secret</tt> is only 19 bytes long.</li></ul>
+<li><b>Replace contact information:</b> If there is contact
+information in a descriptor, the contact line is changed to
+<tt>somebody</tt>.</li>
+<li><b>Remove pluggable transport addresses and arguments:</b> Bridges may
+provide transports in addition to the onion-routing protocol and include
+information about these transports in their extra-info descriptors for
+BridgeDB.  In that case, any IP addresses, TCP ports, or additional
+arguments are removed, only leaving in the supported transport names.</li>
+</ol>
+
+<p>
+Apart from these processing steps, sanitized bridge server descriptors
+follow the same format as relay server descriptors.
+The same applies to sanitized bridge extra-info descriptors.
+Sanitized bridge network statuses are similar to version 2 relay network
+statuses, but with only a <tt>published</tt> line in the header and
+without any lines in the footer.
+</p>
+
+<p>
+The two documents below show an example bridge server
+descriptor that is referenced from a bridge network status.
+For more details about this process, see the
+<a href="https://gitweb.torproject.org/metrics-db.git">metrics data processor</a> software.
+</p>
+
+<blockquote>
+<p>
+<i>Sanitized bridge server descriptor:</i>
+</p>
+<p>
+<tt>@type bridge-server-descriptor 1.0</tt><br>
+<tt>router Hawthorne 10.175.105.22 443 0 0</tt><br>
+<tt>platform Tor 0.2.2.19-alpha (git-1988927edecce4c7) on Linux i686</tt><br>
+<tt>opt protocols Link 1 2 Circuit 1</tt><br>
+<tt>published 2010-12-27 18:55:01</tt><br>
+<tt>opt fingerprint A5FA 7F38 B02A 415E 72FE 614C 64A1 E5A9 2BA9 9BBD</tt><br>
+<tt>uptime 2347112</tt><br>
+<tt>bandwidth 5242880 10485760 1016594</tt><br>
+<tt>opt extra-info-digest E729BCB5E06A5657A73151B55354EB003D2BAE0F</tt><br>
+<tt>opt hidden-service-dir</tt><br>
+<tt>contact somebody</tt><br>
+<tt>reject *:*</tt><br>
+<tt>router-digest 46DFDBE7B67B7C90A1962B0B5AA4526FAF406979</tt><br>
+</p>
+</blockquote>
+
+<blockquote>
+<p>
+<i>Sanitized bridge network status:</i>
+</p>
+<p>
+<tt>@type bridge-network-status 1.0</tt><br>
+<tt>published 2010-12-27 22:07:03</tt><br>
+<tt>[...status entries...]</tt><br>
+<tt>r Hawthorne pfp/OLAqQV5y/mFMZKHlqSupm70 Rt/b57Z7fJChlisLWqRSb69AaXk
+  2010-12-27 18:55:01 10.175.105.22 443 0</tt><br>
+<tt>s Fast Guard HSDir Running Stable Valid</tt><br>
+<tt>[...status entries...]</tt><br>
+</p>
+</blockquote>
+<hr>
+<br>
+
+<a name="bytehist"></a>
+<h3><a href="#bytehist" class="anchor">Byte histories</a></h3>
+<br>
+<p>
+Relays include aggregate statistics in their descriptors that they upload
+to the directory authorities.
+These aggregate statistics are contained in extra-info descriptors that
+are published in companion with server descriptors.
+Extra-info descriptors are not required for clients to build circuits.
+An extra-info descriptor belonging to a server descriptor is referenced by
+its SHA1 hash value.
+</p>
+
+<p>
+Byte histories were the first statistical data that relays published about
+their usage.
+Relays report the number of written and read bytes in 15-minute intervals
+throughout the last 24 hours.
+The extra-info descriptor in the document below contains the byte
+histories in the two lines starting with <tt>write-history</tt> and
+<tt>read-history</tt>.
+More details about these statistics can be found in the <a href="https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt">directory protocol
+specification</a>.
+</p>
+
+<blockquote>
+<p>
+<i>Extra-info descriptor published by relay <tt>blutmagie</tt>
+(without cryptographic signature and with long lines being truncated):</i>
+</p>
+<p>
+<tt>extra-info blutmagie 6297B13A687B521A59C6BD79188A2501EC03A065</tt><br>
+<tt>published 2010-12-27 14:35:27</tt><br>
+<tt>write-history 2010-12-27 14:34:05 (900 s) 12902389760,12902402048,12859373568,12894131200,[...]</tt><br>
+<tt>read-history 2010-12-27 14:34:05 (900 s) 12770249728,12833485824,12661140480,12872439808,[...]</tt><br>
+<tt>dirreq-write-history 2010-12-27 14:26:13 (900 s) 51731456,60808192,56740864,54948864,[...]</tt><br>
+<tt>dirreq-read-history 2010-12-27 14:26:13 (900 s) 4747264,4767744,4511744,4752384,[...]</tt><br>
+<tt>dirreq-stats-end 2010-12-27 10:51:09 (86400 s)</tt><br>
+<tt>dirreq-v3-ips us=2000,de=1344,fr=744,kr=712,[...]</tt><br>
+<tt>dirreq-v2-ips ??=8,au=8,cn=8,cz=8,[...]</tt><br>
+<tt>dirreq-v3-reqs us=2368,de=1680,kr=1048,fr=800,[...]</tt><br>
+<tt>dirreq-v2-reqs id=48,??=8,au=8,cn=8,[...]</tt><br>
+<tt>dirreq-v3-resp ok=12504,not-enough-sigs=0,unavailable=0,not-found=0,not-modified=0,busy=128</tt><br>
+<tt>dirreq-v2-resp ok=64,unavailable=0,not-found=8,not-modified=0,busy=8</tt><br>
+<tt>dirreq-v2-share 1.03%</tt><br>
+<tt>dirreq-v3-share 1.03%</tt><br>
+<tt>dirreq-v3-direct-dl complete=316,timeout=4,running=0,min=4649,d1=36436,d2=68056,q1=76600,d3=87891,d4=131294,md=173579,d6=229695,d7=294528,q3=332053,d8=376301,d9=530252,max=2129698</tt><br>
+<tt>dirreq-v2-direct-dl complete=16,timeout=52,running=0,min=9769,d1=9769,d2=9844,q1=9981,d3=9981,d4=27297,md=33640,d6=60814,d7=205884,q3=205884,d8=361137,d9=628256,max=956009</tt><br>
+<tt>dirreq-v3-tunneled-dl complete=12088,timeout=92,running=4,min=534,d1=31351,d2=49166,q1=58490,d3=70774,d4=88192,md=109778,d6=152389,d7=203435,q3=246377,d8=323837,d9=559237,max=26601000</tt><br>
+<tt>dirreq-v2-tunneled-dl complete=0,timeout=0,running=0</tt><br>
+<tt>entry-stats-end 2010-12-27 10:51:09 (86400 s)</tt><br>
+<tt>entry-ips de=11024,us=10672,ir=5936,fr=5040,[...]</tt><br>
+<tt>exit-stats-end 2010-12-27 10:51:09 (86400 s)</tt><br>
+<tt>exit-kibibytes-written 80=6758009,443=498987,4000=227483,5004=1182656,11000=22767,19371=1428809,31551=8212,41500=965584,51413=3772428,56424=1912605,other=175227777</tt><br>
+<tt>exit-kibibytes-read 80=197075167,443=5954607,4000=1660990,5004=1808563,11000=1893893,19371=130360,31551=7588414,41500=756287,51413=2994144,56424=1646509,other=288412366</tt><br>
+<tt>exit-streams-opened 80=5095484,443=359256,4000=4508,5004=22288,11000=124,19371=24,31551=40,41500=96,51413=16840,56424=28,other=1970964</tt><br>
+</p>
+</blockquote>
+<hr>
+<br>
+
+<a name="dirreqstats"></a>
+<h3><a href="#dirreqstats" class="anchor">Directory requests</a></h3>
+<br>
+<p>
+The directory authorities and directory mirrors report statistical data
+about processed directory requests.
+Starting with Tor version 0.2.2.15-alpha, all directories report the
+number of written and read bytes for answering directory requests.
+The format is similar to the format of byte histories as described in the
+previous section.
+The relevant lines are <tt>dirreq-write-history</tt> and
+<tt>dirreq-read-history</tt> in the document listed in the
+<a href="#bytehist">Byte histories</a> section above.
+These two lines contain the subset of total read and written bytes that
+the directory mirror spent on responding to any kind of directory request,
+including network statuses, server descriptors, extra-info descriptors,
+authority certificates, etc.
+</p>
+
+<p>
+The directories further report statistics on answering directory requests
+for network statuses only.
+For Tor versions before 0.2.3.x, relay operators had to manually enable
+these statistics, which is why only a few directories report them.
+The lines starting with <tt>dirreq-v3-</tt> all belong to the directory
+request statistics (the lines starting with <tt>dirreq-v2-</tt> report
+similar statistics for version 2 of the directory protocol which is
+deprecated at the time of writing this report).
+The following fields may be relevant for statistical analysis:
+</p>
+
+<ul>
+<li><b>Unique IP addresses:</b> The numbers in <tt>dirreq-v3-ips</tt>
+denote the unique IP addresses of clients requesting network statuses by
+country.</li>
+<li><b>Network status requests:</b> The numbers in
+<tt>dirreq-v3-reqs</tt> constitute the total network status requests by
+country.</li>
+<li><b>Request share:</b> The percentage in <tt>dirreq-v3-share</tt> is
+an estimate of the share of directory requests that the reporting relay
+expects to see in the Tor network.
+In a tech report (<a href="https://research.torproject.org/techreports/countingusers-2010-11-30.pdf">PDF</a>)
+we found that this estimate isn't very useful
+for statistical analysis because of the different approaches that clients
+take to select directory mirrors.
+The fraction of written directory bytes (<tt>dirreq-write-history</tt>) can
+be used to derive a better metric for the share of directory requests.</li>
+<li><b>Network status responses:</b> The directories also report
+whether they could provide the requested network status to clients in
+<tt>dirreq-v3-resp</tt>.
+This information was mostly used to diagnose error rates in version 2 of
+the directory protocol where a lot of directories replied to network
+status requests with <tt>503 Busy</tt>.
+In version 3 of the directory protocol, most responses contain the status
+code <tt>200 OK</tt>.</li>
+<li><b>Network status download times:</b> The line
+<tt>dirreq-v3-direct-dl</tt> contains statistics on the download of network
+statuses via the relay's directory port.
+The line <tt>dirreq-v3-tunneled-dl</tt> contains similar statistics on
+downloads via a 1-hop circuit between client and directory (which is the
+common approach in version 3 of the directory protocol).
+Relays report how many requests have been completed, have timed out, and
+are still running at the end of a 24-hour time interval as well as the
+minimum, maximum, median, quartiles, and deciles of download times.</li>
+</ul>
+
+<p>
+More details about these statistics can be found in the <a href="https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt">directory protocol
+specification</a>.
+</p>
+<hr>
+<br>
+
+<a name="entrystats"></a>
+<h3><a href="#entrystats" class="anchor">Connecting clients</a></h3>
+<br>
+<p>
+Relays can be configured to report per-country statistics on directly
+connecting clients.
+This metric includes clients connecting to a relay in order to build
+circuits and clients creating a 1-hop circuit to request directory
+information.
+In practice, the latter number outweighs the former number.
+The <tt>entry-ips</tt> line in the document listed in the
+<a href="#bytehist">Byte histories</a> section above
+shows the number
+of unique IP addresses connecting to the relay by country.
+More details about these statistics can be found in the <a href="https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt">directory protocol
+specification</a>.
+</p>
+<hr>
+<br>
+
+<a name="bridgestats"></a>
+<h3><a href="#bridgestats" class="anchor">Bridge users</a></h3>
+<br>
+<p>
+Bridges report statistics on connecting bridge clients in their extra-info
+descriptors.
+The document below shows a bridge extra-info descriptor
+with the bridge user statistics in the <tt>bridge-ips</tt> line.
+
+<blockquote>
+<p>
+<i>Sanitized bridge extra-info descriptor:</i>
+</p>
+<p>
+<tt>extra-info Unnamed A5FA7F38B02A415E72FE614C64A1E5A92BA99BBD</tt><br>
+<tt>published 2010-12-27 18:55:01</tt><br>
+<tt>write-history 2010-12-27 18:43:50 (900 s) 151712768,176698368,180030464,163150848,[...]</tt><br>
+<tt>read-history 2010-12-27 18:43:50 (900 s) 148109312,172274688,172168192,161094656,[...]</tt><br>
+<tt>bridge-stats-end 2010-12-27 14:56:29 (86400 s)</tt><br>
+<tt>bridge-ips sa=48,us=40,de=32,ir=32,[...]</tt><br>
+</p>
+</blockquote>
+
+<p>
+Bridges running Tor version 0.2.2.3-alpha or earlier report bridge users
+in a similar line starting with <tt>geoip-client-origins</tt>.
+The reason for switching to <tt>bridge-ips</tt> was that the measurement
+interval in <tt>geoip-client-origins</tt> had a variable length, whereas the
+measurement interval in 0.2.2.4-alpha and later is set to exactly
+24 hours.
+In order to clearly distinguish the new measurement intervals from the old
+ones, the new keywords have been introduced.
+More details about these statistics can be found in the <a href="https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt">directory protocol
+specification</a>.
+</p>
+<hr>
+<br>
+
+<a name="cellstats"></a>
+<h3><a href="#cellstats" class="anchor">Cell-queue statistics</a></h3>
+<br>
+<p>
+Relays can be configured to report aggregate statistics on their cell
+queues.
+These statistics include average processed cells, average number of queued
+cells, and average time that cells spend in circuits.
+Circuits are split into deciles based on the number of processed cells.
+The statistics are provided for circuit deciles from loudest to quietest
+circuits.
+The document below shows the cell statistics contained in an
+extra-info descriptor by relay <tt>gabelmoo</tt>.
+An early analysis of cell-queue statistics can be found in a tech report
+(<a href="https://research.torproject.org/techreports/bufferstats-2009-08-25.pdf">PDF</a>).
+More details about these statistics can be found in the <a href="https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt">directory protocol
+specification</a>.
+</p>
+
+<blockquote>
+<p>
+<i>Cell statistics in extra-info descriptor by relay <tt>gabelmoo</tt>:</i>
+</p>
+<p>
+<tt>cell-stats-end 2010-12-27 09:59:50 (86400 s)</tt><br>
+<tt>cell-processed-cells 4563,153,42,15,7,7,6,5,4,2</tt><br>
+<tt>cell-queued-cells 9.39,0.98,0.09,0.01,0.00,0.00,0.00,0.01,0.00,
+  0.01</tt><br>
+<tt>cell-time-in-queue 2248,807,277,92,49,22,52,55,81,148</tt><br>
+<tt>cell-circuits-per-decile 7233</tt><br>
+</p>
+</blockquote>
+<hr>
+<br>
+
+<a name="exitstats"></a>
+<h3><a href="#exitstats" class="anchor">Exit-port statistics</a></h3>
+<br>
+<p>
+Exit relays running Tor version 0.2.1.1-alpha or higher can be configured
+to report aggregate statistics on exiting connections.
+These relays report the number of opened streams, written and read bytes
+by exiting port.
+Until version 0.2.2.19-alpha, relays reported all ports exceeding a
+threshold of 0.01 % of all written and read exit bytes.
+Starting with version 0.2.2.20-alpha, relays only report the top 10 ports
+in exit-port statistics in order not to exceed the maximum extra-info
+descriptor length of 50 KB.
+The document listed in the
+<a href="#bytehist">Byte histories</a> section above contains
+exit-port statistics in the lines starting with <tt>exit-</tt>.
+More details about these statistics can be found in the <a href="https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt">directory protocol
+specification</a>.
+</p>
+<hr>
+<br>
+
+<a name="bidistats"></a>
+<h3><a href="#bidistats" class="anchor">Bidirectional connection
+use</a></h3>
+<br>
+<p>
+Relays running Tor version 0.2.3.1-alpha or higher can be configured to
+report what fraction of connections is used uni- or bi-directionally.
+Every 10 seconds, relays determine for every connection whether they read
+and wrote less than a threshold of 20 KiB.
+Connections below this threshold are labeled as "Below Threshold".
+For the remaining connections, relays report whether they read/wrote at
+least 10 times as many bytes as they wrote/read.
+If so, they classify a connection as "Mostly reading" or "Mostly
+writing," respectively.
+All other connections are classified as "Both reading and writing."
+After classifying connections, read and write counters are reset for the
+next 10-second interval.
+Statistics are aggregated over 24 hours.
+The document below shows the bidirectional connection use
+statistics in an extra-info descriptor by relay <tt>zweifaltigkeit</tt>.
+The four numbers denote the number of connections "Below threshold,"
+"Mostly reading," "Mostly writing," and "Both reading and writing."
+More details about these statistics can be found in the <a href="https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt">directory protocol
+specification</a>.
+</p>
+
+<blockquote>
+<p>
+<i>Bidirectional connection use statistic in extra-info descriptor
+by relay <tt>zweifaltigkeit</tt>:</i>
+</p>
+<p>
+<tt>conn-bi-direct 2010-12-28 15:55:11 (86400 s) 387465,45285,55361,81786</tt>
+</p>
+</blockquote>
+<hr>
+<br>
+
+<a name="torperf"></a>
+<h3><a href="#torperf" class="anchor">Torperf output files</a></h3>
+<br>
+<p>
+Torperf is a little tool that measures Tor's performance as users
+experience it.
+Torperf uses a trivial SOCKS client to download files of various sizes
+over the Tor network and notes how long substeps take.
+Torperf can be
+<a href="https://metrics.torproject.org/tools.html">downloaded</a>
+from the metrics
+website.
+A Torperf results file contains a single line per Torperf run with
+<tt>key=value</tt> pairs.
+Such a result line is sufficient to learn about 1) the Tor and Torperf
+configuration, 2) measurement results, and 3) additional information that
+might help explain the results.
+Known keys are explained below.
+</p>
+<ul>
+<li>Configuration
+<ul>
+<li><tt>SOURCE:</tt> Configured name of the data source; required.</li>
+<li><tt>FILESIZE:</tt> Configured file size in bytes; required.</li>
+<li>Other meta data describing the Tor or Torperf configuration, e.g.,
+GUARD for a custom guard choice; optional.</li>
+</ul>
+<li>Measurement results
+<ul>
+<li><tt>START:</tt> Time when the connection process starts;
+required.</li>
+<li><tt>SOCKET:</tt> Time when the socket was created; required.</li>
+<li><tt>CONNECT:</tt> Time when the socket was connected; required.</li>
+<li><tt>NEGOTIATE:</tt> Time when SOCKS 5 authentication methods have been
+negotiated; required.</li>
+<li><tt>REQUEST:</tt> Time when the SOCKS request was sent; required.</li>
+<li><tt>RESPONSE:</tt> Time when the SOCKS response was received;
+required.</li>
+<li><tt>DATAREQUEST:</tt> Time when the HTTP request was written;
+required.</li>
+<li><tt>DATARESPONSE:</tt> Time when the first response was received;
+required.</li>
+<li><tt>DATACOMPLETE:</tt> Time when the payload was complete;
+required.</li>
+<li><tt>WRITEBYTES:</tt> Total number of bytes written; required.</li>
+<li><tt>READBYTES:</tt> Total number of bytes read; required.</li>
+<li><tt>DIDTIMEOUT:</tt> 1 if the request timed out, 0 otherwise;
+optional.</li>
+<li><tt>DATAPERCx:</tt> Time when x% of expected bytes were read for
+x = { 10, 20, 30, 40, 50, 60, 70, 80, 90 }; optional.</li>
+<li>Other measurement results, e.g., START_RENDCIRC, GOT_INTROCIRC, etc.
+for hidden-service measurements; optional.</li>
+</ul>
+<li>Additional information
+<ul>
+<li><tt>LAUNCH:</tt> Time when the circuit was launched; optional.</li>
+<li><tt>USED_AT:</tt> Time when this circuit was used; optional.</li>
+<li><tt>PATH:</tt> List of relays in the circuit, separated by commas;
+optional.</li>
+<li><tt>BUILDTIMES:</tt> List of times when circuit hops were built,
+separated by commas; optional.</li>
+<li><tt>TIMEOUT:</tt> Circuit build timeout that the Tor client used when
+building this circuit; optional.</li>
+<li><tt>QUANTILE:</tt> Circuit build time quantile that the Tor client
+uses to determine its circuit-build timeout; optional.</li>
+<li><tt>CIRC_ID:</tt> Circuit identifier of the circuit used for this
+measurement; optional.</li>
+<li><tt>USED_BY:</tt> Stream identifier of the stream used for this
+measurement; optional.</li>
+<li>Other fields containing additional information; optional.</li>
+</ul>
+</ul>
+
+<blockquote>
+<p>
+<i>Torperf <tt>.tpf</tt> output lines for a single request to download a
+50 KiB file (reformatted):</i>
+</p>
+
+<p>
+<tt>BUILDTIMES=1.16901898384,1.86555600166,2.13295292854</tt><br>
+<tt>CIRC_ID=9878</tt><br>
+<tt>CONNECT=1338357901.42</tt><br>
+<tt>DATACOMPLETE=1338357902.91</tt><br>
+<tt>DATAPERC10=1338357902.48</tt><br>
+<tt>DATAPERC20=1338357902.48</tt><br>
+<tt>DATAPERC30=1338357902.61</tt><br>
+<tt>DATAPERC40=1338357902.64</tt><br>
+<tt>DATAPERC50=1338357902.65</tt><br>
+<tt>DATAPERC60=1338357902.74</tt><br>
+<tt>DATAPERC70=1338357902.74</tt><br>
+<tt>DATAPERC80=1338357902.75</tt><br>
+<tt>DATAPERC90=1338357902.79</tt><br>
+<tt>DATAREQUEST=1338357901.83</tt><br>
+<tt>DATARESPONSE=1338357902.25</tt><br>
+<tt>DIDTIMEOUT=0</tt><br>
+<tt>FILESIZE=51200</tt><br>
+<tt>LAUNCH=1338357661.74</tt><br>
+<tt>NEGOTIATE=1338357901.42</tt><br>
+<tt>PATH=$980D326017CEF4CBBF4089FBABE767DC83D059AF,$03545609092A24C71CCAD2F4523F5CCC6714F159,$CAC3CF7154AE9C656C4096DC38B4EFA145905654</tt><br>
+<tt>QUANTILE=0.800000</tt><br>
+<tt>READBYTES=51442</tt><br>
+<tt>REQUEST=1338357901.42</tt><br>
+<tt>RESPONSE=1338357901.83</tt><br>
+<tt>SOCKET=1338357901.42</tt><br>
+<tt>SOURCE=torperf</tt><br>
+<tt>START=1338357901.42</tt><br>
+<tt>TIMEOUT=5049</tt><br>
+<tt>USED_AT=1338357902.91</tt><br>
+<tt>USED_BY=18869</tt><br>
+<tt>WRITEBYTES=75</tt><br>
+</p>
+</blockquote>
+<br>
+
+<p>
+Torperf can produce two output files: <tt>.data</tt> and
+<tt>.extradata</tt>.
+The <tt>.data</tt> file contains timestamps for request substeps and the
+byte summaries for downloading a test file via Tor.
+The document below shows an example output of a Torperf run.
+The timestamps are seconds and microseconds since 1970-01-01
+00:00:00.000000.
+Torperf can be configured to write <tt>.extradata</tt> files by attaching
+a Tor controller and writing certain controller events to disk.
+The format of a <tt>.extradata</tt> line is similar to the combined format
+as specified above, except that it can only contain "Additional
+information" keywords.
+</p>
+
+<blockquote>
+<p>
+<i>Torperf <tt>.data</tt> and <tt>.extradata</tt> output lines for a
+single request to download a 50 KiB file (reformatted and annotated with
+comments):</i>
+</p>
+
+<p>
+<tt># Timestamps and byte summaries contained in .data files:</tt><br>
+<tt>1338357901 422336   # Connection process started</tt><br>
+<tt>1338357901 422346   # After socket is created</tt><br>
+<tt>1338357901 422521   # After socket is connected</tt><br>
+<tt>1338357901 422604   # After authentication methods are negotiated (SOCKS 5 only)</tt><br>
+<tt>1338357901 423550   # After SOCKS request is sent</tt><br>
+<tt>1338357901 839639   # After SOCKS response is received</tt><br>
+<tt>1338357901 839849   # After HTTP request is written</tt><br>
+<tt>1338357902 258157   # After first response is received</tt><br>
+<tt>1338357902 914263   # After payload is complete</tt><br>
+<tt>75                  # Written bytes</tt><br>
+<tt>51442               # Read bytes</tt><br>
+<tt>0                   # Timeout (optional field)</tt><br>
+<tt>1338357902 481591   # After 10% of expected bytes are read (optional field)</tt><br>
+<tt>1338357902 482719   # After 20% of expected bytes are read (optional field)</tt><br>
+<tt>1338357902 613169   # After 30% of expected bytes are read (optional field)</tt><br>
+<tt>1338357902 647108   # After 40% of expected bytes are read (optional field)</tt><br>
+<tt>1338357902 651764   # After 50% of expected bytes are read (optional field)</tt><br>
+<tt>1338357902 743705   # After 60% of expected bytes are read (optional field)</tt><br>
+<tt>1338357902 743876   # After 70% of expected bytes are read (optional field)</tt><br>
+<tt>1338357902 757475   # After 80% of expected bytes are read (optional field)</tt><br>
+<tt>1338357902 795100   # After 90% of expected bytes are read (optional field)</tt><br>
+</p>
+
+<p>
+<tt># Path information contained in .extradata files:</tt><br>
+<tt>CIRC_ID=9878</tt><br>
+<tt>LAUNCH=1338357661.74</tt><br>
+<tt>PATH=$980D326017CEF4CBBF4089FBABE767DC83D059AF,$03545609092A24C71CCAD2F4523F5CCC6714F159,$CAC3CF7154AE9C656C4096DC38B4EFA145905654</tt><br>
+<tt>BUILDTIMES=1.16901898384,1.86555600166,2.13295292854</tt><br>
+<tt>USED_AT=1338357902.91</tt><br>
+<tt>USED_BY=18869</tt><br>
+<tt>TIMEOUT=5049</tt><br>
+<tt>QUANTILE=0.800000</tt><br>
+</p>
+</blockquote>
+<hr>
+<br>
+
+<a name="bridgepool"></a>
+<h3><a href="#bridgepool" class="anchor">BridgeDB pool assignment
+files</a></h3>
+<br>
+<p>
+BridgeDB is the software that receives bridge network statuses containing
+the information which bridges are running from the bridge authority,
+assigns these bridges to persistent distribution rings, and hands them out
+to bridge users.
+BridgeDB periodically dumps the list of running bridges with information
+about the rings, subrings, and file buckets to which they are assigned to
+a local file.
+The sanitized versions of these lists containing SHA-1 hashes of bridge
+fingerprints instead of the original fingerprints are available for
+statistical analysis.
+</p>
+
+<blockquote>
+<p>
+<i>BridgeDB pool assignment file from March 13, 2011:</i>
+</p>
+<p>
+<tt>bridge-pool-assignment 2011-03-13 14:38:03</tt><br>
+<tt>00b834117566035736fc6bd4ece950eace8e057a unallocated</tt><br>
+<tt>00e923e7a8d87d28954fee7503e480f3a03ce4ee email port=443 flag=stable</tt><br>
+<tt>0103bb5b00ad3102b2dbafe9ce709a0a7c1060e4 https ring=2 port=443 flag=stable</tt><br>
+<tt>[...]</tt><br>
+</p>
+</blockquote>
+
+<p>
+The document above shows a BridgeDB pool assignment file
+from March 13, 2011.
+Every such file begins with a line containing the timestamp when BridgeDB
+wrote this file.
+Subsequent lines always start with the SHA-1 hash of a bridge fingerprint,
+followed by ring, subring, and/or file bucket information.
+There are currently three distributor ring types in BridgeDB:
+</p>
+
+<ol>
+<li><b>unallocated:</b> These bridges are not distributed by BridgeDB,
+but are either reserved for manual distribution or are written to file
+buckets for distribution via an external tool.
+If a bridge in the <tt>unallocated</tt> ring is assigned to a file bucket,
+this is noted by <tt>bucket=$bucketname</tt>.</li>
+<li><b>email:</b> These bridges are distributed via an e-mail
+autoresponder.  Bridges can be assigned to subrings by their OR port or
+relay flag which is defined by <tt>port=$port</tt> and/or <tt>flag=$flag</tt>.
+</li>
+<li><b>https:</b> These bridges are distributed via https server.
+There are multiple https rings to further distribute bridges by IP address
+ranges, which is denoted by <tt>ring=$ring</tt>.
+Bridges in the <tt>https</tt> ring can also be assigned to subrings by
+OR port or relay flag which is defined by <tt>port=$port</tt> and/or
+<tt>flag=$flag</tt>.</li>
+</ol>
+<hr>
+<br>
+
+<a name="exitlist">
+<h3><a href="#exitlist" class="anchor">Tor Check exit lists</a></h3>
+<br>
+<p>
+<a href="https://www.torproject.org/tordnsel/dist/">TorDNSEL</a> is an
+implementation of the active testing, DNS-based exit list
+for Tor exit
+nodes.
+Tor Check makes the list of known exits and corresponding exit IP
+addresses available in a specific format.
+The document below shows an entry of the exit list written on
+December 28, 2010 at 15:21:44 UTC.
+This entry means that the relay with fingerprint <tt>63BA..</tt> which
+published a descriptor at 07:35:55 and was contained in a version 2
+network status from 08:10:11 uses two different IP addresses for exiting.
+The first address <tt>91.102.152.236</tt> was found in a test performed at
+07:10:30.
+When looking at the corresponding server descriptor, one finds that this
+is also the IP address on which the relay accepts connections from inside
+the Tor network.
+A second test performed at 10:35:30 reveals that the relay also uses IP
+address <tt>91.102.152.227</tt> for exiting.
+</p>
+
+<blockquote>
+<p>
+<i>Exit list entry written on December 28, 2010 at 15:21:44 UTC:</i>
+</p>
+<p>
+<tt>ExitNode 63BA28370F543D175173E414D5450590D73E22DC</tt><br>
+<tt>Published 2010-12-28 07:35:55</tt><br>
+<tt>LastStatus 2010-12-28 08:10:11</tt><br>
+<tt>ExitAddress 91.102.152.236 2010-12-28 07:10:30</tt><br>
+<tt>ExitAddress 91.102.152.227 2010-12-28 10:35:30</tt><br>
+</p>
+</blockquote>
+
+</div>
+</div>
+<div class="bottom" id="bottom">
+<%@ include file="footer.jsp"%>
+</div>
+</body>
+</html>
+
diff --git a/website/web/WEB-INF/graphs.jsp b/website/web/WEB-INF/graphs.jsp
new file mode 100644
index 0000000..58c122c
--- /dev/null
+++ b/website/web/WEB-INF/graphs.jsp
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<html>
+<head>
+  <title>Tor Metrics Portal: Graphs</title>
+  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
+  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
+  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
+</head>
+<body>
+  <div class="center">
+    <%@ include file="banner.jsp"%>
+    <div class="main-column">
+        <h2>Tor Metrics Portal: Graphs</h2>
+        <br>
+        <p>The graphs on this page visualize a small portion of the data
+        gathered in the Tor Metrics Project. The following graphs are
+        available:</p>
+        <ul>
+          <li>The <a href="network.html">Network page</a> has numerous
+          statistics on the network of relays and bridges.</li>
+          <li>The <a href="users.html">Users page</a> attempts to estimate
+          the number of users in the network.</li>
+          <li>There are active and passive performance measurements of the
+          Tor network available on the
+          <a href="performance.html">Performance page</a>.</li>
+        </ul>
+        <br>
+    </div>
+  </div>
+  <div class="bottom" id="bottom">
+    <%@ include file="footer.jsp"%>
+  </div>
+</body>
+</html>
diff --git a/website/web/WEB-INF/index.jsp b/website/web/WEB-INF/index.jsp
new file mode 100644
index 0000000..9accc2b
--- /dev/null
+++ b/website/web/WEB-INF/index.jsp
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<html>
+<head>
+  <title>Tor Metrics Portal</title>
+  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
+  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
+  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
+</head>
+<body>
+  <div class="center">
+    <%@ include file="banner.jsp"%>
+    <div class="main-column">
+        <h2>Tor Metrics Portal</h2>
+        <br>
+        <p>The Tor Metrics Portal aggregates all kinds of interesting
+        data about the Tor network and visualizes them in graphs and
+        reports. This portal also provides easy access to the underlying
+        data and documentation for performing own analyses based on these
+        data. Find out more here:</p>
+        <ul>
+          <li>View daily updated <a href="graphs.html">graphs</a> on
+          estimated client numbers, on network performance, and other
+          statistics on the Tor network</li>
+          <li>Read <a href="http://freehaven.net/anonbib/">papers</a> and
+          <a href="https://research.torproject.org/techreports.html">technical
+          reports</a>
+          on the measurement techniques and results of statistical
+          analysis of metrics data</li>
+          <li>Download the <a href="data.html">data</a> that is behind the
+          graphs and reports to make your own evaluations</li>
+          <li>Try out the <a href="tools.html">tools</a> to parse and
+          evaluate the metrics data</li>
+        </ul>
+    </div>
+  </div>
+  <div class="bottom" id="bottom">
+    <%@ include file="footer.jsp"%>
+  </div>
+</body>
+</html>
diff --git a/website/web/WEB-INF/network.jsp b/website/web/WEB-INF/network.jsp
new file mode 100644
index 0000000..e0b297a
--- /dev/null
+++ b/website/web/WEB-INF/network.jsp
@@ -0,0 +1,305 @@
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
+<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<html>
+<head>
+  <title>Tor Metrics Portal: Network</title>
+  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
+  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
+  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
+</head>
+<body>
+  <div class="center">
+    <%@ include file="banner.jsp"%>
+    <div class="main-column">
+<h2>Tor Metrics Portal: Network</h2>
+<br>
+<a name="networksize"></a>
+<h3><a href="#networksize" class="anchor">Relays and bridges in the
+network</a></h3>
+<br>
+<p>The following graph shows the average daily number of relays and
+bridges in the network.</p>
+<img src="networksize.png${networksize_url}"
+     width="576" height="360" alt="Network size graph">
+<form action="network.html#networksize">
+  <div class="formrow">
+    <input type="hidden" name="graph" value="networksize">
+    <p>
+    <label>Start date (yyyy-mm-dd):</label>
+      <input type="text" name="start" size="10"
+             value="<c:choose><c:when test="${fn:length(networksize_start) == 0}">${default_start_date}</c:when><c:otherwise>${networksize_start[0]}</c:otherwise></c:choose>">
+    <label>End date (yyyy-mm-dd):</label>
+      <input type="text" name="end" size="10"
+             value="<c:choose><c:when test="${fn:length(networksize_end) == 0}">${default_end_date}</c:when><c:otherwise>${networksize_end[0]}</c:otherwise></c:choose>">
+    </p><p>
+    <input class="submit" type="submit" value="Update graph">
+    </p>
+  </div>
+</form>
+<p>Download graph as
+<a href="networksize.pdf${networksize_url}">PDF</a> or
+<a href="networksize.svg${networksize_url}">SVG</a>.</p>
+<p><a href="stats/servers.csv">CSV</a> file containing all data.</p>
+<br>
+
+<a name="relayflags"></a>
+<h3><a href="#relayflags" class="anchor">Relays with Exit, Fast, Guard,
+Stable, and HSDir flags</a></h3>
+<br>
+<p>The directory authorities assign certain flags to relays that clients
+use for their path selection decisions. The following graph shows the
+average number of relays with these flags assigned.</p>
+<img src="relayflags.png${relayflags_url}"
+     width="576" height="360" alt="Relay flags graph">
+<form action="network.html#relayflags">
+  <div class="formrow">
+    <input type="hidden" name="graph" value="relayflags">
+    <p>
+    <label>Start date (yyyy-mm-dd):</label>
+      <input type="text" name="start" size="10"
+             value="<c:choose><c:when test="${fn:length(relayflags_start) == 0}">${default_start_date}</c:when><c:otherwise>${relayflags_start[0]}</c:otherwise></c:choose>">
+    <label>End date (yyyy-mm-dd):</label>
+      <input type="text" name="end" size="10"
+             value="<c:choose><c:when test="${fn:length(relayflags_end) == 0}">${default_end_date}</c:when><c:otherwise>${relayflags_end[0]}</c:otherwise></c:choose>">
+    </p><p>
+      <label>Relay flags: </label>
+      <input type="checkbox" name="flag" value="Running"<c:if test="${fn:length(relayflags_flag) == 0 or fn:contains(fn:join(relayflags_flag, ','), 'Running')}"> checked</c:if>> Running
+      <input type="checkbox" name="flag" value="Exit"<c:if test="${fn:length(relayflags_flag) == 0 or fn:contains(fn:join(relayflags_flag, ','), 'Exit')}"> checked</c:if>> Exit
+      <input type="checkbox" name="flag" value="Fast"<c:if test="${fn:length(relayflags_flag) == 0 or fn:contains(fn:join(relayflags_flag, ','), 'Fast')}"> checked</c:if>> Fast
+      <input type="checkbox" name="flag" value="Guard"<c:if test="${fn:length(relayflags_flag) == 0 or fn:contains(fn:join(relayflags_flag, ','), 'Guard')}"> checked</c:if>> Guard
+      <input type="checkbox" name="flag" value="Stable"<c:if test="${fn:length(relayflags_flag) == 0 or fn:contains(fn:join(relayflags_flag, ','), 'Stable')}"> checked</c:if>> Stable
+      <input type="checkbox" name="flag" value="HSDir"<c:if test="${fn:length(relayflags_flag) > 0 and fn:contains(fn:join(relayflags_flag, ','), 'HSDir')}"> checked</c:if>> HSDir
+    </p><p>
+    <input class="submit" type="submit" value="Update graph">
+    </p>
+  </div>
+</form>
+<p>Download graph as
+<a href="relayflags.pdf${relayflags_url}">PDF</a> or
+<a href="relayflags.svg${relayflags_url}">SVG</a>.</p>
+<p><a href="stats/servers.csv">CSV</a> file containing all data.</p>
+<br>
+
+<a name="versions"></a>
+<h3><a href="#versions" class="anchor">Relays by version</a></h3>
+<br>
+<p>Relays report the Tor version that they are running to the directory
+authorities. See the
+<a href="https://www.torproject.org/download/download.html.en">download
+page</a> and
+<a href="https://gitweb.torproject.org/tor.git/blob/HEAD:/ChangeLog">ChangeLog file</a>
+to find out which Tor versions are stable and unstable.
+The following graph shows the number of relays by version.</p>
+<img src="versions.png${versions_url}"
+     width="576" height="360" alt="Relay versions graph">
+<form action="network.html#versions">
+  <div class="formrow">
+    <input type="hidden" name="graph" value="versions">
+    <p>
+    <label>Start date (yyyy-mm-dd):</label>
+      <input type="text" name="start" size="10"
+             value="<c:choose><c:when test="${fn:length(versions_start) == 0}">${default_start_date}</c:when><c:otherwise>${versions_start[0]}</c:otherwise></c:choose>">
+    <label>End date (yyyy-mm-dd):</label>
+      <input type="text" name="end" size="10"
+             value="<c:choose><c:when test="${fn:length(versions_end) == 0}">${default_end_date}</c:when><c:otherwise>${versions_end[0]}</c:otherwise></c:choose>">
+    </p><p>
+    <input class="submit" type="submit" value="Update graph">
+    </p>
+  </div>
+</form>
+<p>Download graph as
+<a href="versions.pdf${versions_url}">PDF</a> or
+<a href="versions.svg${versions_url}">SVG</a>.</p>
+<p><a href="stats/servers.csv">CSV</a> file containing all data.</p>
+<br>
+
+<a name="platforms"></a>
+<h3><a href="#platforms" class="anchor">Relays by platform</a></h3>
+<br>
+<p>Relays report the operating system they are running to the directory
+authorities. The following graph shows the number of relays by
+platform.</p>
+<img src="platforms.png${platforms_url}"
+     width="576" height="360" alt="Relay platforms graph">
+<form action="network.html#platforms">
+  <div class="formrow">
+    <input type="hidden" name="graph" value="platforms">
+    <p>
+    <label>Start date (yyyy-mm-dd):</label>
+      <input type="text" name="start" size="10"
+             value="<c:choose><c:when test="${fn:length(platforms_start) == 0}">${default_start_date}</c:when><c:otherwise>${platforms_start[0]}</c:otherwise></c:choose>">
+    <label>End date (yyyy-mm-dd):</label>
+      <input type="text" name="end" size="10"
+             value="<c:choose><c:when test="${fn:length(platforms_end) == 0}">${default_end_date}</c:when><c:otherwise>${platforms_end[0]}</c:otherwise></c:choose>">
+    </p><p>
+    <input class="submit" type="submit" value="Update graph">
+    </p>
+  </div>
+</form>
+<p>Download graph as
+<a href="platforms.pdf${platforms_url}">PDF</a> or
+<a href="platforms.svg${platforms_url}">SVG</a>.</p>
+<p><a href="stats/servers.csv">CSV</a> file containing all data.</p>
+<br>
+
+<a name="cloudbridges"></a>
+<h3><a href="#cloudbridges" class="anchor">Tor Cloud bridges</a></h3>
+<br>
+<p>The following graph shows the average daily number of
+<a href="http://cloud.torproject.org/">Tor Cloud</a> bridges in the
+network.</p>
+<img src="cloudbridges.png${cloudbridges_url}"
+     width="576" height="360" alt="Tor Cloud bridges graph">
+<form action="network.html#cloudbridges">
+  <div class="formrow">
+    <input type="hidden" name="graph" value="cloudbridges">
+    <p>
+    <label>Start date (yyyy-mm-dd):</label>
+      <input type="text" name="start" size="10"
+             value="<c:choose><c:when test="${fn:length(cloudbridges_start) == 0}">${default_start_date}</c:when><c:otherwise>${cloudbridges_start[0]}</c:otherwise></c:choose>">
+    <label>End date (yyyy-mm-dd):</label>
+      <input type="text" name="end" size="10"
+             value="<c:choose><c:when test="${fn:length(cloudbridges_end) == 0}">${default_end_date}</c:when><c:otherwise>${cloudbridges_end[0]}</c:otherwise></c:choose>">
+    </p><p>
+    <input class="submit" type="submit" value="Update graph">
+    </p>
+  </div>
+</form>
+<p>Download graph as
+<a href="cloudbridges.pdf${cloudbridges_url}">PDF</a> or
+<a href="cloudbridges.svg${cloudbridges_url}">SVG</a>.</p>
+<p><a href="stats/servers.csv">CSV</a> file containing all data.</p>
+<br>
+
+<a name="bandwidth"></a>
+<h3><a href="#bandwidth" class="anchor">Total relay bandwidth in the
+network</a></h3>
+<br>
+<p>Relays report how much bandwidth they are willing to contribute and how
+many bytes they have read and written in the past 24 hours. The following
+graph shows total advertised bandwidth and bandwidth history of all relays
+in the network.</p>
+<img src="bandwidth.png${bandwidth_url}"
+     width="576" height="360" alt="Relay bandwidth graph">
+<form action="network.html#bandwidth">
+  <div class="formrow">
+    <input type="hidden" name="graph" value="bandwidth">
+    <p>
+    <label>Start date (yyyy-mm-dd):</label>
+      <input type="text" name="start" size="10"
+             value="<c:choose><c:when test="${fn:length(bandwidth_start) == 0}">${default_start_date}</c:when><c:otherwise>${bandwidth_start[0]}</c:otherwise></c:choose>">
+    <label>End date (yyyy-mm-dd):</label>
+      <input type="text" name="end" size="10"
+             value="<c:choose><c:when test="${fn:length(bandwidth_end) == 0}">${default_end_date}</c:when><c:otherwise>${bandwidth_end[0]}</c:otherwise></c:choose>">
+    </p><p>
+    <input class="submit" type="submit" value="Update graph">
+    </p>
+  </div>
+</form>
+<p>Download graph as
+<a href="bandwidth.pdf${bandwidth_url}">PDF</a> or
+<a href="bandwidth.svg${bandwidth_url}">SVG</a>.</p>
+<p><a href="stats/bandwidth.csv">CSV</a> file containing all data.</p>
+<br>
+
+<a name="bwhist-flags"></a>
+<h3><a href="#bwhist-flags" class="anchor">Relay bandwidth by Exit and/or
+Guard flags</a></h3>
+<br>
+<p>The following graph shows the relay bandwidth of all relays with the
+Exit and/or Guard flags assigned by the directory authorities.</p>
+<img src="bwhist-flags.png${bwhist_flags_url}"
+     width="576" height="360" alt="Relay bandwidth by flags graph">
+<form action="network.html#bwhist-flags">
+  <div class="formrow">
+    <input type="hidden" name="graph" value="bwhist-flags">
+    <p>
+    <label>Start date (yyyy-mm-dd):</label>
+      <input type="text" name="start" size="10"
+             value="<c:choose><c:when test="${fn:length(bwhist_flags_start) == 0}">${default_start_date}</c:when><c:otherwise>${bwhist_flags_start[0]}</c:otherwise></c:choose>">
+    <label>End date (yyyy-mm-dd):</label>
+      <input type="text" name="end" size="10"
+             value="<c:choose><c:when test="${fn:length(bwhist_flags_end) == 0}">${default_end_date}</c:when><c:otherwise>${bwhist_flags_end[0]}</c:otherwise></c:choose>">
+    </p><p>
+    <input class="submit" type="submit" value="Update graph">
+    </p>
+  </div>
+</form>
+<p>Download graph as
+<a href="bwhist-flags.pdf${bwhist_flags_url}">PDF</a> or
+<a href="bwhist-flags.svg${bwhist_flags_url}">SVG</a>.</p>
+<p><a href="stats/bandwidth.csv">CSV</a> file containing all data.</p>
+<br>
+
+<a name="bandwidth-flags"></a>
+<h3><a href="#bandwidth-flags" class="anchor">Advertised bandwidth and
+bandwidth history by relay flags</a></h3>
+<br>
+<p>The following graph shows the advertised bandwidth and bandwidth
+history of all relays with the Exit and/or Guard flags assigned by the
+directory authorities.
+Note that these sets possibly overlap with relays having both Exit and
+Guard flag.</p>
+<img src="bandwidth-flags.png${bandwidth_flags_url}"
+     width="576" height="360" alt="Advertised bandwidth and bandwidth history by relay flags graph">
+<form action="network.html#bandwidth-flags">
+  <div class="formrow">
+    <input type="hidden" name="graph" value="bandwidth-flags">
+    <p>
+    <label>Start date (yyyy-mm-dd):</label>
+      <input type="text" name="start" size="10"
+             value="<c:choose><c:when test="${fn:length(bandwidth_flags_start) == 0}">${default_start_date}</c:when><c:otherwise>${bandwidth_flags_start[0]}</c:otherwise></c:choose>">
+    <label>End date (yyyy-mm-dd):</label>
+      <input type="text" name="end" size="10"
+             value="<c:choose><c:when test="${fn:length(bandwidth_flags_end) == 0}">${default_end_date}</c:when><c:otherwise>${bandwidth_flags_end[0]}</c:otherwise></c:choose>">
+    </p><p>
+    <input class="submit" type="submit" value="Update graph">
+    </p>
+  </div>
+</form>
+<p>Download graph as
+<a href="bandwidth-flags.pdf${bandwidth_flags_url}">PDF</a> or
+<a href="bandwidth-flags.svg${bandwidth_flags_url}">SVG</a>.</p>
+<p><a href="stats/bandwidth.csv">CSV</a> file containing all data.</p>
+<br>
+
+<a name="dirbytes"></a>
+<h3><a href="#dirbytes" class="anchor">Number of bytes spent on answering
+directory requests</a></h3>
+<br>
+<p>Relays running on 0.2.2.15-alpha or higher report the number of bytes
+they spend on answering directory requests. The following graph shows
+total written and read bytes as well as written and read dir bytes. The
+dir bytes are extrapolated from those relays who report them to reflect
+the number of written and read dir bytes by all relays.</p>
+<img src="dirbytes.png${dirbytes_url}"
+     width="576" height="360" alt="Dir bytes graph">
+<form action="network.html#dirbytes">
+  <div class="formrow">
+    <input type="hidden" name="graph" value="dirbytes">
+    <p>
+    <label>Start date (yyyy-mm-dd):</label>
+      <input type="text" name="start" size="10"
+             value="<c:choose><c:when test="${fn:length(dirbytes_start) == 0}">${default_start_date}</c:when><c:otherwise>${dirbytes_start[0]}</c:otherwise></c:choose>">
+    <label>End date (yyyy-mm-dd):</label>
+      <input type="text" name="end" size="10"
+             value="<c:choose><c:when test="${fn:length(dirbytes_end) == 0}">${default_end_date}</c:when><c:otherwise>${dirbytes_end[0]}</c:otherwise></c:choose>">
+    </p><p>
+    <input class="submit" type="submit" value="Update graph">
+    </p>
+  </div>
+</form>
+<p>Download graph as
+<a href="dirbytes.pdf${dirbytes_url}">PDF</a> or
+<a href="dirbytes.svg${dirbytes_url}">SVG</a>.</p>
+<p><a href="stats/bandwidth.csv">CSV</a> file containing all data.</p>
+<br>
+    </div>
+  </div>
+  <div class="bottom" id="bottom">
+    <%@ include file="footer.jsp"%>
+  </div>
+</body>
+</html>
diff --git a/website/web/WEB-INF/papers.jsp b/website/web/WEB-INF/papers.jsp
new file mode 100644
index 0000000..1a48491
--- /dev/null
+++ b/website/web/WEB-INF/papers.jsp
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<html>
+<head>
+  <title>Tor Metrics Portal: Papers</title>
+  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
+  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
+  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
+</head>
+<body>
+  <div class="center">
+    <%@ include file="banner.jsp"%>
+    <div class="main-column">
+        <h2>Tor Metrics Portal: Papers</h2>
+        <br>
+        <p>The <a href="#papers">papers</a>,
+        <a href="#techreports">technical reports</a>, and
+        <a href="#blogposts">blog posts</a> listed on this page originate
+        from, are based on, or are related to work performed in the Tor
+        Metrics Project.</p>
+        <p><font color="red">This page will be removed soon.
+        Its main purpose was to list metrics-related tech reports, but
+        those have now moved
+        <a href="https://research.torproject.org/techreports.html">here</a>.
+        </font></p>
+        <br>
+        <a name="papers"></a>
+        <h3><a href="#papers" class="anchor">Papers</a></h3>
+        <br>
+        These papers summarize some of the results of of the Tor Metrics
+        Project and have been accepted for publication at academic
+        conferences or workshops.
+        <ul>
+          <li>Karsten Loesing, Steven J. Murdoch, Roger Dingledine. A Case
+          Study on Measuring Statistical Data in the Tor Anonymity
+          Network. Workshop on Ethics in
+          Computer Security Research (WECSR 2010), Tenerife, Spain,
+          January 2010. (<a href="http://freehaven.net/anonbib/cache/wecsr10measuring-tor.pdf">PDF</a>)</li>
+          <li>Karsten Loesing. Measuring the Tor Network from Public
+          Directory Information. 2nd Hot Topics in Privacy Enhancing
+          Technologies (HotPETs 2009), Seattle, WA, USA, August 2009.
+          (<a href="https://research.torproject.org/techreports/metrics-2009-08-07.pdf">PDF</a>)</li>
+        </ul>
+        <br>
+        <a name="techreports"></a>
+        <h3><a href="#techreports" class="anchor">Technical
+        reports</a></h3>
+        <br>
+        <p>
+        Some of the
+        <a href="https://research.torproject.org/techreports.html">Tor
+        Technical Reports</a> have been the first place to
+        publish novel kinds of statistics on the Tor network. Some, but
+        not all, of the results contained in those technical reports have
+        been included in the <a href="#papers">papers</a> above or in the
+        daily updated <a href="graphs.html">graphs</a>.
+        </p>
+        <br>
+        <a name="blogposts"></a>
+        <h3><a href="#blogposts" class="anchor">Blog posts</a></h3>
+        <br>
+        The following blog posts are either the results of metrics
+        research or describe new interesting research questions that can
+        (partly) be answered with metrics data.
+        <ul>
+          <li>Research problems: Ten ways to discover Tor bridges
+          (<a href="https://blog.torproject.org/blog/research-problems-ten-ways-discover-tor-bridges">link</a>,
+          October 31, 2011).</li>
+          <li>Research problem: better guard rotation parameters
+          (<a href="https://blog.torproject.org/blog/research-problem-better-guard-rotation-parameters">link</a>,
+          August 20, 2011).</li>
+          <li>Research problem: measuring the safety of the Tor network
+          (<a href="https://blog.torproject.org/blog/research-problem-measuring-safety-tor-network">link</a>,
+          February 5, 2011).</li>
+        </ul>
+    </div>
+  </div>
+  <div class="bottom" id="bottom">
+    <%@ include file="footer.jsp"%>
+  </div>
+</body>
+</html>
diff --git a/website/web/WEB-INF/performance.jsp b/website/web/WEB-INF/performance.jsp
new file mode 100644
index 0000000..e220b59
--- /dev/null
+++ b/website/web/WEB-INF/performance.jsp
@@ -0,0 +1,150 @@
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<html>
+<head>
+  <title>Tor Metrics Portal: Performance</title>
+  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
+  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
+  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
+</head>
+<body>
+  <div class="center">
+    <%@ include file="banner.jsp"%>
+    <div class="main-column">
+<h2>Tor Metrics Portal: Performance</h2>
+<br>
+<a name="torperf"></a>
+<h3><a href="#torperf" class="anchor">Time to download files over
+Tor</a></h3>
+<br>
+<p>The following graphs show the performance of the Tor network as
+experienced by its users. The graphs contain the average (median) time to
+request files of three different sizes over Tor as well as first and third
+quartile of request times. Medians and quartiles are calculated multiple
+times per day for completed days only, resulting in a delay of 1 to 1.5
+days before changes to network performance become visible in the
+graph.</p>
+<img src="torperf.png${torperf_url}"
+     width="576" height="360" alt="Torperf graph">
+<form action="performance.html#torperf">
+  <div class="formrow">
+    <input type="hidden" name="graph" value="torperf">
+    <p>
+    <label>Start date (yyyy-mm-dd):</label>
+      <input type="text" name="start" size="10"
+             value="<c:choose><c:when test="${fn:length(torperf_start) == 0}">${default_start_date}</c:when><c:otherwise>${torperf_start[0]}</c:otherwise></c:choose>">
+    <label>End date (yyyy-mm-dd):</label>
+      <input type="text" name="end" size="10"
+             value="<c:choose><c:when test="${fn:length(torperf_end) == 0}">${default_end_date}</c:when><c:otherwise>${torperf_end[0]}</c:otherwise></c:choose>">
+    </p><p>
+      Source:
+      <input type="radio" name="source" value="all"<c:if test="${fn:length(torperf_source) == 0 or torperf_source[0] eq 'all'}"> checked</c:if>> all
+      <input type="radio" name="source" value="torperf"<c:if test="${torperf_source[0] eq 'torperf'}"> checked</c:if>> torperf
+      <input type="radio" name="source" value="moria"<c:if test="${torperf_source[0] eq 'moria'}"> checked</c:if>> moria
+      <input type="radio" name="source" value="siv"<c:if test="${torperf_source[0] eq 'siv'}"> checked</c:if>> siv
+    </p><p>
+      <label>File size: </label>
+      <input type="radio" name="filesize" value="50kb"<c:if test="${fn:length(torperf_filesize) == 0 or torperf_filesize[0] eq '50kb'}"> checked</c:if>> 50 KiB
+      <input type="radio" name="filesize" value="1mb"<c:if test="${torperf_filesize[0] eq '1mb'}"> checked</c:if>> 1 MiB
+      <input type="radio" name="filesize" value="5mb"<c:if test="${torperf_filesize[0] eq '5mb'}"> checked</c:if>> 5 MiB
+    </p><p>
+    <input class="submit" type="submit" value="Update graph">
+    </p>
+  </div>
+</form>
+<p>Download graph as
+<a href="torperf.pdf${torperf_url}">PDF</a> or
+<a href="torperf.svg${torperf_url}">SVG</a>.</p>
+<p><a href="stats/torperf.csv">CSV</a> file containing all data.</p>
+
+<br>
+<a name="torperf-failures"></a>
+<h3><a href="#torperf-failures" class="anchor">Timeouts and failures of
+downloading files over Tor</a></h3>
+<br>
+<p>The following graphs show the fraction of timeouts and failures of
+downloading files over Tor as experienced by users.
+A timeout occurs when a 50 KiB (1 MiB, 5 MiB) download does not complete
+within 4:55 minutes (29:55 minutes, 59:55 minutes).
+A failure occurs when the download completes, but the response is smaller
+than 50 KiB (1 MiB, 5 MiB).</p>
+<img src="torperf-failures.png${torperf_failures_url}"
+     width="576" height="360" alt="Torperf failures graph">
+<form action="performance.html#torperf-failures">
+  <div class="formrow">
+    <input type="hidden" name="graph" value="torperf-failures">
+    <p>
+    <label>Start date (yyyy-mm-dd):</label>
+      <input type="text" name="start" size="10"
+             value="<c:choose><c:when test="${fn:length(torperf_failures_start) == 0}">${default_start_date}</c:when><c:otherwise>${torperf_failures_start[0]}</c:otherwise></c:choose>">
+    <label>End date (yyyy-mm-dd):</label>
+      <input type="text" name="end" size="10"
+             value="<c:choose><c:when test="${fn:length(torperf_failures_end) == 0}">${default_end_date}</c:when><c:otherwise>${torperf_failures_end[0]}</c:otherwise></c:choose>">
+    </p><p>
+      Source:
+      <input type="radio" name="source" value="all"<c:if test="${fn:length(torperf_failures_source) == 0 or torperf_failures_source[0] eq 'all'}"> checked</c:if>> all
+      <input type="radio" name="source" value="torperf"<c:if test="${torperf_failures_source[0] eq 'torperf'}"> checked</c:if>> torperf
+      <input type="radio" name="source" value="moria"<c:if test="${torperf_failures_source[0] eq 'moria'}"> checked</c:if>> moria
+      <input type="radio" name="source" value="siv"<c:if test="${torperf_failures_source[0] eq 'siv'}"> checked</c:if>> siv
+    </p><p>
+      <label>File size: </label>
+      <input type="radio" name="filesize" value="50kb"<c:if test="${fn:length(torperf_failures_filesize) == 0 or torperf_failures_filesize[0] eq '50kb'}"> checked</c:if>> 50 KiB
+      <input type="radio" name="filesize" value="1mb"<c:if test="${torperf_failures_filesize[0] eq '1mb'}"> checked</c:if>> 1 MiB
+      <input type="radio" name="filesize" value="5mb"<c:if test="${torperf_failures_filesize[0] eq '5mb'}"> checked</c:if>> 5 MiB
+    </p><p>
+    <input class="submit" type="submit" value="Update graph">
+    </p>
+  </div>
+</form>
+<p>Download graph as
+<a href="torperf-failures.pdf${torperf_failures_url}">PDF</a> or
+<a href="torperf-failures.svg${torperf_failures_url}">SVG</a>.</p>
+<p><a href="stats/torperf.csv">CSV</a> file containing all data.</p>
+
+<br>
+<a name="connbidirect"></a>
+<h3><a href="#connbidirect" class="anchor">Fraction of connections used
+uni-/bidirectionally</a></h3>
+<br>
+<p>The following graph shows the fraction of connections that is used
+uni- or bi-directionally.  Every 10 seconds, relays determine for every
+connection whether they read and wrote less than a threshold of 20 KiB.
+Connections below this threshold are excluded from these statistics.  For
+the remaining connections, relays report whether they read/wrote at least
+10 times as many bytes as they wrote/read.  If so, they classify a
+connection as "Mostly reading" or "Mostly writing," respectively.  All
+other connections are classified as "Both reading and writing."  After
+classifying connections, read and write counters are reset for the next
+10-second interval.  Statistics are aggregated over 24 hours.</p>
+<img src="connbidirect.png${connbidirect_url}"
+     width="576" height="360"
+     alt="Fraction of direct connections used uni-/bidirectionally">
+<form action="performance.html#connbidirect">
+  <div class="formrow">
+    <input type="hidden" name="graph" value="connbidirect">
+    <p>
+    <label>Start date (yyyy-mm-dd):</label>
+      <input type="text" name="start" size="10"
+             value="<c:choose><c:when test="${fn:length(connbidirect_start) == 0}">${default_start_date}</c:when><c:otherwise>${connbidirect_start[0]}</c:otherwise></c:choose>">
+    <label>End date (yyyy-mm-dd):</label>
+      <input type="text" name="end" size="10"
+             value="<c:choose><c:when test="${fn:length(connbidirect_end) == 0}">${default_end_date}</c:when><c:otherwise>${connbidirect_end[0]}</c:otherwise></c:choose>">
+    </p><p>
+    <input class="submit" type="submit" value="Update graph">
+    </p>
+  </div>
+</form>
+<p>Download graph as
+<a href="connbidirect.pdf${connbidirect_url}">PDF</a> or
+<a href="connbidirect.svg${connbidirect_url}">SVG</a>.</p>
+<p><a href="stats/connbidirect.csv">CSV</a> file containing all data.</p>
+<br>
+
+    </div>
+  </div>
+  <div class="bottom" id="bottom">
+    <%@ include file="footer.jsp"%>
+  </div>
+</body>
+</html>
diff --git a/website/web/WEB-INF/relay-search.jsp b/website/web/WEB-INF/relay-search.jsp
new file mode 100644
index 0000000..90fdd86
--- /dev/null
+++ b/website/web/WEB-INF/relay-search.jsp
@@ -0,0 +1,85 @@
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<html>
+<head>
+  <title>Tor Metrics Portal: Relay Search</title>
+  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
+  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
+  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
+</head>
+<body>
+  <div class="center">
+    <%@ include file="banner.jsp"%>
+    <div class="main-column">
+      <h2>Tor Metrics Portal: Relay Search</h2>
+      <p>Search for a relay in the relay descriptor archive by typing
+      (part of) a <b>nickname</b>, <b>$-prefixed fingerprint</b>, or <b>IP
+      address</b> and optionally a <b>month (yyyy-mm)</b> or up to three
+      <b>days (yyyy-mm-dd)</b> in the following search field and
+      clicking Search. The search will stop after 30 hits or, unless you
+      provide a month or a day, after parsing the last 30 days of relay
+      lists.</p>
+      <br>
+      <form action="relay-search.html">
+        <table>
+          <tr>
+            <td><input type="text" name="search"
+                       value="<c:out value="${param.search}"/>"></td>
+            <td><input type="submit" value="Search"></td>
+          </tr>
+        </table>
+      </form>
+      <br>
+      <c:if test="${not empty invalidQuery}">
+        <p>Sorry, I didn't understand your query. Please provide a
+        nickname (e.g., "gabelmoo"), at least the first 8 hex characters
+        of a fingerprint prefixed by $ (e.g., "$F2044413"), or at least
+        the first two octets of an IPv4 address in dotted-decimal notation
+        (e.g., "80.190"). You can also provide at most three months or
+        days in ISO 8601 format (e.g., "2010-09" or "2010-09-17").</p>
+      </c:if>
+      <c:if test="${not empty outsideInterval}">
+        <p>${outsideInterval}</p>
+      </c:if>
+      <c:if test="${not empty searchNotice}">
+        <p>${searchNotice}</p>
+      </c:if>
+      <c:if test="${not empty query}">
+        <!-- ${query} -->
+      </c:if>
+      <c:if test="${not empty queryTime}">
+        <c:forEach var="consensus" items="${foundDescriptors}">
+          ${rawValidAfterLines[consensus.key]}
+          <c:forEach var="statusentry" items="${consensus.value}">
+            ${rawStatusEntries[statusentry]}
+          </c:forEach>
+          <br>
+        </c:forEach>
+        <p>Found
+        <c:choose>
+          <c:when test="${matches > 30}">
+            more than 30 relays (displaying only those in the last
+            consensuses)
+          </c:when>
+          <c:otherwise>
+            ${matches} relays
+          </c:otherwise>
+        </c:choose>
+        in <fmt:formatNumber value="${queryTime / 1000}" pattern="#.###"/>
+        seconds.</p>
+        <c:if test="${queryTime > 10000}">
+          <p>In theory, search time should not exceed 10 seconds. The
+          query was '${query}'. If this or similar searches remain slow,
+          please <a href="mailto:tor-assistants at torproject.org">let us
+          know</a>!</p>
+        </c:if>
+      </c:if>
+    </div>
+  </div>
+  <div class="bottom" id="bottom">
+    <%@ include file="footer.jsp"%>
+  </div>
+</body>
+</html>
+
diff --git a/website/web/WEB-INF/research.jsp b/website/web/WEB-INF/research.jsp
new file mode 100644
index 0000000..b0a7277
--- /dev/null
+++ b/website/web/WEB-INF/research.jsp
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<html>
+<head>
+  <title>Tor Metrics Portal: Research</title>
+  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
+  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
+  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
+</head>
+<body>
+  <div class="center">
+    <%@ include file="banner.jsp"%>
+    <div class="main-column">
+        <h2>Tor Metrics Portal: Research</h2>
+        <br>
+        <p>The Tor Metrics Project aims at supporting privacy enhancing
+        technologies research by making gathered network
+        <a href="data.html">data</a>, and
+        <a href="tools.html">tools</a>
+        for processing these data available to the public.
+        Some results from analyzing these data can be found in
+        <a href="https://research.torproject.org/techreports.html">Tor
+        Tech Reports</a>.  If you are
+        missing anything for your Tor-related research or want to share
+        your research results with others, please
+        <a href="mailto:tor-assistants at torproject.org">let us know</a>!
+    </div>
+  </div>
+  <div class="bottom" id="bottom">
+    <%@ include file="footer.jsp"%>
+  </div>
+</body>
+</html>
diff --git a/website/web/WEB-INF/stats.jsp b/website/web/WEB-INF/stats.jsp
new file mode 100644
index 0000000..ccc2540
--- /dev/null
+++ b/website/web/WEB-INF/stats.jsp
@@ -0,0 +1,313 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<html>
+<head>
+  <title>Tor Metrics Portal: Statistics</title>
+  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
+  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
+  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
+</head>
+<body>
+  <div class="center">
+    <%@ include file="banner.jsp"%>
+    <div class="main-column">
+<h2>Tor Metrics Portal: Statistics</h2>
+<br>
+
+<p>The metrics portal aggregates large amounts of Tor network
+<a href="data.html">data</a> and visualizes results in customizable
+<a href="graphs.html">graphs</a> and tables.
+All aggregated data are also available for download, so that people can
+easily plot their own graphs or even develop a prettier metrics website
+without writing their own data aggregation code.
+Data formats of aggregate statistics are specified below.</p>
+<hr>
+<br>
+
+<a name="servers"></a>
+<h3><a href="#servers" class="anchor">Number of relays and
+bridges</a></h3>
+<br>
+<p>Statistics file <a href="stats/servers.csv">servers.csv</a> contains
+the average number of relays and bridges in the Tor network.
+All averages are calculated per day by evaluating the relay and bridge
+lists published by the directory authorities.
+Statistics include subsets of relays or bridges by relay flag (only
+relays), country code (only relays, only until February 2013), Tor
+software version (only relays), operating system (only relays), and EC2
+cloud (only bridges).
+The statistics file contains the following columns:</p>
+
+<ul>
+<li><b>date:</b> UTC date (YYYY-MM-DD) when relays or bridges have been
+listed as running.</li>
+<li><b>flag:</b> Relay flag assigned by the directory authorities.
+Examples are <b>"Exit"</b>, <b>"Guard"</b>, <b>"Fast"</b>,
+<b>"Stable"</b>, and <b>"HSDir"</b>.
+Relays can have none, some, or all these relay flags assigned.
+Relays that don't have the <b>"Running"</b> flag are not included in these
+statistics regardless of their other flags.
+If this column contains the empty string, all running relays are included,
+regardless of assigned flags.
+There are no statistics on the number of bridges by relay flag.</li>
+<li><b>country:</b> Two-letter lower-case country code as found in a GeoIP
+database by resolving the relay's first onion-routing IP address, or
+<b>"??"</b> if an IP addresses could not be resolved.
+If this column contains the empty string, all running relays are included,
+regardless of their resolved country code.
+Statistics on relays by country code are only available until January 31,
+2013.
+There are no statistics on the number of bridges by country code.</li>
+<li><b>version:</b> First three dotted numbers of the Tor software version
+as reported by the relay.
+An example is <b>"0.2.5"</b>.
+If this column contains the empty string, all running relays are included,
+regardless of the Tor software version they run.
+There are no statistics on the number of bridges by Tor software
+version.</li>
+<li><b>platform:</b> Operating system as reported by the relay.
+Examples are <b>"Linux"</b>, <b>"Darwin"</b> (Mac OS X), <b>"FreeBSD"</b>,
+<b>"Windows"</b>, and <b>"Other"</b>.
+If this column contains the empty string, all running relays are included,
+regardless of the operating system they run on.
+There are no statistics on the number of bridges by operating system.</li>
+<li><b>ec2bridge:</b> Whether bridges are running in the EC2 cloud or not.
+More precisely, bridges in the EC2 cloud running an image provided by Tor
+by default set their nickname to <b>"ec2bridger"</b> plus 8 random hex
+characters.
+This column either contains <b>"t"</b> for bridges matching this naming
+scheme, or the empty string for all bridges regardless of their nickname.
+There are no statistics on the number of relays running in the EC2
+cloud.</li>
+<li><b>relays:</b> The average number of relays matching the criteria in
+the previous columns.
+If the values in previous columns are specific to bridges only, this
+column contains the empty string.</li>
+<li><b>bridges:</b> The average number of bridges matching the criteria in
+the previous columns.
+If the values in previous columns are specific to relays only, this column
+contains the empty string.</li>
+</ul>
+<hr>
+<br>
+
+<a name="bandwidth"></a>
+<h3><a href="#bandwidth" class="anchor">Bandwidth provided and consumed by
+relays</a></h3>
+<br>
+<p>Statistics on bandwidth provided and consumed by relays are contained
+in file <a href="stats/bandwidth.csv">bandwidth.csv</a>.
+This file contains three different bandwidth metrics:
+(1) bandwidth that relays are capable to provide and bandwidth that relays
+report to have consumed, either (2) for any traffic, or (3) only traffic
+from serving directory data.
+Relays providing bandwidth statistics are categorized by having the
+<b>"Exit"</b> and <b>"Guard"</b> relay flag, having both, or not having
+either.
+The statistics file contains the following columns:</p>
+
+<ul>
+<li><b>date:</b> UTC date (YYYY-MM-DD) that relays reported bandwidth data
+for.</li>
+<li><b>isexit:</b> Whether relays included in this line have the
+<b>"Exit"</b> relay flag or not, which can be <b>"t"</b> or <b>"f"</b>.
+If this column contains the empty string, bandwidth data from all running
+relays are included, regardless of assigned relay flags.</li>
+<li><b>isguard:</b> Whether relays included in this line have the
+<b>"Guard"</b> relay flag or not, which can be <b>"t"</b> or <b>"f"</b>.
+If this column contains the empty string, bandwidth data from all running
+relays are included, regardless of assigned relay flags.</li>
+<li><b>advbw:</b> Total advertised bandwidth in bytes per second that
+relays are capable to provide.</li>
+<li><b>bwread:</b> Total bandwidth in bytes per second that relays have
+read.
+This metric includes any kind of traffic.</li>
+<li><b>bwwrite:</b> Similar to <b>bwread</b>, but for traffic written by
+relays.</li>
+<li><b>dirread:</b> Bandwidth in bytes per second that relays have read
+when serving directory data.
+Not all relays report how many bytes they read when serving directory data
+which is why this value is an estimate from the available data.
+This metric is not available for subsets of relays with certain relay
+flags, so that this column will contain the empty string if either
+<b>isexit</b> or <b>isguard</b> is non-empty.</li>
+<li><b>dirwrite:</b> Similar to <b>dirread</b>, but for traffic written by
+relays when serving directory data.</li>
+</ul>
+<hr>
+<br>
+
+<a name="fast-exits"></a>
+<h3><a href="#fast-exits" class="anchor">Relays meeting or almost meeting
+fast-exit requirements</a></h3>
+<br>
+<p>Statistics file <a href="stats/fast-exits.csv">fast-exits.csv</a>
+contains the number of relays meeting or almost meeting fast-exit
+requirements.
+These requirements originate from a Tor sponsor contract and are defined as
+follows:
+a Tor relay is fast if it has at least 95 Mbit/s configured bandwidth
+rate, at least 5000 KB/s advertised bandwidth capacity, and permits
+exiting to ports 80, 443, 554, and 1755; furthermore, there may be at most
+2 relays per /24 network in the set of fast exits.
+Similarly, an almost fast exit is one that almost meets the fast-exit
+requirements, but fails at least one of them.
+In particular, an almost fast exit is one that has at least 80 Mbit/s
+configured bandwidth rate, at least 2000 KB/s advertised bandwidth
+capacity, and permits exiting to ports 80 and 443; also, if there are more
+than 2 relays per /24 network meeting fast-exit requirements, all but two
+are considered almost fast.
+The statistics file contains the following columns:</p>
+
+<ul>
+<li><b>date:</b> UTC date (YYYY-MM-DD) when relays have been listed as
+running.</li>
+<li><b>fastnum:</b> Average number of relays matching fast-exit
+requirements.</li>
+<li><b>almostnum:</b> Average number of relays almost matching
+fast-exit requirements.</li>
+<li><b>fastprob:</b> Total exit probability of all relays matching
+fast-exit requirements.</li>
+<li><b>almostprob:</b> Total exit probability of all relays almost
+matching fast-exit requirements.</li>
+</li>
+</ul>
+<hr>
+<br>
+
+<a name="clients"></a>
+<h3><a href="#clients" class="anchor">Estimated number of clients in the
+Tor network</a></h3>
+<br>
+<p>Statistics file <a href="stats/clients.csv">clients.csv</a> contains
+estimates on the number of clients in the Tor network.
+These estimates are based on the number of directory requests counted on
+directory mirrors and bridges.
+Statistics are available for clients connecting directly to the Tor
+network and clients connecting via bridges.
+For relays, there exist statistics on the number of clients by country,
+and for bridges, statistics are available by country, by transport, and by
+IP version.
+Statistics further include expected client numbers from past observations
+which can be used to detect censorship or release of censorship.
+The statistics file contains the following columns:</p>
+
+<ul>
+<li><b>date:</b> UTC date (YYYY-MM-DD) for which client numbers are
+estimated.</li>
+<li><b>node:</b> The node type to which clients connect first, which can
+be either <b>"relay"</b> or <b>"bridge"</b>.</li>
+<li><b>country:</b> Two-letter lower-case country code as found in a GeoIP
+database by resolving clients' IP addresses, or <b>"??"</b> if client IP
+addresses could not be resolved.
+If this column contains the empty string, all clients are included,
+regardless of their country code.</li>
+<li><b>transport:</b> Transport name used by clients to connect to the Tor
+network using bridges.
+Examples are <b>"obfs2"</b>, <b>"obfs3"</b>, <b>"websocket"</b>, or
+<b>"<OR>"</b> (original onion routing protocol).
+If this column contains the empty string, all clients are included,
+regardless of their transport.
+There are no statistics on the number of clients by transport that connect
+to the Tor network via relays.</li>
+<li><b>version:</b> IP version used by clients to connect to the Tor
+network using bridges.
+Examples are <b>"v4"</b> and <b>"v6"</b>.
+If this column contains the empty string, all clients are included,
+regardless of their IP version.
+There are no statistics on the number of clients by IP version that connect
+directly to the Tor network using relays.</li>
+<li><b>lower:</b> Lower number of expected clients under the assumption
+that there has been no censorship event.
+If this column contains the empty string, there are no expectations on the
+number of clients.</li>
+<li><b>upper:</b> Upper number of expected clients under the assumption
+that there has been no release of censorship.
+If this column contains the empty string, there are no expectations on the
+number of clients.</li>
+<li><b>clients:</b> Estimated number of clients.</li>
+<li><b>frac:</b> Fraction of relays or bridges in percent that the
+estimate is based on.
+The higher this value, the more reliable is the estimate.
+Values above 50 can be considered reliable enough for most purposes,
+lower values should be handled with more care.</li>
+</ul>
+<hr>
+<br>
+
+<a name="torperf"></a>
+<h3><a href="#torperf" class="anchor">Performance of downloading static
+files over Tor</a></h3>
+<br>
+<p>Statistics file <a href="stats/torperf.csv">torperf.csv</a> contains
+aggregate statistics on download performance over time.
+These statistics come from the Torperf service that periodically downloads
+static files over Tor.
+The statistics file contains the following columns:</p>
+
+<ul>
+<li><b>date:</b> UTC date (YYYY-MM-DD) when download performance was
+measured.</li>
+<li><b>size:</b> Size of the downloaded file in bytes.</li>
+<li><b>source:</b> Name of the Torperf service performing measurements.
+If this column contains the empty string, all measurements are included,
+regardless of which Torperf service performed them.
+Examples are <b>"moria"</b>, <b>"siv"</b>, and <b>"torperf"</b>.</li>
+<li><b>q1:</b> First quartile of time until receiving the last byte in
+milliseconds.</li>
+<li><b>md:</b> Median of time until receiving the last byte in
+milliseconds.</li>
+<li><b>q3:</b> Third quartile of time until receiving the last byte in
+milliseconds.</li>
+<li><b>timeouts:</b> Number of timeouts that occurred when attempting to
+download the static file over Tor.</li>
+<li><b>failures:</b> Number of failures that occurred when attempting to
+download the static file over Tor.</li>
+<li><b>requests:</b> Total number of requests made to download the static
+file over Tor.</li>
+</ul>
+<hr>
+<br>
+
+<a name="connbidirect"></a>
+<h3><a href="#connbidirect" class="anchor">Fraction of connections used
+uni-/bidirectionally</a></h3>
+<br>
+<p>Statistics file <a href="stats/connbidirect.csv">connbidirect.csv</a>
+contains statistics on the fraction of connections that is used uni- or
+bidirectionally.
+Every 10 seconds, relays determine for every connection whether they read
+and wrote less than a threshold of 20 KiB.
+For the remaining connections, relays report whether they read/wrote at
+least 10 times as many bytes as they wrote/read.
+If so, they classify a connection as "mostly reading" or "mostly writing,"
+respectively.
+All other connections are classified as "both reading and writing."
+After classifying connections, read and write counters are reset for the
+next 10-second interval.
+Statistics are aggregated over 24 hours.
+The statistics file contains the following columns:</p>
+
+<ul>
+<li><b>date:</b> UTC date (YYYY-MM-DD) for which statistics on
+uni-/bidirectional connection usage were reported.</li>
+<li><b>source:</b> Fingerprint of the relay reporting statistics.</li>
+<li><b>below:</b> Number of 10-second intervals of connections with less
+than 20 KiB read and written data.</li>
+<li><b>read:</b> Number of 10-second intervals of connections with 10
+times as many read bytes as written bytes.</li>
+<li><b>write:</b> Number of 10-second intervals of connections with 10
+times as many written bytes as read bytes.</li>
+<li><b>both:</b> Number of 10-second intervals of connections with less
+than 10 times as many written or read bytes as in the other
+direction.</li>
+</ul>
+    </div>
+  </div>
+  <div class="bottom" id="bottom">
+    <%@ include file="footer.jsp"%>
+  </div>
+</body>
+</html>
+</body>
+</html>
+
diff --git a/website/web/WEB-INF/status.jsp b/website/web/WEB-INF/status.jsp
new file mode 100644
index 0000000..d6b565e
--- /dev/null
+++ b/website/web/WEB-INF/status.jsp
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<html>
+<head>
+  <title>Tor Metrics Portal: Status</title>
+  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
+  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
+  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
+</head>
+<body>
+  <div class="center">
+    <%@ include file="banner.jsp"%>
+    <div class="main-column">
+        <h2>Tor Metrics Portal: Status</h2>
+        <br>
+        <p>The network data collected by the Tor Metrics Project can be
+        used to analyze the Tor network status from a few years ago until
+        an hour ago. There are currently two applications for this data:
+        The <a href="exonerator.html">ExoneraTor</a> tells you whether
+        some IP address was a Tor relay at a given time, the
+        <a href="relay-search.html">Relay Search</a> lets you search the
+        descriptor archive for a relay, and the
+        <a href="consensus-health.html">Consensus Health</a> summarizes
+        information about the latest network consensus voting process.
+    </div>
+  </div>
+  <div class="bottom" id="bottom">
+    <%@ include file="footer.jsp"%>
+  </div>
+</body>
+</html>
diff --git a/website/web/WEB-INF/tools.jsp b/website/web/WEB-INF/tools.jsp
new file mode 100644
index 0000000..8929d74
--- /dev/null
+++ b/website/web/WEB-INF/tools.jsp
@@ -0,0 +1,110 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<html>
+<head>
+  <title>Tor Metrics Portal: Tools</title>
+  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
+  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
+  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
+</head>
+<body>
+  <div class="center">
+    <%@ include file="banner.jsp"%>
+    <div class="main-column">
+        <h2>Tor Metrics Portal: Tools</h2>
+        <br>
+        <p>The metrics website and related websites depend on a collection
+        of services that measure, archive, process, and finally present Tor
+        network data.
+        This page gives an overview of these services.</p>
+        <img src="/images/tor-metrics-overview.png"/>
+        <br>
+        <a name="measure"></a>
+        <h3><a href="#measure" class="anchor">Measuring Tor network
+        data</a></h3>
+        <br>
+        <p>Tor network data is measured at various places:
+        <ul>
+          <li><a href="https://gitweb.torproject.org/torperf.git">Torperf</a>
+          is a set of utilities for testing Tor performance from a client
+          perspective.</li>
+          <li><a href="https://gitweb.torproject.org/tor.git">tor</a>
+          relays and bridges gather aggregate usage statistics and publish
+          descriptors containing data about Tor network structure and
+          usage.</li>
+          <li><a href="https://gitweb.torproject.org/tordnsel.git">TorDNSEL</a>
+          is a Tor DNS-based exit list that runs periodic checks whether
+          relays use different IP addresses for exiting to the Internet
+          than they use to register in the Tor network.</li>
+          <li><a href="https://gitweb.torproject.org/bridgedb.git">BridgeDB</a>
+          assigns bridges to distributors and gives them out via HTTPS or
+          email.</li>
+        </ul>
+        <br>
+        <a name="archive"></a>
+        <h3><a href="#archive" class="anchor">Archiving Tor network
+        data</a></h3>
+        <br>
+        <p>All Tor network data is downloaded, possibly sanitized, and
+        then archived by a single tool:</p>
+        <ul>
+          <li><a href="https://gitweb.torproject.org/metrics-db.git">metrics-db</a>
+          contains five components for archiving relay descriptors, bridge
+          descriptors, Torperf results, TorDNSEL exit lists, and BridgeDB
+          pool assignments.</li>
+        </ul>
+        <br>
+        <a name="process"></a>
+        <h3><a href="#process" class="anchor">Processing Tor network
+        data</a></h3>
+        <br>
+        <p>In some cases, processing and presenting Tor network data is
+        separated for maximum flexibility.
+        In particular, there are currently two main tools that process Tor
+        network data and write an intermediate data format, but don't
+        directly present results:</p>
+        <ul>
+          <li><a href="https://gitweb.torproject.org/metrics-web.git">metrics-web</a>
+          is the software behind this website, including aggregation code
+          that produces statistics files.</li>
+          <li><a href="https://gitweb.torproject.org/metrics-tasks.git/tree/HEAD:/task-6498">task-6498</a>
+          is a submodule of metric-web that
+          aggregates data to visualize fast exits in the Tor network.</li>
+          <li><a href="https://gitweb.torproject.org/metrics-tasks.git/tree/HEAD:/task-8462">task-8462</a>
+          is another submodule of metric-web that
+          estimates daily users from reported directory request
+          statistics.</li>
+          <li><a href="https://gitweb.torproject.org/onionoo.git">Onionoo</a>
+          provides Tor network status information in JSON format via a
+          RESTful web service.</li>
+        </ul>
+        <br>
+        <a name="present"></a>
+        <h3><a href="#present" class="anchor">Presenting Tor network
+        data</a></h3>
+        <br>
+        <p>There are a few websites and additional tools presenting Tor
+        network data:
+        <ul>
+          <li><a href="https://gitweb.torproject.org/metrics-web.git">metrics-web</a>
+          also contains the code that presents aggregate statistics on
+          this website.</li>
+          <li><a href="https://gitweb.torproject.org/exonerator.git">ExoneraTor</a>
+          is a website that tells you whether a given IP address was a Tor
+          relay.</li>
+          <li><a href="https://gitweb.torproject.org/atlas.git">Atlas</a>
+          is a web application to discover relays that uses Onionoo as its
+          data back-end.</li>
+          <li><a href="https://gitweb.torproject.org/globe.git">Globe</a>
+          is a Tor relay and bridge explorer that also uses Onionoo as its
+          data back-end.</li>
+          <li><a href="https://gitweb.torproject.org/compass.git">Compass</a>
+          is a web application that uses Onionoo's data to display
+          information about fast exits in the Tor network.</li>
+        </ul>
+    </div>
+  </div>
+  <div class="bottom" id="bottom">
+    <%@ include file="footer.jsp"%>
+  </div>
+</body>
+</html>
diff --git a/website/web/WEB-INF/users.jsp b/website/web/WEB-INF/users.jsp
new file mode 100644
index 0000000..ec9f200
--- /dev/null
+++ b/website/web/WEB-INF/users.jsp
@@ -0,0 +1,390 @@
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
+<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
+<html>
+<head>
+  <title>Tor Metrics Portal: Users</title>
+  <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
+  <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet">
+  <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon">
+</head>
+<body>
+  <div class="center">
+    <%@ include file="banner.jsp"%>
+    <div class="main-column">
+<h2>Tor Metrics Portal: Users</h2>
+<br>
+
+<a name="userstats-relay-country"></a>
+<p><b>Direct users by country:</b></p>
+
+<img src="userstats-relay-country.png${userstats_relay_country_url}"
+     width="576" height="360" alt="Direct users by country graph">
+<form action="users.html#userstats-relay-country">
+  <div class="formrow">
+    <input type="hidden" name="graph" value="userstats-relay-country">
+    <p>
+    <label>Start date (yyyy-mm-dd):</label>
+      <input type="text" name="start" size="10"
+             value="<c:choose><c:when test="${fn:length(userstats_relay_country_start) == 0}">${default_start_date}</c:when><c:otherwise>${userstats_relay_country_start[0]}</c:otherwise></c:choose>">
+    <label>End date (yyyy-mm-dd):</label>
+      <input type="text" name="end" size="10"
+             value="<c:choose><c:when test="${fn:length(userstats_relay_country_end) == 0}">${default_end_date}</c:when><c:otherwise>${userstats_relay_country_end[0]}</c:otherwise></c:choose>">
+    </p><p>
+      Source: <select name="country">
+        <option value="all"<c:if test="${userstats_relay_country_country[0] eq 'all'}"> selected</c:if>>All users</option>
+        <c:forEach var="country" items="${countries}" >
+          <option value="${country[0]}"<c:if test="${userstats_relay_country_country[0] eq country[0]}"> selected</c:if>>${country[1]}</option>
+        </c:forEach>
+      </select>
+    </p><p>
+      Show possible censorship events if available (<a
+      href="http://research.torproject.org/techreports/detector-2011-09-09.pdf">BETA</a>)
+      <select name="events">
+        <option value="off">Off</option>
+        <option value="on"<c:if test="${userstats_relay_country_events[0] eq 'on'}"> selected</c:if>>On: both points and expected range</option>
+        <option value="points"<c:if test="${userstats_relay_country_events[0] eq 'points'}"> selected</c:if>>On: points only, no expected range</option>
+      </select>
+    </p><p>
+    <input class="submit" type="submit" value="Update graph">
+    </p>
+  </div>
+</form>
+<p>Download graph as
+<a href="userstats-relay-country.pdf${userstats_relay_country_url}">PDF</a> or
+<a href="userstats-relay-country.svg${userstats_relay_country_url}">SVG</a>.</p>
+<hr>
+<a name="userstats-relay-table"></a>
+<p><b>Top-10 countries by directly connecting users:</b></p>
+<form action="users.html#userstats-relay-table">
+  <div class="formrow">
+    <input type="hidden" name="table" value="userstats-relay">
+    <p>
+    <label>Start date (yyyy-mm-dd):</label>
+      <input type="text" name="start" size="10"
+             value="<c:choose><c:when test="${fn:length(userstats_relay_start) == 0}">${default_start_date}</c:when><c:otherwise>${userstats_relay_start[0]}</c:otherwise></c:choose>">
+    <label>End date (yyyy-mm-dd):</label>
+      <input type="text" name="end" size="10"
+             value="<c:choose><c:when test="${fn:length(userstats_relay_end) == 0}">${default_end_date}</c:when><c:otherwise>${userstats_relay_end[0]}</c:otherwise></c:choose>">
+    </p><p>
+    <input class="submit" type="submit" value="Update table">
+    </p>
+  </div>
+</form>
+<br>
+<table>
+  <tr>
+    <th>Country</th>
+    <th>Mean daily users</th>
+  </tr>
+  <c:forEach var="row" items="${userstats_relay_tabledata}">
+    <tr>
+      <td><a href="users.html?graph=userstats-relay-country&country=${row['cc']}#userstats-relay-country">${row['country']}</a> </td>
+      <td>${row['abs']} (<fmt:formatNumber type="number" minFractionDigits="2" value="${row['rel']}" /> %)</td>
+    </tr>
+  </c:forEach>
+</table>
+<hr>
+<a name="userstats-censorship-events"></a>
+<p><b>Top-10 countries by possible censorship events (<a
+      href="http://research.torproject.org/techreports/detector-2011-09-09.pdf">BETA</a>):</b></p>
+<form action="users.html#userstats-censorship-events">
+  <div class="formrow">
+    <input type="hidden" name="table" value="userstats-censorship-events">
+    <p>
+    <label>Start date (yyyy-mm-dd):</label>
+      <input type="text" name="start" size="10"
+             value="<c:choose><c:when test="${fn:length(userstats_censorship_events_start) == 0}">${default_start_date}</c:when><c:otherwise>${userstats_censorship_events_start[0]}</c:otherwise></c:choose>">
+    <label>End date (yyyy-mm-dd):</label>
+      <input type="text" name="end" size="10"
+             value="<c:choose><c:when test="${fn:length(userstats_censorship_events_end) == 0}">${default_end_date}</c:when><c:otherwise>${userstats_censorship_events_end[0]}</c:otherwise></c:choose>">
+    </p><p>
+    <input class="submit" type="submit" value="Update table">
+    </p>
+  </div>
+</form>
+<br>
+<table>
+  <tr>
+    <th>Country</th>
+    <th>Downturns</th>
+    <th>Upturns</th>
+  </tr>
+  <c:forEach var="row" items="${userstats_censorship_events_tabledata}">
+    <tr>
+      <td><a href="users.html?graph=userstats-relay-country&country=${row['cc']}&events=on#userstats-relay-country">${row['country']}</a> </td>
+      <td>${row['downturns']}</td>
+      <td>${row['upturns']}</td>
+    </tr>
+  </c:forEach>
+</table>
+<hr>
+
+<a name="userstats-bridge-country"></a>
+<p><b>Bridge users by country:</b></p>
+
+<img src="userstats-bridge-country.png${userstats_bridge_country_url}"
+     width="576" height="360" alt="Bridge users by country graph">
+<form action="users.html#userstats-bridge-country">
+  <div class="formrow">
+    <input type="hidden" name="graph" value="userstats-bridge-country">
+    <p>
+    <label>Start date (yyyy-mm-dd):</label>
+      <input type="text" name="start" size="10"
+             value="<c:choose><c:when test="${fn:length(userstats_bridge_country_start) == 0}">${default_start_date}</c:when><c:otherwise>${userstats_bridge_country_start[0]}</c:otherwise></c:choose>">
+    <label>End date (yyyy-mm-dd):</label>
+      <input type="text" name="end" size="10"
+             value="<c:choose><c:when test="${fn:length(userstats_bridge_country_end) == 0}">${default_end_date}</c:when><c:otherwise>${userstats_bridge_country_end[0]}</c:otherwise></c:choose>">
+    </p><p>
+      Source: <select name="country">
+        <option value="all"<c:if test="${userstats_bridge_country_country[0] eq 'all'}"> selected</c:if>>All users</option>
+        <c:forEach var="country" items="${countries}" >
+          <option value="${country[0]}"<c:if test="${userstats_bridge_country_country[0] eq country[0]}"> selected</c:if>>${country[1]}</option>
+        </c:forEach>
+      </select>
+    </p><p>
+    <input class="submit" type="submit" value="Update graph">
+    </p>
+  </div>
+</form>
+<p>Download graph as
+<a href="userstats-bridge-country.pdf${userstats_bridge_country_url}">PDF</a> or
+<a href="userstats-bridge-country.svg${userstats_bridge_country_url}">SVG</a>.</p>
+<hr>
+<a name="userstats-bridge-table"></a>
+<p><b>Top-10 countries by bridge users:</b></p>
+<form action="users.html#userstats-bridge-table">
+  <div class="formrow">
+    <input type="hidden" name="table" value="userstats-bridge">
+    <p>
+    <label>Start date (yyyy-mm-dd):</label>
+      <input type="text" name="start" size="10"
+             value="<c:choose><c:when test="${fn:length(userstats_bridge_start) == 0}">${default_start_date}</c:when><c:otherwise>${userstats_bridge_start[0]}</c:otherwise></c:choose>">
+    <label>End date (yyyy-mm-dd):</label>
+      <input type="text" name="end" size="10"
+             value="<c:choose><c:when test="${fn:length(userstats_bridge_end) == 0}">${default_end_date}</c:when><c:otherwise>${userstats_bridge_end[0]}</c:otherwise></c:choose>">
+    </p><p>
+    <input class="submit" type="submit" value="Update table">
+    </p>
+  </div>
+</form>
+<br>
+<table>
+  <tr>
+    <th>Country</th>
+    <th>Mean daily users</th>
+  </tr>
+  <c:forEach var="row" items="${userstats_bridge_tabledata}">
+    <tr>
+      <td><a href="users.html?graph=userstats-bridge-country&country=${row['cc']}#userstats-bridge-country">${row['country']}</a> </td>
+      <td>${row['abs']} (<fmt:formatNumber type="number" minFractionDigits="2" value="${row['rel']}" /> %)</td>
+    </tr>
+  </c:forEach>
+</table>
+<hr>
+
+<a name="userstats-bridge-transport"></a>
+<p><b>Bridge users by transport:</b></p>
+
+<img src="userstats-bridge-transport.png${userstats_bridge_transport_url}"
+     width="576" height="360" alt="Bridge users by transport graph">
+<form action="users.html#userstats-bridge-transport">
+  <div class="formrow">
+    <input type="hidden" name="graph" value="userstats-bridge-transport">
+    <p>
+    <label>Start date (yyyy-mm-dd):</label>
+      <input type="text" name="start" size="10"
+             value="<c:choose><c:when test="${fn:length(userstats_bridge_transport_start) == 0}">${default_start_date}</c:when><c:otherwise>${userstats_bridge_transport_start[0]}</c:otherwise></c:choose>">
+    <label>End date (yyyy-mm-dd):</label>
+      <input type="text" name="end" size="10"
+             value="<c:choose><c:when test="${fn:length(userstats_bridge_transport_end) == 0}">${default_end_date}</c:when><c:otherwise>${userstats_bridge_transport_end[0]}</c:otherwise></c:choose>">
+    </p><p>
+      Source: <select name="transport">
+        <option value="<OR>"<c:if test="${userstats_bridge_transport_transport[0] eq '<OR>'}"> selected</c:if>>Default OR protocol</option>
+        <option value="obfs2"<c:if test="${userstats_bridge_transport_transport[0] eq 'obfs2'}"> selected</c:if>>obfs2</option>
+        <option value="obfs3"<c:if test="${userstats_bridge_transport_transport[0] eq 'obfs3'}"> selected</c:if>>obfs3</option>
+        <option value="websocket"<c:if test="${userstats_bridge_transport_transport[0] eq 'websocket'}"> selected</c:if>>Flash proxy/websocket</option>
+        <option value="<??>"<c:if test="${userstats_bridge_transport_transport[0] eq '<??>'}"> selected</c:if>>Unknown transport</option>
+      </select>
+    </p><p>
+    <input class="submit" type="submit" value="Update graph">
+    </p>
+  </div>
+</form>
+<p>Download graph as
+<a href="userstats-bridge-transport.pdf${userstats_bridge_transport_url}">PDF</a> or
+<a href="userstats-bridge-transport.svg${userstats_bridge_transport_url}">SVG</a>.</p>
+<hr>
+
+<a name="userstats-bridge-version"></a>
+<p><b>Bridge users by IP version:</b></p>
+
+<img src="userstats-bridge-version.png${userstats_bridge_version_url}"
+     width="576" height="360" alt="Bridge users by IP version graph">
+<form action="users.html#userstats-bridge-version">
+  <div class="formrow">
+    <input type="hidden" name="graph" value="userstats-bridge-version">
+    <p>
+    <label>Start date (yyyy-mm-dd):</label>
+      <input type="text" name="start" size="10"
+             value="<c:choose><c:when test="${fn:length(userstats_bridge_version_start) == 0}">${default_start_date}</c:when><c:otherwise>${userstats_bridge_version_start[0]}</c:otherwise></c:choose>">
+    <label>End date (yyyy-mm-dd):</label>
+      <input type="text" name="end" size="10"
+             value="<c:choose><c:when test="${fn:length(userstats_bridge_version_end) == 0}">${default_end_date}</c:when><c:otherwise>${userstats_bridge_version_end[0]}</c:otherwise></c:choose>">
+    </p><p>
+      Source: <select name="version">
+        <option value="v4"<c:if test="${userstats_bridge_version_version[0] eq 'v4'}"> selected</c:if>>IPv4</option>
+        <option value="v6"<c:if test="${userstats_bridge_version_version[0] eq 'v6'}"> selected</c:if>>IPv6</option>
+      </select>
+    </p><p>
+    <input class="submit" type="submit" value="Update graph">
+    </p>
+  </div>
+</form>
+<p>Download graph as
+<a href="userstats-bridge-version.pdf${userstats_bridge_version_url}">PDF</a> or
+<a href="userstats-bridge-version.svg${userstats_bridge_version_url}">SVG</a>.</p>
+<hr>
+
+<p><a href="stats/clients.csv">CSV</a> file containing new user
+estimates.</p>
+<br>
+
+<hr>
+<a name="questions-and-answers"></a>
+<p><b>Questions and answers</b></p>
+<p>
+Q: How is it even possible to count users in an anonymity network?<br/>
+A: We actually don't count users, but we count requests to the directories
+that clients make periodically to update their list of relays and estimate
+user numbers indirectly from there.
+</p>
+<p>
+Q: Do all directories report these directory request numbers?<br/>
+A: No, but we can see what fraction of directories reported them, and then
+we can extrapolate the total number in the network.
+</p>
+
+<p>
+Q: How do you get from these directory requests to user numbers?<br/>
+A: We put in the assumption that the average client makes 10 such requests
+per day.  A tor client that is connected 24/7 makes about 15 requests per
+day, but not all clients are connected 24/7, so we picked the number 10
+for the average client.  We simply divide directory requests by 10 and
+consider the result as the number of users.
+</p>
+
+<p>
+Q: So, are these distinct users per day, average number of users connected
+over the day, or what?<br/>
+A: Average number of users connected over the day.  We can't say how many
+distinct users there are.
+</p>
+
+<p>
+Q: Are these tor clients or users?  What if there's more than one user
+behind a tor client?<br/>
+A: Then we count those users as one.  We really count clients, but it's
+more intuitive for most people to think of users, that's why we say users
+and not clients.
+</p>
+
+<p>
+Q: What if a user runs tor on a laptop and changes their IP address a few
+times per day?  Don't you overcount that user?<br/>
+A: No, because that user updates their list of relays as often as a user
+that doesn't change IP address over the day.
+</p>
+
+<p>
+Q: How do you know which countries users come from?<br/>
+A: The directories resolve IP addresses to country codes and report these
+numbers in aggregate form.  This is one of the reasons why tor ships with
+a GeoIP database.
+</p>
+
+<p>
+Q: Why are there so few bridge users that are not using the default OR
+protocol or that are using IPv6?<br/>
+A: Very few bridges report data on transports or IP versions yet, and by
+default we consider requests to use the default OR protocol and IPv4.
+Once more bridges report these data, the numbers will become more
+accurate.
+</p>
+
+<p>
+Q: Why do the graphs end 2 days in the past and not today?<br/>
+A: Relays and bridges report some of the data in 24-hour intervals which
+may end at any time of the day.  And after such an interval is over relays
+and bridges might take another 18 hours to report the data.  We cut off
+the last two days from the graphs, because we want to avoid that the last
+data point in a graph indicates a recent trend change which is in fact
+just an artifact of the algorithm.
+</p>
+
+<p>
+Q: But I noticed that the last data point went up/down a bit since I last
+looked a few hours ago.  Why is that?<br/>
+A: You're an excellent observer!  The reason is that we publish user
+numbers once we're confident enough that they won't change significantly
+anymore.  But it's always possible that a directory reports data a few
+hours after we were confident enough, but which then slightly changed the
+graph.
+</p>
+
+<p>
+Q: Why are no numbers available before September 2011?<br/>
+A: We do have descriptor archives from before that time, but those
+descriptors didn't contain all the data we use to estimate user numbers.
+We do have older user numbers from an earlier estimation approach
+<a href="/data/old-user-number-estimates.tar.gz">here</a>, but we believe
+the current approach is more accurate.
+</p>
+
+<p>
+Q: Why do you believe the current approach to estimate user numbers is
+more accurate?<br/>
+A: For direct users, we include all directories which we didn't do in the
+old approach.  We also use histories that only contain bytes written to
+answer directory requests, which is more precise than using general byte
+histories.
+</p>
+
+<p>
+Q: And what about the advantage of the current approach over the old one
+when it comes to bridge users?<br/>
+A: Oh, that's a whole different story.  We wrote a 13 page long
+<a href="https://research.torproject.org/techreports/counting-daily-bridge-users-2012-10-24.pdf">technical
+report</a> explaining the reasons for retiring the old approach.
+tl;dr: in the old approach we measured the wrong thing, and now we measure
+the right thing.
+</p>
+
+<p>
+Q: Are the data and the source code for estimating these user numbers
+available?<br/>
+A: Sure, <a href="/data.html">data</a> and
+<a href="https://gitweb.torproject.org/metrics-tasks.git/tree/HEAD:/task-8462">source
+code</a> are publicly available.
+</p>
+
+<p>
+Q: What are these red and blue dots indicating possible censorship
+events?<br/>
+A: We run an anomaly-based censorship-detection system that looks at
+estimated user numbers over a series of days and predicts the user number
+in the next days.  If the actual number is higher or lower, this might
+indicate a possible censorship event or release of censorship.  For more
+details, see our
+<a href="https://research.torproject.org/techreports/detector-2011-09-09.pdf">technical
+report</a>.
+</p>
+
+    </div>
+  </div>
+  <div class="bottom" id="bottom">
+    <%@ include file="footer.jsp"%>
+  </div>
+</body>
+</html>
diff --git a/website/web/css/stylesheet-ltr.css b/website/web/css/stylesheet-ltr.css
new file mode 100644
index 0000000..ce0c54e
--- /dev/null
+++ b/website/web/css/stylesheet-ltr.css
@@ -0,0 +1,161 @@
+body {
+    background-color: white;
+    margin-top: 0px;
+    font-family: Arial, Helvetica, sans-serif;
+    font-size: 1em;
+    font-style: normal;
+    color: #000000;
+    padding-top: 0px;
+}
+
+/* images */
+
+img {
+    border: 0;
+}
+
+
+li {
+   margin: .2em .2em .2em 1em;
+}
+
+/* this centers the page */
+
+.center {
+    text-align: center;
+    background-color: white;
+    margin: 0px auto 0 auto;
+    width: 85%;
+}
+
+.center table {
+    margin-left: auto;
+    margin-right: auto;
+    text-align: left;
+}
+
+div.bottom {
+    font-size: 0.8em;
+    margin-top: 0.5cm;
+    margin-left: 1em;
+    margin-right: 1em;
+    text-align: center;
+}
+
+/* The main column (left text) */
+
+div.main-column {
+    padding: 15px 0 10px 10px;
+    text-indent: 0pt;
+    font-size: 1em;
+    direction: ltr;
+    text-align: left;
+}
+
+/* formatting styles */
+
+h2 {
+    font-size: 1.4em;
+    margin-bottom: 0em;
+    font-weight: bold;
+    margin-top: 0;
+}
+
+h3 {
+    font-size: 1.2em;
+    margin-bottom: 0em;
+    font-weight: bold;
+    margin-top: 0;
+}
+
+p {
+    margin-top: 0;
+    margin-bottom: 1em;
+}
+
+a:link {
+    color: blue;
+    font-size: 1em;
+}
+
+a:visited {
+    color: purple;
+    font-size: 1em;
+}
+
+a.anchor {
+    font-size: 1em;
+    color: black;
+    font-weight: bold;
+    text-decoration: none;
+}
+
+td {
+    vertical-align: top;
+}
+
+/* the banner */
+
+table.banner {
+    width: 100%;
+    height: 79px;
+    margin-left: auto;
+    margin-right: auto;
+}
+
+td.banner-left {
+	/* This is done with an <img> in the HTML so it can be clickable
+    background-image: url("images/top-left.png");
+    background-repeat: no-repeat; */
+    width: 193px;
+}
+
+td.banner-middle {
+    background-color: #00802B;
+    background-image: url("/images/top-middle.png");
+    background-repeat: repeat-x;
+    vertical-align: bottom;
+    padding-bottom: 10px;
+    color: white;
+    font-weight: bold;
+    font-size: 1.2em;
+}
+
+td.banner-middle a, td.banner-middle a:visited {
+    margin-right: 5px;
+    color: white;
+    font-weight: bold;
+    font-size: 1em;
+}
+
+td.banner-middle a:hover {
+    color: #FF7F00;
+    font-weight: bold;
+    font-size: 1em;
+}
+
+td.banner-right {
+    background-image: url("/images/top-right.png");
+    background-repeat: no-repeat;
+    width: 15px;
+    background-position: right;
+    padding-top: 8px;
+}
+
+.banner-middle a.current {
+    text-decoration: none;
+    color: #FF7F00;
+    font-weight: bold;
+    font-size: 1em;
+    width: auto;
+    left: -50px;
+}
+
+hr {
+	background-color:#002200;
+	color:#666666;
+	font-size:1px;
+	height:1px;
+	line-height:0;
+	margin:15px 0 5px;
+}
diff --git a/website/web/images/favicon.ico b/website/web/images/favicon.ico
new file mode 100644
index 0000000..48060b1
Binary files /dev/null and b/website/web/images/favicon.ico differ
diff --git a/website/web/images/top-left.png b/website/web/images/top-left.png
new file mode 100644
index 0000000..9927355
Binary files /dev/null and b/website/web/images/top-left.png differ
diff --git a/website/web/images/top-middle.png b/website/web/images/top-middle.png
new file mode 100644
index 0000000..1035dc5
Binary files /dev/null and b/website/web/images/top-middle.png differ
diff --git a/website/web/images/top-right.png b/website/web/images/top-right.png
new file mode 100644
index 0000000..54623ef
Binary files /dev/null and b/website/web/images/top-right.png differ
diff --git a/website/web/images/tor-metrics-overview.png b/website/web/images/tor-metrics-overview.png
new file mode 100644
index 0000000..ee2bc07
Binary files /dev/null and b/website/web/images/tor-metrics-overview.png differ
diff --git a/website/web/js/bubbles.js b/website/web/js/bubbles.js
new file mode 100644
index 0000000..8030e20
--- /dev/null
+++ b/website/web/js/bubbles.js
@@ -0,0 +1,288 @@
+function get_prefix(relay) { return /^[0-9]+\.[0-9]+\./.exec(relay.or_addresses[0]); }
+
+var graphs = {
+     'default': {
+         extra_fields: [],
+         group: undefined,
+         group_id_func: function(relay) { return undefined; },
+         group_name_func: function(relay) { return 'Relays'; },
+  }, 'as': {
+         extra_fields: ['as_number', 'as_name'],
+         group: "autonomous systems",
+         group_id_func: function(relay) { return relay.as_number; },
+         group_name_func: function(relay) { return relay.as_name; },
+  }, 'contact': {
+         extra_fields: ['contact'],
+         group: "contact infos",
+         group_id_func: function(relay) { return relay.contact; },
+         group_name_func: function(relay) { return relay.contact; },
+  }, 'country': {
+         extra_fields: ['country', 'country_name'],
+         group: "countries",
+         group_id_func: function(relay) { return relay.country; },
+         group_name_func: function(relay) { return relay.country_name; },
+  }, 'network-family': {
+         extra_fields: ['or_addresses'],
+         group: "network families (/16)",
+         group_id_func: function(relay) { return get_prefix(relay); },
+         group_name_func: function(relay) { return get_prefix(relay) + "0.0/16"; },
+  },
+};
+
+function make_bubble_graph(graph_name) {
+  var onionoo_url = "https://onionoo.torproject.org/details?type=relay&running=true&fields=consensus_weight,running,nickname,exit_probability,advertised_bandwidth";
+  var diameter = 800;
+  var legendWidth = 270;
+  var legendIconSize = 50;
+  var legendItems = 3;
+  var legendIconMargin = 15;
+  var legendHeight = legendItems * (legendIconMargin * 2 + legendIconSize) - legendIconMargin;
+
+  var cutOff = 100 / 8.0 * 1000.0 * 1000.0; // 100 Mbit/s
+
+  var format = d3.format(",d");
+  var color = d3.scale.category20c();
+
+  var old_graph = document.getElementById("bubble-graph");
+  if (old_graph) {
+     old_graph.parentNode.removeChild(old_graph);
+  }
+
+  var bubble = d3.layout.pack()
+    .sort(null)
+    .size([diameter, diameter])
+    .padding(1.5);
+
+  var svg = d3.select("body").append("svg")
+    .attr("id", "bubble-graph")
+    .attr("width", diameter + legendWidth)
+    .attr("height", diameter)
+    .attr("class", "bubble");
+
+  var defs = svg.append("defs")
+
+  defs.append("filter")
+      .attr("id", "middle-filter")
+      .append("feColorMatrix")
+        .attr("type", "hueRotate")
+        .attr("in", "SourceGraphic")
+        .attr("values", "90");
+
+  var node_circle = defs.append("svg").attr("id", "node-circle").attr("viewBox", "0 0 120.50655 184.78298");
+
+  node_circle.append("path")
+      .attr("style", "fill:#7d4698;fill-opacity:1;stroke:none")
+      .attr("d", "m 119.19492,135.63045 a 56.525425,56.525425 0 1 1 -113.0508541,0 56.525425,56.525425 0 1 1 113.0508541,0 z")
+      .attr("transform", "matrix(1.048913,0,0,0.95108692,-5.4815686,2.0260454)");
+
+  var node_onion = defs.append("svg").attr("id", "node-onion").attr("viewBox", "0 0 120.50655 184.78298")
+      .append("g")
+        .attr("transform", "translate(-195.35846,-64.183782)");
+
+  node_onion.append("path")
+        .attr("style", "fill:#abcd03;fill-rule:evenodd;stroke:none")
+        .attr("d", "m 264.513,77.977773 -4.917,19.529001 c 6.965,-13.793001 18.027,-24.172001 30.729,-33.323001 -9.287,10.789 -17.754,21.579001 -22.944,32.368001 8.741,-12.292001 20.486,-19.120001 33.733,-23.627001 -17.618,15.706001 -31.60228,32.559277 -42.25528,49.494277 l -8.467,-3.687 c 1.501,-13.521 6.60928,-27.369276 14.12128,-40.754277 z");
+  node_onion.append("path")
+        .attr("style", "fill:#fffcdb;fill-rule:evenodd;stroke:none")
+        .attr("d", "m 241.90113,115.14152 16.116,6.68594 c 0,4.098 -0.33313,16.59703 2.22938,20.28403 26.80289,34.5191 22.29349,103.71329 -5.42951,105.48829 -42.21656,0 -58.317,-28.679 -58.317,-55.03801 0,-24.037 28.816,-40.016 46.025,-54.219 4.37,-3.824 3.61113,-12.27525 -0.62387,-23.20125 z")
+  node_onion.append("path")
+        .attr("style", "fill:#7d4698;fill-rule:evenodd;stroke:none")
+        .attr("d", "m 258.02197,121.58695 5.80803,2.96282 c -0.546,3.823 0.273,12.292 4.096,14.476 16.936,10.516 32.914,21.988 39.197,33.46 22.398,40.42601 -15.706,77.84601 -48.62,74.29501 17.891,-13.248 23.081,-40.42501 16.389,-70.06201 -2.731,-11.609 -6.966,-22.125 -14.478,-34.007 -3.25421,-5.83246 -2.11803,-13.06582 -2.39203,-21.12482 z");
+  node_onion.append("path")
+        .attr("style", "fill:#000000;fill-opacity:1;stroke:none")
+        .attr("d", "m 255.226,120.58877 12.018,1.639 c -3.551,11.745 6.966,19.939 10.38,21.852 7.64801,4.234 15.02301,8.604 20.89601,13.93 11.063,10.106 17.345,24.31 17.345,39.333 0,14.886 -6.829,29.226 -18.301,38.786 -10.789,9.014 -25.67501,12.838 -40.15201,12.838 -9.014,0 -17.072,-0.409 -25.812,-3.278 -19.939,-6.692 -34.826,-23.763 -36.055,-44.25 -1.093,-15.979 2.458,-28.134 14.887,-40.835 6.418,-6.692 19.393,-14.34 28.271,-20.486 4.371,-3.005 9.014,-11.473 0.136,-27.451 l 1.776,-1.366 13.15659,8.81203 -11.10759,-4.57803 c 0.956,1.366 3.551,7.512 4.098,9.287 1.229,5.053 0.683,9.971 -0.41,12.155 -5.599,10.107 -15.159,12.838 -22.124,18.574 -12.292,10.106 -25.676,18.164 -24.174,45.888 0.683,13.657 11.336,30.319 27.314,38.104 9.014,4.371 19.394,6.146 29.91,6.692 9.423,0.41 27.45101,-5.19 37.28401,-13.384 10.516,-8.74 16.389,-21.988 16.389,-35.508 0,-13.658 -5.463,-26.632 -15.706,-35.783 -5.873,-5.326 -15.56901,-11.745 -21.57801,-15.16 -6.009,-3.414 -13.521,-12.974 -11.063,-22.124 z");
+  node_onion.append("path")
+        .attr("style", "fill:#000000;fill-opacity:1;stroke:none")
+        .attr("d", "m 251.539,140.80177 c -1.229,6.283 -2.595,17.618 -8.058,21.852 -2.322,1.638 -4.644,3.278 -7.102,4.916 -9.833,6.693 -19.667,12.974 -24.173,29.09 -0.956,3.415 -0.136,7.102 0.684,10.516 2.458,9.833 9.423,20.486 14.886,26.769 0,0.273 1.093,0.956 1.093,1.229 4.507,5.327 5.873,6.829 22.944,10.652 l -0.41,1.913 c -10.243,-2.731 -18.71,-5.189 -24.037,-11.336 0,-0.136 -0.956,-1.093 -0.956,-1.093 -5.736,-6.556 -12.702,-17.481 -15.296,-27.724 -0.956,-4.098 -1.775,-7.238 -0.683,-11.473 4.643,-16.661 14.75,-23.217 24.993,-30.182 2.322,-1.502 5.053,-2.869 7.238,-4.644 4.233,-3.14 6.554,-12.701 8.877,-20.485 z");
+  node_onion.append("path")
+        .attr("style", "fill:#000000;fill-opacity:1;stroke:none")
+        .attr("d", "m 255.90625,166.74951 c 0.137,7.102 -0.55625,10.66475 1.21875,15.71875 1.092,3.004 4.782,7.1015 5.875,11.0625 1.502,5.327 3.138,11.19901 3,14.75001 0,4.09799 -0.25625,11.74249 -2.03125,19.93749 -1.35362,6.77108 -4.47323,12.58153 -9.71875,15.875 -5.37327,-1.10644 -11.68224,-2.99521 -15.40625,-6.1875 -7.238,-6.282 -13.64875,-16.7865 -14.46875,-25.9375 -0.682,-7.51099 6.27275,-18.5885 15.96875,-24.1875 8.194,-4.78 10.1,-10.22775 11.875,-18.96875 -2.458,7.648 -4.7665,14.05925 -12.6875,18.15625 -11.472,6.009 -17.3585,16.09626 -16.8125,25.65625 0.819,12.291 5.7415,20.6195 15.4375,27.3125 4.097,2.868 11.75125,5.89875 16.53125,6.71875 l 0,-0.625 c 3.62493,-0.67888 8.31818,-6.63267 10.65625,-14.6875 2.049,-7.238 2.85675,-16.502 2.71875,-22.37499 -0.137,-3.414 -1.643,-10.80801 -4.375,-17.50001 -1.502,-3.687 -3.8095,-7.37375 -5.3125,-9.96875 -1.637,-2.597 -1.64875,-8.195 -2.46875,-14.75 z");
+  node_onion.append("path")
+        .attr("style", "fill:#000000;fill-opacity:1;stroke:none")
+        .attr("d", "m 255.09375,193.53076 c 0.136,4.78 2.056,10.90451 2.875,17.18751 0.684,4.64399 0.387,9.30824 0.25,13.40624 -0.13495,4.74323 -1.7152,13.24218 -3.875,17.375 -2.03673,-0.93403 -2.83294,-1.99922 -4.15625,-3.71875 -1.638,-2.322 -2.75075,-4.644 -3.84375,-7.375 -0.819,-2.049 -1.7765,-4.394 -2.1875,-7.125 -0.546,-4.097 -0.393,-10.5065 4.25,-17.06249 3.551,-5.19001 4.36475,-5.58476 5.59375,-11.59376 -1.64,5.326 -2.8625,5.869 -6.6875,10.37501 -4.233,4.917 -4.9375,12.15924 -4.9375,18.03124 0,2.459 0.9805,5.18725 1.9375,7.78125 1.092,2.732 2.02925,5.452 3.53125,7.5 2.25796,3.32082 5.14798,5.20922 6.5625,5.5625 0.009,0.002 0.022,-0.002 0.0312,0 0.0303,0.007 0.0649,0.0255 0.0937,0.0312 l 0,-0.15625 c 2.64982,-2.95437 4.24444,-5.88934 4.78125,-8.84375 0.683,-3.551 0.84,-7.10975 1.25,-11.34375 0.409,-3.551 0.11225,-8.334 -0.84375,-13.24999 -1.365,-6.146 -3.669,-12.41226 -4.625,-16.78126 z");
+  node_onion.append("path")
+        .attr("style", "fill:#000000;fill-opacity:1;stroke:none")
+        .attr("d", "m 255.499,135.06577 c 0.137,7.101 0.683,20.35 2.595,25.539 0.546,1.775 5.599,9.56 9.149,18.983 2.459,6.556 3.005,12.565 3.415,14.34 1.639,7.785 -0.41,20.896 -3.142,33.324 -1.365,6.692 -6.009,15.023 -11.335,18.301 l -1.092,1.912 c 3.005,-0.137 10.379,-7.375 12.974,-16.389 4.371,-15.296 6.146,-22.398 4.098,-39.333 -0.273,-1.64 -0.956,-7.238 -3.551,-13.248 -3.824,-9.151 -9.287,-17.891 -9.969,-19.667 -1.23,-2.867 -2.869,-15.295 -3.142,-23.762 z");
+  node_onion.append("path")
+        .attr("style", "fill:#000000;fill-opacity:1;stroke:none")
+        .attr("d", "m 258.06151,125.35303 c -0.40515,7.29812 -0.51351,9.98574 0.85149,15.31174 1.502,5.873 9.151,14.34 12.292,24.037 6.009,18.574 4.507,42.884 0.136,61.867 -1.638,6.691 -9.424,16.389 -17.208,19.529 l 5.736,1.366 c 3.141,-0.137 11.198,-7.648 14.34,-16.252 5.052,-13.521 6.009,-29.636 3.96,-46.571 -0.137,-1.639 -2.869,-16.252 -5.463,-22.398 -3.688,-9.15 -10.244,-17.345 -10.926,-19.119 -1.228,-3.005 -3.92651,-9.24362 -3.71849,-17.77074 z");
+
+  if (!graph_name) {
+    graph_name = window.location.hash.substring(1);
+  }
+  var exits_only = false;
+  if (/-exits-only$/.exec(graph_name)) {
+    exits_only = true;
+    graph_name = graph_name.replace("-exits-only", "");
+  }
+
+  var graph = graphs[graph_name];
+  if (!graph) {
+    graph = graphs['default'];
+  }
+
+  onionoo_url += ',' + graph.extra_fields.join(',')
+
+  d3.json(onionoo_url, function(error, data) {
+    var groups = {};
+    var relay_count = 0;
+    data.relays.forEach(function(relay) {
+      if (0 == relay.consensus_weight || !relay.running) {
+        return;
+      }
+      if (exits_only && relay.exit_probability == 0) {
+        return;
+      }
+      group_id = graph.group_id_func(relay);
+      group_name = graph.group_name_func(relay);
+      if (!group_id) {
+        group_id = 'unknown';
+        group_name = 'Unknown';
+      }
+      if (!groups.hasOwnProperty(group_id)) {
+        groups[group_id] = { name: group_name, children: [] };
+      }
+      groups[group_id].children.push(
+          { name: relay.nickname ? relay.fingerprint : relay.nickname,
+            value: relay.consensus_weight,
+            exit: relay.exit_probability > 0,
+            bandwidth: relay.advertised_bandwidth,
+          });
+      relay_count++;
+    });
+
+    var bubbles = svg.selectAll(".node")
+        .data(bubble.nodes({ children: d3.values(groups) }));
+    var node = bubbles.enter().append("g")
+        .attr("class", "node")
+        .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
+
+    if (graph.group) {
+      node.append("circle")
+          .filter(function(d) { return d.children && d.name; })
+          .attr("r", function(d) { return d.r; })
+          .style("fill", "#888888")
+          .style("fill-opacity", ".25");
+    }
+
+    var relays = node.filter(function(d) { return !d.children && d.r > 1;})
+      .append("use")
+        .attr("xlink:href", function(d) { return "#node-" + (d.bandwidth > cutOff ? "onion" : "circle"); })
+        .attr("transform", function(d) { return "translate(" + -d.r + "," + -d.r + ")"; })
+        .attr("width", function(d) { return d.r * 2; })
+        .attr("height", function(d) { return d.r * 2; })
+        .attr("preserveAspectRatio", "xMidYMin")
+        .attr("filter", function(d) { return d.exit ? "" : "url(#middle-filter)"; });
+
+    if (!graph.group) {
+      relays
+        .on("mouseover", function(d) {
+             svg.append("text")
+                 .attr("transform", "translate(" + diameter + "," + (diameter - legendHeight - 50) + ")")
+                 .attr("id", "relay-bw")
+                 .style("text-anchor", "start")
+                 .style("font-size", "14pt")
+                 .text(d.className.substring(0, 20) + ": " + (d.bandwidth * 8.0 / 1000.0 / 1000.0).toFixed(2) + " Mbit/s");
+           })
+        .on("mouseout", function() {
+             d3.select("#relay-bw").remove();
+           })
+    };
+
+    if (graph.group) {
+      node.filter(function(d) { return d.children && d.name; })
+        .each(function(d) {
+          var g = svg.append("g")
+            .attr("transform", "translate(" + d.x + "," + d.y + ")");
+          g.append("circle")
+            .attr("r", d.r)
+            .style("fill", "#000000")
+            .style("fill-opacity", "0")
+            .style("stroke", "none");
+          g.on("mouseover", function() {
+                svg.append("text")
+                    .attr("transform", "translate(" + d.x + "," + (d.y - d.r) + ")")
+                    .attr("id", "group-name")
+                    .style("text-anchor", "middle")
+                    .style("font-size", "14pt")
+                    .text((d.name + "").substring(0, 50));
+              })
+           .on("mouseout", function() {
+                d3.select("#group-name").remove();
+              });
+        });
+    }
+
+    var titleText = relay_count + " " + (exits_only ? "exits" : "relays") + " (" +
+        (node.filter(function(d) { return !d.children && d.r > 1; }).size())  + " visible)";
+    if (graph.group) {
+      titleText = Object.keys(groups).length + " " + graph.group + " with " + titleText;
+    }
+    var title = svg.append("g")
+        .attr("transform", "translate(10, " + (diameter - 30) + ")");
+    title.append("text")
+        .text(titleText)
+        .attr("text-anchor", "start")
+        .attr("style", "font-size: 18pt");
+    title.append("text")
+        .text(data['relays_published'])
+        .attr("text-anchor", "start")
+        .attr("dy", "15")
+        .attr("style", "font-size: 10pt");
+
+    var legend = svg.append("g")
+        .attr("transform", "translate(" + (diameter - 10) +", " + (diameter - legendHeight - 10) + ")")
+    legend.append("rect")
+        .attr("width", legendWidth)
+        .attr("height", legendHeight)
+        .attr("fill", "#cccccc")
+        .attr("stroke", "#000000");
+    var legendOnion = legend.append("g")
+        .attr("transform", "translate(0, " + legendIconMargin + ")");
+    legendOnion.append("use")
+      .attr("xlink:href", "#node-onion")
+        .attr("width", legendIconSize)
+        .attr("height", legendIconSize)
+        .attr("preserveAspectRatio", "xMidYMin");
+    legendOnion.append("text")
+        .text((cutOff * 8 / 1000 / 1000) + "+ Mbit/s relays")
+        .attr("text-anchor", "start")
+        .attr("dx", legendIconSize)
+        .attr("dy", legendIconSize / 2)
+    var legendCircle = legend.append("g")
+        .attr("transform", "translate(0, " + (legendIconSize + legendIconMargin * 2) + ")");
+    legendCircle.append("use")
+      .attr("xlink:href", "#node-circle")
+        .attr("width", legendIconSize)
+        .attr("height", legendIconSize)
+        .attr("preserveAspectRatio", "xMidYMin");
+    legendCircle.append("text")
+        .text("smaller relays")
+        .attr("text-anchor", "start")
+        .attr("dx", legendIconSize)
+        .attr("dy", legendIconSize / 2)
+    var legendExit = legend.append("g")
+        .attr("transform", "translate(0, " + ((legendIconSize + legendIconMargin * 2) * 2) + ")");
+    legendExit.append("use")
+      .attr("xlink:href", "#node-onion")
+        .attr("width", legendIconSize / 2)
+        .attr("height", legendIconSize / 2)
+        .attr("preserveAspectRatio", "xMidYMin")
+        .attr("filter", "url(#middle-filter)");
+    legendExit.append("use")
+      .attr("xlink:href", "#node-circle")
+        .attr("width", legendIconSize / 2)
+        .attr("height", legendIconSize / 2)
+        .attr("preserveAspectRatio", "xMidYMin")
+        .attr("transform", "translate(" + (legendIconSize / 2) + ", " + (legendIconSize / 2) + ")")
+        .attr("filter", "url(#middle-filter)");
+    legendExit.append("text")
+        .text("non-exits")
+        .attr("text-anchor", "start")
+        .attr("dx", legendIconSize)
+        .attr("dy", legendIconSize / 2);
+
+  });
+
+  d3.select(self.frameElement).style("height", diameter + "px");
+}
diff --git a/website/web/js/d3.min.js b/website/web/js/d3.min.js
new file mode 100644
index 0000000..5c30406
--- /dev/null
+++ b/website/web/js/d3.min.js
@@ -0,0 +1,5 @@
+d3=function(){function n(n){return null!=n&&!isNaN(n)}function t(n){return n.length}function e(n){for(var t=1;n*t%1;)t*=10;return t}function r(n,t){try{for(var e in t)Object.defineProperty(n.prototype,e,{value:t[e],enumerable:!1})}catch(r){n.prototype=t}}function i(){}function u(){}function a(n,t,e){return function(){var r=e.apply(t,arguments);return r===t?n:r}}function o(){}function c(n){function t(){for(var t,r=e,i=-1,u=r.length;++i<u;)(t=r[i].on)&&t.apply(this,arguments);return n}var e=[],r=new i;return t.on=function(t,i){var u,a=r.get(t);return arguments.length<2?a&&a.on:(a&&(a.on=null,e=e.slice(0,u=e.indexOf(a)).concat(e.slice(u+1)),r.remove(t)),i&&e.push(r.set(t,{on:i})),n)},t}function l(){va.event.stopPropagation(),va.event.preventDefault()}function f(){for(var n,t=va.event;n=t.sourceEvent;)t=n;return t}function s(n,t){function e(){n.on(t,null)}n.on(t,function(){l(),e()},!0),setTimeout(e,0)}function h(n){for(var t=new o,e=0,r=arguments.length;++e<r;)t[arguments[e]]=c(t);retur
 n t.of=function(e,r){return function(i){try{var u=i.sourceEvent=va.event;i.target=n,va.event=i,t[i.type].apply(e,r)}finally{va.event=u}}},t}function g(n,t){var e=n.ownerSVGElement||n;if(e.createSVGPoint){var r=e.createSVGPoint();if(0>Aa&&(xa.scrollX||xa.scrollY)){e=va.select("body").append("svg").style({position:"absolute",top:0,left:0,margin:0,padding:0,border:"none"},"important");var i=e[0][0].getScreenCTM();Aa=!(i.f||i.e),e.remove()}return Aa?(r.x=t.pageX,r.y=t.pageY):(r.x=t.clientX,r.y=t.clientY),r=r.matrixTransform(n.getScreenCTM().inverse()),[r.x,r.y]}var u=n.getBoundingClientRect();return[t.clientX-u.left-n.clientLeft,t.clientY-u.top-n.clientTop]}function p(n){for(var t=-1,e=n.length,r=[];++t<e;)r.push(n[t]);return r}function d(n){return Array.prototype.slice.call(n)}function m(n,t){if(t in n)return t;t=t.charAt(0).toUpperCase()+t.substring(1);for(var e=0,r=Ca.length;r>e;++e){var i=Ca[e]+t;if(i in n)return i}}function v(n){return Ta(n,Pa),n}function y(n){return function(){ret
 urn ja(n,this)}}function M(n){return function(){return La(n,this)}}function x(n,t){function e(){this.removeAttribute(n)}function r(){this.removeAttributeNS(n.space,n.local)}function i(){this.setAttribute(n,t)}function u(){this.setAttributeNS(n.space,n.local,t)}function a(){var e=t.apply(this,arguments);null==e?this.removeAttribute(n):this.setAttribute(n,e)}function o(){var e=t.apply(this,arguments);null==e?this.removeAttributeNS(n.space,n.local):this.setAttributeNS(n.space,n.local,e)}return n=va.ns.qualify(n),null==t?n.local?r:e:"function"==typeof t?n.local?o:a:n.local?u:i}function b(n){return n.trim().replace(/\s+/g," ")}function _(n){return RegExp("(?:^|\\s+)"+va.requote(n)+"(?:\\s+|$)","g")}function w(n,t){function e(){for(var e=-1;++e<i;)n[e](this,t)}function r(){for(var e=-1,r=t.apply(this,arguments);++e<i;)n[e](this,r)}n=n.trim().split(/\s+/).map(S);var i=n.length;return"function"==typeof t?r:e}function S(n){var t=_(n);return function(e,r){if(i=e.classList)return r?i.add(n):i.
 remove(n);var i=e.getAttribute("class")||"";r?(t.lastIndex=0,t.test(i)||e.setAttribute("class",b(i+" "+n))):e.setAttribute("class",b(i.replace(t," ")))}}function E(n,t,e){function r(){this.style.removeProperty(n)}function i(){this.style.setProperty(n,t,e)}function u(){var r=t.apply(this,arguments);null==r?this.style.removeProperty(n):this.style.setProperty(n,r,e)}return null==t?r:"function"==typeof t?u:i}function k(n,t){function e(){delete this[n]}function r(){this[n]=t}function i(){var e=t.apply(this,arguments);null==e?delete this[n]:this[n]=e}return null==t?e:"function"==typeof t?i:r}function A(n){return{__data__:n}}function N(n){return function(){return Fa(this,n)}}function q(n){return arguments.length||(n=va.ascending),function(t,e){return!t-!e||n(t.__data__,e.__data__)}}function T(){}function C(n,t,e){function r(){var t=this[a];t&&(this.removeEventListener(n,t,t.$),delete this[a])}function i(){var i=c(t,Na(arguments));r.call(this),this.addEventListener(n,this[a]=i,i.$=e),i._=t}
 function u(){var t,e=RegExp("^__on([^.]+)"+va.requote(n)+"$");for(var r in this)if(t=r.match(e)){var i=this[r];this.removeEventListener(t[1],i,i.$),delete this[r]}}var a="__on"+n,o=n.indexOf("."),c=z;o>0&&(n=n.substring(0,o));var l=Ya.get(n);return l&&(n=l,c=D),o?t?i:r:t?T:u}function z(n,t){return function(e){var r=va.event;va.event=e,t[0]=this.__data__;try{n.apply(this,t)}finally{va.event=r}}}function D(n,t){var e=z(n,t);return function(n){var t=this,r=n.relatedTarget;r&&(r===t||r.compareDocumentPosition(t)&8)||e.call(t,n)}}function j(n,t){for(var e=0,r=n.length;r>e;e++)for(var i,u=n[e],a=0,o=u.length;o>a;a++)(i=u[a])&&t(i,a,e);return n}function L(n){return Ta(n,Ua),n}function H(){}function F(n,t,e){return new P(n,t,e)}function P(n,t,e){this.h=n,this.s=t,this.l=e}function O(n,t,e){function r(n){return n>360?n-=360:0>n&&(n+=360),60>n?u+(a-u)*n/60:180>n?a:240>n?u+(a-u)*(240-n)/60:u}function i(n){return Math.round(r(n)*255)}var u,a;return n=isNaN(n)?0:(n%=360)<0?n+360:n,t=isNaN(t)?0:0
 >t?0:t>1?1:t,e=0>e?0:e>1?1:e,a=.5>=e?e*(1+t):e+t-e*t,u=2*e-a,et(i(n+120),i(n),i(n-120))}function R(n){return n>0?1:0>n?-1:0}function Y(n){return n>1?0:-1>n?$a:Math.acos(n)}function U(n){return n>1?$a/2:-1>n?-$a/2:Math.asin(n)}function I(n){return(Math.exp(n)-Math.exp(-n))/2}function V(n){return(Math.exp(n)+Math.exp(-n))/2}function X(n){return(n=Math.sin(n/2))*n}function Z(n,t,e){return new B(n,t,e)}function B(n,t,e){this.h=n,this.c=t,this.l=e}function $(n,t,e){return isNaN(n)&&(n=0),isNaN(t)&&(t=0),W(e,Math.cos(n*=Ga)*t,Math.sin(n)*t)}function W(n,t,e){return new J(n,t,e)}function J(n,t,e){this.l=n,this.a=t,this.b=e}function G(n,t,e){var r=(n+16)/116,i=r+t/500,u=r-e/200;return i=Q(i)*to,r=Q(r)*eo,u=Q(u)*ro,et(tt(3.2404542*i-1.5371385*r-.4985314*u),tt(-.969266*i+1.8760108*r+.041556*u),tt(.0556434*i-.2040259*r+1.0572252*u))}function K(n,t,e){return n>0?Z(Math.atan2(e,t)*Ka,Math.sqrt(t*t+e*e),n):Z(0/0,0/0,n)}function Q(n){return n>.206893034?n*n*n:(n-4/29)/7.787037}function nt(n){retur
 n n>.008856?Math.pow(n,1/3):7.787037*n+4/29}function tt(n){return Math.round(255*(.00304>=n?12.92*n:1.055*Math.pow(n,1/2.4)-.055))}function et(n,t,e){return new rt(n,t,e)}function rt(n,t,e){this.r=n,this.g=t,this.b=e}function it(n){return 16>n?"0"+Math.max(0,n).toString(16):Math.min(255,n).toString(16)}function ut(n,t,e){var r,i,u,a=0,o=0,c=0;if(r=/([a-z]+)\((.*)\)/i.exec(n))switch(i=r[2].split(","),r[1]){case"hsl":return e(parseFloat(i[0]),parseFloat(i[1])/100,parseFloat(i[2])/100);case"rgb":return t(lt(i[0]),lt(i[1]),lt(i[2]))}return(u=ao.get(n))?t(u.r,u.g,u.b):(null!=n&&n.charAt(0)==="#"&&(n.length===4?(a=n.charAt(1),a+=a,o=n.charAt(2),o+=o,c=n.charAt(3),c+=c):n.length===7&&(a=n.substring(1,3),o=n.substring(3,5),c=n.substring(5,7)),a=parseInt(a,16),o=parseInt(o,16),c=parseInt(c,16)),t(a,o,c))}function at(n,t,e){var r,i,u=Math.min(n/=255,t/=255,e/=255),a=Math.max(n,t,e),o=a-u,c=(a+u)/2;return o?(i=.5>c?o/(a+u):o/(2-a-u),r=n==a?(t-e)/o+(e>t?6:0):t==a?(e-n)/o+2:(n-t)/o+4,r*=60):(r=0
 /0,i=c>0&&1>c?0:r),F(r,i,c)}function ot(n,t,e){n=ct(n),t=ct(t),e=ct(e);var r=nt((.4124564*n+.3575761*t+.1804375*e)/to),i=nt((.2126729*n+.7151522*t+.072175*e)/eo),u=nt((.0193339*n+.119192*t+.9503041*e)/ro);return W(116*i-16,500*(r-i),200*(i-u))}function ct(n){return(n/=255)<=.04045?n/12.92:Math.pow((n+.055)/1.055,2.4)}function lt(n){var t=parseFloat(n);return n.charAt(n.length-1)==="%"?Math.round(2.55*t):t}function ft(n){return"function"==typeof n?n:function(){return n}}function st(n){return n}function ht(n){return function(t,e,r){return arguments.length===2&&"function"==typeof e&&(r=e,e=null),gt(t,e,n,r)}}function gt(n,t,e,r){function i(){var n,t=c.status;if(!t&&c.responseText||t>=200&&300>t||304===t){try{n=e.call(u,c)}catch(r){return a.error.call(u,r),void 0}a.load.call(u,n)}else a.error.call(u,c)}var u={},a=va.dispatch("progress","load","error"),o={},c=new XMLHttpRequest,l=null;return!xa.XDomainRequest||"withCredentials"in c||!/^(http(s)?:)?\/\//.test(n)||(c=new XDomainRequest),"o
 nload"in c?c.onload=c.onerror=i:c.onreadystatechange=function(){c.readyState>3&&i()},c.onprogress=function(n){var t=va.event;va.event=n;try{a.progress.call(u,c)}finally{va.event=t}},u.header=function(n,t){return n=(n+"").toLowerCase(),arguments.length<2?o[n]:(null==t?delete o[n]:o[n]=t+"",u)},u.mimeType=function(n){return arguments.length?(t=null==n?null:n+"",u):t},u.responseType=function(n){return arguments.length?(l=n,u):l},u.response=function(n){return e=n,u},["get","post"].forEach(function(n){u[n]=function(){return u.send.apply(u,[n].concat(Na(arguments)))}}),u.send=function(e,r,i){if(arguments.length===2&&"function"==typeof r&&(i=r,r=null),c.open(e,n,!0),null==t||"accept"in o||(o.accept=t+",*/*"),c.setRequestHeader)for(var a in o)c.setRequestHeader(a,o[a]);return null!=t&&c.overrideMimeType&&c.overrideMimeType(t),null!=l&&(c.responseType=l),null!=i&&u.on("error",i).on("load",function(n){i(null,n)}),c.send(null==r?null:r),u},u.abort=function(){return c.abort(),u},va.rebind(u,a,"
 on"),null==r?u:u.get(pt(r))}function pt(n){return n.length===1?function(t,e){n(null==t?e:null)}:n}function dt(){var n=mt(),t=vt()-n;t>24?(isFinite(t)&&(clearTimeout(fo),fo=setTimeout(dt,t)),lo=0):(lo=1,so(dt))}function mt(){for(var n=Date.now(),t=oo;t;)n>=t.time&&(t.flush=t.callback(n-t.time)),t=t.next;return n}function vt(){for(var n,t=oo,e=1/0;t;)t.flush?t=n?n.next=t.next:oo=t.next:(t.time<e&&(e=t.time),t=(n=t).next);return co=n,e}function yt(n,t){var e=Math.pow(10,Math.abs(8-t)*3);return{scale:t>8?function(n){return n/e}:function(n){return n*e},symbol:n}}function Mt(n,t){return t-(n?Math.ceil(Math.log(n)/Math.LN10):1)}function xt(n){return n+""}function bt(){}function _t(n,t,e){var r=e.s=n+t,i=r-n,u=r-i;e.t=n-u+(t-i)}function wt(n,t){n&&wo.hasOwnProperty(n.type)&&wo[n.type](n,t)}function St(n,t,e){var r,i=-1,u=n.length-e;for(t.lineStart();++i<u;)r=n[i],t.point(r[0],r[1]);t.lineEnd()}function Et(n,t){var e=-1,r=n.length;for(t.polygonStart();++e<r;)St(n[e],t,1);t.polygonEnd()}funct
 ion kt(){function n(n,t){n*=Ga,t=t*Ga/2+$a/4;var e=n-r,a=Math.cos(t),o=Math.sin(t),c=u*o,l=i*a+c*Math.cos(e),f=c*Math.sin(e);Eo.add(Math.atan2(f,l)),r=n,i=a,u=o}var t,e,r,i,u;ko.point=function(a,o){ko.point=n,r=(t=a)*Ga,i=Math.cos(o=(e=o)*Ga/2+$a/4),u=Math.sin(o)},ko.lineEnd=function(){n(t,e)}}function At(n){var t=n[0],e=n[1],r=Math.cos(e);return[r*Math.cos(t),r*Math.sin(t),Math.sin(e)]}function Nt(n,t){return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]}function qt(n,t){return[n[1]*t[2]-n[2]*t[1],n[2]*t[0]-n[0]*t[2],n[0]*t[1]-n[1]*t[0]]}function Tt(n,t){n[0]+=t[0],n[1]+=t[1],n[2]+=t[2]}function Ct(n,t){return[n[0]*t,n[1]*t,n[2]*t]}function zt(n){var t=Math.sqrt(n[0]*n[0]+n[1]*n[1]+n[2]*n[2]);n[0]/=t,n[1]/=t,n[2]/=t}function Dt(n){return[Math.atan2(n[1],n[0]),U(n[2])]}function jt(n,t){return Math.abs(n[0]-t[0])<Wa&&Math.abs(n[1]-t[1])<Wa}function Lt(n,t){n*=Ga;var e=Math.cos(t*=Ga);Ht(e*Math.cos(n),e*Math.sin(n),Math.sin(t))}function Ht(n,t,e){++Ao,qo+=(n-qo)/Ao,To+=(t-To)/Ao,Co+=(e-Co)/Ao}functio
 n Ft(){function n(n,i){n*=Ga;var u=Math.cos(i*=Ga),a=u*Math.cos(n),o=u*Math.sin(n),c=Math.sin(i),l=Math.atan2(Math.sqrt((l=e*c-r*o)*l+(l=r*a-t*c)*l+(l=t*o-e*a)*l),t*a+e*o+r*c);No+=l,zo+=l*(t+(t=a)),Do+=l*(e+(e=o)),jo+=l*(r+(r=c)),Ht(t,e,r)}var t,e,r;Po.point=function(i,u){i*=Ga;var a=Math.cos(u*=Ga);t=a*Math.cos(i),e=a*Math.sin(i),r=Math.sin(u),Po.point=n,Ht(t,e,r)}}function Pt(){Po.point=Lt}function Ot(){function n(n,t){n*=Ga;var e=Math.cos(t*=Ga),a=e*Math.cos(n),o=e*Math.sin(n),c=Math.sin(t),l=i*c-u*o,f=u*a-r*c,s=r*o-i*a,h=Math.sqrt(l*l+f*f+s*s),g=r*a+i*o+u*c,p=h&&-Y(g)/h,d=Math.atan2(h,g);Lo+=p*l,Ho+=p*f,Fo+=p*s,No+=d,zo+=d*(r+(r=a)),Do+=d*(i+(i=o)),jo+=d*(u+(u=c)),Ht(r,i,u)}var t,e,r,i,u;Po.point=function(a,o){t=a,e=o,Po.point=n,a*=Ga;var c=Math.cos(o*=Ga);r=c*Math.cos(a),i=c*Math.sin(a),u=Math.sin(o),Ht(r,i,u)},Po.lineEnd=function(){n(t,e),Po.lineEnd=Pt,Po.point=Lt}}function Rt(){return!0}function Yt(n,t,e,r,i){var u=[],a=[];if(n.forEach(function(n){if(!((t=n.length-1)<=0)){var
  t,e=n[0],r=n[t];if(jt(e,r)){i.lineStart();for(var o=0;t>o;++o)i.point((e=n[o])[0],e[1]);return i.lineEnd(),void 0}var c={point:e,points:n,other:null,visited:!1,entry:!0,subject:!0},l={point:e,points:[e],other:c,visited:!1,entry:!1,subject:!1};c.other=l,u.push(c),a.push(l),c={point:r,points:[r],other:null,visited:!1,entry:!1,subject:!0},l={point:r,points:[r],other:c,visited:!1,entry:!0,subject:!1},c.other=l,u.push(c),a.push(l)}}),a.sort(t),Ut(u),Ut(a),u.length){if(e)for(var o=1,c=!e(a[0].point),l=a.length;l>o;++o)a[o].entry=c=!c;for(var f,s,h,g=u[0];;){for(f=g;f.visited;)if((f=f.next)===g)return;s=f.points,i.lineStart();do{if(f.visited=f.other.visited=!0,f.entry){if(f.subject)for(var o=0;o<s.length;o++)i.point((h=s[o])[0],h[1]);else r(f.point,f.next.point,1,i);f=f.next}else{if(f.subject){s=f.prev.points;for(var o=s.length;--o>=0;)i.point((h=s[o])[0],h[1])}else r(f.point,f.prev.point,-1,i);f=f.prev}f=f.other,s=f.points}while(!f.visited);i.lineEnd()}}}function Ut(n){if(t=n.length){for
 (var t,e,r=0,i=n[0];++r<t;)i.next=e=n[r],e.prev=i,i=e;i.next=e=n[0],e.prev=i}}function It(n,t,e,r){return function(i){function u(t,e){n(t,e)&&i.point(t,e)}function a(n,t){d.point(n,t)}function o(){m.point=a,d.lineStart()}function c(){m.point=u,d.lineEnd()}function l(n,t){y.point(n,t),p.push([n,t])}function f(){y.lineStart(),p=[]}function s(){l(p[0][0],p[0][1]),y.lineEnd();var n,t=y.clean(),e=v.buffer(),r=e.length;if(p.pop(),g.push(p),p=null,r){if(1&t){n=e[0];var u,r=n.length-1,a=-1;for(i.lineStart();++a<r;)i.point((u=n[a])[0],u[1]);return i.lineEnd(),void 0}r>1&&2&t&&e.push(e.pop().concat(e.shift())),h.push(e.filter(Vt))}}var h,g,p,d=t(i),m={point:u,lineStart:o,lineEnd:c,polygonStart:function(){m.point=l,m.lineStart=f,m.lineEnd=s,h=[],g=[],i.polygonStart()},polygonEnd:function(){m.point=u,m.lineStart=o,m.lineEnd=c,h=va.merge(h),h.length?Yt(h,Zt,null,e,i):r(g)&&(i.lineStart(),e(null,null,1,i),i.lineEnd()),i.polygonEnd(),h=g=null},sphere:function(){i.polygonStart(),i.lineStart(),e(nul
 l,null,1,i),i.lineEnd(),i.polygonEnd()}},v=Xt(),y=t(v);return m}}function Vt(n){return n.length>1}function Xt(){var n,t=[];return{lineStart:function(){t.push(n=[])},point:function(t,e){n.push([t,e])},lineEnd:T,buffer:function(){var e=t;return t=[],n=null,e},rejoin:function(){t.length>1&&t.push(t.pop().concat(t.shift()))}}}function Zt(n,t){return((n=n.point)[0]<0?n[1]-$a/2-Wa:$a/2-n[1])-((t=t.point)[0]<0?t[1]-$a/2-Wa:$a/2-t[1])}function Bt(n,t){var e=n[0],r=n[1],i=[Math.sin(e),-Math.cos(e),0],u=0,a=!1,o=!1,c=0;Eo.reset();for(var l=0,f=t.length;f>l;++l){var s=t[l],h=s.length;if(h){for(var g=s[0],p=g[0],d=g[1]/2+$a/4,m=Math.sin(d),v=Math.cos(d),y=1;;){y===h&&(y=0),n=s[y];var M=n[0],x=n[1]/2+$a/4,b=Math.sin(x),_=Math.cos(x),w=M-p,S=Math.abs(w)>$a,E=m*b;if(Eo.add(Math.atan2(E*Math.sin(w),v*_+E*Math.cos(w))),Math.abs(x)<Wa&&(o=!0),u+=S?w+(w>=0?2:-2)*$a:w,S^p>=e^M>=e){var k=qt(At(g),At(n));zt(k);var A=qt(i,k);zt(A);var N=(S^w>=0?-1:1)*U(A[2]);r>N&&(c+=S^w>=0?1:-1)}if(!y++)break;p=M,m=b,v=_
 ,g=n}Math.abs(u)>Wa&&(a=!0)}}return(!o&&!a&&0>Eo||-Wa>u)^1&c}function $t(n){var t,e=0/0,r=0/0,i=0/0;return{lineStart:function(){n.lineStart(),t=1},point:function(u,a){var o=u>0?$a:-$a,c=Math.abs(u-e);Math.abs(c-$a)<Wa?(n.point(e,r=(r+a)/2>0?$a/2:-$a/2),n.point(i,r),n.lineEnd(),n.lineStart(),n.point(o,r),n.point(u,r),t=0):i!==o&&c>=$a&&(Math.abs(e-i)<Wa&&(e-=i*Wa),Math.abs(u-o)<Wa&&(u-=o*Wa),r=Wt(e,r,u,a),n.point(i,r),n.lineEnd(),n.lineStart(),n.point(o,r),t=0),n.point(e=u,r=a),i=o},lineEnd:function(){n.lineEnd(),e=r=0/0},clean:function(){return 2-t}}}function Wt(n,t,e,r){var i,u,a=Math.sin(n-e);return Math.abs(a)>Wa?Math.atan((Math.sin(t)*(u=Math.cos(r))*Math.sin(e)-Math.sin(r)*(i=Math.cos(t))*Math.sin(n))/(i*u*a)):(t+r)/2}function Jt(n,t,e,r){var i;if(null==n)i=e*$a/2,r.point(-$a,i),r.point(0,i),r.point($a,i),r.point($a,0),r.point($a,-i),r.point(0,-i),r.point(-$a,-i),r.point(-$a,0),r.point(-$a,i);else if(Math.abs(n[0]-t[0])>Wa){var u=(n[0]<t[0]?1:-1)*$a;i=e*u/2,r.point(-u,i),r.poin
 t(0,i),r.point(u,i)}else r.point(t[0],t[1])}function Gt(n){return Bt(Ro,n)}function Kt(n){function t(n,t){return Math.cos(n)*Math.cos(t)>a}function e(n){var e,u,a,c,f;return{lineStart:function(){c=a=!1,f=1},point:function(s,h){var g,p=[s,h],d=t(s,h),m=o?d?0:i(s,h):d?i(s+(0>s?$a:-$a),h):0;if(!e&&(c=a=d)&&n.lineStart(),d!==a&&(g=r(e,p),(jt(e,g)||jt(p,g))&&(p[0]+=Wa,p[1]+=Wa,d=t(p[0],p[1]))),d!==a)f=0,d?(n.lineStart(),g=r(p,e),n.point(g[0],g[1])):(g=r(e,p),n.point(g[0],g[1]),n.lineEnd()),e=g;else if(l&&e&&o^d){var v;m&u||!(v=r(p,e,!0))||(f=0,o?(n.lineStart(),n.point(v[0][0],v[0][1]),n.point(v[1][0],v[1][1]),n.lineEnd()):(n.point(v[1][0],v[1][1]),n.lineEnd(),n.lineStart(),n.point(v[0][0],v[0][1])))}!d||e&&jt(e,p)||n.point(p[0],p[1]),e=p,a=d,u=m},lineEnd:function(){a&&n.lineEnd(),e=null},clean:function(){return f|(c&&a)<<1}}}function r(n,t,e){var r=At(n),i=At(t),u=[1,0,0],o=qt(r,i),c=Nt(o,o),l=o[0],f=c-l*l;if(!f)return!e&&n;var s=a*c/f,h=-a*l/f,g=qt(u,o),p=Ct(u,s),d=Ct(o,h);Tt(p,d);var m
 =g,v=Nt(p,m),y=Nt(m,m),M=v*v-y*(Nt(p,p)-1);if(!(0>M)){var x=Math.sqrt(M),b=Ct(m,(-v-x)/y);if(Tt(b,p),b=Dt(b),!e)return b;var _,w=n[0],S=t[0],E=n[1],k=t[1];w>S&&(_=w,w=S,S=_);var A=S-w,N=Math.abs(A-$a)<Wa,q=N||Wa>A;if(!N&&E>k&&(_=E,E=k,k=_),q?N?E+k>0^b[1]<(Math.abs(b[0]-w)<Wa?E:k):E<=b[1]&&b[1]<=k:A>$a^(w<=b[0]&&b[0]<=S)){var T=Ct(m,(-v+x)/y);return Tt(T,p),[b,Dt(T)]}}}function i(t,e){var r=o?n:$a-n,i=0;return-r>t?i|=1:t>r&&(i|=2),-r>e?i|=4:e>r&&(i|=8),i}function u(n){return Bt(c,n)}var a=Math.cos(n),o=a>0,c=[n,0],l=Math.abs(a)>Wa,f=we(n,6*Ga);return It(t,e,f,u)}function Qt(n,t,e,r){function i(r,i){return Math.abs(r[0]-n)<Wa?i>0?0:3:Math.abs(r[0]-e)<Wa?i>0?2:1:Math.abs(r[1]-t)<Wa?i>0?1:0:i>0?3:2}function u(n,t){return a(n.point,t.point)}function a(n,t){var e=i(n,1),r=i(t,1);return e!==r?e-r:0===e?t[1]-n[1]:1===e?n[0]-t[0]:2===e?n[1]-t[1]:t[0]-n[0]}function o(i,u){var a=u[0]-i[0],o=u[1]-i[1],c=[0,1];return Math.abs(a)<Wa&&Math.abs(o)<Wa?n<=i[0]&&i[0]<=e&&t<=i[1]&&i[1]<=r:ne(n-i[0],a,c
 )&&ne(i[0]-e,-a,c)&&ne(t-i[1],o,c)&&ne(i[1]-r,-o,c)?(c[1]<1&&(u[0]=i[0]+c[1]*a,u[1]=i[1]+c[1]*o),c[0]>0&&(i[0]+=c[0]*a,i[1]+=c[0]*o),!0):!1}return function(c){function l(u){var a=i(u,-1),o=f([0===a||3===a?n:e,a>1?r:t]);return o}function f(n){for(var t=0,e=M.length,r=n[1],i=0;e>i;++i)for(var u,a=1,o=M[i],c=o.length,l=o[0];c>a;++a)u=o[a],l[1]<=r?u[1]>r&&s(l,u,n)>0&&++t:u[1]<=r&&s(l,u,n)<0&&--t,l=u;return 0!==t}function s(n,t,e){return(t[0]-n[0])*(e[1]-n[1])-(e[0]-n[0])*(t[1]-n[1])}function h(u,o,c,l){var f=0,s=0;if(null==u||(f=i(u,c))!==(s=i(o,c))||a(u,o)<0^c>0){do l.point(0===f||3===f?n:e,f>1?r:t);while((f=(f+c+4)%4)!==s)}else l.point(o[0],o[1])}function g(i,u){return i>=n&&e>=i&&u>=t&&r>=u}function p(n,t){g(n,t)&&c.point(n,t)}function d(){T.point=v,M&&M.push(x=[]),A=!0,k=!1,S=E=0/0}function m(){y&&(v(b,_),w&&k&&q.rejoin(),y.push(q.buffer())),T.point=p,k&&c.lineEnd()}function v(n,t){n=Math.max(-Yo,Math.min(Yo,n)),t=Math.max(-Yo,Math.min(Yo,t));var e=g(n,t);if(M&&x.push([n,t]),A)b=n,_
 =t,w=e,A=!1,e&&(c.lineStart(),c.point(n,t));else if(e&&k)c.point(n,t);else{var r=[S,E],i=[n,t];o(r,i)?(k||(c.lineStart(),c.point(r[0],r[1])),c.point(i[0],i[1]),e||c.lineEnd()):e&&(c.lineStart(),c.point(n,t))}S=n,E=t,k=e}var y,M,x,b,_,w,S,E,k,A,N=c,q=Xt(),T={point:p,lineStart:d,lineEnd:m,polygonStart:function(){c=q,y=[],M=[]},polygonEnd:function(){c=N,(y=va.merge(y)).length?(c.polygonStart(),Yt(y,u,l,h,c),c.polygonEnd()):f([n,t])&&(c.polygonStart(),c.lineStart(),h(null,null,1,c),c.lineEnd(),c.polygonEnd()),y=M=x=null}};return T}}function ne(n,t,e){if(Math.abs(t)<Wa)return 0>=n;var r=n/t;if(t>0){if(r>e[1])return!1;r>e[0]&&(e[0]=r)}else{if(r<e[0])return!1;r<e[1]&&(e[1]=r)}return!0}function te(n,t){function e(e,r){return e=n(e,r),t(e[0],e[1])}return n.invert&&t.invert&&(e.invert=function(e,r){return e=t.invert(e,r),e&&n.invert(e[0],e[1])}),e}function ee(n){var t=0,e=$a/3,r=me(n),i=r(t,e);return i.parallels=function(n){return arguments.length?r(t=n[0]*$a/180,e=n[1]*$a/180):[180*(t/$a),18
 0*(e/$a)]},i}function re(n,t){function e(n,t){var e=Math.sqrt(u-2*i*Math.sin(t))/i;return[e*Math.sin(n*=i),a-e*Math.cos(n)]}var r=Math.sin(n),i=(r+Math.sin(t))/2,u=1+r*(2*i-r),a=Math.sqrt(u)/i;return e.invert=function(n,t){var e=a-t;return[Math.atan2(n,e)/i,U((u-(n*n+e*e)*i*i)/(2*i))]},e}function ie(){function n(n,t){Io+=i*n-r*t,r=n,i=t}var t,e,r,i;$o.point=function(u,a){$o.point=n,t=r=u,e=i=a},$o.lineEnd=function(){n(t,e)}}function ue(n,t){Vo>n&&(Vo=n),n>Zo&&(Zo=n),Xo>t&&(Xo=t),t>Bo&&(Bo=t)}function ae(){function n(n,t){a.push("M",n,",",t,u)}function t(n,t){a.push("M",n,",",t),o.point=e}function e(n,t){a.push("L",n,",",t)}function r(){o.point=n}function i(){a.push("Z")}var u=oe(4.5),a=[],o={point:n,lineStart:function(){o.point=t},lineEnd:r,polygonStart:function(){o.lineEnd=i},polygonEnd:function(){o.lineEnd=r,o.point=n},pointRadius:function(n){return u=oe(n),o},result:function(){if(a.length){var n=a.join("");return a=[],n}}};return o}function oe(n){return"m0,"+n+"a"+n+","+n+" 0 1,1
  0,"+-2*n+"a"+n+","+n+" 0 1,1 0,"+2*n+"z"}function ce(n,t){qo+=n,To+=t,++Co}function le(){function n(n,r){var i=n-t,u=r-e,a=Math.sqrt(i*i+u*u);zo+=a*(t+n)/2,Do+=a*(e+r)/2,jo+=a,ce(t=n,e=r)}var t,e;Jo.point=function(r,i){Jo.point=n,ce(t=r,e=i)}}function fe(){Jo.point=ce}function se(){function n(n,t){var e=n-r,u=t-i,a=Math.sqrt(e*e+u*u);zo+=a*(r+n)/2,Do+=a*(i+t)/2,jo+=a,a=i*n-r*t,Lo+=a*(r+n),Ho+=a*(i+t),Fo+=3*a,ce(r=n,i=t)}var t,e,r,i;Jo.point=function(u,a){Jo.point=n,ce(t=r=u,e=i=a)},Jo.lineEnd=function(){n(t,e)}}function he(n){function t(t,e){n.moveTo(t,e),n.arc(t,e,a,0,2*$a)}function e(t,e){n.moveTo(t,e),o.point=r}function r(t,e){n.lineTo(t,e)}function i(){o.point=t}function u(){n.closePath()}var a=4.5,o={point:t,lineStart:function(){o.point=e},lineEnd:i,polygonStart:function(){o.lineEnd=u},polygonEnd:function(){o.lineEnd=i,o.point=t},pointRadius:function(n){return a=n,o},result:T};return o}function ge(n){function t(t){function r(e,r){e=n(e,r),t.point(e[0],e[1])}function u(){x=0/0,
 E.point=a,t.lineStart()}function a(r,u){var a=At([r,u]),o=n(r,u),c=[];e(x,b,M,_,w,S,x=o[0],b=o[1],M=r,_=a[0],w=a[1],S=a[2],i,c),s(c,t),t.point(x,b)}function o(){E.point=r,t.lineEnd()}function c(){u(),E.point=l,E.lineEnd=f}function l(n,t){a(h=n,g=t),p=x,d=b,m=_,v=w,y=S,E.point=a}function f(){var n=[];e(x,b,M,_,w,S,p,d,h,m,v,y,i,n),s(n,t),E.lineEnd=o,o()}function s(n,t){for(var e,r=0,i=n.length;i>r;++r)t.point((e=n[r])[0],e[1])}var h,g,p,d,m,v,y,M,x,b,_,w,S,E={point:r,lineStart:u,lineEnd:o,polygonStart:function(){t.polygonStart(),E.lineStart=c},polygonEnd:function(){t.polygonEnd(),E.lineStart=u}};return E}function e(t,i,u,a,o,c,l,f,s,h,g,p,d,m){var v=l-t,y=f-i,M=v*v+y*y;if(M>4*r&&d--){var x=a+h,b=o+g,_=c+p,w=Math.sqrt(x*x+b*b+_*_),S=Math.asin(_/=w),E=Math.abs(Math.abs(_)-1)<Wa?(u+s)/2:Math.atan2(b,x),k=n(E,S),A=k[0],N=k[1],q=A-t,T=N-i,C=y*q-v*T,z=!1;if(C*C/M>r||Math.abs((v*q+y*T)/M-.5)>.3||(z=q*q+T*T>256*r)){var D=e(t,i,u,a,o,c,A,N,E,x/=w,b/=w,_,d,m);m.push(k);var j=e(A,N,E,x,b,_,l,f,
 s,h,g,p,d,m);return!z||D||j||(m.pop(),!1)}}}var r=.5,i=16;return t.precision=function(n){return arguments.length?(i=(r=n*n)>0&&16,t):Math.sqrt(r)},t}function pe(n){var t=ge(function(t,e){return n([t*Ka,e*Ka])});return function(n){return n=t(n),{point:function(t,e){n.point(t*Ga,e*Ga)},sphere:function(){n.sphere()},lineStart:function(){n.lineStart()},lineEnd:function(){n.lineEnd()},polygonStart:function(){n.polygonStart()},polygonEnd:function(){n.polygonEnd()}}}}function de(n){return me(function(){return n})()}function me(n){function t(n){return n=o(n[0]*Ga,n[1]*Ga),[n[0]*h+c,l-n[1]*h]}function e(n){return n=o.invert((n[0]-c)/h,(l-n[1])/h),n&&[n[0]*Ka,n[1]*Ka]}function r(){o=te(a=Me(v,y,M),u);var n=u(d,m);return c=g-n[0]*h,l=p+n[1]*h,i()}function i(){return f&&(f.valid=!1,f=null),t}var u,a,o,c,l,f,s=ge(function(n,t){return n=u(n,t),[n[0]*h+c,l-n[1]*h]}),h=150,g=480,p=250,d=0,m=0,v=0,y=0,M=0,x=Oo,b=st,_=null,w=null;return t.stream=function(n){return f&&(f.valid=!1),f=ve(a,x(s(b(n)))),f
 .valid=!0,f},t.clipAngle=function(n){return arguments.length?(x=null==n?(_=n,Oo):Kt((_=+n)*Ga),i()):_},t.clipExtent=function(n){return arguments.length?(w=n,b=null==n?st:Qt(n[0][0],n[0][1],n[1][0],n[1][1]),i()):w},t.scale=function(n){return arguments.length?(h=+n,r()):h},t.translate=function(n){return arguments.length?(g=+n[0],p=+n[1],r()):[g,p]},t.center=function(n){return arguments.length?(d=n[0]%360*Ga,m=n[1]%360*Ga,r()):[d*Ka,m*Ka]},t.rotate=function(n){return arguments.length?(v=n[0]%360*Ga,y=n[1]%360*Ga,M=n.length>2?n[2]%360*Ga:0,r()):[v*Ka,y*Ka,M*Ka]},va.rebind(t,s,"precision"),function(){return u=n.apply(this,arguments),t.invert=u.invert&&e,r()}}function ve(n,t){return{point:function(e,r){r=n(e*Ga,r*Ga),e=r[0],t.point(e>$a?e-2*$a:-$a>e?e+2*$a:e,r[1])},sphere:function(){t.sphere()},lineStart:function(){t.lineStart()},lineEnd:function(){t.lineEnd()},polygonStart:function(){t.polygonStart()},polygonEnd:function(){t.polygonEnd()}}}function ye(n,t){return[n,t]}function Me(n,t,e){
 return n?t||e?te(be(n),_e(t,e)):be(n):t||e?_e(t,e):ye}function xe(n){return function(t,e){return t+=n,[t>$a?t-2*$a:-$a>t?t+2*$a:t,e]}}function be(n){var t=xe(n);return t.invert=xe(-n),t}function _e(n,t){function e(n,t){var e=Math.cos(t),o=Math.cos(n)*e,c=Math.sin(n)*e,l=Math.sin(t),f=l*r+o*i;return[Math.atan2(c*u-f*a,o*r-l*i),U(f*u+c*a)]}var r=Math.cos(n),i=Math.sin(n),u=Math.cos(t),a=Math.sin(t);return e.invert=function(n,t){var e=Math.cos(t),o=Math.cos(n)*e,c=Math.sin(n)*e,l=Math.sin(t),f=l*u-c*a;return[Math.atan2(c*u+l*a,o*r+f*i),U(f*r-o*i)]},e}function we(n,t){var e=Math.cos(n),r=Math.sin(n);return function(i,u,a,o){null!=i?(i=Se(e,i),u=Se(e,u),(a>0?u>i:i>u)&&(i+=2*a*$a)):(i=n+2*a*$a,u=n);for(var c,l=a*t,f=i;a>0?f>u:u>f;f-=l)o.point((c=Dt([e,-r*Math.cos(f),-r*Math.sin(f)]))[0],c[1])}}function Se(n,t){var e=At(t);e[0]-=n,zt(e);var r=Y(-e[1]);return((-e[2]<0?-r:r)+2*Math.PI-Wa)%(2*Math.PI)}function Ee(n,t,e){var r=va.range(n,t-Wa,e).concat(t);return function(n){return r.map(functi
 on(t){return[n,t]})}}function ke(n,t,e){var r=va.range(n,t-Wa,e).concat(t);return function(n){return r.map(function(t){return[t,n]})}}function Ae(n){return n.source}function Ne(n){return n.target}function qe(n,t,e,r){var i=Math.cos(t),u=Math.sin(t),a=Math.cos(r),o=Math.sin(r),c=i*Math.cos(n),l=i*Math.sin(n),f=a*Math.cos(e),s=a*Math.sin(e),h=2*Math.asin(Math.sqrt(X(r-t)+i*a*X(e-n))),g=1/Math.sin(h),p=h?function(n){var t=Math.sin(n*=h)*g,e=Math.sin(h-n)*g,r=e*c+t*f,i=e*l+t*s,a=e*u+t*o;return[Math.atan2(i,r)*Ka,Math.atan2(a,Math.sqrt(r*r+i*i))*Ka]}:function(){return[n*Ka,t*Ka]};return p.distance=h,p}function Te(){function n(n,i){var u=Math.sin(i*=Ga),a=Math.cos(i),o=Math.abs((n*=Ga)-t),c=Math.cos(o);Go+=Math.atan2(Math.sqrt((o=a*Math.sin(o))*o+(o=r*u-e*a*c)*o),e*u+r*a*c),t=n,e=u,r=a}var t,e,r;Ko.point=function(i,u){t=i*Ga,e=Math.sin(u*=Ga),r=Math.cos(u),Ko.point=n},Ko.lineEnd=function(){Ko.point=Ko.lineEnd=T}}function Ce(n,t){function e(t,e){var r=Math.cos(t),i=Math.cos(e),u=n(r*i);ret
 urn[u*i*Math.sin(t),u*Math.sin(e)]}return e.invert=function(n,e){var r=Math.sqrt(n*n+e*e),i=t(r),u=Math.sin(i),a=Math.cos(i);return[Math.atan2(n*u,r*a),Math.asin(r&&e*u/r)]},e}function ze(n,t){function e(n,t){var e=Math.abs(Math.abs(t)-$a/2)<Wa?0:a/Math.pow(i(t),u);return[e*Math.sin(u*n),a-e*Math.cos(u*n)]}var r=Math.cos(n),i=function(n){return Math.tan($a/4+n/2)},u=n===t?Math.sin(n):Math.log(r/Math.cos(t))/Math.log(i(t)/i(n)),a=r*Math.pow(i(n),u)/u;return u?(e.invert=function(n,t){var e=a-t,r=R(u)*Math.sqrt(n*n+e*e);return[Math.atan2(n,e)/u,2*Math.atan(Math.pow(a/r,1/u))-$a/2]},e):je}function De(n,t){function e(n,t){var e=u-t;return[e*Math.sin(i*n),u-e*Math.cos(i*n)]}var r=Math.cos(n),i=n===t?Math.sin(n):(r-Math.cos(t))/(t-n),u=r/i+n;return Math.abs(i)<Wa?ye:(e.invert=function(n,t){var e=u-t;return[Math.atan2(n,e)/i,u-R(i)*Math.sqrt(n*n+e*e)]},e)}function je(n,t){return[n,Math.log(Math.tan($a/4+t/2))]}function Le(n){var t,e=de(n),r=e.scale,i=e.translate,u=e.clipExtent;return e.scal
 e=function(){var n=r.apply(e,arguments);return n===e?t?e.clipExtent(null):e:n},e.translate=function(){var n=i.apply(e,arguments);return n===e?t?e.clipExtent(null):e:n},e.clipExtent=function(n){var a=u.apply(e,arguments);if(a===e){if(t=null==n){var o=$a*r(),c=i();u([[c[0]-o,c[1]-o],[c[0]+o,c[1]+o]])}}else t&&(a=null);return a},e.clipExtent(null)}function He(n,t){var e=Math.cos(t)*Math.sin(n);return[Math.log((1+e)/(1-e))/2,Math.atan2(Math.tan(t),Math.cos(n))]}function Fe(n){function t(t){function a(){l.push("M",u(n(f),o))}for(var c,l=[],f=[],s=-1,h=t.length,g=ft(e),p=ft(r);++s<h;)i.call(this,c=t[s],s)?f.push([+g.call(this,c,s),+p.call(this,c,s)]):f.length&&(a(),f=[]);return f.length&&a(),l.length?l.join(""):null}var e=Pe,r=Oe,i=Rt,u=Re,a=u.key,o=.7;return t.x=function(n){return arguments.length?(e=n,t):e},t.y=function(n){return arguments.length?(r=n,t):r},t.defined=function(n){return arguments.length?(i=n,t):i},t.interpolate=function(n){return arguments.length?(a="function"==typeof n?
 u=n:(u=ic.get(n)||Re).key,t):a},t.tension=function(n){return arguments.length?(o=n,t):o},t}function Pe(n){return n[0]}function Oe(n){return n[1]}function Re(n){return n.join("L")}function Ye(n){return Re(n)+"Z"}function Ue(n){for(var t=0,e=n.length,r=n[0],i=[r[0],",",r[1]];++t<e;)i.push("H",(r[0]+(r=n[t])[0])/2,"V",r[1]);return e>1&&i.push("H",r[0]),i.join("")}function Ie(n){for(var t=0,e=n.length,r=n[0],i=[r[0],",",r[1]];++t<e;)i.push("V",(r=n[t])[1],"H",r[0]);return i.join("")}function Ve(n){for(var t=0,e=n.length,r=n[0],i=[r[0],",",r[1]];++t<e;)i.push("H",(r=n[t])[0],"V",r[1]);return i.join("")}function Xe(n,t){return n.length<4?Re(n):n[1]+$e(n.slice(1,n.length-1),We(n,t))}function Ze(n,t){return n.length<3?Re(n):n[0]+$e((n.push(n[0]),n),We([n[n.length-2]].concat(n,[n[1]]),t))}function Be(n,t){return n.length<3?Re(n):n[0]+$e(n,We(n,t))}function $e(n,t){if(t.length<1||n.length!=t.length&&n.length!=t.length+2)return Re(n);var e=n.length!=t.length,r="",i=n[0],u=n[1],a=t[0],o=a,c=1;i
 f(e&&(r+="Q"+(u[0]-a[0]*2/3)+","+(u[1]-a[1]*2/3)+","+u[0]+","+u[1],i=n[1],c=2),t.length>1){o=t[1],u=n[c],c++,r+="C"+(i[0]+a[0])+","+(i[1]+a[1])+","+(u[0]-o[0])+","+(u[1]-o[1])+","+u[0]+","+u[1];for(var l=2;l<t.length;l++,c++)u=n[c],o=t[l],r+="S"+(u[0]-o[0])+","+(u[1]-o[1])+","+u[0]+","+u[1]}if(e){var f=n[c];r+="Q"+(u[0]+o[0]*2/3)+","+(u[1]+o[1]*2/3)+","+f[0]+","+f[1]}return r}function We(n,t){for(var e,r=[],i=(1-t)/2,u=n[0],a=n[1],o=1,c=n.length;++o<c;)e=u,u=a,a=n[o],r.push([i*(a[0]-e[0]),i*(a[1]-e[1])]);return r}function Je(n){if(n.length<3)return Re(n);var t=1,e=n.length,r=n[0],i=r[0],u=r[1],a=[i,i,i,(r=n[1])[0]],o=[u,u,u,r[1]],c=[i,",",u];for(tr(c,a,o);++t<e;)r=n[t],a.shift(),a.push(r[0]),o.shift(),o.push(r[1]),tr(c,a,o);for(t=-1;++t<2;)a.shift(),a.push(r[0]),o.shift(),o.push(r[1]),tr(c,a,o);return c.join("")}function Ge(n){if(n.length<4)return Re(n);for(var t,e=[],r=-1,i=n.length,u=[0],a=[0];++r<3;)t=n[r],u.push(t[0]),a.push(t[1]);for(e.push(nr(oc,u)+","+nr(oc,a)),--r;++r<i;)t=n
 [r],u.shift(),u.push(t[0]),a.shift(),a.push(t[1]),tr(e,u,a);return e.join("")}function Ke(n){for(var t,e,r=-1,i=n.length,u=i+4,a=[],o=[];++r<4;)e=n[r%i],a.push(e[0]),o.push(e[1]);for(t=[nr(oc,a),",",nr(oc,o)],--r;++r<u;)e=n[r%i],a.shift(),a.push(e[0]),o.shift(),o.push(e[1]),tr(t,a,o);return t.join("")}function Qe(n,t){var e=n.length-1;if(e)for(var r,i,u=n[0][0],a=n[0][1],o=n[e][0]-u,c=n[e][1]-a,l=-1;++l<=e;)r=n[l],i=l/e,r[0]=t*r[0]+(1-t)*(u+i*o),r[1]=t*r[1]+(1-t)*(a+i*c);return Je(n)}function nr(n,t){return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]+n[3]*t[3]}function tr(n,t,e){n.push("C",nr(uc,t),",",nr(uc,e),",",nr(ac,t),",",nr(ac,e),",",nr(oc,t),",",nr(oc,e))}function er(n,t){return(t[1]-n[1])/(t[0]-n[0])}function rr(n){for(var t=0,e=n.length-1,r=[],i=n[0],u=n[1],a=r[0]=er(i,u);++t<e;)r[t]=(a+(a=er(i=u,u=n[t+1])))/2;return r[t]=a,r}function ir(n){for(var t,e,r,i,u=[],a=rr(n),o=-1,c=n.length-1;++o<c;)t=er(n[o],n[o+1]),Math.abs(t)<1e-6?a[o]=a[o+1]=0:(e=a[o]/t,r=a[o+1]/t,i=e*e+r*r,i>9&&(i=3*t/Ma
 th.sqrt(i),a[o]=i*e,a[o+1]=i*r));for(o=-1;++o<=c;)i=(n[Math.min(c,o+1)][0]-n[Math.max(0,o-1)][0])/(6*(1+a[o]*a[o])),u.push([i||0,a[o]*i||0]);
+return u}function ur(n){return n.length<3?Re(n):n[0]+$e(n,ir(n))}function ar(n,t,e,r){var i,u,a,o,c,l,f;return i=r[n],u=i[0],a=i[1],i=r[t],o=i[0],c=i[1],i=r[e],l=i[0],f=i[1],(f-a)*(o-u)-(c-a)*(l-u)>0}function or(n,t,e){return(e[0]-t[0])*(n[1]-t[1])<(e[1]-t[1])*(n[0]-t[0])}function cr(n,t,e,r){var i=n[0],u=e[0],a=t[0]-i,o=r[0]-u,c=n[1],l=e[1],f=t[1]-c,s=r[1]-l,h=(o*(c-l)-s*(i-u))/(s*a-o*f);return[i+h*a,c+h*f]}function lr(n,t){var e={list:n.map(function(n,t){return{index:t,x:n[0],y:n[1]}}).sort(function(n,t){return n.y<t.y?-1:n.y>t.y?1:n.x<t.x?-1:n.x>t.x?1:0}),bottomSite:null},r={list:[],leftEnd:null,rightEnd:null,init:function(){r.leftEnd=r.createHalfEdge(null,"l"),r.rightEnd=r.createHalfEdge(null,"l"),r.leftEnd.r=r.rightEnd,r.rightEnd.l=r.leftEnd,r.list.unshift(r.leftEnd,r.rightEnd)},createHalfEdge:function(n,t){return{edge:n,side:t,vertex:null,l:null,r:null}},insert:function(n,t){t.l=n,t.r=n.r,n.r.l=t,n.r=t},leftBound:function(n){var t=r.leftEnd;do t=t.r;while(t!=r.rightEnd&&i.righ
 tOf(t,n));return t=t.l},del:function(n){n.l.r=n.r,n.r.l=n.l,n.edge=null},right:function(n){return n.r},left:function(n){return n.l},leftRegion:function(n){return n.edge==null?e.bottomSite:n.edge.region[n.side]},rightRegion:function(n){return n.edge==null?e.bottomSite:n.edge.region[cc[n.side]]}},i={bisect:function(n,t){var e={region:{l:n,r:t},ep:{l:null,r:null}},r=t.x-n.x,i=t.y-n.y,u=r>0?r:-r,a=i>0?i:-i;return e.c=n.x*r+n.y*i+.5*(r*r+i*i),u>a?(e.a=1,e.b=i/r,e.c/=r):(e.b=1,e.a=r/i,e.c/=i),e},intersect:function(n,t){var e=n.edge,r=t.edge;if(!e||!r||e.region.r==r.region.r)return null;var i=e.a*r.b-e.b*r.a;if(Math.abs(i)<1e-10)return null;var u,a,o=(e.c*r.b-r.c*e.b)/i,c=(r.c*e.a-e.c*r.a)/i,l=e.region.r,f=r.region.r;l.y<f.y||l.y==f.y&&l.x<f.x?(u=n,a=e):(u=t,a=r);var s=o>=a.region.r.x;return s&&u.side==="l"||!s&&u.side==="r"?null:{x:o,y:c}},rightOf:function(n,t){var e=n.edge,r=e.region.r,i=t.x>r.x;if(i&&n.side==="l")return 1;if(!i&&n.side==="r")return 0;if(e.a===1){var u=t.y-r.y,a=t.x-r.x,
 o=0,c=0;if(!i&&e.b<0||i&&e.b>=0?c=o=u>=e.b*a:(c=t.x+t.y*e.b>e.c,e.b<0&&(c=!c),c||(o=1)),!o){var l=r.x-e.region.l.x;c=e.b*(a*a-u*u)<l*u*(1+2*a/l+e.b*e.b),e.b<0&&(c=!c)}}else{var f=e.c-e.a*t.x,s=t.y-f,h=t.x-r.x,g=f-r.y;c=s*s>h*h+g*g}return n.side==="l"?c:!c},endPoint:function(n,e,r){n.ep[e]=r,n.ep[cc[e]]&&t(n)},distance:function(n,t){var e=n.x-t.x,r=n.y-t.y;return Math.sqrt(e*e+r*r)}},u={list:[],insert:function(n,t,e){n.vertex=t,n.ystar=t.y+e;for(var r=0,i=u.list,a=i.length;a>r;r++){var o=i[r];if(!(n.ystar>o.ystar||n.ystar==o.ystar&&t.x>o.vertex.x))break}i.splice(r,0,n)},del:function(n){for(var t=0,e=u.list,r=e.length;r>t&&e[t]!=n;++t);e.splice(t,1)},empty:function(){return u.list.length===0},nextEvent:function(n){for(var t=0,e=u.list,r=e.length;r>t;++t)if(e[t]==n)return e[t+1];return null},min:function(){var n=u.list[0];return{x:n.vertex.x,y:n.ystar}},extractMin:function(){return u.list.shift()}};r.init(),e.bottomSite=e.list.shift();for(var a,o,c,l,f,s,h,g,p,d,m,v,y,M=e.list.shift();
 ;)if(u.empty()||(a=u.min()),M&&(u.empty()||M.y<a.y||M.y==a.y&&M.x<a.x))o=r.leftBound(M),c=r.right(o),h=r.rightRegion(o),v=i.bisect(h,M),s=r.createHalfEdge(v,"l"),r.insert(o,s),d=i.intersect(o,s),d&&(u.del(o),u.insert(o,d,i.distance(d,M))),o=s,s=r.createHalfEdge(v,"r"),r.insert(o,s),d=i.intersect(s,c),d&&u.insert(s,d,i.distance(d,M)),M=e.list.shift();else{if(u.empty())break;o=u.extractMin(),l=r.left(o),c=r.right(o),f=r.right(c),h=r.leftRegion(o),g=r.rightRegion(c),m=o.vertex,i.endPoint(o.edge,o.side,m),i.endPoint(c.edge,c.side,m),r.del(o),u.del(c),r.del(c),y="l",h.y>g.y&&(p=h,h=g,g=p,y="r"),v=i.bisect(h,g),s=r.createHalfEdge(v,y),r.insert(l,s),i.endPoint(v,cc[y],m),d=i.intersect(l,s),d&&(u.del(l),u.insert(l,d,i.distance(d,h))),d=i.intersect(s,f),d&&u.insert(s,d,i.distance(d,h))}for(o=r.right(r.leftEnd);o!=r.rightEnd;o=r.right(o))t(o.edge)}function fr(n){return n.x}function sr(n){return n.y}function hr(){return{leaf:!0,nodes:[],point:null,x:null,y:null}}function gr(n,t,e,r,i,u){if(!n(
 t,e,r,i,u)){var a=.5*(e+i),o=.5*(r+u),c=t.nodes;c[0]&&gr(n,c[0],e,r,a,o),c[1]&&gr(n,c[1],a,r,i,o),c[2]&&gr(n,c[2],e,o,a,u),c[3]&&gr(n,c[3],a,o,i,u)}}function pr(n,t){n=va.rgb(n),t=va.rgb(t);var e=n.r,r=n.g,i=n.b,u=t.r-e,a=t.g-r,o=t.b-i;return function(n){return"#"+it(Math.round(e+u*n))+it(Math.round(r+a*n))+it(Math.round(i+o*n))}}function dr(n){var t=[n.a,n.b],e=[n.c,n.d],r=vr(t),i=mr(t,e),u=vr(yr(e,t,-i))||0;t[0]*e[1]<e[0]*t[1]&&(t[0]*=-1,t[1]*=-1,r*=-1,i*=-1),this.rotate=(r?Math.atan2(t[1],t[0]):Math.atan2(-e[0],e[1]))*Ka,this.translate=[n.e,n.f],this.scale=[r,u],this.skew=u?Math.atan2(i,u)*Ka:0}function mr(n,t){return n[0]*t[0]+n[1]*t[1]}function vr(n){var t=Math.sqrt(mr(n,n));return t&&(n[0]/=t,n[1]/=t),t}function yr(n,t,e){return n[0]+=e*t[0],n[1]+=e*t[1],n}function Mr(n,t){return t-=n=+n,function(e){return n+t*e}}function xr(n,t){var e,r=[],i=[],u=va.transform(n),a=va.transform(t),o=u.translate,c=a.translate,l=u.rotate,f=a.rotate,s=u.skew,h=a.skew,g=u.scale,p=a.scale;return o[
 0]!=c[0]||o[1]!=c[1]?(r.push("translate(",null,",",null,")"),i.push({i:1,x:Mr(o[0],c[0])},{i:3,x:Mr(o[1],c[1])})):c[0]||c[1]?r.push("translate("+c+")"):r.push(""),l!=f?(l-f>180?f+=360:f-l>180&&(l+=360),i.push({i:r.push(r.pop()+"rotate(",null,")")-2,x:Mr(l,f)})):f&&r.push(r.pop()+"rotate("+f+")"),s!=h?i.push({i:r.push(r.pop()+"skewX(",null,")")-2,x:Mr(s,h)}):h&&r.push(r.pop()+"skewX("+h+")"),g[0]!=p[0]||g[1]!=p[1]?(e=r.push(r.pop()+"scale(",null,",",null,")"),i.push({i:e-4,x:Mr(g[0],p[0])},{i:e-2,x:Mr(g[1],p[1])})):(p[0]!=1||p[1]!=1)&&r.push(r.pop()+"scale("+p+")"),e=i.length,function(n){for(var t,u=-1;++u<e;)r[(t=i[u]).i]=t.x(n);return r.join("")}}function br(n,t){var e,r={},i={};for(e in n)e in t?r[e]=Sr(e)(n[e],t[e]):i[e]=n[e];for(e in t)e in n||(i[e]=t[e]);return function(n){for(e in r)i[e]=r[e](n);return i}}function _r(n,t){var e,r,i,u,a,o=0,c=0,l=[],f=[];for(n+="",t+="",fc.lastIndex=0,r=0;e=fc.exec(t);++r)e.index&&l.push(t.substring(o,c=e.index)),f.push({i:l.length,x:e[0]}),l.p
 ush(null),o=fc.lastIndex;for(o<t.length&&l.push(t.substring(o)),r=0,u=f.length;(e=fc.exec(n))&&u>r;++r)if(a=f[r],a.x==e[0]){if(a.i)if(l[a.i+1]==null)for(l[a.i-1]+=a.x,l.splice(a.i,1),i=r+1;u>i;++i)f[i].i--;else for(l[a.i-1]+=a.x+l[a.i+1],l.splice(a.i,2),i=r+1;u>i;++i)f[i].i-=2;else if(l[a.i+1]==null)l[a.i]=a.x;else for(l[a.i]=a.x+l[a.i+1],l.splice(a.i+1,1),i=r+1;u>i;++i)f[i].i--;f.splice(r,1),u--,r--}else a.x=Mr(parseFloat(e[0]),parseFloat(a.x));for(;u>r;)a=f.pop(),l[a.i+1]==null?l[a.i]=a.x:(l[a.i]=a.x+l[a.i+1],l.splice(a.i+1,1)),u--;return l.length===1?l[0]==null?(a=f[0].x,function(n){return a(n)+""}):function(){return t}:function(n){for(r=0;u>r;++r)l[(a=f[r]).i]=a.x(n);return l.join("")}}function wr(n,t){for(var e,r=va.interpolators.length;--r>=0&&!(e=va.interpolators[r](n,t)););return e}function Sr(n){return"transform"==n?xr:wr}function Er(n,t){var e,r=[],i=[],u=n.length,a=t.length,o=Math.min(n.length,t.length);for(e=0;o>e;++e)r.push(wr(n[e],t[e]));for(;u>e;++e)i[e]=n[e];for(;a>e
 ;++e)i[e]=t[e];return function(n){for(e=0;o>e;++e)i[e]=r[e](n);return i}}function kr(n){return function(t){return 0>=t?0:t>=1?1:n(t)}}function Ar(n){return function(t){return 1-n(1-t)}}function Nr(n){return function(t){return.5*(.5>t?n(2*t):2-n(2-2*t))}}function qr(n){return n*n}function Tr(n){return n*n*n}function Cr(n){if(0>=n)return 0;if(n>=1)return 1;var t=n*n,e=t*n;return 4*(.5>n?e:3*(n-t)+e-.75)}function zr(n){return function(t){return Math.pow(t,n)}}function Dr(n){return 1-Math.cos(n*$a/2)}function jr(n){return Math.pow(2,10*(n-1))}function Lr(n){return 1-Math.sqrt(1-n*n)}function Hr(n,t){var e;return arguments.length<2&&(t=.45),arguments.length?e=t/(2*$a)*Math.asin(1/n):(n=1,e=t/4),function(r){return 1+n*Math.pow(2,10*-r)*Math.sin(2*(r-e)*$a/t)}}function Fr(n){return n||(n=1.70158),function(t){return t*t*((n+1)*t-n)}}function Pr(n){return 1/2.75>n?7.5625*n*n:2/2.75>n?7.5625*(n-=1.5/2.75)*n+.75:2.5/2.75>n?7.5625*(n-=2.25/2.75)*n+.9375:7.5625*(n-=2.625/2.75)*n+.984375}function
  Or(n,t){n=va.hcl(n),t=va.hcl(t);var e=n.h,r=n.c,i=n.l,u=t.h-e,a=t.c-r,o=t.l-i;return isNaN(a)&&(a=0,r=isNaN(r)?t.c:r),isNaN(u)?(u=0,e=isNaN(e)?t.h:e):u>180?u-=360:-180>u&&(u+=360),function(n){return $(e+u*n,r+a*n,i+o*n)+""}}function Rr(n,t){n=va.hsl(n),t=va.hsl(t);var e=n.h,r=n.s,i=n.l,u=t.h-e,a=t.s-r,o=t.l-i;return isNaN(a)&&(a=0,r=isNaN(r)?t.s:r),isNaN(u)?(u=0,e=isNaN(e)?t.h:e):u>180?u-=360:-180>u&&(u+=360),function(n){return O(e+u*n,r+a*n,i+o*n)+""}}function Yr(n,t){n=va.lab(n),t=va.lab(t);var e=n.l,r=n.a,i=n.b,u=t.l-e,a=t.a-r,o=t.b-i;return function(n){return G(e+u*n,r+a*n,i+o*n)+""}}function Ur(n,t){return t-=n,function(e){return Math.round(n+t*e)}}function Ir(n,t){return t=t-(n=+n)?1/(t-n):0,function(e){return(e-n)*t}}function Vr(n,t){return t=t-(n=+n)?1/(t-n):0,function(e){return Math.max(0,Math.min(1,(e-n)*t))}}function Xr(n){for(var t=n.source,e=n.target,r=Br(t,e),i=[t];t!==r;)t=t.parent,i.push(t);for(var u=i.length;e!==r;)i.splice(u,0,e),e=e.parent;return i}function Zr(n)
 {for(var t=[],e=n.parent;null!=e;)t.push(n),n=e,e=e.parent;return t.push(n),t}function Br(n,t){if(n===t)return n;for(var e=Zr(n),r=Zr(t),i=e.pop(),u=r.pop(),a=null;i===u;)a=i,i=e.pop(),u=r.pop();return a}function $r(n){n.fixed|=2}function Wr(n){n.fixed&=-7}function Jr(n){n.fixed|=4,n.px=n.x,n.py=n.y}function Gr(n){n.fixed&=-5}function Kr(n,t,e){var r=0,i=0;if(n.charge=0,!n.leaf)for(var u,a=n.nodes,o=a.length,c=-1;++c<o;)u=a[c],null!=u&&(Kr(u,t,e),n.charge+=u.charge,r+=u.charge*u.cx,i+=u.charge*u.cy);if(n.point){n.leaf||(n.point.x+=Math.random()-.5,n.point.y+=Math.random()-.5);var l=t*e[n.point.index];n.charge+=n.pointCharge=l,r+=l*n.point.x,i+=l*n.point.y}n.cx=r/n.charge,n.cy=i/n.charge}function Qr(n,t){return va.rebind(n,t,"sort","children","value"),n.nodes=n,n.links=ri,n}function ni(n){return n.children}function ti(n){return n.value}function ei(n,t){return t.value-n.value}function ri(n){return va.merge(n.map(function(n){return(n.children||[]).map(function(t){return{source:n,target
 :t}})}))}function ii(n){return n.x}function ui(n){return n.y}function ai(n,t,e){n.y0=t,n.y=e}function oi(n){return va.range(n.length)}function ci(n){for(var t=-1,e=n[0].length,r=[];++t<e;)r[t]=0;return r}function li(n){for(var t,e=1,r=0,i=n[0][1],u=n.length;u>e;++e)(t=n[e][1])>i&&(r=e,i=t);return r}function fi(n){return n.reduce(si,0)}function si(n,t){return n+t[1]}function hi(n,t){return gi(n,Math.ceil(Math.log(t.length)/Math.LN2+1))}function gi(n,t){for(var e=-1,r=+n[0],i=(n[1]-r)/t,u=[];++e<=t;)u[e]=i*e+r;return u}function pi(n){return[va.min(n),va.max(n)]}function di(n,t){return n.parent==t.parent?1:2}function mi(n){var t=n.children;return t&&t.length?t[0]:n._tree.thread}function vi(n){var t,e=n.children;return e&&(t=e.length)?e[t-1]:n._tree.thread}function yi(n,t){var e=n.children;if(e&&(i=e.length))for(var r,i,u=-1;++u<i;)t(r=yi(e[u],t),n)>0&&(n=r);return n}function Mi(n,t){return n.x-t.x}function xi(n,t){return t.x-n.x}function bi(n,t){return n.depth-t.depth}function _i(n,t){
 function e(n,r){var i=n.children;if(i&&(a=i.length))for(var u,a,o=null,c=-1;++c<a;)u=i[c],e(u,o),o=u;t(n,r)}e(n,null)}function wi(n){for(var t,e=0,r=0,i=n.children,u=i.length;--u>=0;)t=i[u]._tree,t.prelim+=e,t.mod+=e,e+=t.shift+(r+=t.change)}function Si(n,t,e){n=n._tree,t=t._tree;var r=e/(t.number-n.number);n.change+=r,t.change-=r,t.shift+=e,t.prelim+=e,t.mod+=e}function Ei(n,t,e){return n._tree.ancestor.parent==t.parent?n._tree.ancestor:e}function ki(n,t){return n.value-t.value}function Ai(n,t){var e=n._pack_next;n._pack_next=t,t._pack_prev=n,t._pack_next=e,e._pack_prev=t}function Ni(n,t){n._pack_next=t,t._pack_prev=n}function qi(n,t){var e=t.x-n.x,r=t.y-n.y,i=n.r+t.r;return.999*i*i>e*e+r*r}function Ti(n){function t(n){f=Math.min(n.x-n.r,f),s=Math.max(n.x+n.r,s),h=Math.min(n.y-n.r,h),g=Math.max(n.y+n.r,g)}if((e=n.children)&&(l=e.length)){var e,r,i,u,a,o,c,l,f=1/0,s=-1/0,h=1/0,g=-1/0;if(e.forEach(Ci),r=e[0],r.x=-r.r,r.y=0,t(r),l>1&&(i=e[1],i.x=i.r,i.y=0,t(i),l>2))for(u=e[2],ji(r,i,u
 ),t(u),Ai(r,u),r._pack_prev=u,Ai(u,i),i=r._pack_next,a=3;l>a;a++){ji(r,i,u=e[a]);var p=0,d=1,m=1;for(o=i._pack_next;o!==i;o=o._pack_next,d++)if(qi(o,u)){p=1;break}if(1==p)for(c=r._pack_prev;c!==o._pack_prev&&!qi(c,u);c=c._pack_prev,m++);p?(m>d||d==m&&i.r<r.r?Ni(r,i=o):Ni(r=c,i),a--):(Ai(r,u),i=u,t(u))}var v=(f+s)/2,y=(h+g)/2,M=0;for(a=0;l>a;a++)u=e[a],u.x-=v,u.y-=y,M=Math.max(M,u.r+Math.sqrt(u.x*u.x+u.y*u.y));n.r=M,e.forEach(zi)}}function Ci(n){n._pack_next=n._pack_prev=n}function zi(n){delete n._pack_next,delete n._pack_prev}function Di(n,t,e,r){var i=n.children;if(n.x=t+=r*n.x,n.y=e+=r*n.y,n.r*=r,i)for(var u=-1,a=i.length;++u<a;)Di(i[u],t,e,r)}function ji(n,t,e){var r=n.r+e.r,i=t.x-n.x,u=t.y-n.y;if(r&&(i||u)){var a=t.r+e.r,o=i*i+u*u;a*=a,r*=r;var c=.5+(r-a)/(2*o),l=Math.sqrt(Math.max(0,2*a*(r+o)-(r-=o)*r-a*a))/(2*o);e.x=n.x+c*i+l*u,e.y=n.y+c*u-l*i}else e.x=n.x+r,e.y=n.y}function Li(n){return 1+va.max(n,function(n){return n.y})}function Hi(n){return n.reduce(function(n,t){return n+
 t.x},0)/n.length}function Fi(n){var t=n.children;return t&&t.length?Fi(t[0]):n}function Pi(n){var t,e=n.children;return e&&(t=e.length)?Pi(e[t-1]):n}function Oi(n){return{x:n.x,y:n.y,dx:n.dx,dy:n.dy}}function Ri(n,t){var e=n.x+t[3],r=n.y+t[0],i=n.dx-t[1]-t[3],u=n.dy-t[0]-t[2];return 0>i&&(e+=i/2,i=0),0>u&&(r+=u/2,u=0),{x:e,y:r,dx:i,dy:u}}function Yi(n){var t=n[0],e=n[n.length-1];return e>t?[t,e]:[e,t]}function Ui(n){return n.rangeExtent?n.rangeExtent():Yi(n.range())}function Ii(n,t,e,r){var i=e(n[0],n[1]),u=r(t[0],t[1]);return function(n){return u(i(n))}}function Vi(n,t){var e,r=0,i=n.length-1,u=n[r],a=n[i];return u>a&&(e=r,r=i,i=e,e=u,u=a,a=e),n[r]=t.floor(u),n[i]=t.ceil(a),n}function Xi(n){return n?{floor:function(t){return Math.floor(t/n)*n},ceil:function(t){return Math.ceil(t/n)*n}}:Mc}function Zi(n,t,e,r){var i=[],u=[],a=0,o=Math.min(n.length,t.length)-1;for(n[o]<n[0]&&(n=n.slice().reverse(),t=t.slice().reverse());++a<=o;)i.push(e(n[a-1],n[a])),u.push(r(t[a-1],t[a]));return fun
 ction(t){var e=va.bisect(n,t,1,o)-1;return u[e](i[e](t))}}function Bi(n,t,e,r){function i(){var i=Math.min(n.length,t.length)>2?Zi:Ii,c=r?Vr:Ir;return a=i(n,t,c,e),o=i(t,n,c,wr),u}function u(n){return a(n)}var a,o;return u.invert=function(n){return o(n)},u.domain=function(t){return arguments.length?(n=t.map(Number),i()):n},u.range=function(n){return arguments.length?(t=n,i()):t},u.rangeRound=function(n){return u.range(n).interpolate(Ur)},u.clamp=function(n){return arguments.length?(r=n,i()):r},u.interpolate=function(n){return arguments.length?(e=n,i()):e},u.ticks=function(t){return Ki(n,t)},u.tickFormat=function(t,e){return Qi(n,t,e)},u.nice=function(t){return Wi(n,t),i()},u.copy=function(){return Bi(n,t,e,r)},i()}function $i(n,t){return va.rebind(n,t,"range","rangeRound","interpolate","clamp")}function Wi(n,t){return Vi(n,Xi(t?Gi(n,t)[2]:Ji(n)))}function Ji(n){var t=Yi(n),e=t[1]-t[0];return Math.pow(10,Math.round(Math.log(e)/Math.LN10)-1)}function Gi(n,t){var e=Yi(n),r=e[1]-e[0],i=
 Math.pow(10,Math.floor(Math.log(r/t)/Math.LN10)),u=t/r*i;return.15>=u?i*=10:.35>=u?i*=5:.75>=u&&(i*=2),e[0]=Math.ceil(e[0]/i)*i,e[1]=Math.floor(e[1]/i)*i+.5*i,e[2]=i,e}function Ki(n,t){return va.range.apply(va,Gi(n,t))}function Qi(n,t,e){var r=-Math.floor(Math.log(Gi(n,t)[2])/Math.LN10+.01);return va.format(e?e.replace(vo,function(n,t,e,i,u,a,o,c,l,f){return[t,e,i,u,a,o,c,l||"."+(r-2*("%"===f)),f].join("")}):",."+r+"f")}function nu(n,t,e,r,i){function u(t){return n(e(t))}return u.invert=function(t){return r(n.invert(t))},u.domain=function(t){return arguments.length?(t[0]<0?(e=ru,r=iu):(e=tu,r=eu),n.domain((i=t.map(Number)).map(e)),u):i},u.base=function(n){return arguments.length?(t=+n,u):t},u.nice=function(){function r(n){return Math.pow(t,Math.floor(Math.log(n)/Math.log(t)))}function a(n){return Math.pow(t,Math.ceil(Math.log(n)/Math.log(t)))}return n.domain(Vi(i,e===tu?{floor:r,ceil:a}:{floor:function(n){return-a(-n)},ceil:function(n){return-r(-n)}}).map(e)),u},u.ticks=function(){v
 ar i=Yi(n.domain()),u=[];if(i.every(isFinite)){var a=Math.log(t),o=Math.floor(i[0]/a),c=Math.ceil(i[1]/a),l=r(i[0]),f=r(i[1]),s=t%1?2:t;if(e===ru)for(u.push(-Math.pow(t,-o));o++<c;)for(var h=s-1;h>0;h--)u.push(-Math.pow(t,-o)*h);else{for(;c>o;o++)for(var h=1;s>h;h++)u.push(Math.pow(t,o)*h);u.push(Math.pow(t,o))}for(o=0;u[o]<l;o++);for(c=u.length;u[c-1]>f;c--);u=u.slice(o,c)}return u},u.tickFormat=function(n,i){if(!arguments.length)return xc;arguments.length<2?i=xc:"function"!=typeof i&&(i=va.format(i));var a,o=Math.log(t),c=Math.max(.1,n/u.ticks().length),l=e===ru?(a=-1e-12,Math.floor):(a=1e-12,Math.ceil);return function(n){return n/r(o*l(e(n)/o+a))<=c?i(n):""}},u.copy=function(){return nu(n.copy(),t,e,r,i)},$i(u,n)}function tu(n){return Math.log(0>n?0:n)}function eu(n){return Math.exp(n)}function ru(n){return-Math.log(n>0?0:-n)}function iu(n){return-Math.exp(-n)}function uu(n,t,e){function r(t){return n(i(t))}var i=au(t),u=au(1/t);return r.invert=function(t){return u(n.invert(t))},
 r.domain=function(t){return arguments.length?(n.domain((e=t.map(Number)).map(i)),r):e},r.ticks=function(n){return Ki(e,n)},r.tickFormat=function(n,t){return Qi(e,n,t)},r.nice=function(n){return r.domain(Wi(e,n))},r.exponent=function(a){return arguments.length?(i=au(t=a),u=au(1/t),n.domain(e.map(i)),r):t},r.copy=function(){return uu(n.copy(),t,e)},$i(r,n)}function au(n){return function(t){return 0>t?-Math.pow(-t,n):Math.pow(t,n)}}function ou(n,t){function e(t){return a[((u.get(t)||u.set(t,n.push(t)))-1)%a.length]}function r(t,e){return va.range(n.length).map(function(n){return t+e*n})}var u,a,o;return e.domain=function(r){if(!arguments.length)return n;n=[],u=new i;for(var a,o=-1,c=r.length;++o<c;)u.has(a=r[o])||u.set(a,n.push(a));return e[t.t].apply(e,t.a)},e.range=function(n){return arguments.length?(a=n,o=0,t={t:"range",a:arguments},e):a},e.rangePoints=function(i,u){arguments.length<2&&(u=0);var c=i[0],l=i[1],f=(l-c)/(Math.max(1,n.length-1)+u);return a=r(n.length<2?(c+l)/2:c+f*u/2,
 f),o=0,t={t:"rangePoints",a:arguments},e},e.rangeBands=function(i,u,c){arguments.length<2&&(u=0),arguments.length<3&&(c=u);var l=i[1]<i[0],f=i[l-0],s=i[1-l],h=(s-f)/(n.length-u+2*c);return a=r(f+h*c,h),l&&a.reverse(),o=h*(1-u),t={t:"rangeBands",a:arguments},e},e.rangeRoundBands=function(i,u,c){arguments.length<2&&(u=0),arguments.length<3&&(c=u);var l=i[1]<i[0],f=i[l-0],s=i[1-l],h=Math.floor((s-f)/(n.length-u+2*c)),g=s-f-(n.length-u)*h;return a=r(f+Math.round(g/2),h),l&&a.reverse(),o=Math.round(h*(1-u)),t={t:"rangeRoundBands",a:arguments},e},e.rangeBand=function(){return o},e.rangeExtent=function(){return Yi(t.a[0])},e.copy=function(){return ou(n,t)},e.domain(n)}function cu(n,t){function e(){var e=0,u=t.length;for(i=[];++e<u;)i[e-1]=va.quantile(n,e/u);return r}function r(n){return isNaN(n=+n)?void 0:t[va.bisect(i,n)]}var i;return r.domain=function(t){return arguments.length?(n=t.filter(function(n){return!isNaN(n)}).sort(va.ascending),e()):n},r.range=function(n){return arguments.lengt
 h?(t=n,e()):t},r.quantiles=function(){return i},r.copy=function(){return cu(n,t)},e()}function lu(n,t,e){function r(t){return e[Math.max(0,Math.min(a,Math.floor(u*(t-n))))]}function i(){return u=e.length/(t-n),a=e.length-1,r}var u,a;return r.domain=function(e){return arguments.length?(n=+e[0],t=+e[e.length-1],i()):[n,t]},r.range=function(n){return arguments.length?(e=n,i()):e},r.copy=function(){return lu(n,t,e)},r.invertExtent=function(t){return t=e.indexOf(t),t=0>t?0/0:t/u+n,[t,t+1/u]},i()}function fu(n,t){function e(e){return e>=e?t[va.bisect(n,e)]:void 0}return e.domain=function(t){return arguments.length?(n=t,e):n},e.range=function(n){return arguments.length?(t=n,e):t},e.invertExtent=function(e){return e=t.indexOf(e),[n[e-1],n[e]]},e.copy=function(){return fu(n,t)},e}function su(n){function t(n){return+n}return t.invert=t,t.domain=t.range=function(e){return arguments.length?(n=e.map(t),t):n},t.ticks=function(t){return Ki(n,t)},t.tickFormat=function(t,e){return Qi(n,t,e)},t.copy=
 function(){return su(n)},t}function hu(n){return n.innerRadius}function gu(n){return n.outerRadius}function pu(n){return n.startAngle}function du(n){return n.endAngle}function mu(n){for(var t,e,r,i=-1,u=n.length;++i<u;)t=n[i],e=t[0],r=t[1]+Ec,t[0]=e*Math.cos(r),t[1]=e*Math.sin(r);return n}function vu(n){function t(t){function c(){d.push("M",o(n(v),s),f,l(n(m.reverse()),s),"Z")}for(var h,g,p,d=[],m=[],v=[],y=-1,M=t.length,x=ft(e),b=ft(i),_=e===r?function(){return g}:ft(r),w=i===u?function(){return p}:ft(u);++y<M;)a.call(this,h=t[y],y)?(m.push([g=+x.call(this,h,y),p=+b.call(this,h,y)]),v.push([+_.call(this,h,y),+w.call(this,h,y)])):m.length&&(c(),m=[],v=[]);return m.length&&c(),d.length?d.join(""):null}var e=Pe,r=Pe,i=0,u=Oe,a=Rt,o=Re,c=o.key,l=o,f="L",s=.7;return t.x=function(n){return arguments.length?(e=r=n,t):r},t.x0=function(n){return arguments.length?(e=n,t):e},t.x1=function(n){return arguments.length?(r=n,t):r},t.y=function(n){return arguments.length?(i=u=n,t):u},t.y0=function(
 n){return arguments.length?(i=n,t):i},t.y1=function(n){return arguments.length?(u=n,t):u},t.defined=function(n){return arguments.length?(a=n,t):a},t.interpolate=function(n){return arguments.length?(c="function"==typeof n?o=n:(o=ic.get(n)||Re).key,l=o.reverse||o,f=o.closed?"M":"L",t):c},t.tension=function(n){return arguments.length?(s=n,t):s},t}function yu(n){return n.radius}function Mu(n){return[n.x,n.y]}function xu(n){return function(){var t=n.apply(this,arguments),e=t[0],r=t[1]+Ec;return[e*Math.cos(r),e*Math.sin(r)]}}function bu(){return 64}function _u(){return"circle"}function wu(n){var t=Math.sqrt(n/$a);return"M0,"+t+"A"+t+","+t+" 0 1,1 0,"+-t+"A"+t+","+t+" 0 1,1 0,"+t+"Z"}function Su(n,t){return Ta(n,Cc),n.id=t,n}function Eu(n,t,e,r){var i=n.id;return j(n,"function"==typeof e?function(n,u,a){n.__transition__[i].tween.set(t,r(e.call(n,n.__data__,u,a)))}:(e=r(e),function(n){n.__transition__[i].tween.set(t,e)}))}function ku(n){return null==n&&(n=""),function(){this.textContent=n}}
 function Au(n,t,e,r){var u=n.__transition__||(n.__transition__={active:0,count:0}),a=u[e];if(!a){var o=r.time;return a=u[e]={tween:new i,event:va.dispatch("start","end"),time:o,ease:r.ease,delay:r.delay,duration:r.duration},++u.count,va.timer(function(r){function i(r){return u.active>e?l():(u.active=e,h.start.call(n,f,t),a.tween.forEach(function(e,r){(r=r.call(n,f,t))&&d.push(r)}),c(r)||va.timer(c,0,o),1)}function c(r){if(u.active!==e)return l();for(var i=(r-g)/p,a=s(i),o=d.length;o>0;)d[--o].call(n,a);return i>=1?(l(),h.end.call(n,f,t),1):void 0}function l(){return--u.count?delete u[e]:delete n.__transition__,1}var f=n.__data__,s=a.ease,h=a.event,g=a.delay,p=a.duration,d=[];return r>=g?i(r):va.timer(i,g,o),1},0,o),a}}function Nu(n,t){n.attr("transform",function(n){return"translate("+t(n)+",0)"})}function qu(n,t){n.attr("transform",function(n){return"translate(0,"+t(n)+")"})}function Tu(n,t,e){if(r=[],e&&t.length>1){for(var r,i,u,a=Yi(n.domain()),o=-1,c=t.length,l=(t[1]-t[0])/++e;++
 o<c;)for(i=e;--i>0;)(u=+t[o]-i*l)>=a[0]&&r.push(u);for(--o,i=0;++i<e&&(u=+t[o]+i*l)<a[1];)r.push(u)}return r}function Cu(){this._=new Date(arguments.length>1?Date.UTC.apply(this,arguments):arguments[0])}function zu(n,t,e){function r(t){var e=n(t),r=u(e,1);return r-t>t-e?e:r}function i(e){return t(e=n(new Pc(e-1)),1),e}function u(n,e){return t(n=new Pc(+n),e),n}function a(n,r,u){var a=i(n),o=[];if(u>1)for(;r>a;)e(a)%u||o.push(new Date(+a)),t(a,1);else for(;r>a;)o.push(new Date(+a)),t(a,1);return o}function o(n,t,e){try{Pc=Cu;var r=new Cu;return r._=n,a(r,t,e)}finally{Pc=Date}}n.floor=n,n.round=r,n.ceil=i,n.offset=u,n.range=a;var c=n.utc=Du(n);return c.floor=c,c.round=Du(r),c.ceil=Du(i),c.offset=Du(u),c.range=o,n}function Du(n){return function(t,e){try{Pc=Cu;var r=new Cu;return r._=t,n(r,e)._}finally{Pc=Date}}}function ju(n,t,e,r){for(var i,u,a=0,o=t.length,c=e.length;o>a;){if(r>=c)return-1;if(i=t.charCodeAt(a++),37===i){if(u=ul[t.charAt(a++)],!u||(r=u(n,e,r))<0)return-1}else if(i!=e.
 charCodeAt(r++))return-1}return r}function Lu(n){return RegExp("^(?:"+n.map(va.requote).join("|")+")","i")}function Hu(n){for(var t=new i,e=-1,r=n.length;++e<r;)t.set(n[e].toLowerCase(),e);return t}function Fu(n,t,e){var r=0>n?"-":"",i=(r?-n:n)+"",u=i.length;return r+(e>u?Array(e-u+1).join(t)+i:i)}function Pu(n,t,e){Jc.lastIndex=0;var r=Jc.exec(t.substring(e));return r?(n.w=Gc.get(r[0].toLowerCase()),e+r[0].length):-1}function Ou(n,t,e){$c.lastIndex=0;var r=$c.exec(t.substring(e));return r?(n.w=Wc.get(r[0].toLowerCase()),e+r[0].length):-1}function Ru(n,t,e){al.lastIndex=0;var r=al.exec(t.substring(e,e+1));return r?(n.w=+r[0],e+r[0].length):-1}function Yu(n,t,e){al.lastIndex=0;var r=al.exec(t.substring(e));return r?(n.U=+r[0],e+r[0].length):-1}function Uu(n,t,e){al.lastIndex=0;var r=al.exec(t.substring(e));return r?(n.W=+r[0],e+r[0].length):-1}function Iu(n,t,e){nl.lastIndex=0;var r=nl.exec(t.substring(e));return r?(n.m=tl.get(r[0].toLowerCase()),e+r[0].length):-1}function Vu(n,t,e){
 Kc.lastIndex=0;var r=Kc.exec(t.substring(e));return r?(n.m=Qc.get(r[0].toLowerCase()),e+r[0].length):-1}function Xu(n,t,e){return ju(n,""+il.c,t,e)}function Zu(n,t,e){return ju(n,""+il.x,t,e)}function Bu(n,t,e){return ju(n,""+il.X,t,e)}function $u(n,t,e){al.lastIndex=0;var r=al.exec(t.substring(e,e+4));return r?(n.y=+r[0],e+r[0].length):-1}function Wu(n,t,e){al.lastIndex=0;var r=al.exec(t.substring(e,e+2));return r?(n.y=Ju(+r[0]),e+r[0].length):-1}function Ju(n){return n+(n>68?1900:2e3)}function Gu(n,t,e){al.lastIndex=0;var r=al.exec(t.substring(e,e+2));return r?(n.m=r[0]-1,e+r[0].length):-1}function Ku(n,t,e){al.lastIndex=0;var r=al.exec(t.substring(e,e+2));return r?(n.d=+r[0],e+r[0].length):-1}function Qu(n,t,e){al.lastIndex=0;var r=al.exec(t.substring(e,e+3));return r?(n.j=+r[0],e+r[0].length):-1}function na(n,t,e){al.lastIndex=0;var r=al.exec(t.substring(e,e+2));return r?(n.H=+r[0],e+r[0].length):-1}function ta(n,t,e){al.lastIndex=0;var r=al.exec(t.substring(e,e+2));return r?(n.
 M=+r[0],e+r[0].length):-1}function ea(n,t,e){al.lastIndex=0;var r=al.exec(t.substring(e,e+2));return r?(n.S=+r[0],e+r[0].length):-1}function ra(n,t,e){al.lastIndex=0;var r=al.exec(t.substring(e,e+3));return r?(n.L=+r[0],e+r[0].length):-1}function ia(n,t,e){var r=ol.get(t.substring(e,e+=2).toLowerCase());return null==r?-1:(n.p=r,e)}function ua(n){var t=n.getTimezoneOffset(),e=t>0?"-":"+",r=~~(Math.abs(t)/60),i=Math.abs(t)%60;return e+Fu(r,"0",2)+Fu(i,"0",2)}function aa(n,t,e){el.lastIndex=0;var r=el.exec(t.substring(e,e+1));return r?e+r[0].length:-1}function oa(n){return n.toISOString()}function ca(n,t,e){function r(t){return n(t)}return r.invert=function(t){return la(n.invert(t))},r.domain=function(t){return arguments.length?(n.domain(t),r):n.domain().map(la)},r.nice=function(n){return r.domain(Vi(r.domain(),n))},r.ticks=function(e,i){var u=Yi(r.domain());if("function"!=typeof e){var a=u[1]-u[0],o=a/e,c=va.bisect(ll,o);if(c==ll.length)return t.year(u,e);if(!c)return n.ticks(e).map(l
 a);Math.log(o/ll[c-1])<Math.log(ll[c]/o)&&--c,e=t[c],i=e[1],e=e[0].range}return e(u[0],new Date(+u[1]+1),i)},r.tickFormat=function(){return e},r.copy=function(){return ca(n.copy(),t,e)},$i(r,n)}function la(n){return new Date(n)}function fa(n){return function(t){for(var e=n.length-1,r=n[e];!r[1](t);)r=n[--e];return r[0](t)}}function sa(n){var t=new Date(n,0,1);return t.setFullYear(n),t}function ha(n){var t=n.getFullYear(),e=sa(t),r=sa(t+1);return t+(n-e)/(r-e)}function ga(n){var t=new Date(Date.UTC(n,0,1));return t.setUTCFullYear(n),t}function pa(n){var t=n.getUTCFullYear(),e=ga(t),r=ga(t+1);return t+(n-e)/(r-e)}function da(n){return JSON.parse(n.responseText)}function ma(n){var t=ya.createRange();return t.selectNode(ya.body),t.createContextualFragment(n.responseText)}var va={version:"3.2.1"};Date.now||(Date.now=function(){return+new Date});var ya=document,Ma=ya.documentElement,xa=window;try{ya.createElement("div").style.setProperty("opacity",0,"")}catch(ba){var _a=xa.CSSStyleDeclara
 tion.prototype,wa=_a.setProperty;_a.setProperty=function(n,t,e){wa.call(this,n,t+"",e)}}va.ascending=function(n,t){return t>n?-1:n>t?1:n>=t?0:0/0},va.descending=function(n,t){return n>t?-1:t>n?1:t>=n?0:0/0},va.min=function(n,t){var e,r,i=-1,u=n.length;if(arguments.length===1){for(;++i<u&&!((e=n[i])!=null&&e>=e);)e=void 0;for(;++i<u;)(r=n[i])!=null&&e>r&&(e=r)}else{for(;++i<u&&!((e=t.call(n,n[i],i))!=null&&e>=e);)e=void 0;for(;++i<u;)(r=t.call(n,n[i],i))!=null&&e>r&&(e=r)}return e},va.max=function(n,t){var e,r,i=-1,u=n.length;if(arguments.length===1){for(;++i<u&&!((e=n[i])!=null&&e>=e);)e=void 0;for(;++i<u;)(r=n[i])!=null&&r>e&&(e=r)}else{for(;++i<u&&!((e=t.call(n,n[i],i))!=null&&e>=e);)e=void 0;for(;++i<u;)(r=t.call(n,n[i],i))!=null&&r>e&&(e=r)}return e},va.extent=function(n,t){var e,r,i,u=-1,a=n.length;if(arguments.length===1){for(;++u<a&&!((e=i=n[u])!=null&&e>=e);)e=i=void 0;for(;++u<a;)(r=n[u])!=null&&(e>r&&(e=r),r>i&&(i=r))}else{for(;++u<a&&!((e=i=t.call(n,n[u],u))!=null&&e>=e);
 )e=void 0;for(;++u<a;)(r=t.call(n,n[u],u))!=null&&(e>r&&(e=r),r>i&&(i=r))}return[e,i]},va.sum=function(n,t){var e,r=0,i=n.length,u=-1;if(arguments.length===1)for(;++u<i;)isNaN(e=+n[u])||(r+=e);else for(;++u<i;)isNaN(e=+t.call(n,n[u],u))||(r+=e);return r},va.mean=function(t,e){var r,i=t.length,u=0,a=-1,o=0;if(arguments.length===1)for(;++a<i;)n(r=t[a])&&(u+=(r-u)/++o);else for(;++a<i;)n(r=e.call(t,t[a],a))&&(u+=(r-u)/++o);return o?u:void 0},va.quantile=function(n,t){var e=(n.length-1)*t+1,r=Math.floor(e),i=+n[r-1],u=e-r;return u?i+u*(n[r]-i):i},va.median=function(t,e){return arguments.length>1&&(t=t.map(e)),t=t.filter(n),t.length?va.quantile(t.sort(va.ascending),.5):void 0},va.bisector=function(n){return{left:function(t,e,r,i){for(arguments.length<3&&(r=0),arguments.length<4&&(i=t.length);i>r;){var u=r+i>>>1;n.call(t,t[u],u)<e?r=u+1:i=u}return r},right:function(t,e,r,i){for(arguments.length<3&&(r=0),arguments.length<4&&(i=t.length);i>r;){var u=r+i>>>1;e<n.call(t,t[u],u)?i=u:r=u+1}retu
 rn r}}};var Sa=va.bisector(function(n){return n});va.bisectLeft=Sa.left,va.bisect=va.bisectRight=Sa.right,va.shuffle=function(n){for(var t,e,r=n.length;r;)e=Math.random()*r--|0,t=n[r],n[r]=n[e],n[e]=t;return n},va.permute=function(n,t){for(var e=[],r=-1,i=t.length;++r<i;)e[r]=n[t[r]];return e},va.zip=function(){if(!(i=arguments.length))return[];for(var n=-1,e=va.min(arguments,t),r=Array(e);++n<e;)for(var i,u=-1,a=r[n]=Array(i);++u<i;)a[u]=arguments[u][n];return r},va.transpose=function(n){return va.zip.apply(va,n)},va.keys=function(n){var t=[];for(var e in n)t.push(e);return t},va.values=function(n){var t=[];for(var e in n)t.push(n[e]);return t},va.entries=function(n){var t=[];for(var e in n)t.push({key:e,value:n[e]});return t},va.merge=function(n){return Array.prototype.concat.apply([],n)},va.range=function(n,t,r){if(arguments.length<3&&(r=1,arguments.length<2&&(t=n,n=0)),1/0===(t-n)/r)throw Error("infinite range");var i,u=[],a=e(Math.abs(r)),o=-1;if(n*=a,t*=a,r*=a,0>r)for(;(i=n+r*
 ++o)>t;)u.push(i/a);else for(;(i=n+r*++o)<t;)u.push(i/a);return u},va.map=function(n){var t=new i;for(var e in n)t.set(e,n[e]);return t},r(i,{has:function(n){return Ea+n in this},get:function(n){return this[Ea+n]},set:function(n,t){return this[Ea+n]=t},remove:function(n){return n=Ea+n,n in this&&delete this[n]},keys:function(){var n=[];return this.forEach(function(t){n.push(t)}),n},values:function(){var n=[];return this.forEach(function(t,e){n.push(e)}),n},entries:function(){var n=[];return this.forEach(function(t,e){n.push({key:t,value:e})}),n},forEach:function(n){for(var t in this)t.charCodeAt(0)===ka&&n.call(this,t.substring(1),this[t])}});var Ea="\0",ka=Ea.charCodeAt(0);va.nest=function(){function n(t,o,c){if(c>=a.length)return r?r.call(u,o):e?o.sort(e):o;for(var l,f,s,h,g=-1,p=o.length,d=a[c++],m=new i;++g<p;)(h=m.get(l=d(f=o[g])))?h.push(f):m.set(l,[f]);return t?(f=t(),s=function(e,r){f.set(e,n(t,r,c))}):(f={},s=function(e,r){f[e]=n(t,r,c)}),m.forEach(s),f}function t(n,e){if(e
 >=a.length)return n;var r=[],i=o[e++];return n.forEach(function(n,i){r.push({key:n,values:t(i,e)})
+}),i?r.sort(function(n,t){return i(n.key,t.key)}):r}var e,r,u={},a=[],o=[];return u.map=function(t,e){return n(e,t,0)},u.entries=function(e){return t(n(va.map,e,0),0)},u.key=function(n){return a.push(n),u},u.sortKeys=function(n){return o[a.length-1]=n,u},u.sortValues=function(n){return e=n,u},u.rollup=function(n){return r=n,u},u},va.set=function(n){var t=new u;if(n)for(var e=0;e<n.length;e++)t.add(n[e]);return t},r(u,{has:function(n){return Ea+n in this},add:function(n){return this[Ea+n]=!0,n},remove:function(n){return n=Ea+n,n in this&&delete this[n]},values:function(){var n=[];return this.forEach(function(t){n.push(t)}),n},forEach:function(n){for(var t in this)t.charCodeAt(0)===ka&&n.call(this,t.substring(1))}}),va.behavior={},va.rebind=function(n,t){for(var e,r=1,i=arguments.length;++r<i;)n[e=arguments[r]]=a(n,t,t[e]);return n},va.dispatch=function(){for(var n=new o,t=-1,e=arguments.length;++t<e;)n[arguments[t]]=c(n);return n},o.prototype.on=function(n,t){var e=n.indexOf("."),r="
 ";if(e>=0&&(r=n.substring(e+1),n=n.substring(0,e)),n)return arguments.length<2?this[n].on(r):this[n].on(r,t);if(arguments.length===2){if(null==t)for(n in this)this.hasOwnProperty(n)&&this[n].on(r,null);return this}},va.event=null,va.mouse=function(n){return g(n,f())};var Aa=/WebKit/.test(xa.navigator.userAgent)?-1:0,Na=d;try{Na(Ma.childNodes)[0].nodeType}catch(qa){Na=p}var Ta=[].__proto__?function(n,t){n.__proto__=t}:function(n,t){for(var e in t)n[e]=t[e]};va.touches=function(n,t){return arguments.length<2&&(t=f().touches),t?Na(t).map(function(t){var e=g(n,t);return e.identifier=t.identifier,e}):[]};var Ca=["webkit","ms","moz","Moz","o","O"],za=m(Ma.style,"userSelect"),Da=za?function(){var n=Ma.style,t=n[za];return n[za]="none",function(){n[za]=t}}:function(n){var t=va.select(xa).on("selectstart."+n,l);return function(){t.on("selectstart."+n,null)}};va.behavior.drag=function(){function n(){this.on("mousedown.drag",t).on("touchstart.drag",t)}function t(){function n(){var n=a.parentNo
 de;return null!=f?va.touches(n).filter(function(n){return n.identifier===f})[0]:va.mouse(n)}function t(){if(!a.parentNode)return i();var t=n(),e=t[0]-h[0],r=t[1]-h[1];g|=e|r,h=t,l(),o({type:"drag",x:t[0]+u[0],y:t[1]+u[1],dx:e,dy:r})}function i(){o({type:"dragend"}),g&&(l(),va.event.target===c&&s(d,"click")),d.on(null!=f?"touchmove.drag-"+f:"mousemove.drag",null).on(null!=f?"touchend.drag-"+f:"mouseup.drag",null),p()}var u,a=this,o=e.of(a,arguments),c=va.event.target,f=va.event.touches?va.event.changedTouches[0].identifier:null,h=n(),g=0,p=Da(null!=f?"drag-"+f:"drag"),d=va.select(xa).on(null!=f?"touchmove.drag-"+f:"mousemove.drag",t).on(null!=f?"touchend.drag-"+f:"mouseup.drag",i,!0);r?(u=r.apply(a,arguments),u=[u.x-h[0],u.y-h[1]]):u=[0,0],o({type:"dragstart"})}var e=h(n,"drag","dragstart","dragend"),r=null;return n.origin=function(t){return arguments.length?(r=t,n):r},va.rebind(n,e,"on")};var ja=function(n,t){return t.querySelector(n)},La=function(n,t){return t.querySelectorAll(n)},
 Ha=Ma[m(Ma,"matchesSelector")],Fa=function(n,t){return Ha.call(n,t)};"function"==typeof Sizzle&&(ja=function(n,t){return Sizzle(n,t)[0]||null},La=function(n,t){return Sizzle.uniqueSort(Sizzle(n,t))},Fa=Sizzle.matchesSelector),va.selection=function(){return Ia};var Pa=va.selection.prototype=[];Pa.select=function(n){var t,e,r,i,u=[];"function"!=typeof n&&(n=y(n));for(var a=-1,o=this.length;++a<o;){u.push(t=[]),t.parentNode=(r=this[a]).parentNode;for(var c=-1,l=r.length;++c<l;)(i=r[c])?(t.push(e=n.call(i,i.__data__,c)),e&&"__data__"in i&&(e.__data__=i.__data__)):t.push(null)}return v(u)},Pa.selectAll=function(n){var t,e,r=[];"function"!=typeof n&&(n=M(n));for(var i=-1,u=this.length;++i<u;)for(var a=this[i],o=-1,c=a.length;++o<c;)(e=a[o])&&(r.push(t=Na(n.call(e,e.__data__,o))),t.parentNode=e);return v(r)};var Oa={svg:"http://www.w3.org/2000/svg",xhtml:"http://www.w3.org/1999/xhtml",xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2
 000/xmlns/"};va.ns={prefix:Oa,qualify:function(n){var t=n.indexOf(":"),e=n;return t>=0&&(e=n.substring(0,t),n=n.substring(t+1)),Oa.hasOwnProperty(e)?{space:Oa[e],local:n}:n}},Pa.attr=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node();return n=va.ns.qualify(n),n.local?e.getAttributeNS(n.space,n.local):e.getAttribute(n)}for(t in n)this.each(x(t,n[t]));return this}return this.each(x(n,t))},va.requote=function(n){return n.replace(Ra,"\\$&")};var Ra=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;Pa.classed=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node(),r=(n=n.trim().split(/^|\s+/g)).length,i=-1;if(t=e.classList){for(;++i<r;)if(!t.contains(n[i]))return!1}else for(t=e.getAttribute("class");++i<r;)if(!_(n[i]).test(t))return!1;return!0}for(t in n)this.each(w(t,n[t]));return this}return this.each(w(n,t))},Pa.style=function(n,t,e){var r=arguments.length;if(3>r){if("string"!=typeof n){2>r&&(t="");for(e in n)this.each(E(e,n[e],t));return this}if(2>r)r
 eturn xa.getComputedStyle(this.node(),null).getPropertyValue(n);e=""}return this.each(E(n,t,e))},Pa.property=function(n,t){if(arguments.length<2){if("string"==typeof n)return this.node()[n];for(t in n)this.each(k(t,n[t]));return this}return this.each(k(n,t))},Pa.text=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.textContent=null==t?"":t}:null==n?function(){this.textContent=""}:function(){this.textContent=n}):this.node().textContent},Pa.html=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.innerHTML=null==t?"":t}:null==n?function(){this.innerHTML=""}:function(){this.innerHTML=n}):this.node().innerHTML},Pa.append=function(n){function t(){return this.appendChild(ya.createElementNS(this.namespaceURI,n))}function e(){return this.appendChild(ya.createElementNS(n.space,n.local))}return n=va.ns.qualify(n),this.select(n.local?e:t)},Pa.insert=function(n,t){functio
 n e(e,r){return this.insertBefore(ya.createElementNS(this.namespaceURI,n),t.call(this,e,r))}function r(e,r){return this.insertBefore(ya.createElementNS(n.space,n.local),t.call(this,e,r))}return n=va.ns.qualify(n),"function"!=typeof t&&(t=y(t)),this.select(n.local?r:e)},Pa.remove=function(){return this.each(function(){var n=this.parentNode;n&&n.removeChild(this)})},Pa.data=function(n,t){function e(n,e){var r,u,a,o=n.length,s=e.length,h=Math.min(o,s),g=Array(s),p=Array(s),d=Array(o);if(t){var m,v=new i,y=new i,M=[];for(r=-1;++r<o;)m=t.call(u=n[r],u.__data__,r),v.has(m)?d[r]=u:v.set(m,u),M.push(m);for(r=-1;++r<s;)m=t.call(e,a=e[r],r),(u=v.get(m))?(g[r]=u,u.__data__=a):y.has(m)||(p[r]=A(a)),y.set(m,a),v.remove(m);for(r=-1;++r<o;)v.has(M[r])&&(d[r]=n[r])}else{for(r=-1;++r<h;)u=n[r],a=e[r],u?(u.__data__=a,g[r]=u):p[r]=A(a);for(;s>r;++r)p[r]=A(e[r]);for(;o>r;++r)d[r]=n[r]}p.update=g,p.parentNode=g.parentNode=d.parentNode=n.parentNode,c.push(p),l.push(g),f.push(d)}var r,u,a=-1,o=this.length
 ;if(!arguments.length){for(n=Array(o=(r=this[0]).length);++a<o;)(u=r[a])&&(n[a]=u.__data__);return n}var c=L([]),l=v([]),f=v([]);if("function"==typeof n)for(;++a<o;)e(r=this[a],n.call(r,r.parentNode.__data__,a));else for(;++a<o;)e(r=this[a],n);return l.enter=function(){return c},l.exit=function(){return f},l},Pa.datum=function(n){return arguments.length?this.property("__data__",n):this.property("__data__")},Pa.filter=function(n){var t,e,r,i=[];"function"!=typeof n&&(n=N(n));for(var u=0,a=this.length;a>u;u++){i.push(t=[]),t.parentNode=(e=this[u]).parentNode;for(var o=0,c=e.length;c>o;o++)(r=e[o])&&n.call(r,r.__data__,o)&&t.push(r)}return v(i)},Pa.order=function(){for(var n=-1,t=this.length;++n<t;)for(var e,r=this[n],i=r.length-1,u=r[i];--i>=0;)(e=r[i])&&(u&&u!==e.nextSibling&&u.parentNode.insertBefore(e,u),u=e);return this},Pa.sort=function(n){n=q.apply(this,arguments);for(var t=-1,e=this.length;++t<e;)this[t].sort(n);return this.order()},Pa.on=function(n,t,e){var r=arguments.length;
 if(3>r){if("string"!=typeof n){2>r&&(t=!1);for(e in n)this.each(C(e,n[e],t));return this}if(2>r)return(r=this.node()["__on"+n])&&r._;e=!1}return this.each(C(n,t,e))};var Ya=va.map({mouseenter:"mouseover",mouseleave:"mouseout"});Ya.forEach(function(n){"on"+n in ya&&Ya.remove(n)}),Pa.each=function(n){return j(this,function(t,e,r){n.call(t,t.__data__,e,r)})},Pa.call=function(n){var t=Na(arguments);return n.apply(t[0]=this,t),this},Pa.empty=function(){return!this.node()},Pa.node=function(){for(var n=0,t=this.length;t>n;n++)for(var e=this[n],r=0,i=e.length;i>r;r++){var u=e[r];if(u)return u}return null},Pa.size=function(){var n=0;return this.each(function(){++n}),n};var Ua=[];va.selection.enter=L,va.selection.enter.prototype=Ua,Ua.append=Pa.append,Ua.insert=Pa.insert,Ua.empty=Pa.empty,Ua.node=Pa.node,Ua.call=Pa.call,Ua.size=Pa.size,Ua.select=function(n){for(var t,e,r,i,u,a=[],o=-1,c=this.length;++o<c;){r=(i=this[o]).update,a.push(t=[]),t.parentNode=i.parentNode;for(var l=-1,f=i.length;++l
 <f;)(u=i[l])?(t.push(r[l]=e=n.call(i.parentNode,u.__data__,l)),e.__data__=u.__data__):t.push(null)}return v(a)},Pa.transition=function(){var n,t,e=Nc||++zc,r=[],i=Object.create(Dc);i.time=Date.now();for(var u=-1,a=this.length;++u<a;){r.push(n=[]);for(var o=this[u],c=-1,l=o.length;++c<l;)(t=o[c])&&Au(t,c,e,i),n.push(t)}return Su(r,e)},va.select=function(n){var t=["string"==typeof n?ja(n,ya):n];return t.parentNode=Ma,v([t])},va.selectAll=function(n){var t=Na("string"==typeof n?La(n,ya):n);return t.parentNode=Ma,v([t])};var Ia=va.select(Ma);va.behavior.zoom=function(){function n(){this.on("mousedown.zoom",o).on("mousemove.zoom",f).on(Za+".zoom",c).on("dblclick.zoom",g).on("touchstart.zoom",p).on("touchmove.zoom",d).on("touchend.zoom",p)}function t(n){return[(n[0]-S[0])/E,(n[1]-S[1])/E]}function e(n){return[n[0]*E+S[0],n[1]*E+S[1]]}function r(n){E=Math.max(k[0],Math.min(k[1],n))}function i(n,t){t=e(t),S[0]+=n[0]-t[0],S[1]+=n[1]-t[1]}function u(){x&&x.domain(M.range().map(function(n){ret
 urn(n-S[0])/E}).map(M.invert)),_&&_.domain(b.range().map(function(n){return(n-S[1])/E}).map(b.invert))}function a(n){u(),va.event.preventDefault(),n({type:"zoom",scale:E,translate:S})}function o(){function n(){c=1,i(va.mouse(r),h),a(u)}function e(){c&&l(),f.on("mousemove.zoom",null).on("mouseup.zoom",null),g(),c&&va.event.target===o&&s(f,"click.zoom")}var r=this,u=A.of(r,arguments),o=va.event.target,c=0,f=va.select(xa).on("mousemove.zoom",n).on("mouseup.zoom",e),h=t(va.mouse(r)),g=Da("zoom")}function c(){m||(m=t(va.mouse(this))),r(Math.pow(2,Va()*.002)*E),i(va.mouse(this),m),a(A.of(this,arguments))}function f(){m=null}function g(){var n=va.mouse(this),e=t(n),u=Math.log(E)/Math.LN2;r(Math.pow(2,va.event.shiftKey?Math.ceil(u)-1:Math.floor(u)+1)),i(n,e),a(A.of(this,arguments))}function p(){var n=va.touches(this),e=Date.now();if(y=E,m={},v=0,n.forEach(function(n){m[n.identifier]=t(n)}),n.length===1){if(500>e-w){var u=n[0],o=t(n[0]);r(2*E),i(u,o),a(A.of(this,arguments))}w=e}else if(n.len
 gth>1){var u=n[0],c=n[1],l=u[0]-c[0],f=u[1]-c[1];v=l*l+f*f}}function d(){var n=va.touches(this),t=n[0],e=m[t.identifier];if(u=n[1]){var u,o=m[u.identifier],c=va.event.scale;if(null==c){var l=(l=u[0]-t[0])*l+(l=u[1]-t[1])*l;c=v&&Math.sqrt(l/v)}t=[(t[0]+u[0])/2,(t[1]+u[1])/2],e=[(e[0]+o[0])/2,(e[1]+o[1])/2],r(c*y)}i(t,e),w=null,a(A.of(this,arguments))}var m,v,y,M,x,b,_,w,S=[0,0],E=1,k=Xa,A=h(n,"zoom");return n.translate=function(t){return arguments.length?(S=t.map(Number),u(),n):S},n.scale=function(t){return arguments.length?(E=+t,u(),n):E},n.scaleExtent=function(t){return arguments.length?(k=null==t?Xa:t.map(Number),n):k},n.x=function(t){return arguments.length?(x=t,M=t.copy(),S=[0,0],E=1,n):x},n.y=function(t){return arguments.length?(_=t,b=t.copy(),S=[0,0],E=1,n):_},va.rebind(n,A,"on")};var Va,Xa=[0,1/0],Za="onwheel"in ya?(Va=function(){return-va.event.deltaY*(va.event.deltaMode?120:1)},"wheel"):"onmousewheel"in ya?(Va=function(){return va.event.wheelDelta},"mousewheel"):(Va=functio
 n(){return-va.event.detail},"MozMousePixelScroll");H.prototype.toString=function(){return this.rgb()+""},va.hsl=function(n,t,e){return arguments.length===1?n instanceof P?F(n.h,n.s,n.l):ut(""+n,at,F):F(+n,+t,+e)};var Ba=P.prototype=new H;Ba.brighter=function(n){return n=Math.pow(.7,arguments.length?n:1),F(this.h,this.s,this.l/n)},Ba.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),F(this.h,this.s,n*this.l)},Ba.rgb=function(){return O(this.h,this.s,this.l)};var $a=Math.PI,Wa=1e-6,Ja=Wa*Wa,Ga=$a/180,Ka=180/$a;va.hcl=function(n,t,e){return arguments.length===1?n instanceof B?Z(n.h,n.c,n.l):n instanceof J?K(n.l,n.a,n.b):K((n=ot((n=va.rgb(n)).r,n.g,n.b)).l,n.a,n.b):Z(+n,+t,+e)};var Qa=B.prototype=new H;Qa.brighter=function(n){return Z(this.h,this.c,Math.min(100,this.l+no*(arguments.length?n:1)))},Qa.darker=function(n){return Z(this.h,this.c,Math.max(0,this.l-no*(arguments.length?n:1)))},Qa.rgb=function(){return $(this.h,this.c,this.l).rgb()},va.lab=function(n,t,e){return arg
 uments.length===1?n instanceof J?W(n.l,n.a,n.b):n instanceof B?$(n.l,n.c,n.h):ot((n=va.rgb(n)).r,n.g,n.b):W(+n,+t,+e)};var no=18,to=.95047,eo=1,ro=1.08883,io=J.prototype=new H;io.brighter=function(n){return W(Math.min(100,this.l+no*(arguments.length?n:1)),this.a,this.b)},io.darker=function(n){return W(Math.max(0,this.l-no*(arguments.length?n:1)),this.a,this.b)},io.rgb=function(){return G(this.l,this.a,this.b)},va.rgb=function(n,t,e){return arguments.length===1?n instanceof rt?et(n.r,n.g,n.b):ut(""+n,et,O):et(~~n,~~t,~~e)};var uo=rt.prototype=new H;uo.brighter=function(n){n=Math.pow(.7,arguments.length?n:1);var t=this.r,e=this.g,r=this.b,i=30;return t||e||r?(t&&i>t&&(t=i),e&&i>e&&(e=i),r&&i>r&&(r=i),et(Math.min(255,Math.floor(t/n)),Math.min(255,Math.floor(e/n)),Math.min(255,Math.floor(r/n)))):et(i,i,i)},uo.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),et(Math.floor(n*this.r),Math.floor(n*this.g),Math.floor(n*this.b))},uo.hsl=function(){return at(this.r,this.g,this.b)}
 ,uo.toString=function(){return"#"+it(this.r)+it(this.g)+it(this.b)};var ao=va.map({aliceblue:"#f0f8ff",antiquewhite:"#faebd7",aqua:"#00ffff",aquamarine:"#7fffd4",azure:"#f0ffff",beige:"#f5f5dc",bisque:"#ffe4c4",black:"#000000",blanchedalmond:"#ffebcd",blue:"#0000ff",blueviolet:"#8a2be2",brown:"#a52a2a",burlywood:"#deb887",cadetblue:"#5f9ea0",chartreuse:"#7fff00",chocolate:"#d2691e",coral:"#ff7f50",cornflowerblue:"#6495ed",cornsilk:"#fff8dc",crimson:"#dc143c",cyan:"#00ffff",darkblue:"#00008b",darkcyan:"#008b8b",darkgoldenrod:"#b8860b",darkgray:"#a9a9a9",darkgreen:"#006400",darkgrey:"#a9a9a9",darkkhaki:"#bdb76b",darkmagenta:"#8b008b",darkolivegreen:"#556b2f",darkorange:"#ff8c00",darkorchid:"#9932cc",darkred:"#8b0000",darksalmon:"#e9967a",darkseagreen:"#8fbc8f",darkslateblue:"#483d8b",darkslategray:"#2f4f4f",darkslategrey:"#2f4f4f",darkturquoise:"#00ced1",darkviolet:"#9400d3",deeppink:"#ff1493",deepskyblue:"#00bfff",dimgray:"#696969",dimgrey:"#696969",dodgerblue:"#1e90ff",firebrick:"#b
 22222",floralwhite:"#fffaf0",forestgreen:"#228b22",fuchsia:"#ff00ff",gainsboro:"#dcdcdc",ghostwhite:"#f8f8ff",gold:"#ffd700",goldenrod:"#daa520",gray:"#808080",green:"#008000",greenyellow:"#adff2f",grey:"#808080",honeydew:"#f0fff0",hotpink:"#ff69b4",indianred:"#cd5c5c",indigo:"#4b0082",ivory:"#fffff0",khaki:"#f0e68c",lavender:"#e6e6fa",lavenderblush:"#fff0f5",lawngreen:"#7cfc00",lemonchiffon:"#fffacd",lightblue:"#add8e6",lightcoral:"#f08080",lightcyan:"#e0ffff",lightgoldenrodyellow:"#fafad2",lightgray:"#d3d3d3",lightgreen:"#90ee90",lightgrey:"#d3d3d3",lightpink:"#ffb6c1",lightsalmon:"#ffa07a",lightseagreen:"#20b2aa",lightskyblue:"#87cefa",lightslategray:"#778899",lightslategrey:"#778899",lightsteelblue:"#b0c4de",lightyellow:"#ffffe0",lime:"#00ff00",limegreen:"#32cd32",linen:"#faf0e6",magenta:"#ff00ff",maroon:"#800000",mediumaquamarine:"#66cdaa",mediumblue:"#0000cd",mediumorchid:"#ba55d3",mediumpurple:"#9370db",mediumseagreen:"#3cb371",mediumslateblue:"#7b68ee",mediumspringgreen:"#00
 fa9a",mediumturquoise:"#48d1cc",mediumvioletred:"#c71585",midnightblue:"#191970",mintcream:"#f5fffa",mistyrose:"#ffe4e1",moccasin:"#ffe4b5",navajowhite:"#ffdead",navy:"#000080",oldlace:"#fdf5e6",olive:"#808000",olivedrab:"#6b8e23",orange:"#ffa500",orangered:"#ff4500",orchid:"#da70d6",palegoldenrod:"#eee8aa",palegreen:"#98fb98",paleturquoise:"#afeeee",palevioletred:"#db7093",papayawhip:"#ffefd5",peachpuff:"#ffdab9",peru:"#cd853f",pink:"#ffc0cb",plum:"#dda0dd",powderblue:"#b0e0e6",purple:"#800080",red:"#ff0000",rosybrown:"#bc8f8f",royalblue:"#4169e1",saddlebrown:"#8b4513",salmon:"#fa8072",sandybrown:"#f4a460",seagreen:"#2e8b57",seashell:"#fff5ee",sienna:"#a0522d",silver:"#c0c0c0",skyblue:"#87ceeb",slateblue:"#6a5acd",slategray:"#708090",slategrey:"#708090",snow:"#fffafa",springgreen:"#00ff7f",steelblue:"#4682b4",tan:"#d2b48c",teal:"#008080",thistle:"#d8bfd8",tomato:"#ff6347",turquoise:"#40e0d0",violet:"#ee82ee",wheat:"#f5deb3",white:"#ffffff",whitesmoke:"#f5f5f5",yellow:"#ffff00",yell
 owgreen:"#9acd32"});ao.forEach(function(n,t){ao.set(n,ut(t,et,O))}),va.functor=ft,va.xhr=ht(st),va.dsv=function(n,t){function e(n,e,u){arguments.length<3&&(u=e,e=null);var a=va.xhr(n,t,u);return a.row=function(n){return arguments.length?a.response((e=n)==null?r:i(n)):e},a.row(e)}function r(n){return e.parse(n.responseText)}function i(n){return function(t){return e.parse(t.responseText,n)}}function a(t){return t.map(o).join(n)}function o(n){return c.test(n)?'"'+n.replace(/\"/g,'""')+'"':n}var c=RegExp('["'+n+"\n]"),l=n.charCodeAt(0);return e.parse=function(n,t){var r;return e.parseRows(n,function(n,e){if(r)return r(n,e-1);var i=Function("d","return {"+n.map(function(n,t){return JSON.stringify(n)+": d["+t+"]"}).join(",")+"}");r=t?function(n,e){return t(i(n),e)}:i})},e.parseRows=function(n,t){function e(){if(f>=c)return a;if(i)return i=!1,u;var t=f;if(n.charCodeAt(t)===34){for(var e=t;e++<c;)if(n.charCodeAt(e)===34){if(n.charCodeAt(e+1)!==34)break;++e}f=e+2;var r=n.charCodeAt(e+1);retu
 rn 13===r?(i=!0,n.charCodeAt(e+2)===10&&++f):10===r&&(i=!0),n.substring(t+1,e).replace(/""/g,'"')}for(;c>f;){var r=n.charCodeAt(f++),o=1;if(10===r)i=!0;else if(13===r)i=!0,n.charCodeAt(f)===10&&(++f,++o);else if(r!==l)continue;return n.substring(t,f-o)}return n.substring(t)}for(var r,i,u={},a={},o=[],c=n.length,f=0,s=0;(r=e())!==a;){for(var h=[];r!==u&&r!==a;)h.push(r),r=e();(!t||(h=t(h,s++)))&&o.push(h)}return o},e.format=function(t){if(Array.isArray(t[0]))return e.formatRows(t);var r=new u,i=[];return t.forEach(function(n){for(var t in n)r.has(t)||i.push(r.add(t))}),[i.map(o).join(n)].concat(t.map(function(t){return i.map(function(n){return o(t[n])}).join(n)})).join("\n")},e.formatRows=function(n){return n.map(a).join("\n")},e},va.csv=va.dsv(",","text/csv"),va.tsv=va.dsv("	","text/tab-separated-values");var oo,co,lo,fo;va.timer=function(n,t,e){if(arguments.length<3){if(arguments.length<2)t=0;else if(!isFinite(t))return;e=Date.now()}var r=e+t,i={callback:n,time:r,next:null};co?co.n
 ext=i:oo=i,co=i,lo||(fo=clearTimeout(fo),lo=1,so(dt))},va.timer.flush=function(){mt(),vt()};var so=xa[m(xa,"requestAnimationFrame")]||function(n){setTimeout(n,17)},ho=".",go=",",po=[3,3],mo=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"].map(yt);va.formatPrefix=function(n,t){var e=0;return n&&(0>n&&(n*=-1),t&&(n=va.round(n,Mt(n,t))),e=1+Math.floor(1e-12+Math.log(n)/Math.LN10),e=Math.max(-24,Math.min(24,Math.floor((0>=e?e+1:e-1)/3)*3))),mo[8+e/3]},va.round=function(n,t){return t?Math.round(n*(t=Math.pow(10,t)))/t:Math.round(n)},va.format=function(n){var t=vo.exec(n),e=t[1]||" ",r=t[2]||">",i=t[3]||"",u=t[4]||"",a=t[5],o=+t[6],c=t[7],l=t[8],f=t[9],s=1,h="",g=!1;switch(l&&(l=+l.substring(1)),(a||"0"===e&&"="===r)&&(a=e="0",r="=",c&&(o-=Math.floor((o-1)/4))),f){case"n":c=!0,f="g";break;case"%":s=100,h="%",f="f";break;case"p":s=100,h="%",f="r";break;case"b":case"o":case"x":case"X":u&&(u="0"+f.toLowerCase());case"c":case"d":g=!0,l=0;break;case"s":s=-1,f="r"}"#"===u&&(
 u=""),"r"!=f||l||(f="g"),null!=l&&("g"==f?l=Math.max(1,Math.min(21,l)):("e"==f||"f"==f)&&(l=Math.max(0,Math.min(20,l)))),f=yo.get(f)||xt;var p=a&&c;return function(n){if(g&&n%1)return"";var t=0>n||0===n&&0>1/n?(n=-n,"-"):i;if(0>s){var d=va.formatPrefix(n,l);n=d.scale(n),h=d.symbol}else n*=s;n=f(n,l),!a&&c&&(n=Mo(n));var m=u.length+n.length+(p?0:t.length),v=o>m?Array(m=o-m+1).join(e):"";return p&&(n=Mo(v+n)),ho&&n.replace(".",ho),t+=u,("<"===r?t+n+v:">"===r?v+t+n:"^"===r?v.substring(0,m>>=1)+t+n+v.substring(m):t+(p?n:v+n))+h}};var vo=/(?:([^{])?([<>=^]))?([+\- ])?(#)?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i,yo=va.map({b:function(n){return n.toString(2)},c:function(n){return String.fromCharCode(n)},o:function(n){return n.toString(8)},x:function(n){return n.toString(16)},X:function(n){return n.toString(16).toUpperCase()},g:function(n,t){return n.toPrecision(t)},e:function(n,t){return n.toExponential(t)},f:function(n,t){return n.toFixed(t)},r:function(n,t){return(n=va.round(n,Mt(n,t))).toFix
 ed(Math.max(0,Math.min(20,Mt(n*(1+1e-15),t))))}}),Mo=st;if(po){var xo=po.length;Mo=function(n){for(var t=n.lastIndexOf("."),e=t>=0?"."+n.substring(t+1):(t=n.length,""),r=[],i=0,u=po[0];t>0&&u>0;)r.push(n.substring(t-=u,t+u)),u=po[i=(i+1)%xo];return r.reverse().join(go||"")+e}}va.geo={},bt.prototype={s:0,t:0,add:function(n){_t(n,this.t,bo),_t(bo.s,this.s,this),this.s?this.t+=bo.t:this.s=bo.t},reset:function(){this.s=this.t=0},valueOf:function(){return this.s}};var bo=new bt;va.geo.stream=function(n,t){n&&_o.hasOwnProperty(n.type)?_o[n.type](n,t):wt(n,t)};var _o={Feature:function(n,t){wt(n.geometry,t)},FeatureCollection:function(n,t){for(var e=n.features,r=-1,i=e.length;++r<i;)wt(e[r].geometry,t)}},wo={Sphere:function(n,t){t.sphere()},Point:function(n,t){var e=n.coordinates;t.point(e[0],e[1])},MultiPoint:function(n,t){for(var e,r=n.coordinates,i=-1,u=r.length;++i<u;)e=r[i],t.point(e[0],e[1])},LineString:function(n,t){St(n.coordinates,t,0)},MultiLineString:function(n,t){for(var e=n.coo
 rdinates,r=-1,i=e.length;++r<i;)St(e[r],t,0)},Polygon:function(n,t){Et(n.coordinates,t)},MultiPolygon:function(n,t){for(var e=n.coordinates,r=-1,i=e.length;++r<i;)Et(e[r],t)},GeometryCollection:function(n,t){for(var e=n.geometries,r=-1,i=e.length;++r<i;)wt(e[r],t)}};va.geo.area=function(n){return So=0,va.geo.stream(n,ko),So};var So,Eo=new bt,ko={sphere:function(){So+=4*$a},point:T,lineStart:T,lineEnd:T,polygonStart:function(){Eo.reset(),ko.lineStart=kt},polygonEnd:function(){var n=2*Eo;So+=0>n?4*$a+n:n,ko.lineStart=ko.lineEnd=ko.point=T}};va.geo.bounds=function(){function n(n,t){M.push(x=[f=n,h=n]),s>t&&(s=t),t>g&&(g=t)}function t(t,e){var r=At([t*Ga,e*Ga]);if(v){var i=qt(v,r),u=[i[1],-i[0],0],a=qt(u,i);zt(a),a=Dt(a);var c=t-p,l=c>0?1:-1,d=a[0]*Ka*l,m=Math.abs(c)>180;if(m^(d>l*p&&l*t>d)){var y=a[1]*Ka;y>g&&(g=y)}else if(d=(d+360)%360-180,m^(d>l*p&&l*t>d)){var y=-a[1]*Ka;s>y&&(s=y)}else s>e&&(s=e),e>g&&(g=e);m?p>t?o(f,t)>o(f,h)&&(h=t):o(t,h)>o(f,h)&&(f=t):h>=f?(f>t&&(f=t),t>h&&(h=t))
 :t>p?o(f,t)>o(f,h)&&(h=t):o(t,h)>o(f,h)&&(f=t)}else n(t,e);v=r,p=t}function e(){b.point=t}function r(){x[0]=f,x[1]=h,b.point=n,v=null}function i(n,e){if(v){var r=n-p;y+=Math.abs(r)>180?r+(r>0?360:-360):r}else d=n,m=e;ko.point(n,e),t(n,e)}function u(){ko.lineStart()}function a(){i(d,m),ko.lineEnd(),Math.abs(y)>Wa&&(f=-(h=180)),x[0]=f,x[1]=h,v=null}function o(n,t){return(t-=n)<0?t+360:t}function c(n,t){return n[0]-t[0]}function l(n,t){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:n<t[0]||t[1]<n}var f,s,h,g,p,d,m,v,y,M,x,b={point:n,lineStart:e,lineEnd:r,polygonStart:function(){b.point=i,b.lineStart=u,b.lineEnd=a,y=0,ko.polygonStart()},polygonEnd:function(){ko.polygonEnd(),b.point=n,b.lineStart=e,b.lineEnd=r,0>Eo?(f=-(h=180),s=-(g=90)):y>Wa?g=90:-Wa>y&&(s=-90),x[0]=f,x[1]=h}};return function(n){g=h=-(f=s=1/0),M=[],va.geo.stream(n,b);var t=M.length;if(t){M.sort(c);for(var e,r=1,i=M[0],u=[i];t>r;++r)e=M[r],l(e[0],i)||l(e[1],i)?(o(i[0],e[1])>o(i[0],i[1])&&(i[1]=e[1]),o(e[0],i[1])>o(i[0],i[1])&&(i[0]=
 e[0])):u.push(i=e);for(var a,e,p=-1/0,t=u.length-1,r=0,i=u[t];t>=r;i=e,++r)e=u[r],(a=o(i[1],e[0]))>p&&(p=a,f=e[0],h=i[1])}return M=x=null,1/0===f||1/0===s?[[0/0,0/0],[0/0,0/0]]:[[f,s],[h,g]]}}(),va.geo.centroid=function(n){Ao=No=qo=To=Co=zo=Do=jo=Lo=Ho=Fo=0,va.geo.stream(n,Po);var t=Lo,e=Ho,r=Fo,i=t*t+e*e+r*r;return Ja>i&&(t=zo,e=Do,r=jo,Wa>No&&(t=qo,e=To,r=Co),i=t*t+e*e+r*r,Ja>i)?[0/0,0/0]:[Math.atan2(e,t)*Ka,U(r/Math.sqrt(i))*Ka]};var Ao,No,qo,To,Co,zo,Do,jo,Lo,Ho,Fo,Po={sphere:T,point:Lt,lineStart:Ft,lineEnd:Pt,polygonStart:function(){Po.lineStart=Ot},polygonEnd:function(){Po.lineStart=Ft}},Oo=It(Rt,$t,Jt,Gt),Ro=[-$a,0],Yo=1e9;(va.geo.conicEqualArea=function(){return ee(re)}).raw=re,va.geo.albers=function(){return va.geo.conicEqualArea().rotate([96,0]).center([-.6,38.7]).parallels([29.5,45.5]).scale(1070)},va.geo.albersUsa=function(){function n(n){var u=n[0],a=n[1];return t=null,e(u,a),t||(r(u,a),t)||i(u,a),t}var t,e,r,i,u=va.geo.albers(),a=va.geo.conicEqualArea().rotate([154,0])
 .center([-2,58.5]).parallels([55,65]),o=va.geo.conicEqualArea().rotate([157,0]).center([-3,19.9]).parallels([8,18]),c={point:function(n,e){t=[n,e]}};return n.invert=function(n){var t=u.scale(),e=u.translate(),r=(n[0]-e[0])/t,i=(n[1]-e[1])/t;return(i>=.12&&.234>i&&r>=-.425&&-.214>r?a:i>=.166&&.234>i&&r>=-.214&&-.115>r?o:u).invert(n)},n.stream=function(n){var t=u.stream(n),e=a.stream(n),r=o.stream(n);return{point:function(n,i){t.point(n,i),e.point(n,i),r.point(n,i)},sphere:function(){t.sphere(),e.sphere(),r.sphere()},lineStart:function(){t.lineStart(),e.lineStart(),r.lineStart()},lineEnd:function(){t.lineEnd(),e.lineEnd(),r.lineEnd()},polygonStart:function(){t.polygonStart(),e.polygonStart(),r.polygonStart()},polygonEnd:function(){t.polygonEnd(),e.polygonEnd(),r.polygonEnd()}}},n.precision=function(t){return arguments.length?(u.precision(t),a.precision(t),o.precision(t),n):u.precision()},n.scale=function(t){return arguments.length?(u.scale(t),a.scale(.35*t),o.scale(t),n.translate(u.tr
 anslate())):u.scale()},n.translate=function(t){if(!arguments.length)return u.translate();var l=u.scale(),f=+t[0],s=+t[1];return e=u.translate(t).clipExtent([[f-.455*l,s-.238*l],[f+.455*l,s+.238*l]]).stream(c).point,r=a.translate([f-.307*l,s+.201*l]).clipExtent([[f-.425*l+Wa,s+.12*l+Wa],[f-.214*l-Wa,s+.234*l-Wa]]).stream(c).point,i=o.translate([f-.205*l,s+.212*l]).clipExtent([[f-.214*l+Wa,s+.166*l+Wa],[f-.115*l-Wa,s+.234*l-Wa]]).stream(c).point,n},n.scale(1070)};var Uo,Io,Vo,Xo,Zo,Bo,$o={point:T,lineStart:T,lineEnd:T,polygonStart:function(){Io=0,$o.lineStart=ie},polygonEnd:function(){$o.lineStart=$o.lineEnd=$o.point=T,Uo+=Math.abs(Io/2)}},Wo={point:ue,lineStart:T,lineEnd:T,polygonStart:T,polygonEnd:T},Jo={point:ce,lineStart:le,lineEnd:fe,polygonStart:function(){Jo.lineStart=se},polygonEnd:function(){Jo.point=ce,Jo.lineStart=le,Jo.lineEnd=fe}};va.geo.path=function(){function n(n){return n&&("function"==typeof o&&u.pointRadius(+o.apply(this,arguments)),a&&a.valid||(a=i(u)),va.geo.strea
 m(n,a)),u.result()}function t(){return a=null,n}var e,r,i,u,a,o=4.5;return n.area=function(n){return Uo=0,va.geo.stream(n,i($o)),Uo},n.centroid=function(n){return qo=To=Co=zo=Do=jo=Lo=Ho=Fo=0,va.geo.stream(n,i(Jo)),Fo?[Lo/Fo,Ho/Fo]:jo?[zo/jo,Do/jo]:Co?[qo/Co,To/Co]:[0/0,0/0]},n.bounds=function(n){return Zo=Bo=-(Vo=Xo=1/0),va.geo.stream(n,i(Wo)),[[Vo,Xo],[Zo,Bo]]},n.projection=function(n){return arguments.length?(i=(e=n)?n.stream||pe(n):st,t()):e},n.context=function(n){return arguments.length?(u=(r=n)==null?new ae:new he(n),"function"!=typeof o&&u.pointRadius(o),t()):r},n.pointRadius=function(t){return arguments.length?(o="function"==typeof t?t:(u.pointRadius(+t),+t),n):o},n.projection(va.geo.albersUsa()).context(null)},va.geo.projection=de,va.geo.projectionMutator=me,(va.geo.equirectangular=function(){return de(ye)}).raw=ye.invert=ye,va.geo.rotation=function(n){function t(t){return t=n(t[0]*Ga,t[1]*Ga),t[0]*=Ka,t[1]*=Ka,t}return n=Me(n[0]%360*Ga,n[1]*Ga,n.length>2?n[2]*Ga:0),t.inver
 t=function(t){return t=n.invert(t[0]*Ga,t[1]*Ga),t[0]*=Ka,t[1]*=Ka,t},t},va.geo.circle=function(){function n(){var n="function"==typeof r?r.apply(this,arguments):r,t=Me(-n[0]*Ga,-n[1]*Ga,0).invert,i=[];return e(null,null,1,{point:function(n,e){i.push(n=t(n,e)),n[0]*=Ka,n[1]*=Ka}}),{type:"Polygon",coordinates:[i]}}var t,e,r=[0,0],i=6;return n.origin=function(t){return arguments.length?(r=t,n):r},n.angle=function(r){return arguments.length?(e=we((t=+r)*Ga,i*Ga),n):t},n.precision=function(r){return arguments.length?(e=we(t*Ga,(i=+r)*Ga),n):i},n.angle(90)},va.geo.distance=function(n,t){var e,r=(t[0]-n[0])*Ga,i=n[1]*Ga,u=t[1]*Ga,a=Math.sin(r),o=Math.cos(r),c=Math.sin(i),l=Math.cos(i),f=Math.sin(u),s=Math.cos(u);return Math.atan2(Math.sqrt((e=s*a)*e+(e=l*f-c*s*o)*e),c*f+l*s*o)},va.geo.graticule=function(){function n(){return{type:"MultiLineString",coordinates:t()}}function t(){return va.range(Math.ceil(u/m)*m,i,m).map(h).concat(va.range(Math.ceil(l/v)*v,c,v).map(g)).concat(va.range(Math.c
 eil(r/p)*p,e,p).filter(function(n){return Math.abs(n%m)>Wa}).map(f)).concat(va.range(Math.ceil(o/d)*d,a,d).filter(function(n){return Math.abs(n%v)>Wa}).map(s))}var e,r,i,u,a,o,c,l,f,s,h,g,p=10,d=p,m=90,v=360,y=2.5;return n.lines=function(){return t().map(function(n){return{type:"LineString",coordinates:n}})},n.outline=function(){return{type:"Polygon",coordinates:[h(u).concat(g(c).slice(1),h(i).reverse().slice(1),g(l).reverse().slice(1))]}},n.extent=function(t){return arguments.length?n.majorExtent(t).minorExtent(t):n.minorExtent()},n.majorExtent=function(t){return arguments.length?(u=+t[0][0],i=+t[1][0],l=+t[0][1],c=+t[1][1],u>i&&(t=u,u=i,i=t),l>c&&(t=l,l=c,c=t),n.precision(y)):[[u,l],[i,c]]},n.minorExtent=function(t){return arguments.length?(r=+t[0][0],e=+t[1][0],o=+t[0][1],a=+t[1][1],r>e&&(t=r,r=e,e=t),o>a&&(t=o,o=a,a=t),n.precision(y)):[[r,o],[e,a]]},n.step=function(t){return arguments.length?n.majorStep(t).minorStep(t):n.minorStep()},n.majorStep=function(t){return arguments.leng
 th?(m=+t[0],v=+t[1],n):[m,v]},n.minorStep=function(t){return arguments.length?(p=+t[0],d=+t[1],n):[p,d]},n.precision=function(t){return arguments.length?(y=+t,f=Ee(o,a,90),s=ke(r,e,y),h=Ee(l,c,90),g=ke(u,i,y),n):y},n.majorExtent([[-180,-90+Wa],[180,90-Wa]]).minorExtent([[-180,-80-Wa],[180,80+Wa]])},va.geo.greatArc=function(){function n(){return{type:"LineString",coordinates:[t||r.apply(this,arguments),e||i.apply(this,arguments)]}}var t,e,r=Ae,i=Ne;return n.distance=function(){return va.geo.distance(t||r.apply(this,arguments),e||i.apply(this,arguments))},n.source=function(e){return arguments.length?(r=e,t="function"==typeof e?null:e,n):r},n.target=function(t){return arguments.length?(i=t,e="function"==typeof t?null:t,n):i},n.precision=function(){return arguments.length?n:0},n},va.geo.interpolate=function(n,t){return qe(n[0]*Ga,n[1]*Ga,t[0]*Ga,t[1]*Ga)},va.geo.length=function(n){return Go=0,va.geo.stream(n,Ko),Go};var Go,Ko={sphere:T,point:T,lineStart:Te,lineEnd:T,polygonStart:T,polyg
 onEnd:T},Qo=Ce(function(n){return Math.sqrt(2/(1+n))},function(n){return 2*Math.asin(n/2)});(va.geo.azimuthalEqualArea=function(){return de(Qo)}).raw=Qo;var nc=Ce(function(n){var t=Math.acos(n);return t&&t/Math.sin(t)},st);(va.geo.azimuthalEquidistant=function(){return de(nc)}).raw=nc,(va.geo.conicConformal=function(){return ee(ze)}).raw=ze,(va.geo.conicEquidistant=function(){return ee(De)}).raw=De;var tc=Ce(function(n){return 1/n},Math.atan);(va.geo.gnomonic=function(){return de(tc)}).raw=tc,je.invert=function(n,t){return[n,2*Math.atan(Math.exp(t))-$a/2]},(va.geo.mercator=function(){return Le(je)}).raw=je;var ec=Ce(function(){return 1},Math.asin);(va.geo.orthographic=function(){return de(ec)}).raw=ec;var rc=Ce(function(n){return 1/(1+n)},function(n){return 2*Math.atan(n)});(va.geo.stereographic=function(){return de(rc)}).raw=rc,He.invert=function(n,t){return[Math.atan2(I(n),Math.cos(t)),U(Math.sin(t)/V(n))]},(va.geo.transverseMercator=function(){return Le(He)}).raw=He,va.geom={},va
 .svg={},va.svg.line=function(){return Fe(st)};var ic=va.map({linear:Re,"linear-closed":Ye,step:Ue,"step-before":Ie,"step-after":Ve,basis:Je,"basis-open":Ge,"basis-closed":Ke,bundle:Qe,cardinal:Be,"cardinal-open":Xe,"cardinal-closed":Ze,monotone:ur});
+ic.forEach(function(n,t){t.key=n,t.closed=/-closed$/.test(n)});var uc=[0,2/3,1/3,0],ac=[0,1/3,2/3,0],oc=[0,1/6,2/3,1/6];va.geom.hull=function(n){function t(n){if(n.length<3)return[];var t,i,u,a,o,c,l,f,s,h,g,p,d=ft(e),m=ft(r),v=n.length,y=v-1,M=[],x=[],b=0;if(d===Pe&&r===Oe)t=n;else for(u=0,t=[];v>u;++u)t.push([+d.call(this,i=n[u],u),+m.call(this,i,u)]);for(u=1;v>u;++u)(t[u][1]<t[b][1]||t[u][1]==t[b][1]&&t[u][0]<t[b][0])&&(b=u);for(u=0;v>u;++u)u!==b&&(c=t[u][1]-t[b][1],o=t[u][0]-t[b][0],M.push({angle:Math.atan2(c,o),index:u}));for(M.sort(function(n,t){return n.angle-t.angle}),g=M[0].angle,h=M[0].index,s=0,u=1;y>u;++u){if(a=M[u].index,g==M[u].angle){if(o=t[h][0]-t[b][0],c=t[h][1]-t[b][1],l=t[a][0]-t[b][0],f=t[a][1]-t[b][1],o*o+c*c>=l*l+f*f){M[u].index=-1;continue}M[s].index=-1}g=M[u].angle,s=u,h=a}for(x.push(b),u=0,a=0;2>u;++a)M[a].index>-1&&(x.push(M[a].index),u++);for(p=x.length;y>a;++a)if(!(M[a].index<0)){for(;!ar(x[p-2],x[p-1],M[a].index,t);)--p;x[p++]=M[a].index}var _=[];for(u=p
 -1;u>=0;--u)_.push(n[x[u]]);return _}var e=Pe,r=Oe;return arguments.length?t(n):(t.x=function(n){return arguments.length?(e=n,t):e},t.y=function(n){return arguments.length?(r=n,t):r},t)},va.geom.polygon=function(n){return n.area=function(){for(var t=0,e=n.length,r=n[e-1][1]*n[0][0]-n[e-1][0]*n[0][1];++t<e;)r+=n[t-1][1]*n[t][0]-n[t-1][0]*n[t][1];return.5*r},n.centroid=function(t){var e,r,i=-1,u=n.length,a=0,o=0,c=n[u-1];for(arguments.length||(t=-1/(6*n.area()));++i<u;)e=c,c=n[i],r=e[0]*c[1]-c[0]*e[1],a+=(e[0]+c[0])*r,o+=(e[1]+c[1])*r;return[a*t,o*t]},n.clip=function(t){for(var e,r,i,u,a,o,c=-1,l=n.length,f=n[l-1];++c<l;){for(e=t.slice(),t.length=0,u=n[c],a=e[(i=e.length)-1],r=-1;++r<i;)o=e[r],or(o,f,u)?(or(a,f,u)||t.push(cr(a,o,f,u)),t.push(o)):or(a,f,u)&&t.push(cr(a,o,f,u)),a=o;f=u}return t},n},va.geom.delaunay=function(n){var t=n.map(function(){return[]}),e=[];return lr(n,function(e){t[e.region.l.index].push(n[e.region.r.index])}),t.forEach(function(t,r){var i=n[r],u=i[0],a=i[1];t.
 forEach(function(n){n.angle=Math.atan2(n[0]-u,n[1]-a)}),t.sort(function(n,t){return n.angle-t.angle});for(var o=0,c=t.length-1;c>o;o++)e.push([i,t[o],t[o+1]])}),e},va.geom.voronoi=function(n){function t(n){var t,u,a,o=n.map(function(){return[]}),c=ft(e),l=ft(r),f=n.length,s=1e6;if(c===Pe&&l===Oe)t=n;else for(t=Array(f),a=0;f>a;++a)t[a]=[+c.call(this,u=n[a],a),+l.call(this,u,a)];if(lr(t,function(n){var t,e,r,i,u,a;n.a===1&&n.b>=0?(t=n.ep.r,e=n.ep.l):(t=n.ep.l,e=n.ep.r),n.a===1?(u=t?t.y:-s,r=n.c-n.b*u,a=e?e.y:s,i=n.c-n.b*a):(r=t?t.x:-s,u=n.c-n.a*r,i=e?e.x:s,a=n.c-n.a*i);var c=[r,u],l=[i,a];o[n.region.l.index].push(c,l),o[n.region.r.index].push(c,l)}),o=o.map(function(n,e){var r=t[e][0],i=t[e][1],u=n.map(function(n){return Math.atan2(n[0]-r,n[1]-i)}),a=va.range(n.length).sort(function(n,t){return u[n]-u[t]});return a.filter(function(n,t){return!t||u[n]-u[a[t-1]]>Wa}).map(function(t){return n[t]})}),o.forEach(function(n,e){var r=n.length;if(!r)return n.push([-s,-s],[-s,s],[s,s],[s,-s]);
 if(!(r>2)){var i=t[e],u=n[0],a=n[1],o=i[0],c=i[1],l=u[0],f=u[1],h=a[0],g=a[1],p=Math.abs(h-l),d=g-f;if(Math.abs(d)<Wa){var m=f>c?-s:s;n.push([-s,m],[s,m])}else if(Wa>p){var v=l>o?-s:s;n.push([v,-s],[v,s])}else{var m=(l-o)*(g-f)>(h-l)*(f-c)?s:-s,y=Math.abs(d)-p;Math.abs(y)<Wa?n.push([0>d?m:-m,m]):(y>0&&(m*=-1),n.push([-s,m],[s,m]))}}}),i)for(a=0;f>a;++a)i.clip(o[a]);for(a=0;f>a;++a)o[a].point=n[a];return o}var e=Pe,r=Oe,i=null;return arguments.length?t(n):(t.x=function(n){return arguments.length?(e=n,t):e},t.y=function(n){return arguments.length?(r=n,t):r},t.clipExtent=function(n){if(!arguments.length)return i&&[i[0],i[2]];if(null==n)i=null;else{var e=+n[0][0],r=+n[0][1],u=+n[1][0],a=+n[1][1];i=va.geom.polygon([[e,r],[e,a],[u,a],[u,r]])}return t},t.size=function(n){return arguments.length?t.clipExtent(n&&[[0,0],n]):i&&i[2]},t.links=function(n){var t,i,u,a=n.map(function(){return[]}),o=[],c=ft(e),l=ft(r),f=n.length;if(c===Pe&&l===Oe)t=n;else for(t=Array(f),u=0;f>u;++u)t[u]=[+c.call(th
 is,i=n[u],u),+l.call(this,i,u)];return lr(t,function(t){var e=t.region.l.index,r=t.region.r.index;a[e][r]||(a[e][r]=a[r][e]=!0,o.push({source:n[e],target:n[r]}))}),o},t.triangles=function(n){if(e===Pe&&r===Oe)return va.geom.delaunay(n);for(var t,i=Array(c),u=ft(e),a=ft(r),o=-1,c=n.length;++o<c;)(i[o]=[+u.call(this,t=n[o],o),+a.call(this,t,o)]).data=t;return va.geom.delaunay(i).map(function(n){return n.map(function(n){return n.data})})},t)};var cc={l:"r",r:"l"};va.geom.quadtree=function(n,t,e,r,i){function u(n){function u(n,t,e,r,i,u,a,o){if(!isNaN(e)&&!isNaN(r))if(n.leaf){var c=n.x,f=n.y;if(null!=c)if(Math.abs(c-e)+Math.abs(f-r)<.01)l(n,t,e,r,i,u,a,o);else{var s=n.point;n.x=n.y=n.point=null,l(n,s,c,f,i,u,a,o),l(n,t,e,r,i,u,a,o)}else n.x=e,n.y=r,n.point=t}else l(n,t,e,r,i,u,a,o)}function l(n,t,e,r,i,a,o,c){var l=.5*(i+o),f=.5*(a+c),s=e>=l,h=r>=f,g=(h<<1)+s;n.leaf=!1,n=n.nodes[g]||(n.nodes[g]=hr()),s?i=l:o=l,h?a=f:c=f,u(n,t,e,r,i,a,o,c)}var f,s,h,g,p,d,m,v,y,M=ft(o),x=ft(c);if(null!=t
 )d=t,m=e,v=r,y=i;else if(v=y=-(d=m=1/0),s=[],h=[],p=n.length,a)for(g=0;p>g;++g)f=n[g],f.x<d&&(d=f.x),f.y<m&&(m=f.y),f.x>v&&(v=f.x),f.y>y&&(y=f.y),s.push(f.x),h.push(f.y);else for(g=0;p>g;++g){var b=+M(f=n[g],g),_=+x(f,g);d>b&&(d=b),m>_&&(m=_),b>v&&(v=b),_>y&&(y=_),s.push(b),h.push(_)}var w=v-d,S=y-m;w>S?y=m+w:v=d+S;var E=hr();if(E.add=function(n){u(E,n,+M(n,++g),+x(n,g),d,m,v,y)},E.visit=function(n){gr(n,E,d,m,v,y)},g=-1,null==t){for(;++g<p;)u(E,n[g],s[g],h[g],d,m,v,y);--g}else n.forEach(E.add);return s=h=n=f=null,E}var a,o=Pe,c=Oe;return(a=arguments.length)?(o=fr,c=sr,3===a&&(i=e,r=t,e=t=0),u(n)):(u.x=function(n){return arguments.length?(o=n,u):o},u.y=function(n){return arguments.length?(c=n,u):c},u.extent=function(n){return arguments.length?(null==n?t=e=r=i=null:(t=+n[0][0],e=+n[0][1],r=+n[1][0],i=+n[1][1]),u):null==t?null:[[t,e],[r,i]]},u.size=function(n){return arguments.length?(null==n?t=e=r=i=null:(t=e=0,r=+n[0],i=+n[1]),u):null==t?null:[r-t,i-e]},u)},va.interpolateRgb=pr,va.t
 ransform=function(n){var t=ya.createElementNS(va.ns.prefix.svg,"g");return(va.transform=function(n){if(null!=n){t.setAttribute("transform",n);var e=t.transform.baseVal.consolidate()}return new dr(e?e.matrix:lc)})(n)},dr.prototype.toString=function(){return"translate("+this.translate+")rotate("+this.rotate+")skewX("+this.skew+")scale("+this.scale+")"};var lc={a:1,b:0,c:0,d:1,e:0,f:0};va.interpolateNumber=Mr,va.interpolateTransform=xr,va.interpolateObject=br,va.interpolateString=_r;var fc=/[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g;va.interpolate=wr,va.interpolators=[function(n,t){var e=typeof t;return("string"===e?ao.has(t)||/^(#|rgb\(|hsl\()/.test(t)?pr:_r:t instanceof H?pr:"object"===e?Array.isArray(t)?Er:br:Mr)(n,t)}],va.interpolateArray=Er;var sc=function(){return st},hc=va.map({linear:sc,poly:zr,quad:function(){return qr},cubic:function(){return Tr},sin:function(){return Dr},exp:function(){return jr},circle:function(){return Lr},elastic:Hr,back:Fr,bounce:function(){return Pr}}
 ),gc=va.map({"in":st,out:Ar,"in-out":Nr,"out-in":function(n){return Nr(Ar(n))}});va.ease=function(n){var t=n.indexOf("-"),e=t>=0?n.substring(0,t):n,r=t>=0?n.substring(t+1):"in";return e=hc.get(e)||sc,r=gc.get(r)||st,kr(r(e.apply(null,Array.prototype.slice.call(arguments,1))))},va.interpolateHcl=Or,va.interpolateHsl=Rr,va.interpolateLab=Yr,va.interpolateRound=Ur,va.layout={},va.layout.bundle=function(){return function(n){for(var t=[],e=-1,r=n.length;++e<r;)t.push(Xr(n[e]));return t}},va.layout.chord=function(){function n(){var n,l,s,h,g,p={},d=[],m=va.range(u),v=[];for(e=[],r=[],n=0,h=-1;++h<u;){for(l=0,g=-1;++g<u;)l+=i[h][g];d.push(l),v.push(va.range(u)),n+=l}for(a&&m.sort(function(n,t){return a(d[n],d[t])}),o&&v.forEach(function(n,t){n.sort(function(n,e){return o(i[t][n],i[t][e])})}),n=(2*$a-f*u)/n,l=0,h=-1;++h<u;){for(s=l,g=-1;++g<u;){var y=m[h],M=v[y][g],x=i[y][M],b=l,_=l+=x*n;p[y+"-"+M]={index:y,subindex:M,startAngle:b,endAngle:_,value:x}}r[y]={index:y,startAngle:s,endAngle:l,va
 lue:(l-s)/n},l+=f}for(h=-1;++h<u;)for(g=h-1;++g<u;){var w=p[h+"-"+g],S=p[g+"-"+h];(w.value||S.value)&&e.push(w.value<S.value?{source:S,target:w}:{source:w,target:S})}c&&t()}function t(){e.sort(function(n,t){return c((n.source.value+n.target.value)/2,(t.source.value+t.target.value)/2)})}var e,r,i,u,a,o,c,l={},f=0;return l.matrix=function(n){return arguments.length?(u=(i=n)&&i.length,e=r=null,l):i},l.padding=function(n){return arguments.length?(f=n,e=r=null,l):f},l.sortGroups=function(n){return arguments.length?(a=n,e=r=null,l):a},l.sortSubgroups=function(n){return arguments.length?(o=n,e=null,l):o},l.sortChords=function(n){return arguments.length?(c=n,e&&t(),l):c},l.chords=function(){return e||n(),e},l.groups=function(){return r||n(),r},l},va.layout.force=function(){function n(n){return function(t,e,r,i){if(t.point!==n){var u=t.cx-n.x,a=t.cy-n.y,o=1/Math.sqrt(u*u+a*a);if(d>(i-e)*o){var c=t.charge*o*o;return n.px-=u*c,n.py-=a*c,!0}if(t.point&&isFinite(o)){var c=t.pointCharge*o*o;n.px-
 =u*c,n.py-=a*c}}return!t.charge}}function t(n){n.px=va.event.x,n.py=va.event.y,o.resume()}var e,r,i,u,a,o={},c=va.dispatch("start","tick","end"),l=[1,1],f=.9,s=pc,h=dc,g=-30,p=.1,d=.8,m=[],v=[];return o.tick=function(){if((r*=.99)<.005)return c.end({type:"end",alpha:r=0}),!0;var t,e,o,s,h,d,y,M,x,b=m.length,_=v.length;for(e=0;_>e;++e)o=v[e],s=o.source,h=o.target,M=h.x-s.x,x=h.y-s.y,(d=M*M+x*x)&&(d=r*u[e]*((d=Math.sqrt(d))-i[e])/d,M*=d,x*=d,h.x-=M*(y=s.weight/(h.weight+s.weight)),h.y-=x*y,s.x+=M*(y=1-y),s.y+=x*y);if((y=r*p)&&(M=l[0]/2,x=l[1]/2,e=-1,y))for(;++e<b;)o=m[e],o.x+=(M-o.x)*y,o.y+=(x-o.y)*y;if(g)for(Kr(t=va.geom.quadtree(m),r,a),e=-1;++e<b;)(o=m[e]).fixed||t.visit(n(o));for(e=-1;++e<b;)o=m[e],o.fixed?(o.x=o.px,o.y=o.py):(o.x-=(o.px-(o.px=o.x))*f,o.y-=(o.py-(o.py=o.y))*f);c.tick({type:"tick",alpha:r})},o.nodes=function(n){return arguments.length?(m=n,o):m},o.links=function(n){return arguments.length?(v=n,o):v},o.size=function(n){return arguments.length?(l=n,o):l},o.linkDistan
 ce=function(n){return arguments.length?(s="function"==typeof n?n:+n,o):s},o.distance=o.linkDistance,o.linkStrength=function(n){return arguments.length?(h="function"==typeof n?n:+n,o):h},o.friction=function(n){return arguments.length?(f=+n,o):f},o.charge=function(n){return arguments.length?(g="function"==typeof n?n:+n,o):g},o.gravity=function(n){return arguments.length?(p=+n,o):p},o.theta=function(n){return arguments.length?(d=+n,o):d},o.alpha=function(n){return arguments.length?(n=+n,r?r=n>0?n:0:n>0&&(c.start({type:"start",alpha:r=n}),va.timer(o.tick)),o):r},o.start=function(){function n(n,r){for(var i,u=t(e),a=-1,o=u.length;++a<o;)if(!isNaN(i=u[a][n]))return i;return Math.random()*r}function t(){if(!c){for(c=[],r=0;p>r;++r)c[r]=[];for(r=0;d>r;++r){var n=v[r];c[n.source.index].push(n.target),c[n.target.index].push(n.source)}}return c[e]}var e,r,c,f,p=m.length,d=v.length,y=l[0],M=l[1];for(e=0;p>e;++e)(f=m[e]).index=e,f.weight=0;for(e=0;d>e;++e)f=v[e],typeof f.source=="number"&&(f.sou
 rce=m[f.source]),typeof f.target=="number"&&(f.target=m[f.target]),++f.source.weight,++f.target.weight;for(e=0;p>e;++e)f=m[e],isNaN(f.x)&&(f.x=n("x",y)),isNaN(f.y)&&(f.y=n("y",M)),isNaN(f.px)&&(f.px=f.x),isNaN(f.py)&&(f.py=f.y);if(i=[],"function"==typeof s)for(e=0;d>e;++e)i[e]=+s.call(this,v[e],e);else for(e=0;d>e;++e)i[e]=s;if(u=[],"function"==typeof h)for(e=0;d>e;++e)u[e]=+h.call(this,v[e],e);else for(e=0;d>e;++e)u[e]=h;if(a=[],"function"==typeof g)for(e=0;p>e;++e)a[e]=+g.call(this,m[e],e);else for(e=0;p>e;++e)a[e]=g;return o.resume()},o.resume=function(){return o.alpha(.1)},o.stop=function(){return o.alpha(0)},o.drag=function(){return e||(e=va.behavior.drag().origin(st).on("dragstart.force",$r).on("drag.force",t).on("dragend.force",Wr)),arguments.length?(this.on("mouseover.force",Jr).on("mouseout.force",Gr).call(e),void 0):e},va.rebind(o,c,"on")};var pc=20,dc=1;va.layout.hierarchy=function(){function n(t,a,o){var c=i.call(e,t,a);if(t.depth=a,o.push(t),c&&(l=c.length)){for(var l,f
 ,s=-1,h=t.children=[],g=0,p=a+1;++s<l;)f=n(c[s],p,o),f.parent=t,h.push(f),g+=f.value;r&&h.sort(r),u&&(t.value=g)}else u&&(t.value=+u.call(e,t,a)||0);return t}function t(n,r){var i=n.children,a=0;if(i&&(o=i.length))for(var o,c=-1,l=r+1;++c<o;)a+=t(i[c],l);else u&&(a=+u.call(e,n,r)||0);return u&&(n.value=a),a}function e(t){var e=[];return n(t,0,e),e}var r=ei,i=ni,u=ti;return e.sort=function(n){return arguments.length?(r=n,e):r},e.children=function(n){return arguments.length?(i=n,e):i},e.value=function(n){return arguments.length?(u=n,e):u},e.revalue=function(n){return t(n,0),n},e},va.layout.partition=function(){function n(t,e,r,i){var u=t.children;if(t.x=e,t.y=t.depth*i,t.dx=r,t.dy=i,u&&(a=u.length)){var a,o,c,l=-1;for(r=t.value?r/t.value:0;++l<a;)n(o=u[l],e,c=o.value*r,i),e+=c}}function t(n){var e=n.children,r=0;if(e&&(i=e.length))for(var i,u=-1;++u<i;)r=Math.max(r,t(e[u]));return 1+r}function e(e,u){var a=r.call(this,e,u);return n(a[0],0,i[0],i[1]/t(a[0])),a}var r=va.layout.hierarchy
 (),i=[1,1];return e.size=function(n){return arguments.length?(i=n,e):i},Qr(e,r)},va.layout.pie=function(){function n(u){var a=u.map(function(e,r){return+t.call(n,e,r)}),o=+("function"==typeof r?r.apply(this,arguments):r),c=(("function"==typeof i?i.apply(this,arguments):i)-o)/va.sum(a),l=va.range(u.length);null!=e&&l.sort(e===mc?function(n,t){return a[t]-a[n]}:function(n,t){return e(u[n],u[t])});var f=[];return l.forEach(function(n){var t;f[n]={data:u[n],value:t=a[n],startAngle:o,endAngle:o+=t*c}}),f}var t=Number,e=mc,r=0,i=2*$a;return n.value=function(e){return arguments.length?(t=e,n):t},n.sort=function(t){return arguments.length?(e=t,n):e},n.startAngle=function(t){return arguments.length?(r=t,n):r},n.endAngle=function(t){return arguments.length?(i=t,n):i},n};var mc={};va.layout.stack=function(){function n(o,c){var l=o.map(function(e,r){return t.call(n,e,r)}),f=l.map(function(t){return t.map(function(t,e){return[u.call(n,t,e),a.call(n,t,e)]})}),s=e.call(n,f,c);l=va.permute(l,s),f=v
 a.permute(f,s);var h,g,p,d=r.call(n,f,c),m=l.length,v=l[0].length;for(g=0;v>g;++g)for(i.call(n,l[0][g],p=d[g],f[0][g][1]),h=1;m>h;++h)i.call(n,l[h][g],p+=f[h-1][g][1],f[h][g][1]);return o}var t=st,e=oi,r=ci,i=ai,u=ii,a=ui;return n.values=function(e){return arguments.length?(t=e,n):t},n.order=function(t){return arguments.length?(e="function"==typeof t?t:vc.get(t)||oi,n):e},n.offset=function(t){return arguments.length?(r="function"==typeof t?t:yc.get(t)||ci,n):r},n.x=function(t){return arguments.length?(u=t,n):u},n.y=function(t){return arguments.length?(a=t,n):a},n.out=function(t){return arguments.length?(i=t,n):i},n};var vc=va.map({"inside-out":function(n){var t,e,r=n.length,i=n.map(li),u=n.map(fi),a=va.range(r).sort(function(n,t){return i[n]-i[t]}),o=0,c=0,l=[],f=[];for(t=0;r>t;++t)e=a[t],c>o?(o+=u[e],l.push(e)):(c+=u[e],f.push(e));return f.reverse().concat(l)},reverse:function(n){return va.range(n.length).reverse()},"default":oi}),yc=va.map({silhouette:function(n){var t,e,r,i=n.len
 gth,u=n[0].length,a=[],o=0,c=[];for(e=0;u>e;++e){for(t=0,r=0;i>t;t++)r+=n[t][e][1];r>o&&(o=r),a.push(r)}for(e=0;u>e;++e)c[e]=(o-a[e])/2;return c},wiggle:function(n){var t,e,r,i,u,a,o,c,l,f=n.length,s=n[0],h=s.length,g=[];for(g[0]=c=l=0,e=1;h>e;++e){for(t=0,i=0;f>t;++t)i+=n[t][e][1];for(t=0,u=0,o=s[e][0]-s[e-1][0];f>t;++t){for(r=0,a=(n[t][e][1]-n[t][e-1][1])/(2*o);t>r;++r)a+=(n[r][e][1]-n[r][e-1][1])/o;u+=a*n[t][e][1]}g[e]=c-=i?u/i*o:0,l>c&&(l=c)}for(e=0;h>e;++e)g[e]-=l;return g},expand:function(n){var t,e,r,i=n.length,u=n[0].length,a=1/i,o=[];for(e=0;u>e;++e){for(t=0,r=0;i>t;t++)r+=n[t][e][1];if(r)for(t=0;i>t;t++)n[t][e][1]/=r;else for(t=0;i>t;t++)n[t][e][1]=a}for(e=0;u>e;++e)o[e]=0;return o},zero:ci});va.layout.histogram=function(){function n(n,u){for(var a,o,c=[],l=n.map(e,this),f=r.call(this,l,u),s=i.call(this,f,l,u),u=-1,h=l.length,g=s.length-1,p=t?1:1/h;++u<g;)a=c[u]=[],a.dx=s[u+1]-(a.x=s[u]),a.y=0;if(g>0)for(u=-1;++u<h;)o=l[u],o>=f[0]&&o<=f[1]&&(a=c[va.bisect(s,o,1,g)-1],a.y+=
 p,a.push(n[u]));return c}var t=!0,e=Number,r=pi,i=hi;return n.value=function(t){return arguments.length?(e=t,n):e},n.range=function(t){return arguments.length?(r=ft(t),n):r},n.bins=function(t){return arguments.length?(i="number"==typeof t?function(n){return gi(n,t)}:ft(t),n):i},n.frequency=function(e){return arguments.length?(t=!!e,n):t},n},va.layout.tree=function(){function n(n,u){function a(n,t){var r=n.children,i=n._tree;if(r&&(u=r.length)){for(var u,o,l,f=r[0],s=f,h=-1;++h<u;)l=r[h],a(l,o),s=c(l,o,s),o=l;wi(n);var g=.5*(f._tree.prelim+l._tree.prelim);t?(i.prelim=t._tree.prelim+e(n,t),i.mod=i.prelim-g):i.prelim=g}else t&&(i.prelim=t._tree.prelim+e(n,t))}function o(n,t){n.x=n._tree.prelim+t;var e=n.children;if(e&&(r=e.length)){var r,i=-1;for(t+=n._tree.mod;++i<r;)o(e[i],t)}}function c(n,t,r){if(t){for(var i,u=n,a=n,o=t,c=n.parent.children[0],l=u._tree.mod,f=a._tree.mod,s=o._tree.mod,h=c._tree.mod;o=vi(o),u=mi(u),o&&u;)c=mi(c),a=vi(a),a._tree.ancestor=n,i=o._tree.prelim+s-u._tree.p
 relim-l+e(o,u),i>0&&(Si(Ei(o,n,r),n,i),l+=i,f+=i),s+=o._tree.mod,l+=u._tree.mod,h+=c._tree.mod,f+=a._tree.mod;o&&!vi(a)&&(a._tree.thread=o,a._tree.mod+=s-f),u&&!mi(c)&&(c._tree.thread=u,c._tree.mod+=l-h,r=n)}return r}var l=t.call(this,n,u),f=l[0];_i(f,function(n,t){n._tree={ancestor:n,prelim:0,mod:0,change:0,shift:0,number:t?t._tree.number+1:0}}),a(f),o(f,-f._tree.prelim);var s=yi(f,xi),h=yi(f,Mi),g=yi(f,bi),p=s.x-e(s,h)/2,d=h.x+e(h,s)/2,m=g.depth||1;return _i(f,i?function(n){n.x*=r[0],n.y=n.depth*r[1],delete n._tree}:function(n){n.x=(n.x-p)/(d-p)*r[0],n.y=n.depth/m*r[1],delete n._tree}),l}var t=va.layout.hierarchy().sort(null).value(null),e=di,r=[1,1],i=!1;return n.separation=function(t){return arguments.length?(e=t,n):e},n.size=function(t){return arguments.length?(i=(r=t)==null,n):i?null:r},n.nodeSize=function(t){return arguments.length?(i=(r=t)!=null,n):i?r:null},Qr(n,t)},va.layout.pack=function(){function n(n,u){var a=e.call(this,n,u),o=a[0],c=i[0],l=i[1],f=t||Math.sqrt;if(o.x=o
 .y=0,_i(o,function(n){n.r=f(n.value)}),_i(o,Ti),r){var s=r*(t?1:Math.max(2*o.r/c,2*o.r/l))/2;_i(o,function(n){n.r+=s}),_i(o,Ti),_i(o,function(n){n.r-=s})}return Di(o,c/2,l/2,t?1:1/Math.max(2*o.r/c,2*o.r/l)),a}var t,e=va.layout.hierarchy().sort(ki),r=0,i=[1,1];return n.size=function(t){return arguments.length?(i=t,n):i},n.radius=function(e){return arguments.length?(t=e,n):t},n.padding=function(t){return arguments.length?(r=+t,n):r},Qr(n,e)},va.layout.cluster=function(){function n(n,u){var a,o=t.call(this,n,u),c=o[0],l=0;_i(c,function(n){var t=n.children;t&&t.length?(n.x=Hi(t),n.y=Li(t)):(n.x=a?l+=e(n,a):0,n.y=0,a=n)});var f=Fi(c),s=Pi(c),h=f.x-e(f,s)/2,g=s.x+e(s,f)/2;return _i(c,i?function(n){n.x=(n.x-c.x)*r[0],n.y=(c.y-n.y)*r[1]}:function(n){n.x=(n.x-h)/(g-h)*r[0],n.y=(1-(c.y?n.y/c.y:1))*r[1]}),o}var t=va.layout.hierarchy().sort(null).value(null),e=di,r=[1,1],i=!1;return n.separation=function(t){return arguments.length?(e=t,n):e},n.size=function(t){return arguments.length?(i=(r=t)==
 null,n):i?null:r},n.nodeSize=function(t){return arguments.length?(i=(r=t)!=null,n):i?r:null},Qr(n,t)},va.layout.treemap=function(){function n(n,t){for(var e,r,i=-1,u=n.length;++i<u;)r=(e=n[i]).value*(0>t?0:t),e.area=isNaN(r)||0>=r?0:r}function t(e){var u=e.children;if(u&&u.length){var a,o,c,l=s(e),f=[],h=u.slice(),p=1/0,d="slice"===g?l.dx:"dice"===g?l.dy:"slice-dice"===g?e.depth&1?l.dy:l.dx:Math.min(l.dx,l.dy);for(n(h,l.dx*l.dy/e.value),f.area=0;(c=h.length)>0;)f.push(a=h[c-1]),f.area+=a.area,"squarify"!==g||(o=r(f,d))<=p?(h.pop(),p=o):(f.area-=f.pop().area,i(f,d,l,!1),d=Math.min(l.dx,l.dy),f.length=f.area=0,p=1/0);f.length&&(i(f,d,l,!0),f.length=f.area=0),u.forEach(t)}}function e(t){var r=t.children;if(r&&r.length){var u,a=s(t),o=r.slice(),c=[];for(n(o,a.dx*a.dy/t.value),c.area=0;u=o.pop();)c.push(u),c.area+=u.area,u.z!=null&&(i(c,u.z?a.dx:a.dy,a,!o.length),c.length=c.area=0);r.forEach(e)}}function r(n,t){for(var e,r=n.area,i=0,u=1/0,a=-1,o=n.length;++a<o;)(e=n[a].area)&&(u>e&&(u=e
 ),e>i&&(i=e));return r*=r,t*=t,r?Math.max(t*i*p/r,r/(t*u*p)):1/0}function i(n,t,e,r){var i,u=-1,a=n.length,o=e.x,l=e.y,f=t?c(n.area/t):0;if(t==e.dx){for((r||f>e.dy)&&(f=e.dy);++u<a;)i=n[u],i.x=o,i.y=l,i.dy=f,o+=i.dx=Math.min(e.x+e.dx-o,f?c(i.area/f):0);i.z=!0,i.dx+=e.x+e.dx-o,e.y+=f,e.dy-=f}else{for((r||f>e.dx)&&(f=e.dx);++u<a;)i=n[u],i.x=o,i.y=l,i.dx=f,l+=i.dy=Math.min(e.y+e.dy-l,f?c(i.area/f):0);i.z=!1,i.dy+=e.y+e.dy-l,e.x+=f,e.dx-=f}}function u(r){var i=a||o(r),u=i[0];return u.x=0,u.y=0,u.dx=l[0],u.dy=l[1],a&&o.revalue(u),n([u],u.dx*u.dy/u.value),(a?e:t)(u),h&&(a=i),i}var a,o=va.layout.hierarchy(),c=Math.round,l=[1,1],f=null,s=Oi,h=!1,g="squarify",p=.5*(1+Math.sqrt(5));return u.size=function(n){return arguments.length?(l=n,u):l},u.padding=function(n){function t(t){var e=n.call(u,t,t.depth);return null==e?Oi(t):Ri(t,"number"==typeof e?[e,e,e,e]:e)}function e(t){return Ri(t,n)}if(!arguments.length)return f;var r;return s=(f=n)==null?Oi:(r=typeof n)=="function"?t:"number"===r?(n=[n,
 n,n,n],e):e,u},u.round=function(n){return arguments.length?(c=n?Math.round:Number,u):c!=Number},u.sticky=function(n){return arguments.length?(h=n,a=null,u):h},u.ratio=function(n){return arguments.length?(p=n,u):p},u.mode=function(n){return arguments.length?(g=n+"",u):g},Qr(u,o)},va.random={normal:function(n,t){var e=arguments.length;return 2>e&&(t=1),1>e&&(n=0),function(){var e,r,i;do e=Math.random()*2-1,r=Math.random()*2-1,i=e*e+r*r;while(!i||i>1);return n+t*e*Math.sqrt(-2*Math.log(i)/i)}},logNormal:function(){var n=va.random.normal.apply(va,arguments);return function(){return Math.exp(n())}},irwinHall:function(n){return function(){for(var t=0,e=0;n>e;e++)t+=Math.random();return t/n}}},va.scale={};var Mc={floor:st,ceil:st};va.scale.linear=function(){return Bi([0,1],[0,1],wr,!1)},va.scale.log=function(){return nu(va.scale.linear().domain([0,Math.LN10]),10,tu,eu,[1,10])};var xc=va.format(".0e");va.scale.pow=function(){return uu(va.scale.linear(),1,[0,1])},va.scale.sqrt=function(){ret
 urn va.scale.pow().exponent(.5)},va.scale.ordinal=function(){return ou([],{t:"range",a:[[]]})},va.scale.category10=function(){return va.scale.ordinal().range(bc)},va.scale.category20=function(){return va.scale.ordinal().range(_c)},va.scale.category20b=function(){return va.scale.ordinal().range(wc)},va.scale.category20c=function(){return va.scale.ordinal().range(Sc)};var bc=["#1f77b4","#ff7f0e","#2ca02c","#d62728","#9467bd","#8c564b","#e377c2","#7f7f7f","#bcbd22","#17becf"],_c=["#1f77b4","#aec7e8","#ff7f0e","#ffbb78","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5","#8c564b","#c49c94","#e377c2","#f7b6d2","#7f7f7f","#c7c7c7","#bcbd22","#dbdb8d","#17becf","#9edae5"],wc=["#393b79","#5254a3","#6b6ecf","#9c9ede","#637939","#8ca252","#b5cf6b","#cedb9c","#8c6d31","#bd9e39","#e7ba52","#e7cb94","#843c39","#ad494a","#d6616b","#e7969c","#7b4173","#a55194","#ce6dbd","#de9ed6"],Sc=["#3182bd","#6baed6","#9ecae1","#c6dbef","#e6550d","#fd8d3c","#fdae6b","#fdd0a2","#31a354","#74c476","#a1d
 99b","#c7e9c0","#756bb1","#9e9ac8","#bcbddc","#dadaeb","#636363","#969696","#bdbdbd","#d9d9d9"];va.scale.quantile=function(){return cu([],[])},va.scale.quantize=function(){return lu(0,1,[0,1])},va.scale.threshold=function(){return fu([.5],[0,1])},va.scale.identity=function(){return su([0,1])},va.svg.arc=function(){function n(){var n=t.apply(this,arguments),u=e.apply(this,arguments),a=r.apply(this,arguments)+Ec,o=i.apply(this,arguments)+Ec,c=(a>o&&(c=a,a=o,o=c),o-a),l=$a>c?"0":"1",f=Math.cos(a),s=Math.sin(a),h=Math.cos(o),g=Math.sin(o);return c>=kc?n?"M0,"+u+"A"+u+","+u+" 0 1,1 0,"+-u+"A"+u+","+u+" 0 1,1 0,"+u+"M0,"+n+"A"+n+","+n+" 0 1,0 0,"+-n+"A"+n+","+n+" 0 1,0 0,"+n+"Z":"M0,"+u+"A"+u+","+u+" 0 1,1 0,"+-u+"A"+u+","+u+" 0 1,1 0,"+u+"Z":n?"M"+u*f+","+u*s+"A"+u+","+u+" 0 "+l+",1 "+u*h+","+u*g+"L"+n*h+","+n*g+"A"+n+","+n+" 0 "+l+",0 "+n*f+","+n*s+"Z":"M"+u*f+","+u*s+"A"+u+","+u+" 0 "+l+",1 "+u*h+","+u*g+"L0,0"+"Z"}var t=hu,e=gu,r=pu,i=du;return n.innerRadius=function(e){return argumen
 ts.length?(t=ft(e),n):t},n.outerRadius=function(t){return arguments.length?(e=ft(t),n):e},n.startAngle=function(t){return arguments.length?(r=ft(t),n):r},n.endAngle=function(t){return arguments.length?(i=ft(t),n):i},n.centroid=function(){var n=(t.apply(this,arguments)+e.apply(this,arguments))/2,u=(r.apply(this,arguments)+i.apply(this,arguments))/2+Ec;return[Math.cos(u)*n,Math.sin(u)*n]},n};var Ec=-$a/2,kc=2*$a-1e-6;va.svg.line.radial=function(){var n=Fe(mu);return n.radius=n.x,delete n.x,n.angle=n.y,delete n.y,n},Ie.reverse=Ve,Ve.reverse=Ie,va.svg.area=function(){return vu(st)},va.svg.area.radial=function(){var n=vu(mu);return n.radius=n.x,delete n.x,n.innerRadius=n.x0,delete n.x0,n.outerRadius=n.x1,delete n.x1,n.angle=n.y,delete n.y,n.startAngle=n.y0,delete n.y0,n.endAngle=n.y1,delete n.y1,n},va.svg.chord=function(){function n(n,o){var c=t(this,u,n,o),l=t(this,a,n,o);return"M"+c.p0+r(c.r,c.p1,c.a1-c.a0)+(e(c,l)?i(c.r,c.p1,c.r,c.p0):i(c.r,c.p1,l.r,l.p0)+r(l.r,l.p1,l.a1-l.a0)+i(l.r,l
 .p1,c.r,c.p0))+"Z"}function t(n,t,e,r){var i=t.call(n,e,r),u=o.call(n,i,r),a=c.call(n,i,r)+Ec,f=l.call(n,i,r)+Ec;return{r:u,a0:a,a1:f,p0:[u*Math.cos(a),u*Math.sin(a)],p1:[u*Math.cos(f),u*Math.sin(f)]}}function e(n,t){return n.a0==t.a0&&n.a1==t.a1}function r(n,t,e){return"A"+n+","+n+" 0 "+ +(e>$a)+",1 "+t}function i(n,t,e,r){return"Q 0,0 "+r}var u=Ae,a=Ne,o=yu,c=pu,l=du;return n.radius=function(t){return arguments.length?(o=ft(t),n):o},n.source=function(t){return arguments.length?(u=ft(t),n):u},n.target=function(t){return arguments.length?(a=ft(t),n):a},n.startAngle=function(t){return arguments.length?(c=ft(t),n):c},n.endAngle=function(t){return arguments.length?(l=ft(t),n):l},n},va.svg.diagonal=function(){function n(n,i){var u=t.call(this,n,i),a=e.call(this,n,i),o=(u.y+a.y)/2,c=[u,{x:u.x,y:o},{x:a.x,y:o},a];return c=c.map(r),"M"+c[0]+"C"+c[1]+" "+c[2]+" "+c[3]}var t=Ae,e=Ne,r=Mu;return n.source=function(e){return arguments.length?(t=ft(e),n):t},n.target=function(t){return arguments.
 length?(e=ft(t),n):e},n.projection=function(t){return arguments.length?(r=t,n):r},n},va.svg.diagonal.radial=function(){var n=va.svg.diagonal(),t=Mu,e=n.projection;return n.projection=function(n){return arguments.length?e(xu(t=n)):t},n},va.svg.symbol=function(){function n(n,r){return(Ac.get(t.call(this,n,r))||wu)(e.call(this,n,r))}var t=_u,e=bu;return n.type=function(e){return arguments.length?(t=ft(e),n):t},n.size=function(t){return arguments.length?(e=ft(t),n):e},n};var Ac=va.map({circle:wu,cross:function(n){var t=Math.sqrt(n/5)/2;return"M"+-3*t+","+-t+"H"+-t+"V"+-3*t+"H"+t+"V"+-t+"H"+3*t+"V"+t+"H"+t+"V"+3*t+"H"+-t+"V"+t+"H"+-3*t+"Z"},diamond:function(n){var t=Math.sqrt(n/(2*Tc)),e=t*Tc;return"M0,"+-t+"L"+e+",0"+" 0,"+t+" "+-e+",0"+"Z"},square:function(n){var t=Math.sqrt(n)/2;return"M"+-t+","+-t+"L"+t+","+-t+" "+t+","+t+" "+-t+","+t+"Z"},"triangle-down":function(n){var t=Math.sqrt(n/qc),e=t*qc/2;return"M0,"+e+"L"+t+","+-e+" "+-t+","+-e+"Z"},"triangle-up":function(n){var t=Math.sqrt
 (n/qc),e=t*qc/2;return"M0,"+-e+"L"+t+","+e+" "+-t+","+e+"Z"}});va.svg.symbolTypes=Ac.keys();var Nc,qc=Math.sqrt(3),Tc=Math.tan(30*Ga),Cc=[],zc=0,Dc={ease:Cr,delay:0,duration:250};Cc.call=Pa.call,Cc.empty=Pa.empty,Cc.node=Pa.node,va.transition=function(n){return arguments.length?Nc?n.transition():n:Ia.transition()},va.transition.prototype=Cc,Cc.select=function(n){var t,e,r,i=this.id,u=[];"function"!=typeof n&&(n=y(n));for(var a=-1,o=this.length;++a<o;){u.push(t=[]);for(var c=this[a],l=-1,f=c.length;++l<f;)(r=c[l])&&(e=n.call(r,r.__data__,l))?("__data__"in r&&(e.__data__=r.__data__),Au(e,l,i,r.__transition__[i]),t.push(e)):t.push(null)}return Su(u,i)},Cc.selectAll=function(n){var t,e,r,i,u,a=this.id,o=[];"function"!=typeof n&&(n=M(n));for(var c=-1,l=this.length;++c<l;)for(var f=this[c],s=-1,h=f.length;++s<h;)if(r=f[s]){u=r.__transition__[a],e=n.call(r,r.__data__,s),o.push(t=[]);for(var g=-1,p=e.length;++g<p;)(i=e[g])&&Au(i,g,a,u),t.push(i)}return Su(o,a)},Cc.filter=function(n){var t,e
 ,r,i=[];"function"!=typeof n&&(n=N(n));for(var u=0,a=this.length;a>u;u++){i.push(t=[]);for(var e=this[u],o=0,c=e.length;c>o;o++)(r=e[o])&&n.call(r,r.__data__,o)&&t.push(r)}return Su(i,this.id,this.time).ease(this.ease())},Cc.tween=function(n,t){var e=this.id;return arguments.length<2?this.node().__transition__[e].tween.get(n):j(this,null==t?function(t){t.__transition__[e].tween.remove(n)}:function(r){r.__transition__[e].tween.set(n,t)})},Cc.attr=function(n,t){function e(){this.removeAttribute(o)}function r(){this.removeAttributeNS(o.space,o.local)}function i(n){return null==n?e:(n+="",function(){var t,e=this.getAttribute(o);return e!==n&&(t=a(e,n),function(n){this.setAttribute(o,t(n))})})}function u(n){return null==n?r:(n+="",function(){var t,e=this.getAttributeNS(o.space,o.local);return e!==n&&(t=a(e,n),function(n){this.setAttributeNS(o.space,o.local,t(n))})})}if(arguments.length<2){for(t in n)this.attr(t,n[t]);return this}var a=Sr(n),o=va.ns.qualify(n);return Eu(this,"attr."+n,t,o
 .local?u:i)},Cc.attrTween=function(n,t){function e(n,e){var r=t.call(this,n,e,this.getAttribute(i));return r&&function(n){this.setAttribute(i,r(n))}}function r(n,e){var r=t.call(this,n,e,this.getAttributeNS(i.space,i.local));return r&&function(n){this.setAttributeNS(i.space,i.local,r(n))}}var i=va.ns.qualify(n);return this.tween("attr."+n,i.local?r:e)},Cc.style=function(n,t,e){function r(){this.style.removeProperty(n)}function i(t){return null==t?r:(t+="",function(){var r,i=xa.getComputedStyle(this,null).getPropertyValue(n);return i!==t&&(r=a(i,t),function(t){this.style.setProperty(n,r(t),e)})})}var u=arguments.length;if(3>u){if("string"!=typeof n){2>u&&(t="");for(e in n)this.style(e,n[e],t);return this}e=""}var a=Sr(n);return Eu(this,"style."+n,t,i)},Cc.styleTween=function(n,t,e){function r(r,i){var u=t.call(this,r,i,xa.getComputedStyle(this,null).getPropertyValue(n));return u&&function(t){this.style.setProperty(n,u(t),e)}}return arguments.length<3&&(e=""),this.tween("style."+n,r)}
 ,Cc.text=function(n){return Eu(this,"text",n,ku)},Cc.remove=function(){return this.each("end.transition",function(){var n;!this.__transition__&&(n=this.parentNode)&&n.removeChild(this)})},Cc.ease=function(n){var t=this.id;return arguments.length<1?this.node().__transition__[t].ease:("function"!=typeof n&&(n=va.ease.apply(va,arguments)),j(this,function(e){e.__transition__[t].ease=n}))},Cc.delay=function(n){var t=this.id;return j(this,"function"==typeof n?function(e,r,i){e.__transition__[t].delay=n.call(e,e.__data__,r,i)|0}:(n|=0,function(e){e.__transition__[t].delay=n}))},Cc.duration=function(n){var t=this.id;return j(this,"function"==typeof n?function(e,r,i){e.__transition__[t].duration=Math.max(1,n.call(e,e.__data__,r,i)|0)}:(n=Math.max(1,0|n),function(e){e.__transition__[t].duration=n}))},Cc.each=function(n,t){var e=this.id;if(arguments.length<2){var r=Dc,i=Nc;Nc=e,j(this,function(t,r,i){Dc=t.__transition__[e],n.call(t,t.__data__,r,i)}),Dc=r,Nc=i}else j(this,function(r){r.__transi
 tion__[e].event.on(n,t)});return this},Cc.transition=function(){for(var n,t,e,r,i=this.id,u=++zc,a=[],o=0,c=this.length;c>o;o++){a.push(n=[]);for(var t=this[o],l=0,f=t.length;f>l;l++)(e=t[l])&&(r=Object.create(e.__transition__[i]),r.delay+=r.duration,Au(e,l,u,r)),n.push(e)}return Su(a,u)},va.svg.axis=function(){function n(n){n.each(function(){var n,s=va.select(this),h=null==l?e.ticks?e.ticks.apply(e,c):e.domain():l,g=null==t?e.tickFormat?e.tickFormat.apply(e,c):String:t,p=Tu(e,h,f),d=s.selectAll(".tick.minor").data(p,String),m=d.enter().insert("line",".tick").attr("class","tick minor").style("opacity",1e-6),v=va.transition(d.exit()).style("opacity",1e-6).remove(),y=va.transition(d).style("opacity",1),M=s.selectAll(".tick.major").data(h,String),x=M.enter().insert("g",".domain").attr("class","tick major").style("opacity",1e-6),b=va.transition(M.exit()).style("opacity",1e-6).remove(),_=va.transition(M).style("opacity",1),w=Ui(e),S=s.selectAll(".domain").data([0]),E=(S.enter().append("p
 ath").attr("class","domain"),va.transition(S)),k=e.copy(),A=this.__chart__||k;this.__chart__=k,x.append("line"),x.append("text");
+var N=x.select("line"),q=_.select("line"),T=M.select("text").text(g),C=x.select("text"),z=_.select("text");switch(r){case"bottom":n=Nu,m.attr("y2",u),y.attr("x2",0).attr("y2",u),N.attr("y2",i),C.attr("y",Math.max(i,0)+o),q.attr("x2",0).attr("y2",i),z.attr("x",0).attr("y",Math.max(i,0)+o),T.attr("dy",".71em").style("text-anchor","middle"),E.attr("d","M"+w[0]+","+a+"V0H"+w[1]+"V"+a);break;case"top":n=Nu,m.attr("y2",-u),y.attr("x2",0).attr("y2",-u),N.attr("y2",-i),C.attr("y",-(Math.max(i,0)+o)),q.attr("x2",0).attr("y2",-i),z.attr("x",0).attr("y",-(Math.max(i,0)+o)),T.attr("dy","0em").style("text-anchor","middle"),E.attr("d","M"+w[0]+","+-a+"V0H"+w[1]+"V"+-a);break;case"left":n=qu,m.attr("x2",-u),y.attr("x2",-u).attr("y2",0),N.attr("x2",-i),C.attr("x",-(Math.max(i,0)+o)),q.attr("x2",-i).attr("y2",0),z.attr("x",-(Math.max(i,0)+o)).attr("y",0),T.attr("dy",".32em").style("text-anchor","end"),E.attr("d","M"+-a+","+w[0]+"H0V"+w[1]+"H"+-a);break;case"right":n=qu,m.attr("x2",u),y.attr("x2",u).
 attr("y2",0),N.attr("x2",i),C.attr("x",Math.max(i,0)+o),q.attr("x2",i).attr("y2",0),z.attr("x",Math.max(i,0)+o).attr("y",0),T.attr("dy",".32em").style("text-anchor","start"),E.attr("d","M"+a+","+w[0]+"H0V"+w[1]+"H"+a)}if(e.ticks)x.call(n,A),_.call(n,k),b.call(n,k),m.call(n,A),y.call(n,k),v.call(n,k);else{var D=k.rangeBand()/2,j=function(n){return k(n)+D};x.call(n,j),_.call(n,j)}})}var t,e=va.scale.linear(),r=jc,i=6,u=6,a=6,o=3,c=[10],l=null,f=0;return n.scale=function(t){return arguments.length?(e=t,n):e},n.orient=function(t){return arguments.length?(r=t in Lc?t+"":jc,n):r},n.ticks=function(){return arguments.length?(c=arguments,n):c},n.tickValues=function(t){return arguments.length?(l=t,n):l},n.tickFormat=function(e){return arguments.length?(t=e,n):t},n.tickSize=function(t,e){if(!arguments.length)return i;var r=arguments.length-1;return i=+t,u=r>1?+e:i,a=r>0?+arguments[r]:i,n},n.tickPadding=function(t){return arguments.length?(o=+t,n):o},n.tickSubdivide=function(t){return arguments
 .length?(f=+t,n):f},n};var jc="bottom",Lc={top:1,right:1,bottom:1,left:1};va.svg.brush=function(){function n(u){u.each(function(){var u,a=va.select(this),l=a.selectAll(".background").data([0]),s=a.selectAll(".extent").data([0]),h=a.selectAll(".resize").data(f,String);a.style("pointer-events","all").on("mousedown.brush",i).on("touchstart.brush",i),l.enter().append("rect").attr("class","background").style("visibility","hidden").style("cursor","crosshair"),s.enter().append("rect").attr("class","extent").style("cursor","move"),h.enter().append("g").attr("class",function(n){return"resize "+n}).style("cursor",function(n){return Hc[n]}).append("rect").attr("x",function(n){return/[ew]$/.test(n)?-3:null}).attr("y",function(n){return/^[ns]/.test(n)?-3:null}).attr("width",6).attr("height",6).style("visibility","hidden"),h.style("display",n.empty()?"none":null),h.exit().remove(),o&&(u=Ui(o),l.attr("x",u[0]).attr("width",u[1]-u[0]),e(a)),c&&(u=Ui(c),l.attr("y",u[0]).attr("height",u[1]-u[0]),r(a)
 ),t(a)})}function t(n){n.selectAll(".resize").attr("transform",function(n){return"translate("+s[+/e$/.test(n)][0]+","+s[+/^s/.test(n)][1]+")"})}function e(n){n.select(".extent").attr("x",s[0][0]),n.selectAll(".extent,.n>rect,.s>rect").attr("width",s[1][0]-s[0][0])}function r(n){n.select(".extent").attr("y",s[0][1]),n.selectAll(".extent,.e>rect,.w>rect").attr("height",s[1][1]-s[0][1])}function i(){function i(){var n=va.event.changedTouches;return n?va.touches(M,n)[0]:va.mouse(M)}function f(){va.event.keyCode==32&&(k||(v=null,A[0]-=s[1][0],A[1]-=s[1][1],k=2),l())}function h(){va.event.keyCode==32&&2==k&&(A[0]+=s[1][0],A[1]+=s[1][1],k=0,l())}function p(){var n=i(),u=!1;y&&(n[0]+=y[0],n[1]+=y[1]),k||(va.event.altKey?(v||(v=[(s[0][0]+s[1][0])/2,(s[0][1]+s[1][1])/2]),A[0]=s[+(n[0]<v[0])][0],A[1]=s[+(n[1]<v[1])][1]):v=null),S&&d(n,o,0)&&(e(_),u=!0),E&&d(n,c,1)&&(r(_),u=!0),u&&(t(_),b({type:"brush",mode:k?"move":"resize"}))}function d(n,t,e){var r,i,a=Ui(t),o=a[0],c=a[1],l=A[e],f=s[1][e]-s[
 0][e];return k&&(o-=l,c-=f+l),r=g[e]?Math.max(o,Math.min(c,n[e])):n[e],k?i=(r+=l)+f:(v&&(l=Math.max(o,Math.min(c,2*v[e]-r))),r>l?(i=r,r=l):i=l),s[0][e]!==r||s[1][e]!==i?(u=null,s[0][e]=r,s[1][e]=i,!0):void 0}function m(){p(),_.style("pointer-events","all").selectAll(".resize").style("display",n.empty()?"none":null),va.select("body").style("cursor",null),N.on("mousemove.brush",null).on("mouseup.brush",null).on("touchmove.brush",null).on("touchend.brush",null).on("keydown.brush",null).on("keyup.brush",null),b({type:"brushend"}),l()}var v,y,M=this,x=va.select(va.event.target),b=a.of(M,arguments),_=va.select(M),w=x.datum(),S=!/^(n|s)$/.test(w)&&o,E=!/^(e|w)$/.test(w)&&c,k=x.classed("extent"),A=i(),N=va.select(xa).on("mousemove.brush",p).on("mouseup.brush",m).on("touchmove.brush",p).on("touchend.brush",m).on("keydown.brush",f).on("keyup.brush",h);if(k)A[0]=s[0][0]-A[0],A[1]=s[0][1]-A[1];else if(w){var q=+/w$/.test(w),T=+/^n/.test(w);y=[s[1-q][0]-A[0],s[1-T][1]-A[1]],A[0]=s[q][0],A[1]=s[T
 ][1]}else va.event.altKey&&(v=A.slice());_.style("pointer-events","none").selectAll(".resize").style("display",null),va.select("body").style("cursor",x.style("cursor")),b({type:"brushstart"}),p(),l()}var u,a=h(n,"brushstart","brush","brushend"),o=null,c=null,f=Fc[0],s=[[0,0],[0,0]],g=[!0,!0];return n.x=function(t){return arguments.length?(o=t,f=Fc[!o<<1|!c],n):o},n.y=function(t){return arguments.length?(c=t,f=Fc[!o<<1|!c],n):c},n.clamp=function(t){return arguments.length?(o&&c?g=[!!t[0],!!t[1]]:(o||c)&&(g[+!o]=!!t),n):o&&c?g:o||c?g[+!o]:null},n.extent=function(t){var e,r,i,a,l;return arguments.length?(u=[[0,0],[0,0]],o&&(e=t[0],r=t[1],c&&(e=e[0],r=r[0]),u[0][0]=e,u[1][0]=r,o.invert&&(e=o(e),r=o(r)),e>r&&(l=e,e=r,r=l),s[0][0]=0|e,s[1][0]=0|r),c&&(i=t[0],a=t[1],o&&(i=i[1],a=a[1]),u[0][1]=i,u[1][1]=a,c.invert&&(i=c(i),a=c(a)),i>a&&(l=i,i=a,a=l),s[0][1]=0|i,s[1][1]=0|a),n):(t=u||s,o&&(e=t[0][0],r=t[1][0],u||(e=s[0][0],r=s[1][0],o.invert&&(e=o.invert(e),r=o.invert(r)),e>r&&(l=e,e=r,r=l))
 ),c&&(i=t[0][1],a=t[1][1],u||(i=s[0][1],a=s[1][1],c.invert&&(i=c.invert(i),a=c.invert(a)),i>a&&(l=i,i=a,a=l))),o&&c?[[e,i],[r,a]]:o?[e,r]:c&&[i,a])},n.clear=function(){return u=null,s[0][0]=s[0][1]=s[1][0]=s[1][1]=0,n},n.empty=function(){return o&&s[0][0]===s[1][0]||c&&s[0][1]===s[1][1]},va.rebind(n,a,"on")};var Hc={n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},Fc=[["n","e","s","w","nw","ne","se","sw"],["e","w"],["n","s"],[]];va.time={};var Pc=Date,Oc=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];Cu.prototype={getDate:function(){return this._.getUTCDate()},getDay:function(){return this._.getUTCDay()},getFullYear:function(){return this._.getUTCFullYear()},getHours:function(){return this._.getUTCHours()},getMilliseconds:function(){return this._.getUTCMilliseconds()},getMinutes:function(){return this._.getUTCMinutes()},getMonth:function(){return this._.getUTCMonth()},getSeconds:fun
 ction(){return this._.getUTCSeconds()},getTime:function(){return this._.getTime()},getTimezoneOffset:function(){return 0},valueOf:function(){return this._.valueOf()},setDate:function(){Rc.setUTCDate.apply(this._,arguments)},setDay:function(){Rc.setUTCDay.apply(this._,arguments)},setFullYear:function(){Rc.setUTCFullYear.apply(this._,arguments)},setHours:function(){Rc.setUTCHours.apply(this._,arguments)},setMilliseconds:function(){Rc.setUTCMilliseconds.apply(this._,arguments)},setMinutes:function(){Rc.setUTCMinutes.apply(this._,arguments)},setMonth:function(){Rc.setUTCMonth.apply(this._,arguments)},setSeconds:function(){Rc.setUTCSeconds.apply(this._,arguments)},setTime:function(){Rc.setTime.apply(this._,arguments)}};var Rc=Date.prototype,Yc="%a %b %e %X %Y",Uc="%m/%d/%Y",Ic="%H:%M:%S",Vc=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],Xc=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],Zc=["January","February","March","April","May","June","July","August","September
 ","October","November","December"],Bc=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];va.time.year=zu(function(n){return n=va.time.day(n),n.setMonth(0,1),n},function(n,t){n.setFullYear(n.getFullYear()+t)},function(n){return n.getFullYear()}),va.time.years=va.time.year.range,va.time.years.utc=va.time.year.utc.range,va.time.day=zu(function(n){var t=new Pc(2e3,0);return t.setFullYear(n.getFullYear(),n.getMonth(),n.getDate()),t},function(n,t){n.setDate(n.getDate()+t)},function(n){return n.getDate()-1}),va.time.days=va.time.day.range,va.time.days.utc=va.time.day.utc.range,va.time.dayOfYear=function(n){var t=va.time.year(n);return Math.floor((n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*6e4)/864e5)},Oc.forEach(function(n,t){n=n.toLowerCase(),t=7-t;var e=va.time[n]=zu(function(n){return(n=va.time.day(n)).setDate(n.getDate()-(n.getDay()+t)%7),n},function(n,t){n.setDate(n.getDate()+Math.floor(t)*7)},function(n){var e=va.time.year(n).getDay();return Math.floor((
 va.time.dayOfYear(n)+(e+t)%7)/7)-(e!==t)});va.time[n+"s"]=e.range,va.time[n+"s"].utc=e.utc.range,va.time[n+"OfYear"]=function(n){var e=va.time.year(n).getDay();return Math.floor((va.time.dayOfYear(n)+(e+t)%7)/7)}}),va.time.week=va.time.sunday,va.time.weeks=va.time.sunday.range,va.time.weeks.utc=va.time.sunday.utc.range,va.time.weekOfYear=va.time.sundayOfYear,va.time.format=function(n){function t(t){for(var r,i,u,a=[],o=-1,c=0;++o<e;)n.charCodeAt(o)===37&&(a.push(n.substring(c,o)),(i=rl[r=n.charAt(++o)])!=null&&(r=n.charAt(++o)),(u=il[r])&&(r=u(t,null==i?"e"===r?" ":"0":i)),a.push(r),c=o+1);return a.push(n.substring(c,o)),a.join("")}var e=n.length;return t.parse=function(t){var e={y:1900,m:0,d:1,H:0,M:0,S:0,L:0},r=ju(e,n,t,0);if(r!=t.length)return null;"p"in e&&(e.H=e.H%12+e.p*12);var i=new Pc;return"j"in e?i.setFullYear(e.y,0,e.j):"w"in e&&("W"in e||"U"in e)?(i.setFullYear(e.y,0,1),i.setFullYear(e.y,0,"W"in e?(e.w+6)%7+e.W*7-(i.getDay()+5)%7:e.w+e.U*7-(i.getDay()+6)%7)):i.setFullYea
 r(e.y,e.m,e.d),i.setHours(e.H,e.M,e.S,e.L),i},t.toString=function(){return n},t};var $c=Lu(Vc),Wc=Hu(Vc),Jc=Lu(Xc),Gc=Hu(Xc),Kc=Lu(Zc),Qc=Hu(Zc),nl=Lu(Bc),tl=Hu(Bc),el=/^%/,rl={"-":"",_:" ",0:"0"},il={a:function(n){return Xc[n.getDay()]},A:function(n){return Vc[n.getDay()]},b:function(n){return Bc[n.getMonth()]},B:function(n){return Zc[n.getMonth()]},c:va.time.format(Yc),d:function(n,t){return Fu(n.getDate(),t,2)},e:function(n,t){return Fu(n.getDate(),t,2)},H:function(n,t){return Fu(n.getHours(),t,2)},I:function(n,t){return Fu(n.getHours()%12||12,t,2)},j:function(n,t){return Fu(1+va.time.dayOfYear(n),t,3)},L:function(n,t){return Fu(n.getMilliseconds(),t,3)},m:function(n,t){return Fu(n.getMonth()+1,t,2)},M:function(n,t){return Fu(n.getMinutes(),t,2)},p:function(n){return n.getHours()>=12?"PM":"AM"},S:function(n,t){return Fu(n.getSeconds(),t,2)},U:function(n,t){return Fu(va.time.sundayOfYear(n),t,2)},w:function(n){return n.getDay()},W:function(n,t){return Fu(va.time.mondayOfYear(n),t,
 2)},x:va.time.format(Uc),X:va.time.format(Ic),y:function(n,t){return Fu(n.getFullYear()%100,t,2)},Y:function(n,t){return Fu(n.getFullYear()%1e4,t,4)},Z:ua,"%":function(){return"%"}},ul={a:Pu,A:Ou,b:Iu,B:Vu,c:Xu,d:Ku,e:Ku,H:na,I:na,j:Qu,L:ra,m:Gu,M:ta,p:ia,S:ea,U:Yu,w:Ru,W:Uu,x:Zu,X:Bu,y:Wu,Y:$u,"%":aa},al=/^\s*\d+/,ol=va.map({am:0,pm:1});va.time.format.utc=function(n){function t(n){try{Pc=Cu;var t=new Pc;return t._=n,e(t)}finally{Pc=Date}}var e=va.time.format(n);return t.parse=function(n){try{Pc=Cu;var t=e.parse(n);return t&&t._}finally{Pc=Date}},t.toString=e.toString,t};var cl=va.time.format.utc("%Y-%m-%dT%H:%M:%S.%LZ");va.time.format.iso=Date.prototype.toISOString&&+new Date("2000-01-01T00:00:00.000Z")?oa:cl,oa.parse=function(n){var t=new Date(n);return isNaN(t)?null:t},oa.toString=cl.toString,va.time.second=zu(function(n){return new Pc(Math.floor(n/1e3)*1e3)},function(n,t){n.setTime(n.getTime()+Math.floor(t)*1e3)},function(n){return n.getSeconds()}),va.time.seconds=va.time.second
 .range,va.time.seconds.utc=va.time.second.utc.range,va.time.minute=zu(function(n){return new Pc(Math.floor(n/6e4)*6e4)},function(n,t){n.setTime(n.getTime()+Math.floor(t)*6e4)},function(n){return n.getMinutes()}),va.time.minutes=va.time.minute.range,va.time.minutes.utc=va.time.minute.utc.range,va.time.hour=zu(function(n){var t=n.getTimezoneOffset()/60;return new Pc((Math.floor(n/36e5-t)+t)*36e5)},function(n,t){n.setTime(n.getTime()+Math.floor(t)*36e5)},function(n){return n.getHours()}),va.time.hours=va.time.hour.range,va.time.hours.utc=va.time.hour.utc.range,va.time.month=zu(function(n){return n=va.time.day(n),n.setDate(1),n},function(n,t){n.setMonth(n.getMonth()+t)},function(n){return n.getMonth()}),va.time.months=va.time.month.range,va.time.months.utc=va.time.month.utc.range;var ll=[1e3,5e3,15e3,3e4,6e4,3e5,9e5,18e5,36e5,108e5,216e5,432e5,864e5,1728e5,6048e5,2592e6,7776e6,31536e6],fl=[[va.time.second,1],[va.time.second,5],[va.time.second,15],[va.time.second,30],[va.time.minute,1],[
 va.time.minute,5],[va.time.minute,15],[va.time.minute,30],[va.time.hour,1],[va.time.hour,3],[va.time.hour,6],[va.time.hour,12],[va.time.day,1],[va.time.day,2],[va.time.week,1],[va.time.month,1],[va.time.month,3],[va.time.year,1]],sl=[[va.time.format("%Y"),Rt],[va.time.format("%B"),function(n){return n.getMonth()}],[va.time.format("%b %d"),function(n){return n.getDate()!=1}],[va.time.format("%a %d"),function(n){return n.getDay()&&n.getDate()!=1}],[va.time.format("%I %p"),function(n){return n.getHours()}],[va.time.format("%I:%M"),function(n){return n.getMinutes()}],[va.time.format(":%S"),function(n){return n.getSeconds()}],[va.time.format(".%L"),function(n){return n.getMilliseconds()}]],hl=va.scale.linear(),gl=fa(sl);fl.year=function(n,t){return hl.domain(n.map(ha)).ticks(t).map(sa)},va.time.scale=function(){return ca(va.scale.linear(),fl,gl)};var pl=fl.map(function(n){return[n[0].utc,n[1]]}),dl=[[va.time.format.utc("%Y"),Rt],[va.time.format.utc("%B"),function(n){return n.getUTCMonth(
 )}],[va.time.format.utc("%b %d"),function(n){return n.getUTCDate()!=1}],[va.time.format.utc("%a %d"),function(n){return n.getUTCDay()&&n.getUTCDate()!=1}],[va.time.format.utc("%I %p"),function(n){return n.getUTCHours()}],[va.time.format.utc("%I:%M"),function(n){return n.getUTCMinutes()}],[va.time.format.utc(":%S"),function(n){return n.getUTCSeconds()}],[va.time.format.utc(".%L"),function(n){return n.getUTCMilliseconds()}]],ml=fa(dl);return pl.year=function(n,t){return hl.domain(n.map(pa)).ticks(t).map(ga)},va.time.scale.utc=function(){return ca(va.scale.linear(),pl,ml)},va.text=ht(function(n){return n.responseText}),va.json=function(n,t){return gt(n,"application/json",da,t)},va.html=function(n,t){return gt(n,"text/html",ma,t)},va.xml=ht(function(n){return n.responseXML}),va}();
\ No newline at end of file
diff --git a/website/web/robots.txt b/website/web/robots.txt
new file mode 100644
index 0000000..c59aca1
--- /dev/null
+++ b/website/web/robots.txt
@@ -0,0 +1,5 @@
+User-agent: *
+Disallow: /relay.html
+Disallow: /csv/
+Disallow: /consensus-health.html
+





More information about the tor-commits mailing list