[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[tor-commits] [metrics-web/master] Move website parts to new website/ subdirectory.
commit c4f76b8096c16a689a94b503f1ce7085a14b1ea4
Author: Karsten Loesing <karsten.loesing@xxxxxxx>
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@xxxxxxxxxxxxxx">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@xxxxxxxxxxxx</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@xxxxxxxxxxxxxx">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@xxxxxxxxxxxxxx">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@xxxxxxxxxxxxxx">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@xxxxxxxxxxxx</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@xxxxxxxxxxxxxx">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@xxxxxxxxxxxxxx">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
+
_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits