commit c4f76b8096c16a689a94b503f1ce7085a14b1ea4 Author: Karsten Loesing karsten.loesing@gmx.net Date: Tue Jan 21 17:16:43 2014 +0100
Move website parts to new website/ subdirectory. --- build.xml | 38 +- etc/context.xml.template | 13 - etc/logging.properties | 6 - etc/web.xml | 320 ------- lib/REngine.jar | Bin 30637 -> 0 bytes lib/RserveEngine.jar | Bin 27827 -> 0 bytes lib/jstl.jar | Bin 16905 -> 0 bytes lib/standard.jar | Bin 293750 -> 0 bytes rserve/Rserv.conf | 2 - rserve/csv.R | 213 ----- rserve/graphs.R | 929 ------------------ rserve/rserve-init.R | 14 - rserve/shutdown.sh | 5 - rserve/start.sh | 3 - rserve/tables.R | 55 -- .../status/doctor/ConsensusHealthServlet.java | 57 -- .../ernie/status/exonerator/ExoneraTorServlet.java | 24 - .../status/relaysearch/RelaySearchServlet.java | 535 ----------- src/org/torproject/ernie/web/GraphsServlet.java | 24 - src/org/torproject/ernie/web/IndexServlet.java | 24 - src/org/torproject/ernie/web/ResearchServlet.java | 24 - src/org/torproject/ernie/web/StatusServlet.java | 24 - .../ernie/web/graphs/BubblesServlet.java | 24 - src/org/torproject/ernie/web/graphs/Countries.java | 285 ------ .../torproject/ernie/web/graphs/CsvServlet.java | 97 -- .../ernie/web/graphs/GraphImageServlet.java | 76 -- .../ernie/web/graphs/GraphParameterChecker.java | 280 ------ .../ernie/web/graphs/GraphsSubpagesServlet.java | 162 ---- src/org/torproject/ernie/web/graphs/RObject.java | 23 - .../ernie/web/graphs/RObjectGenerator.java | 398 -------- .../ernie/web/graphs/TableParameterChecker.java | 120 --- .../ernie/web/research/ResearchDataServlet.java | 260 ------ .../ernie/web/research/ResearchFormatsServlet.java | 24 - .../ernie/web/research/ResearchPapersServlet.java | 24 - .../ernie/web/research/ResearchStatsServlet.java | 132 --- .../ernie/web/research/ResearchToolsServlet.java | 24 - web/WEB-INF/banner.jsp | 81 -- web/WEB-INF/bubbles.jsp | 40 - web/WEB-INF/data.jsp | 286 ------ web/WEB-INF/dir.jsp | 25 - web/WEB-INF/error.jsp | 77 -- web/WEB-INF/exonerator.jsp | 45 - web/WEB-INF/fast-exits.jsp | 81 -- web/WEB-INF/footer.jsp | 13 - web/WEB-INF/formats.jsp | 986 -------------------- web/WEB-INF/graphs.jsp | 34 - web/WEB-INF/index.jsp | 40 - web/WEB-INF/network.jsp | 305 ------ web/WEB-INF/papers.jsp | 81 -- web/WEB-INF/performance.jsp | 150 --- web/WEB-INF/relay-search.jsp | 85 -- web/WEB-INF/research.jsp | 32 - web/WEB-INF/stats.jsp | 313 ------- web/WEB-INF/status.jsp | 30 - web/WEB-INF/tools.jsp | 110 --- web/WEB-INF/users.jsp | 390 -------- web/css/stylesheet-ltr.css | 161 ---- web/images/favicon.ico | Bin 1150 -> 0 bytes web/images/top-left.png | Bin 11137 -> 0 bytes web/images/top-middle.png | Bin 240 -> 0 bytes web/images/top-right.png | Bin 607 -> 0 bytes web/images/tor-metrics-overview.png | Bin 309306 -> 0 bytes web/js/bubbles.js | 288 ------ web/js/d3.min.js | 5 - web/robots.txt | 5 - website/build.xml | 64 ++ website/etc/context.xml | 3 + website/etc/logging.properties | 6 + website/etc/web.xml | 320 +++++++ website/lib/REngine.jar | Bin 0 -> 30637 bytes website/lib/RserveEngine.jar | Bin 0 -> 27827 bytes website/lib/jstl.jar | Bin 0 -> 16905 bytes website/lib/standard.jar | Bin 0 -> 293750 bytes website/rserve/Rserv.conf | 2 + website/rserve/csv.R | 213 +++++ website/rserve/graphs.R | 929 ++++++++++++++++++ website/rserve/rserve-init.R | 14 + website/rserve/shutdown.sh | 5 + website/rserve/start.sh | 3 + website/rserve/tables.R | 55 ++ .../status/doctor/ConsensusHealthServlet.java | 57 ++ .../ernie/status/exonerator/ExoneraTorServlet.java | 24 + .../status/relaysearch/RelaySearchServlet.java | 535 +++++++++++ .../org/torproject/ernie/web/GraphsServlet.java | 24 + .../src/org/torproject/ernie/web/IndexServlet.java | 24 + .../org/torproject/ernie/web/ResearchServlet.java | 24 + .../org/torproject/ernie/web/StatusServlet.java | 24 + .../ernie/web/graphs/BubblesServlet.java | 24 + .../org/torproject/ernie/web/graphs/Countries.java | 285 ++++++ .../torproject/ernie/web/graphs/CsvServlet.java | 97 ++ .../ernie/web/graphs/GraphImageServlet.java | 76 ++ .../ernie/web/graphs/GraphParameterChecker.java | 280 ++++++ .../ernie/web/graphs/GraphsSubpagesServlet.java | 162 ++++ .../org/torproject/ernie/web/graphs/RObject.java | 23 + .../ernie/web/graphs/RObjectGenerator.java | 398 ++++++++ .../ernie/web/graphs/TableParameterChecker.java | 120 +++ .../ernie/web/research/ResearchDataServlet.java | 260 ++++++ .../ernie/web/research/ResearchFormatsServlet.java | 24 + .../ernie/web/research/ResearchPapersServlet.java | 24 + .../ernie/web/research/ResearchStatsServlet.java | 132 +++ .../ernie/web/research/ResearchToolsServlet.java | 24 + website/web/WEB-INF/banner.jsp | 81 ++ website/web/WEB-INF/bubbles.jsp | 40 + website/web/WEB-INF/data.jsp | 286 ++++++ website/web/WEB-INF/dir.jsp | 25 + website/web/WEB-INF/error.jsp | 77 ++ website/web/WEB-INF/exonerator.jsp | 45 + website/web/WEB-INF/fast-exits.jsp | 81 ++ website/web/WEB-INF/footer.jsp | 13 + website/web/WEB-INF/formats.jsp | 986 ++++++++++++++++++++ website/web/WEB-INF/graphs.jsp | 34 + website/web/WEB-INF/index.jsp | 40 + website/web/WEB-INF/network.jsp | 305 ++++++ website/web/WEB-INF/papers.jsp | 81 ++ website/web/WEB-INF/performance.jsp | 150 +++ website/web/WEB-INF/relay-search.jsp | 85 ++ website/web/WEB-INF/research.jsp | 32 + website/web/WEB-INF/stats.jsp | 313 +++++++ website/web/WEB-INF/status.jsp | 30 + website/web/WEB-INF/tools.jsp | 110 +++ website/web/WEB-INF/users.jsp | 390 ++++++++ website/web/css/stylesheet-ltr.css | 161 ++++ website/web/images/favicon.ico | Bin 0 -> 1150 bytes website/web/images/top-left.png | Bin 0 -> 11137 bytes website/web/images/top-middle.png | Bin 0 -> 240 bytes website/web/images/top-right.png | Bin 0 -> 607 bytes website/web/images/tor-metrics-overview.png | Bin 0 -> 309306 bytes website/web/js/bubbles.js | 288 ++++++ website/web/js/d3.min.js | 5 + website/web/robots.txt | 5 + 130 files changed, 7919 insertions(+), 7901 deletions(-)
diff --git a/build.xml b/build.xml index cfcfdac..393e2d8 100644 --- a/build.xml +++ b/build.xml @@ -4,10 +4,6 @@ <property name="sources" value="src"/> <property name="classes" value="classes"/> <property name="config" value="etc"/> - <property name="webxmlfile" value="${config}/web.xml"/> - <property name="contextxmltemplate" - value="${config}/context.xml.template"/> - <property name="contextxml" value="${config}/context.xml"/> <property name="warfile" value="ernie.war"/> <path id="classpath"> <pathelement path="${classes}"/> @@ -29,7 +25,6 @@ </path>
<target name="init"> - <copy file="${contextxmltemplate}" tofile="${contextxml}"/> <copy file="config.template" tofile="config"/> <mkdir dir="${classes}"/> </target> @@ -37,7 +32,7 @@ <ant dir="deps/metrics-lib"/> </target>
- <!-- Compile all servlets and plain Java classes. --> + <!-- Compile all plain Java classes. --> <target name="compile" depends="metrics-lib,init"> <javac destdir="${classes}" srcdir="${sources}" @@ -72,36 +67,5 @@ </batchtest> </junit> </target> - - <!-- Create a .war file for deployment. --> - <target name="war" - depends="compile"> - <war destfile="${warfile}" - webxml="${webxmlfile}"> - <fileset dir="web"/> - <lib dir="lib"> - <include name="jstl.jar"/> - <include name="REngine.jar"/> - <include name="RserveEngine.jar"/> - <include name="standard.jar"/> - </lib> - <lib dir="/usr/share/java"> - <include name="commons-codec.jar"/> - <include name="commons-compress.jar"/> - <include name="postgresql-jdbc3.jar"/> - <include name="junit4.jar"/> - <include name="commons-lang.jar"/> - </lib> - <lib dir="deps/metrics-lib"> - <include name="descriptor.jar"/> - </lib> - <classes dir="${classes}"/> - <zipfileset dir="${config}" - prefix="WEB-INF/classes" - includes="logging.properties"/> - <metainf dir="${config}" - includes="context.xml"/> - </war> - </target> </project>
diff --git a/etc/context.xml.template b/etc/context.xml.template deleted file mode 100644 index 00f14fe..0000000 --- a/etc/context.xml.template +++ /dev/null @@ -1,13 +0,0 @@ -<Context cookies="false"> - <Resource name="jdbc/tordir" - type="javax.sql.DataSource" - auth="Container" - username="metrics" - password="password" - driverClassName="org.postgresql.Driver" - url="jdbc:postgresql://localhost/tordir" - maxActive="8" - maxIdle="4" - maxWait="15000"/> -</Context> - diff --git a/etc/logging.properties b/etc/logging.properties deleted file mode 100644 index 6f12902..0000000 --- a/etc/logging.properties +++ /dev/null @@ -1,6 +0,0 @@ -handlers = org.apache.juli.FileHandler - -org.apache.juli.FileHandler.level = FINE -org.apache.juli.FileHandler.directory = ${catalina.base}/logs -org.apache.juli.FileHandler.prefix = ernie. - diff --git a/etc/web.xml b/etc/web.xml deleted file mode 100644 index 992de93..0000000 --- a/etc/web.xml +++ /dev/null @@ -1,320 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> - -<web-app version="2.4" - xmlns="http://java.sun.com/xml/ns/j2ee" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee - http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" > - - <servlet> - <servlet-name>Index</servlet-name> - <servlet-class> - org.torproject.ernie.web.IndexServlet - </servlet-class> - </servlet> - <servlet-mapping> - <servlet-name>Index</servlet-name> - <url-pattern>/index.html</url-pattern> - </servlet-mapping> - - <servlet> - <servlet-name>Graphs</servlet-name> - <servlet-class> - org.torproject.ernie.web.GraphsServlet - </servlet-class> - </servlet> - <servlet-mapping> - <servlet-name>Graphs</servlet-name> - <url-pattern>/graphs.html</url-pattern> - </servlet-mapping> - - <servlet> - <servlet-name>GraphsSubpages</servlet-name> - <servlet-class> - org.torproject.ernie.web.graphs.GraphsSubpagesServlet - </servlet-class> - </servlet> - <servlet-mapping> - <servlet-name>GraphsSubpages</servlet-name> - <url-pattern>/network.html</url-pattern> - </servlet-mapping> - <servlet-mapping> - <servlet-name>GraphsSubpages</servlet-name> - <url-pattern>/fast-exits.html</url-pattern> - </servlet-mapping> - <servlet-mapping> - <servlet-name>GraphsSubpages</servlet-name> - <url-pattern>/users.html</url-pattern> - </servlet-mapping> - <servlet-mapping> - <servlet-name>GraphsSubpages</servlet-name> - <url-pattern>/performance.html</url-pattern> - </servlet-mapping> - - <servlet> - <servlet-name>Bubbles</servlet-name> - <servlet-class> - org.torproject.ernie.web.graphs.BubblesServlet - </servlet-class> - </servlet> - <servlet-mapping> - <servlet-name>Bubbles</servlet-name> - <url-pattern>/bubbles.html</url-pattern> - </servlet-mapping> - - <servlet> - <servlet-name>Research</servlet-name> - <servlet-class> - org.torproject.ernie.web.ResearchServlet - </servlet-class> - </servlet> - <servlet-mapping> - <servlet-name>Research</servlet-name> - <url-pattern>/research.html</url-pattern> - </servlet-mapping> - - <servlet> - <servlet-name>ResearchData</servlet-name> - <servlet-class> - org.torproject.ernie.web.research.ResearchDataServlet - </servlet-class> - <init-param> - <param-name>localDataDir</param-name> - <param-value> - /srv/metrics.torproject.org/db/data/ - </param-value> - </init-param> - </servlet> - <servlet-mapping> - <servlet-name>ResearchData</servlet-name> - <url-pattern>/data.html</url-pattern> - </servlet-mapping> - - <servlet> - <servlet-name>ResearchPapers</servlet-name> - <servlet-class> - org.torproject.ernie.web.research.ResearchPapersServlet - </servlet-class> - </servlet> - <servlet-mapping> - <servlet-name>ResearchPapers</servlet-name> - <url-pattern>/papers.html</url-pattern> - </servlet-mapping> - - <servlet> - <servlet-name>ResearchTools</servlet-name> - <servlet-class> - org.torproject.ernie.web.research.ResearchToolsServlet - </servlet-class> - </servlet> - <servlet-mapping> - <servlet-name>ResearchTools</servlet-name> - <url-pattern>/tools.html</url-pattern> - </servlet-mapping> - - <servlet> - <servlet-name>ResearchFormats</servlet-name> - <servlet-class> - org.torproject.ernie.web.research.ResearchFormatsServlet - </servlet-class> - </servlet> - <servlet-mapping> - <servlet-name>ResearchFormats</servlet-name> - <url-pattern>/formats.html</url-pattern> - </servlet-mapping> - - <servlet> - <servlet-name>ResearchStats</servlet-name> - <servlet-class> - org.torproject.ernie.web.research.ResearchStatsServlet - </servlet-class> - <init-param> - <param-name>statsDir</param-name> - <param-value> - /srv/metrics.torproject.org/web/stats/ - </param-value> - </init-param> - </servlet> - <servlet-mapping> - <servlet-name>ResearchStats</servlet-name> - <url-pattern>/stats/*</url-pattern> - <url-pattern>/stats.html</url-pattern> - </servlet-mapping> - - <servlet> - <servlet-name>Status</servlet-name> - <servlet-class> - org.torproject.ernie.web.StatusServlet - </servlet-class> - </servlet> - <servlet-mapping> - <servlet-name>Status</servlet-name> - <url-pattern>/status.html</url-pattern> - </servlet-mapping> - - <servlet> - <servlet-name>RelaySearch</servlet-name> - <servlet-class> - org.torproject.ernie.status.relaysearch.RelaySearchServlet - </servlet-class> - </servlet> - <servlet-mapping> - <servlet-name>RelaySearch</servlet-name> - <url-pattern>/relay-search.html</url-pattern> - </servlet-mapping> - - <servlet> - <servlet-name>GraphImage</servlet-name> - <servlet-class> - org.torproject.ernie.web.graphs.GraphImageServlet - </servlet-class> - </servlet> - <servlet-mapping> - <servlet-name>GraphImage</servlet-name> - <url-pattern>/networksize.png</url-pattern> - <url-pattern>/networksize.pdf</url-pattern> - <url-pattern>/networksize.svg</url-pattern> - <url-pattern>/cloudbridges.png</url-pattern> - <url-pattern>/cloudbridges.pdf</url-pattern> - <url-pattern>/cloudbridges.svg</url-pattern> - <url-pattern>/relaycountries.png</url-pattern> - <url-pattern>/relaycountries.pdf</url-pattern> - <url-pattern>/relaycountries.svg</url-pattern> - <url-pattern>/relayflags.png</url-pattern> - <url-pattern>/relayflags.pdf</url-pattern> - <url-pattern>/relayflags.svg</url-pattern> - <url-pattern>/versions.png</url-pattern> - <url-pattern>/versions.pdf</url-pattern> - <url-pattern>/versions.svg</url-pattern> - <url-pattern>/platforms.png</url-pattern> - <url-pattern>/platforms.pdf</url-pattern> - <url-pattern>/platforms.svg</url-pattern> - <url-pattern>/bandwidth.png</url-pattern> - <url-pattern>/bandwidth.pdf</url-pattern> - <url-pattern>/bandwidth.svg</url-pattern> - <url-pattern>/bwhist-flags.png</url-pattern> - <url-pattern>/bwhist-flags.pdf</url-pattern> - <url-pattern>/bwhist-flags.svg</url-pattern> - <url-pattern>/bandwidth-flags.png</url-pattern> - <url-pattern>/bandwidth-flags.pdf</url-pattern> - <url-pattern>/bandwidth-flags.svg</url-pattern> - <url-pattern>/dirbytes.png</url-pattern> - <url-pattern>/dirbytes.pdf</url-pattern> - <url-pattern>/dirbytes.svg</url-pattern> - <url-pattern>/torperf.png</url-pattern> - <url-pattern>/torperf.pdf</url-pattern> - <url-pattern>/torperf.svg</url-pattern> - <url-pattern>/torperf-failures.png</url-pattern> - <url-pattern>/torperf-failures.pdf</url-pattern> - <url-pattern>/torperf-failures.svg</url-pattern> - <url-pattern>/connbidirect.png</url-pattern> - <url-pattern>/connbidirect.pdf</url-pattern> - <url-pattern>/connbidirect.svg</url-pattern> - <url-pattern>/fast-exits.png</url-pattern> - <url-pattern>/fast-exits.pdf</url-pattern> - <url-pattern>/fast-exits.svg</url-pattern> - <url-pattern>/almost-fast-exits.png</url-pattern> - <url-pattern>/almost-fast-exits.pdf</url-pattern> - <url-pattern>/almost-fast-exits.svg</url-pattern> - <url-pattern>/userstats-relay-country.png</url-pattern> - <url-pattern>/userstats-relay-country.pdf</url-pattern> - <url-pattern>/userstats-relay-country.svg</url-pattern> - <url-pattern>/userstats-bridge-country.png</url-pattern> - <url-pattern>/userstats-bridge-country.pdf</url-pattern> - <url-pattern>/userstats-bridge-country.svg</url-pattern> - <url-pattern>/userstats-bridge-transport.png</url-pattern> - <url-pattern>/userstats-bridge-transport.pdf</url-pattern> - <url-pattern>/userstats-bridge-transport.svg</url-pattern> - <url-pattern>/userstats-bridge-version.png</url-pattern> - <url-pattern>/userstats-bridge-version.pdf</url-pattern> - <url-pattern>/userstats-bridge-version.svg</url-pattern> - </servlet-mapping> - - <servlet> - <servlet-name>Csv</servlet-name> - <servlet-class> - org.torproject.ernie.web.graphs.CsvServlet - </servlet-class> - </servlet> - <servlet-mapping> - <servlet-name>Csv</servlet-name> - <url-pattern>/csv/*</url-pattern> - </servlet-mapping> - - <servlet> - <servlet-name>ExoneraTor</servlet-name> - <servlet-class> - org.torproject.ernie.status.exonerator.ExoneraTorServlet - </servlet-class> - </servlet> - <servlet-mapping> - <servlet-name>ExoneraTor</servlet-name> - <url-pattern>/exonerator.html</url-pattern> - </servlet-mapping> - - <servlet> - <servlet-name>ConsensusHealthServlet</servlet-name> - <servlet-class> - org.torproject.ernie.status.doctor.ConsensusHealthServlet - </servlet-class> - </servlet> - <servlet-mapping> - <servlet-name>ConsensusHealthServlet</servlet-name> - <url-pattern>/consensus-health.html</url-pattern> - </servlet-mapping> - - <welcome-file-list> - <welcome-file>index.html</welcome-file> - </welcome-file-list> - - <error-page> - <error-code>400</error-code> - <location>/WEB-INF/error.jsp</location> - </error-page> - <error-page> - <error-code>404</error-code> - <location>/WEB-INF/error.jsp</location> - </error-page> - <error-page> - <error-code>500</error-code> - <location>/WEB-INF/error.jsp</location> - </error-page> - <error-page> - <exception-type>java.lang.Throwable</exception-type> - <location>/WEB-INF/error.jsp</location> - </error-page> - - <resource-ref> - <description>Tor Metrics Database</description> - <res-ref-name>jdbc/tordir</res-ref-name> - <res-type>javax.sql.DataSource</res-type> - <res-auth>Container</res-auth> - </resource-ref> - - <context-param> - <param-name>rserveHost</param-name> - <param-value>localhost</param-value> - </context-param> - <context-param> - <param-name>rservePort</param-name> - <param-value>6311</param-value> - </context-param> - <context-param> - <param-name>maxCacheAge</param-name> - <param-value>21600</param-value> - </context-param> - <context-param> - <param-name>cachedGraphsDir</param-name> - <param-value> - /srv/metrics.torproject.org/web/rserve/graphs/ - </param-value> - </context-param> - - <listener> - <listener-class> - org.torproject.ernie.web.graphs.RObjectGenerator - </listener-class> - </listener> - -</web-app> - diff --git a/lib/REngine.jar b/lib/REngine.jar deleted file mode 100644 index ddf4059..0000000 Binary files a/lib/REngine.jar and /dev/null differ diff --git a/lib/RserveEngine.jar b/lib/RserveEngine.jar deleted file mode 100644 index 77b22ce..0000000 Binary files a/lib/RserveEngine.jar and /dev/null differ diff --git a/lib/jstl.jar b/lib/jstl.jar deleted file mode 100644 index 6b41358..0000000 Binary files a/lib/jstl.jar and /dev/null differ diff --git a/lib/standard.jar b/lib/standard.jar deleted file mode 100644 index 258daae..0000000 Binary files a/lib/standard.jar and /dev/null differ diff --git a/rserve/Rserv.conf b/rserve/Rserv.conf deleted file mode 100644 index a23af49..0000000 --- a/rserve/Rserv.conf +++ /dev/null @@ -1,2 +0,0 @@ -workdir /srv/metrics.torproject.org/web/rserve/workdir -source rserve-init.R diff --git a/rserve/csv.R b/rserve/csv.R deleted file mode 100644 index 2ec6e7b..0000000 --- a/rserve/csv.R +++ /dev/null @@ -1,213 +0,0 @@ -options(scipen = 15) - -export_networksize <- function(path) { - s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv", - stringsAsFactors = FALSE) - s <- s[s$flag == '' & s$country == '' & s$version == '' & - s$platform == '' & s$ec2bridge == '', - c("date", "relays", "bridges")] - write.csv(s, path, quote = FALSE, row.names = FALSE) -} - -export_cloudbridges <- function(path) { - s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv", - stringsAsFactors = FALSE) - s <- s[s$flag == '' & s$country == '' & s$version == '' & - s$platform == '' & s$ec2bridge == 't', ] - cloudbridges <- data.frame(date = s$date, cloudbridges = s$bridges) - write.csv(cloudbridges, path, quote = FALSE, row.names = FALSE) -} - -export_relaycountries <- function(path) { - s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv", - stringsAsFactors = FALSE) - s <- s[s$flag == '' & s$country != '' & s$version == '' & - s$platform == '' & s$ec2bridge == '', - c("date", "country", "relays")] - write.csv(s, path, quote = FALSE, row.names = FALSE) -} - -export_versions <- function(path) { - s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv", - stringsAsFactors = FALSE) - s <- s[s$flag == '' & s$country == '' & s$version != '' & - s$platform == '' & s$ec2bridge == '', - c("date", "version", "relays")] - versions <- cast(s, date ~ version, value = "relays") - versions <- versions[order(versions$date), ] - write.csv(versions, path, quote = FALSE, row.names = FALSE) -} - -export_platforms <- function(path) { - s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv", - stringsAsFactors = FALSE) - s <- s[s$flag == '' & s$country == '' & s$version == '' & - s$platform != '' & s$ec2bridge == '', - c("date", "platform", "relays")] - s <- data.frame(date = s$date, - platform = ifelse(s$platform == 'FreeBSD', 'bsd', - tolower(s$platform)), relays = s$relays) - s <- cast(s, date ~ platform, value = "relays") - platforms <- s[order(s$date), ] - write.csv(platforms, path, quote = FALSE, row.names = FALSE) -} - -export_bandwidth <- function(path) { - b <- read.csv("/srv/metrics.torproject.org/web/stats/bandwidth.csv", - stringsAsFactors = FALSE) - b <- b[b$isexit == '' & b$isguard == '', ] - b <- data.frame(date = as.Date(b$date, "%Y-%m-%d"), - bwadv = b$advbw, - bwhist = floor((b$bwread + b$bwwrite) / 2)) - b <- b[order(b$date), ] - write.csv(b, path, quote = FALSE, row.names = FALSE) -} - -export_bwhist_flags <- function(path) { - b <- read.csv("/srv/metrics.torproject.org/web/stats/bandwidth.csv", - stringsAsFactors = FALSE) - b <- b[b$isexit != '' & b$isguard != '' & !is.na(b$bwread) & - !is.na(b$bwwrite), ] - b <- data.frame(date = as.Date(b$date, "%Y-%m-%d"), - isexit = b$isexit == 't', isguard = b$isguard == 't', - read = b$bwread, written = b$bwwrite) - write.csv(b, path, quote = FALSE, row.names = FALSE) -} - -export_dirbytes <- function(path) { - b <- read.csv("/srv/metrics.torproject.org/web/stats/bandwidth.csv", - stringsAsFactors = FALSE) - b <- b[b$isexit == '' & b$isguard == '' & !is.na(b$dirread) & - !is.na(b$dirwrite), ] - b <- data.frame(date = as.Date(b$date, "%Y-%m-%d"), - dirread = b$dirread, dirwrite = b$dirwrite) - b <- b[order(b$date), ] - write.csv(b, path, quote = FALSE, row.names = FALSE) -} - -export_relayflags <- function(path) { - s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv", - stringsAsFactors = FALSE) - s <- s[s$country == '' & s$version == '' & s$platform == '' & - s$ec2bridge == '', ] - s <- data.frame(date = as.Date(s$date, "%Y-%m-%d"), - flag = ifelse(s$flag == '', 'running', tolower(s$flag)), - relays = s$relays) - s <- cast(s, date ~ flag, value = "relays") - relayflags <- s[order(s$date), ] - write.csv(relayflags, path, quote = FALSE, row.names = FALSE) -} - -export_torperf <- function(path) { - t <- read.csv("/srv/metrics.torproject.org/web/stats/torperf.csv", - stringsAsFactors = FALSE) - t <- data.frame( - source = paste(ifelse(t$source == '', 'all', t$source), - ifelse(t$size == 50 * 1024, '50kb', - ifelse(t$size == 1024 * 1024, '1mb', '5mb')), - sep = '-'), - date = as.Date(t$date, "%Y-%m-%d"), - q1 = t$q1, md = t$md, q3 = t$q3) - torperf <- t[order(t$source, t$date), ] - write.csv(torperf, path, quote = FALSE, row.names = FALSE) -} - -export_torperf_failures <- function(path) { - t <- read.csv("/srv/metrics.torproject.org/web/stats/torperf.csv", - stringsAsFactors = FALSE) - t <- data.frame( - source = paste(ifelse(t$source == '', 'all', t$source), - ifelse(t$size == 50 * 1024, '50kb', - ifelse(t$size == 1024 * 1024, '1mb', '5mb')), - sep = '-'), - date = as.Date(t$date, "%Y-%m-%d"), - timeouts = t$timeouts, failures = t$failures, requests = t$requests) - torperf <- t[order(t$source, t$date), ] - write.csv(torperf, path, quote = FALSE, row.names = FALSE) -} - -export_connbidirect <- function(path) { - c <- read.csv("/srv/metrics.torproject.org/web/stats/connbidirect.csv", - stringsAsFactors = FALSE) - write.csv(format(c, trim = TRUE, scientific = FALSE), path, - quote = FALSE, row.names = FALSE) -} - -export_bandwidth_flags <- function(path) { - b <- read.csv("/srv/metrics.torproject.org/web/stats/bandwidth.csv", - stringsAsFactors = FALSE) - b <- b[b$isexit != '' & b$isguard != '', ] - b <- data.frame(date = as.Date(b$date, "%Y-%m-%d"), - isexit = b$isexit == 't', isguard = b$isguard == 't', - advbw = b$advbw, - bwhist = floor((b$bwread + b$bwwrite) / 2)) - b <- rbind( - data.frame(b[b$isguard == TRUE, ], flag = "guard"), - data.frame(b[b$isexit == TRUE, ], flag = "exit")) - b <- data.frame(date = b$date, advbw = b$advbw, bwhist = b$bwhist, - flag = b$flag) - b <- aggregate(list(advbw = b$advbw, bwhist = b$bwhist), - by = list(date = b$date, flag = b$flag), FUN = sum, - na.rm = TRUE, na.action = NULL) - b <- melt(b, id.vars = c("date", "flag")) - b <- data.frame(date = b$date, type = b$variable, flag = b$flag, - value = b$value) - b <- b[b$value > 0, ] - write.csv(b, path, quote = FALSE, row.names = FALSE) -} - -export_userstats <- function(path) { - c <- read.csv("/srv/metrics.torproject.org/web/stats/clients.csv", - stringsAsFactors = FALSE) - c <- data.frame(date = c$date, node = c$node, country = c$country, - transport = c$transport, version = c$version, - frac = c$frac, users = c$clients) - write.csv(format(c, trim = TRUE, scientific = FALSE), path, - quote = FALSE, row.names = FALSE) -} - -help_export_monthly_userstats <- function(path, aggr_fun) { - c <- read.csv("/srv/metrics.torproject.org/web/stats/clients.csv", - stringsAsFactors = FALSE) - c <- c[c$country != '' & c$transport == '' & c$version == '', ] - u <- data.frame(date = c$date, country = c$country, users = c$clients, - stringsAsFactors = FALSE) - u <- aggregate(list(users = u$users), - by = list(date = u$date, country = u$country), sum) - u <- aggregate(list(users = u$users), - by = list(country = u$country, - month = substr(u$date, 1, 7)), aggr_fun) - u <- rbind(u, data.frame(country = "zy", - aggregate(list(users = u$users), - by = list(month = u$month), sum))) - u <- cast(u, country ~ month, value = "users") - u[u$country == "zy", "country"] <- "all" - u[, 2:length(u)] <- floor(u[, 2:length(u)]) - write.csv(u, path, quote = FALSE, row.names = FALSE) -} - -export_monthly_userstats_peak <- function(path) { - help_export_monthly_userstats(path, max) -} - -export_monthly_userstats_average <- function(path) { - help_export_monthly_userstats(path, mean) -} - -export_userstats_detector <- function(path) { - c <- read.csv("/srv/metrics.torproject.org/web/stats/clients.csv", - stringsAsFactors = FALSE) - c <- c[c$country != '' & c$transport == '' & c$version == '' & - c$node == 'relay', ] - u <- data.frame(country = c$country, date = c$date, users = c$clients, - stringsAsFactors = FALSE) - u <- rbind(u, data.frame(country = "zy", - aggregate(list(users = u$users), - by = list(date = u$date), sum))) - u <- data.frame(date = u$date, country = u$country, - users = floor(u$users)) - u <- cast(u, date ~ country, value = "users") - names(u)[names(u) == "zy"] <- "all" - write.csv(u, path, quote = FALSE, row.names = FALSE) -} - diff --git a/rserve/graphs.R b/rserve/graphs.R deleted file mode 100644 index c4170e7..0000000 --- a/rserve/graphs.R +++ /dev/null @@ -1,929 +0,0 @@ -countrylist <- list( - "ad" = "Andorra", - "ae" = "the United Arab Emirates", - "af" = "Afghanistan", - "ag" = "Antigua and Barbuda", - "ai" = "Anguilla", - "al" = "Albania", - "am" = "Armenia", - "an" = "the Netherlands Antilles", - "ao" = "Angola", - "aq" = "Antarctica", - "ar" = "Argentina", - "as" = "American Samoa", - "at" = "Austria", - "au" = "Australia", - "aw" = "Aruba", - "ax" = "the Aland Islands", - "az" = "Azerbaijan", - "ba" = "Bosnia and Herzegovina", - "bb" = "Barbados", - "bd" = "Bangladesh", - "be" = "Belgium", - "bf" = "Burkina Faso", - "bg" = "Bulgaria", - "bh" = "Bahrain", - "bi" = "Burundi", - "bj" = "Benin", - "bl" = "Saint Bartelemey", - "bm" = "Bermuda", - "bn" = "Brunei", - "bo" = "Bolivia", - "br" = "Brazil", - "bs" = "the Bahamas", - "bt" = "Bhutan", - "bv" = "the Bouvet Island", - "bw" = "Botswana", - "by" = "Belarus", - "bz" = "Belize", - "ca" = "Canada", - "cc" = "the Cocos (Keeling) Islands", - "cd" = "the Democratic Republic of the Congo", - "cf" = "Central African Republic", - "cg" = "Congo", - "ch" = "Switzerland", - "ci" = "CÃŽte d'Ivoire", - "ck" = "the Cook Islands", - "cl" = "Chile", - "cm" = "Cameroon", - "cn" = "China", - "co" = "Colombia", - "cr" = "Costa Rica", - "cu" = "Cuba", - "cv" = "Cape Verde", - "cx" = "the Christmas Island", - "cy" = "Cyprus", - "cz" = "the Czech Republic", - "de" = "Germany", - "dj" = "Djibouti", - "dk" = "Denmark", - "dm" = "Dominica", - "do" = "the Dominican Republic", - "dz" = "Algeria", - "ec" = "Ecuador", - "ee" = "Estonia", - "eg" = "Egypt", - "eh" = "the Western Sahara", - "er" = "Eritrea", - "es" = "Spain", - "et" = "Ethiopia", - "fi" = "Finland", - "fj" = "Fiji", - "fk" = "the Falkland Islands (Malvinas)", - "fm" = "the Federated States of Micronesia", - "fo" = "the Faroe Islands", - "fr" = "France", - "fx" = "Metropolitan France", - "ga" = "Gabon", - "gb" = "the United Kingdom", - "gd" = "Grenada", - "ge" = "Georgia", - "gf" = "French Guiana", - "gg" = "Guernsey", - "gh" = "Ghana", - "gi" = "Gibraltar", - "gl" = "Greenland", - "gm" = "Gambia", - "gn" = "Guinea", - "gp" = "Guadeloupe", - "gq" = "Equatorial Guinea", - "gr" = "Greece", - "gs" = "South Georgia and the South Sandwich Islands", - "gt" = "Guatemala", - "gu" = "Guam", - "gw" = "Guinea-Bissau", - "gy" = "Guyana", - "hk" = "Hong Kong", - "hm" = "Heard Island and McDonald Islands", - "hn" = "Honduras", - "hr" = "Croatia", - "ht" = "Haiti", - "hu" = "Hungary", - "id" = "Indonesia", - "ie" = "Ireland", - "il" = "Israel", - "im" = "the Isle of Man", - "in" = "India", - "io" = "the British Indian Ocean Territory", - "iq" = "Iraq", - "ir" = "Iran", - "is" = "Iceland", - "it" = "Italy", - "je" = "Jersey", - "jm" = "Jamaica", - "jo" = "Jordan", - "jp" = "Japan", - "ke" = "Kenya", - "kg" = "Kyrgyzstan", - "kh" = "Cambodia", - "ki" = "Kiribati", - "km" = "Comoros", - "kn" = "Saint Kitts and Nevis", - "kp" = "North Korea", - "kr" = "the Republic of Korea", - "kw" = "Kuwait", - "ky" = "the Cayman Islands", - "kz" = "Kazakhstan", - "la" = "Laos", - "lb" = "Lebanon", - "lc" = "Saint Lucia", - "li" = "Liechtenstein", - "lk" = "Sri Lanka", - "lr" = "Liberia", - "ls" = "Lesotho", - "lt" = "Lithuania", - "lu" = "Luxembourg", - "lv" = "Latvia", - "ly" = "Libya", - "ma" = "Morocco", - "mc" = "Monaco", - "md" = "the Republic of Moldova", - "me" = "Montenegro", - "mf" = "Saint Martin", - "mg" = "Madagascar", - "mh" = "the Marshall Islands", - "mk" = "Macedonia", - "ml" = "Mali", - "mm" = "Burma", - "mn" = "Mongolia", - "mo" = "Macau", - "mp" = "the Northern Mariana Islands", - "mq" = "Martinique", - "mr" = "Mauritania", - "ms" = "Montserrat", - "mt" = "Malta", - "mu" = "Mauritius", - "mv" = "the Maldives", - "mw" = "Malawi", - "mx" = "Mexico", - "my" = "Malaysia", - "mz" = "Mozambique", - "na" = "Namibia", - "nc" = "New Caledonia", - "ne" = "Niger", - "nf" = "Norfolk Island", - "ng" = "Nigeria", - "ni" = "Nicaragua", - "nl" = "the Netherlands", - "no" = "Norway", - "np" = "Nepal", - "nr" = "Nauru", - "nu" = "Niue", - "nz" = "New Zealand", - "om" = "Oman", - "pa" = "Panama", - "pe" = "Peru", - "pf" = "French Polynesia", - "pg" = "Papua New Guinea", - "ph" = "the Philippines", - "pk" = "Pakistan", - "pl" = "Poland", - "pm" = "Saint Pierre and Miquelon", - "pn" = "the Pitcairn Islands", - "pr" = "Puerto Rico", - "ps" = "the Palestinian Territory", - "pt" = "Portugal", - "pw" = "Palau", - "py" = "Paraguay", - "qa" = "Qatar", - "re" = "Reunion", - "ro" = "Romania", - "rs" = "Serbia", - "ru" = "Russia", - "rw" = "Rwanda", - "sa" = "Saudi Arabia", - "sb" = "the Solomon Islands", - "sc" = "the Seychelles", - "sd" = "Sudan", - "se" = "Sweden", - "sg" = "Singapore", - "sh" = "Saint Helena", - "si" = "Slovenia", - "sj" = "Svalbard and Jan Mayen", - "sk" = "Slovakia", - "sl" = "Sierra Leone", - "sm" = "San Marino", - "sn" = "Senegal", - "so" = "Somalia", - "sr" = "Suriname", - "ss" = "South Sudan", - "st" = "São Tomé and PrÃncipe", - "sv" = "El Salvador", - "sy" = "the Syrian Arab Republic", - "sz" = "Swaziland", - "tc" = "Turks and Caicos Islands", - "td" = "Chad", - "tf" = "the French Southern Territories", - "tg" = "Togo", - "th" = "Thailand", - "tj" = "Tajikistan", - "tk" = "Tokelau", - "tl" = "East Timor", - "tm" = "Turkmenistan", - "tn" = "Tunisia", - "to" = "Tonga", - "tr" = "Turkey", - "tt" = "Trinidad and Tobago", - "tv" = "Tuvalu", - "tw" = "Taiwan", - "tz" = "the United Republic of Tanzania", - "ua" = "Ukraine", - "ug" = "Uganda", - "um" = "the United States Minor Outlying Islands", - "us" = "the United States", - "uy" = "Uruguay", - "uz" = "Uzbekistan", - "va" = "Vatican City", - "vc" = "Saint Vincent and the Grenadines", - "ve" = "Venezuela", - "vg" = "the British Virgin Islands", - "vi" = "the United States Virgin Islands", - "vn" = "Vietnam", - "vu" = "Vanuatu", - "wf" = "Wallis and Futuna", - "ws" = "Samoa", - "ye" = "Yemen", - "yt" = "Mayotte", - "za" = "South Africa", - "zm" = "Zambia", - "zw" = "Zimbabwe") - -countryname <- function(country) { - res <- countrylist[[country]] - if (is.null(res)) - res <- "no-man's-land" - res -} - -date_breaks <- function(days) { - length <- cut(days, c(0, 7, 12, 56, 180, 600, 5000, Inf), labels=FALSE) - major <- c("days", "2 days", "weeks", "months", "3 months", "years", - "5 years")[length] - minor <- c("10 years", "days", "days", "weeks", "months", "months", - "years")[length] - format <- c("%d-%b", "%d-%b", "%d-%b", "%b-%Y", "%b-%Y", "%Y", - "%Y")[length] - list(major = major, minor = minor, format = format) -} - -plot_networksize <- function(start, end, path) { - end <- min(end, as.character(Sys.Date() - 2)) - s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv", - stringsAsFactors = FALSE) - s <- s[s$date >= start & s$date <= end & s$flag == '' & - s$country == '' & s$version == '' & s$platform == '' & - s$ec2bridge == '', ] - s <- data.frame(date = as.Date(s$date, "%Y-%m-%d"), relays = s$relays, - bridges = s$bridges) - dates <- seq(from = as.Date(start, "%Y-%m-%d"), - to = as.Date(end, "%Y-%m-%d"), by="1 day") - missing <- setdiff(dates, as.Date(s$date, origin = "1970-01-01")) - if (length(missing) > 0) - s <- rbind(s, - data.frame(date = as.Date(missing, origin = "1970-01-01"), - relays = NA, bridges = NA)) - networksize <- melt(s, id = "date") - date_breaks <- date_breaks( - as.numeric(max(as.Date(networksize$date, "%Y-%m-%d")) - - min(as.Date(networksize$date, "%Y-%m-%d")))) - ggplot(networksize, aes(x = as.Date(date, "%Y-%m-%d"), y = value, - colour = variable)) + geom_line(size = 1) + - scale_x_date(name = paste("\nThe Tor Project - ", - "https://metrics.torproject.org/", sep = ""), - format = date_breaks$format, major = date_breaks$major, - minor = date_breaks$minor) + - scale_y_continuous(name = "", limits = c(0, max(networksize$value, - na.rm = TRUE))) + - scale_colour_hue("", breaks = c("relays", "bridges"), - labels = c("Relays", "Bridges")) + - opts(title = "Number of relays\n") - ggsave(filename = path, width = 8, height = 5, dpi = 72) -} - -plot_cloudbridges <- function(start, end, path) { - end <- min(end, as.character(Sys.Date() - 2)) - s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv", - stringsAsFactors = FALSE) - s <- s[s$date >= start & s$date <= end & s$flag == '' & - s$country == '' & s$version == '' & s$platform == '' & - s$ec2bridge == 't', ] - s <- data.frame(date = as.Date(s$date, "%Y-%m-%d"), bridges = s$bridges) - dates <- seq(from = as.Date(start, "%Y-%m-%d"), - to = as.Date(end, "%Y-%m-%d"), by="1 day") - missing <- setdiff(dates, s$date) - if (length(missing) > 0) - s <- rbind(s, - data.frame(date = as.Date(missing, origin = "1970-01-01"), - bridges = NA)) - date_breaks <- date_breaks( - as.numeric(max(as.Date(s$date, "%Y-%m-%d")) - - min(as.Date(s$date, "%Y-%m-%d")))) - ggplot(s, aes(x = as.Date(date, "%Y-%m-%d"), y = bridges)) + - geom_line(size = 1, colour = "green3") + - scale_x_date(name = paste("\nThe Tor Project - ", - "https://metrics.torproject.org/", sep = ""), - format = date_breaks$format, major = date_breaks$major, - minor = date_breaks$minor) + - scale_y_continuous(name = "", limits = c(0, - max(s$bridges, na.rm = TRUE))) + - opts(title = "Number of Tor Cloud bridges\n") - ggsave(filename = path, width = 8, height = 5, dpi = 72) -} - -plot_relaycountries <- function(start, end, country, path) { - end <- min(end, as.character(Sys.Date() - 2)) - s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv", - stringsAsFactors = FALSE) - s <- s[s$date >= start & s$date <= end & s$flag == '' & - s$country == ifelse(country == "all", '', country) & - s$version == '' & s$platform == '' & s$ec2bridge == '', ] - s <- data.frame(date = as.Date(s$date, "%Y-%m-%d"), relays = s$relays) - dates <- seq(from = as.Date(start, "%Y-%m-%d"), - to = as.Date(end, "%Y-%m-%d"), by="1 day") - missing <- setdiff(dates, s$date) - if (length(missing) > 0) - s <- rbind(s, - data.frame(date = as.Date(missing, origin = "1970-01-01"), - relays = NA)) - title <- ifelse(country == "all", - "Number of relays in all countries\n", - paste("Number of relays in ", countryname(country), "\n", sep = "")) - formatter <- function(x, ...) { format(x, scientific = FALSE, ...) } - date_breaks <- date_breaks( - as.numeric(max(as.Date(s$date, "%Y-%m-%d")) - - min(as.Date(s$date, "%Y-%m-%d")))) - ggplot(s, aes(x = as.Date(date, "%Y-%m-%d"), y = relays)) + - geom_line(size = 1) + - scale_x_date(name = paste("\nThe Tor Project - ", - "https://metrics.torproject.org/", sep = ""), - format = date_breaks$format, major = date_breaks$major, - minor = date_breaks$minor) + - scale_y_continuous(name = "", limits = c(0, max(s$relays, - na.rm = TRUE)), formatter = formatter) + - opts(title = title) - ggsave(filename = path, width = 8, height = 5, dpi = 72) -} - -plot_versions <- function(start, end, path) { - end <- min(end, as.character(Sys.Date() - 2)) - s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv", - stringsAsFactors = FALSE) - s <- s[s$date >= start & s$date <= end & s$flag == '' & - s$country == '' & s$version != '' & s$platform == '' & - s$ec2bridge == '', ] - s <- data.frame(date = as.Date(s$date, "%Y-%m-%d"), version = s$version, - relays = s$relays) - known_versions <- c("0.1.0", "0.1.1", "0.1.2", "0.2.0", "0.2.1", - "0.2.2", "0.2.3", "0.2.4", "0.2.5") - colours <- data.frame(breaks = known_versions, - values = brewer.pal(length(known_versions), "Paired"), - stringsAsFactors = FALSE) - versions <- s[s$version %in% known_versions, ] - visible_versions <- sort(unique(versions$version)) - date_breaks <- date_breaks( - as.numeric(max(as.Date(versions$date, "%Y-%m-%d")) - - min(as.Date(versions$date, "%Y-%m-%d")))) - ggplot(versions, aes(x = as.Date(date, "%Y-%m-%d"), y = relays, - colour = version)) + - geom_line(size = 1) + - scale_x_date(name = paste("\nThe Tor Project - ", - "https://metrics.torproject.org/", sep = ""), - format = date_breaks$format, major = date_breaks$major, - minor = date_breaks$minor) + - scale_y_continuous(name = "", - limits = c(0, max(versions$relays, na.rm = TRUE))) + - scale_colour_manual(name = "Tor version", - values = colours[colours$breaks %in% visible_versions, 2], - breaks = visible_versions) + - opts(title = "Relay versions\n") - ggsave(filename = path, width = 8, height = 5, dpi = 72) -} - -plot_platforms <- function(start, end, path) { - end <- min(end, as.character(Sys.Date() - 2)) - s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv", - stringsAsFactors = FALSE) - s <- s[s$date >= start & s$date <= end & s$flag == '' & - s$country == '' & s$version == '' & s$platform != '' & - s$ec2bridge == '', ] - platforms <- data.frame(date = as.Date(s$date, "%Y-%m-%d"), - variable = s$platform, value = s$relays) - date_breaks <- date_breaks( - as.numeric(max(as.Date(platforms$date, "%Y-%m-%d")) - - min(as.Date(platforms$date, "%Y-%m-%d")))) - ggplot(platforms, aes(x = as.Date(date, "%Y-%m-%d"), y = value, - colour = variable)) + - geom_line(size = 1) + - scale_x_date(name = paste("\nThe Tor Project - ", - "https://metrics.torproject.org/", sep = ""), - format = date_breaks$format, major = date_breaks$major, - minor = date_breaks$minor) + - scale_y_continuous(name = "", - limits = c(0, max(platforms$value, na.rm = TRUE))) + - scale_colour_manual(name = "Platform", - breaks = c("Linux", "Darwin", "FreeBSD", "Windows", "Other"), - values = c("#E69F00", "#56B4E9", "#009E73", "#0072B2", "#333333")) + - opts(title = "Relay platforms\n") - ggsave(filename = path, width = 8, height = 5, dpi = 72) -} - -plot_bandwidth <- function(start, end, path) { - end <- min(end, as.character(Sys.Date() - 4)) - b <- read.csv("/srv/metrics.torproject.org/web/stats/bandwidth.csv", - stringsAsFactors = FALSE) - b <- b[b$date >= start & b$date <= end & b$isexit == '' & - b$isguard == '', ] - b <- data.frame(date = as.Date(b$date, "%Y-%m-%d"), - bwadv = b$advbw, - bwhist = (b$bwread + b$bwwrite) / 2) - bandwidth <- melt(b, id = "date") - date_breaks <- date_breaks( - as.numeric(max(as.Date(bandwidth$date, "%Y-%m-%d")) - - min(as.Date(bandwidth$date, "%Y-%m-%d")))) - ggplot(bandwidth, aes(x = as.Date(date, "%Y-%m-%d"), y = value / 2^20, - colour = variable)) + - geom_line(size = 1) + - scale_x_date(name = paste("\nThe Tor Project - ", - "https://metrics.torproject.org/", sep = ""), - format = date_breaks$format, major = date_breaks$major, - minor = date_breaks$minor) + - scale_y_continuous(name="Bandwidth (MiB/s)", - limits = c(0, max(bandwidth$value, na.rm = TRUE) / 2^20)) + - scale_colour_hue(name = "", h.start = 90, - breaks = c("bwadv", "bwhist"), - labels = c("Advertised bandwidth", "Bandwidth history")) + - opts(title = "Total relay bandwidth", legend.position = "top") - ggsave(filename = path, width = 8, height = 5, dpi = 72) -} - -plot_bwhist_flags <- function(start, end, path) { - end <- min(end, as.character(Sys.Date() - 4)) - b <- read.csv("/srv/metrics.torproject.org/web/stats/bandwidth.csv", - stringsAsFactors = FALSE) - b <- b[b$date >= start & b$date <= end & b$isexit != '' & - b$isguard != '', ] - bw <- data.frame(date = as.Date(b$date, "%Y-%m-%d"), - isexit = b$isexit == 't', isguard = b$isguard == 't', - read = b$bwread, written = b$bwwrite) - dates <- seq(from = as.Date(start, "%Y-%m-%d"), - to = as.Date(end, "%Y-%m-%d"), by = "1 day") - missing <- setdiff(dates, as.Date(bw$date, origin = "1970-01-01")) - if (length(missing) > 0) - bw <- rbind(bw, - data.frame(date = as.Date(missing, origin = "1970-01-01"), - isexit = FALSE, isguard = FALSE, read = NA, written = NA), - data.frame(date = as.Date(missing, origin = "1970-01-01"), - isexit = FALSE, isguard = TRUE, read = NA, written = NA), - data.frame(date = as.Date(missing, origin = "1970-01-01"), - isexit = TRUE, isguard = FALSE, read = NA, written = NA), - data.frame(date = as.Date(missing, origin = "1970-01-01"), - isexit = TRUE, isguard = TRUE, read = NA, written = NA)) - bw <- data.frame(date = bw$date, variable = ifelse(bw$isexit, - ifelse(bw$isguard, "Guard & Exit", "Exit only"), - ifelse(bw$isguard, "Guard only", "Middle only")), - value = (bw$read + bw$written) / 2) - date_breaks <- date_breaks( - as.numeric(max(as.Date(bw$date, "%Y-%m-%d")) - - min(as.Date(bw$date, "%Y-%m-%d")))) - ggplot(bw, aes(x = as.Date(date, "%Y-%m-%d"), y = value / 2^20, - colour = variable)) + - geom_line(size = 1) + - scale_x_date(name = paste("\nThe Tor Project - ", - "https://metrics.torproject.org/", sep = ""), - format = date_breaks$format, major = date_breaks$major, - minor = date_breaks$minor) + - scale_y_continuous(name="Bandwidth (MiB/s)", - limits = c(0, max(bw$value, na.rm = TRUE) / 2^20)) + - scale_colour_manual(name = "", - values = c("#E69F00", "#56B4E9", "#009E73", "#0072B2")) + - opts(title = "Bandwidth history by relay flags", - legend.position = "top") - ggsave(filename = path, width = 8, height = 5, dpi = 72) -} - -plot_dirbytes <- function(start, end, path) { - end <- min(end, as.character(Sys.Date() - 4)) - b <- read.csv("/srv/metrics.torproject.org/web/stats/bandwidth.csv", - stringsAsFactors = FALSE) - b <- b[b$date >= start & b$date <= end & b$isexit == '' & - b$isguard == '', ] - b <- data.frame(date = as.Date(b$date, "%Y-%m-%d"), - dirread = b$dirread, dirwrite = b$dirwrite) - dir <- melt(b, id = "date") - date_breaks <- date_breaks( - as.numeric(max(as.Date(dir$date, "%Y-%m-%d")) - - min(as.Date(dir$date, "%Y-%m-%d")))) - ggplot(dir, aes(x = as.Date(date, "%Y-%m-%d"), y = value / 2^20, - colour = variable)) + - geom_line(size = 1) + - scale_x_date(name = paste("\nThe Tor Project - ", - "https://metrics.torproject.org/", sep = ""), - format = date_breaks$format, major = date_breaks$major, - minor = date_breaks$minor) + - scale_y_continuous(name="Bandwidth (MiB/s)", - limits = c(0, max(dir$value, na.rm = TRUE) / 2^20)) + - scale_colour_hue(name = "", - breaks = c("dirwrite", "dirread"), - labels = c("Written dir bytes", "Read dir bytes")) + - opts(title = "Number of bytes spent on answering directory requests", - legend.position = "top") - ggsave(filename = path, width = 8, height = 5, dpi = 72) -} - -plot_relayflags <- function(start, end, flags, path) { - end <- min(end, as.character(Sys.Date() - 2)) - s <- read.csv("/srv/metrics.torproject.org/web/stats/servers.csv", - stringsAsFactors = FALSE) - s <- s[s$date >= start & s$date <= end & s$country == '' & - s$version == '' & s$platform == '' & s$ec2bridge == '', ] - s <- data.frame(date = as.Date(s$date, "%Y-%m-%d"), - variable = ifelse(s$flag == '', 'Running', s$flag), - value = s$relays) - networksize <- s[s$variable %in% flags, ] - networksize <- rbind(data.frame( - date = as.Date(end) + 1, - variable = c("Running", "Exit", "Guard", "Fast", "Stable", "HSDir"), - value = NA), networksize) - dates <- seq(from = as.Date(start, "%Y-%m-%d"), - to = as.Date(end, "%Y-%m-%d"), by="1 day") - missing <- setdiff(dates, networksize$date) - if (length(missing) > 0) - networksize <- rbind(data.frame( - date = as.Date(rep(missing, 6), origin = "1970-01-01"), - variable = c("Running", "Exit", "Guard", "Fast", "Stable", "HSDir"), - value = rep(NA, length(missing) * 6)), networksize) - date_breaks <- date_breaks( - as.numeric(max(as.Date(end, "%Y-%m-%d")) - - min(as.Date(networksize$date, "%Y-%m-%d")))) - ggplot(networksize, aes(x = as.Date(date, "%Y-%m-%d"), y = value, - colour = as.factor(variable))) + geom_line(size = 1) + - scale_x_date(name = paste("\nThe Tor Project - ", - "https://metrics.torproject.org/", sep = ""), - format = date_breaks$format, major = date_breaks$major, - minor = date_breaks$minor, limits = as.Date(c(start, end))) + - scale_y_continuous(name = "", limits = c(0, max(networksize$value, - na.rm = TRUE))) + - scale_colour_manual(name = "Relay flags", values = c("#E69F00", - "#56B4E9", "#009E73", "#EE6A50", "#000000", "#0072B2"), - breaks = flags, labels = flags) + - opts(title = "Number of relays with relay flags assigned\n") - ggsave(filename = path, width = 8, height = 5, dpi = 72) -} - -plot_torperf <- function(start, end, source, filesize, path) { - end <- min(end, as.character(Sys.Date() - 2)) - size <- ifelse(filesize == '50kb', 50 * 1024, - ifelse(filesize == '1mb', 1024 * 1024, 5 * 1024 * 1024)) - t <- read.csv("/srv/metrics.torproject.org/web/stats/torperf.csv", - stringsAsFactors = FALSE) - t <- t[t$date >= start & t$date <= end & t$size == size & - t$source == ifelse(source == 'all', '', source), ] - torperf <- data.frame(date = as.Date(t$date, "%Y-%m-%d"), - q1 = t$q1, md = t$md, q3 = t$q3) - dates <- seq(from = as.Date(start, "%Y-%m-%d"), - to = as.Date(end, "%Y-%m-%d"), by="1 day") - missing <- setdiff(dates, torperf$date) - if (length(missing) > 0) - torperf <- rbind(torperf, - data.frame(date = as.Date(missing, origin = "1970-01-01"), - q1 = NA, md = NA, q3 = NA)) - colours <- data.frame(source = c("all", "siv", "moria", "torperf"), - colour = c("#FF8C00", "#0000EE", "#EE0000", "#00CD00"), - stringsAsFactors = FALSE) - colour <- colours[colours$source == source, "colour"] - filesizes <- data.frame(filesizes = c("5mb", "1mb", "50kb"), - label = c("5 MiB", "1 MiB", "50 KiB"), stringsAsFactors = FALSE) - filesizeStr <- filesizes[filesizes$filesize == filesize, "label"] - maxY <- max(torperf$q3, na.rm = TRUE) - date_breaks <- date_breaks( - as.numeric(max(as.Date(torperf$date, "%Y-%m-%d")) - - min(as.Date(torperf$date, "%Y-%m-%d")))) - ggplot(torperf, aes(x = as.Date(date, "%Y-%m-%d"), y = md/1e3, - fill = "line")) + - geom_line(colour = colour, size = 0.75) + - geom_ribbon(data = torperf, aes(x = date, ymin = q1/1e3, - ymax = q3/1e3, fill = "ribbon")) + - scale_x_date(name = paste("\nThe Tor Project - ", - "https://metrics.torproject.org/", sep = ""), - format = date_breaks$format, major = date_breaks$major, - minor = date_breaks$minor) + - scale_y_continuous(name = "", limits = c(0, maxY) / 1e3) + - scale_fill_manual(name = paste("Measured times on", - ifelse(source == "all", "all sources", source), "per day"), - breaks = c("line", "ribbon"), - labels = c("Median", "1st to 3rd quartile"), - values = paste(colour, c("", "66"), sep = "")) + - opts(title = paste("Time in seconds to complete", filesizeStr, - "request"), legend.position = "top") - ggsave(filename = path, width = 8, height = 5, dpi = 72) -} - -plot_torperf_failures <- function(start, end, source, filesize, path) { - end <- min(end, as.character(Sys.Date() - 2)) - size <- ifelse(filesize == '50kb', 50 * 1024, - ifelse(filesize == '1mb', 1024 * 1024, 5 * 1024 * 1024)) - t <- read.csv("/srv/metrics.torproject.org/web/stats/torperf.csv", - stringsAsFactors = FALSE) - t <- t[t$date >= start & t$date <= end & t$size == size & - t$source == ifelse(source == 'all', '', source), ] - torperf <- data.frame(date = as.Date(t$date, "%Y-%m-%d"), - timeouts = t$timeouts, failures = t$failures, - requests = t$requests) - dates <- seq(from = as.Date(start, "%Y-%m-%d"), - to = as.Date(end, "%Y-%m-%d"), by="1 day") - missing <- setdiff(dates, torperf$date) - if (length(missing) > 0) - torperf <- rbind(torperf, - data.frame(date = as.Date(missing, origin = "1970-01-01"), - timeouts = NA, failures = NA, requests = NA)) - colours <- data.frame(source = c("all", "siv", "moria", "torperf"), - colour = c("#FF8C00", "#0000EE", "#EE0000", "#00CD00"), - stringsAsFactors = FALSE) - colour <- colours[colours$source == source, "colour"] - filesizes <- data.frame(filesizes = c("5mb", "1mb", "50kb"), - label = c("5 MiB", "1 MiB", "50 KiB"), stringsAsFactors = FALSE) - filesizeStr <- filesizes[filesizes$filesize == filesize, "label"] - torperf <- rbind(data.frame(date = torperf$date, - value = ifelse(torperf$requests > 0, - torperf$timeouts / torperf$requests, 0), - variable = "timeouts"), - data.frame(date = torperf$date, - value = ifelse(torperf$requests > 0, - torperf$failures / torperf$requests, 0), - variable = "failures")) - date_breaks <- date_breaks( - as.numeric(max(as.Date(torperf$date, "%Y-%m-%d")) - - min(as.Date(torperf$date, "%Y-%m-%d")))) - ggplot(torperf, aes(x = as.Date(date, "%Y-%m-%d"), y = value, - colour = variable)) + - geom_point(size = 2) + - scale_x_date(name = paste("\nThe Tor Project - ", - "https://metrics.torproject.org/", sep = ""), - format = date_breaks$format, major = date_breaks$major, - minor = date_breaks$minor) + - scale_y_continuous(name = "", formatter = "percent") + - scale_colour_hue(name = paste("Problems encountered on", - ifelse(source == "all", "all sources", source)), - h.start = 45, breaks = c("timeouts", "failures"), - labels = c("Timeouts", "Failures")) + - opts(title = paste("Timeouts and failures of", filesizeStr, - "requests"), legend.position = "top") - ggsave(filename = path, width = 8, height = 5, dpi = 72) -} - -plot_connbidirect <- function(start, end, path) { - end <- min(end, as.character(Sys.Date() - 2)) - c <- read.csv("/srv/metrics.torproject.org/web/stats/connbidirect.csv", - stringsAsFactors = FALSE) - c <- c[c$date >= start & c$date <= end & - c$read + c$write + c$both > 0, ] - c <- data.frame(date = as.Date(c$date, "%Y-%m-%d"), - both = c$both / (c$read + c$write + c$both), - read = c$read / (c$read + c$write + c$both), - write = c$write / (c$read + c$write + c$both)) - c <- aggregate(list(both = c$both, read = c$read, write = c$write), - by = list(date = c$date), quantile, - probs = c(0.25, 0.5, 0.75)) - c <- rbind( - data.frame(date = as.Date(c$date), data.frame(c$both), - variable = "both"), - data.frame(date = as.Date(c$date), data.frame(c$write), - variable = "write"), - data.frame(date = as.Date(c$date), data.frame(c$read), - variable = "read")) - date_breaks <- date_breaks( - as.numeric(max(as.Date(c$date, "%Y-%m-%d")) - - min(as.Date(c$date, "%Y-%m-%d")))) - ggplot(c, aes(x = date, y = X50., colour = variable)) + - geom_line(size = 0.75) + - geom_ribbon(aes(x = date, ymin = X25., ymax = X75., fill = variable), - alpha = 0.5, legend = FALSE) + - scale_x_date(name = paste("\nThe Tor Project - ", - "https://metrics.torproject.org/", sep = ""), - format = date_breaks$format, major = date_breaks$major, - minor = date_breaks$minor) + - scale_y_continuous(name = "", formatter = "percent") + - scale_colour_hue(name = "Medians and interquartile ranges", - breaks = c("both", "write", "read"), - labels = c("Both reading and writing", "Mostly writing", - "Mostly reading")) + - scale_fill_hue(name = "Medians and interquartile ranges", - breaks = c("both", "write", "read"), - labels = c("Both reading and writing", "Mostly writing", - "Mostly reading")) + - opts(title = "Fraction of connections used uni-/bidirectionally\n", - legend.position = "top") - ggsave(filename = path, width = 8, height = 5, dpi = 72) -} - -plot_fast_exits <- function(start, end, path) { - f <- read.csv("/srv/metrics.torproject.org/web/stats/fast-exits.csv", - stringsAsFactors = FALSE) - f <- f[f$date >= start & f$date <= end, ] - f <- data.frame(date = as.Date(f$date, "%Y-%m-%d"), - relays = f$fastnum, P_exit = f$fastprob) - r <- melt(f, id.vars = c("date")) - r <- data.frame(r, type = ifelse(r$variable == "P_exit", - "Total exit probability (in %)", "Number of relays")) - ggplot(r, aes(x = date, y = value)) + - geom_line(colour = "purple", size = 0.75) + - facet_grid(type ~ ., scales = "free_y") + - scale_x_date(name = "") + - scale_y_continuous(name = "") + - scale_colour_manual(values = c("purple", "orange")) + - opts(title = paste("Fast exits (95+ Mbit/s configured bandwidth ", - "rate,\n5000+ KB/s advertised bandwidth capacity,\n", - "exit to ports 80, 443, 554, and 1755,\n", - "at most 2 relays per /24 network)\n", sep = "")) - ggsave(filename = path, width = 8, height = 6, dpi = 72) -} - -plot_almost_fast_exits <- function(start, end, path) { - f <- read.csv("/srv/metrics.torproject.org/web/stats/fast-exits.csv", - stringsAsFactors = FALSE) - f <- f[f$date >= start & f$date <= end, ] - f <- melt(f, id.vars = c("date")) - t <- data.frame(date = as.Date(f$date, "%Y-%m-%d"), - var = ifelse(f$variable == 'fastnum' | f$variable == 'almostnum', - "Number of relays", "Total exit probability (in %)"), - variable = ifelse(f$variable == 'fastnum' | - f$variable == 'fastprob', "fast", "almost fast"), - value = floor(f$value)) - t <- data.frame(t, type = ifelse(t$variable == "fast", - "fast exits (95+ Mbit/s, 5000+ KB/s, 80/443/554/1755, 2- per /24", - paste("almost fast exits (80+ Mbit/s, 2000+ KB/s, 80/443,", - "not in set of fast exits)"))) - ggplot(t, aes(x = date, y = value, colour = type)) + - geom_line(size = 0.75) + - facet_grid(var ~ ., scales = "free_y") + - scale_x_date(name = "") + - scale_y_continuous(name = "") + - scale_colour_manual(name = "", values = c("orange", "purple")) + - opts(title = "Relays almost meeting the fast-exit requirements", - legend.position = "top") - ggsave(filename = path, width = 8, height = 6, dpi = 72) -} - -plot_bandwidth_flags <- function(start, end, path) { - end <- min(end, as.character(Sys.Date() - 4)) - b <- read.csv("/srv/metrics.torproject.org/web/stats/bandwidth.csv", - stringsAsFactors = FALSE) - b <- b[b$date >= start & b$date <= end & b$isexit != '' & - b$isguard != '', ] - b <- data.frame(date = as.Date(b$date, "%Y-%m-%d"), - isexit = b$isexit == 't', isguard = b$isguard == 't', - advbw = b$advbw, - bwhist = floor((b$bwread + b$bwwrite) / 2)) - b <- rbind( - data.frame(b[b$isguard == TRUE, ], flag = "Guard"), - data.frame(b[b$isexit == TRUE, ], flag = "Exit")) - b <- data.frame(date = b$date, advbw = b$advbw, bwhist = b$bwhist, - flag = b$flag) - b <- aggregate(list(advbw = b$advbw, bwhist = b$bwhist), - by = list(date = b$date, flag = b$flag), FUN = sum, - na.rm = TRUE, na.action = NULL) - b <- melt(b, id.vars = c("date", "flag")) - b <- data.frame(date = b$date, - type = ifelse(b$variable == 'advbw', 'advertised bandwidth', - 'bandwidth history'), - flag = b$flag, value = b$value) - bandwidth <- b[b$value > 0, ] - date_breaks <- date_breaks( - as.numeric(max(as.Date(bandwidth$date, "%Y-%m-%d")) - - min(as.Date(bandwidth$date, "%Y-%m-%d")))) - dates <- seq(from = as.Date(start, "%Y-%m-%d"), - to = as.Date(end, "%Y-%m-%d"), by = "1 day") - missing <- setdiff(dates, as.Date(bandwidth$date, - origin = "1970-01-01")) - if (length(missing) > 0) { - bandwidth <- rbind(bandwidth, - data.frame(date = as.Date(missing, origin = "1970-01-01"), - type = "advertised bandwidth", flag = "Exit", value = NA), - data.frame(date = as.Date(missing, origin = "1970-01-01"), - type = "bandwidth history", flag = "Exit", value = NA), - data.frame(date = as.Date(missing, origin = "1970-01-01"), - type = "advertised bandwidth", flag = "Guard", value = NA), - data.frame(date = as.Date(missing, origin = "1970-01-01"), - type = "bandwidth history", flag = "Guard", value = NA)) - } - bandwidth <- data.frame(date = bandwidth$date, - variable = as.factor(paste(bandwidth$flag, ", ", bandwidth$type, - sep = "")), value = bandwidth$value) - bandwidth$variable <- factor(bandwidth$variable, - levels = levels(bandwidth$variable)[c(3, 4, 1, 2)]) - ggplot(bandwidth, aes(x = as.Date(date, "%Y-%m-%d"), y = value / 2^20, - colour = variable)) + - geom_line(size = 1) + - scale_x_date(name = paste("\nThe Tor Project - ", - "https://metrics.torproject.org/", sep = ""), - format = date_breaks$format, major = date_breaks$major, - minor = date_breaks$minor) + - scale_y_continuous(name="Bandwidth (MiB/s)", - limits = c(0, max(bandwidth$value, na.rm = TRUE) / 2^20)) + - scale_colour_manual(name = "", - values = c("#E69F00", "#D6C827", "#009E73", "#00C34F")) + - opts(title = paste("Advertised bandwidth and bandwidth history by", - "relay flags"), legend.position = "top") - ggsave(filename = path, width = 8, height = 5, dpi = 72) -} - -plot_userstats <- function(start, end, node, variable, value, events, - path) { - end <- min(end, as.character(Sys.Date() - 2)) - c <- read.csv("/srv/metrics.torproject.org/web/stats/clients.csv", - stringsAsFactors = FALSE) - u <- c[c$date >= start & c$date <= end, ] - if (node == 'relay') { - if (value != 'all') { - u <- u[u$country == value & u$node == 'relay', ] - title <- paste("Directly connecting users from ", - countryname(value), "\n", sep = "") - } else { - u <- u[u$country == '' & u$transport == '' & u$version == '' & - u$node == 'relay', ] - title <- "Directly connecting users\n" - } - } else if (variable == 'transport') { - u <- u[u$transport == value & u$node == 'bridge', ] - title <- paste("Bridge users using transport ", value, "\n", - sep = "") - } else if (variable == 'version') { - u <- u[u$version== value & u$node == 'bridge', ] - title <- paste("Bridge users using IP", value, "\n", sep = "") - } else { - if (value != 'all') { - u <- u[u$country == value & u$node == 'bridge', ] - title <- paste("Bridge users from ", countryname(value), - "\n", sep = "") - } else { - u <- u[u$country == '' & u$transport == '' & u$version == '' & - u$node == 'bridge', ] - title <- "Bridge users\n" - } - } - u <- data.frame(date = as.Date(u$date, "%Y-%m-%d"), users = u$clients, - lower = u$lower, upper = u$upper) - dates <- seq(from = as.Date(start, "%Y-%m-%d"), - to = as.Date(end, "%Y-%m-%d"), by="1 day") - missing <- setdiff(dates, u$date) - if (length(missing) > 0) { - u <- rbind(u, - data.frame(date = as.Date(missing, origin = "1970-01-01"), - users = NA, lower = NA, upper = NA)) - } - formatter <- function(x, ...) { format(x, scientific = FALSE, ...) } - date_breaks <- date_breaks( - as.numeric(max(u$date) - min(u$date))) - max_y <- ifelse(length(na.omit(u$users)) == 0, 0, - max(u$users, na.rm = TRUE)) - plot <- ggplot(u, aes(x = date, y = users)) - if (length(na.omit(u$users)) > 0 & events != "off" & - variable == 'country' & value != "all") { - upturns <- u[u$users > u$upper, c("date", "users")] - downturns <- u[u$users <= u$lower, c("date", "users")] - if (events == "on") { - if (length(u$upper) > 0) - max_y <- max(max_y, max(u$upper, na.rm = TRUE)) - u[!is.na(u$lower) & u$lower < 0, "lower"] <- 0 - plot <- plot + - geom_ribbon(aes(ymin = lower, ymax = upper), fill = "gray") - } - if (length(upturns$date) > 0) - plot <- plot + - geom_point(data = upturns, aes(x = date, y = users), size = 5, - colour = "dodgerblue2") - if (length(downturns$date) > 0) - plot <- plot + - geom_point(data = downturns, aes(x = date, y = users), size = 5, - colour = "firebrick2") - } - plot <- plot + - geom_line(size = 1) + - scale_x_date(name = paste("\nThe Tor Project - ", - "https://metrics.torproject.org/", sep = ""), - format = date_breaks$format, major = date_breaks$major, - minor = date_breaks$minor) + - scale_y_continuous(name = "", limits = c(0, max_y), - formatter = formatter) + - opts(title = title) - ggsave(filename = path, width = 8, height = 5, dpi = 72) -} - -plot_userstats_relay_country <- function(start, end, country, events, - path) { - plot_userstats(start, end, 'relay', 'country', country, events, path) -} - -plot_userstats_bridge_country <- function(start, end, country, path) { - plot_userstats(start, end, 'bridge', 'country', country, 'off', path) -} - -plot_userstats_bridge_transport <- function(start, end, transport, path) { - plot_userstats(start, end, 'bridge', 'transport', transport, 'off', - path) -} - -plot_userstats_bridge_version <- function(start, end, version, path) { - plot_userstats(start, end, 'bridge', 'version', version, 'off', path) -} - diff --git a/rserve/rserve-init.R b/rserve/rserve-init.R deleted file mode 100644 index 7a87b16..0000000 --- a/rserve/rserve-init.R +++ /dev/null @@ -1,14 +0,0 @@ -##Pre-loaded libraries and graphing functions to speed things up - -library("ggplot2") -library("proto") -library("grid") -library("reshape") -library("plyr") -library("digest") -library("RColorBrewer") - -source('graphs.R') -source('csv.R') -source('tables.R') - diff --git a/rserve/shutdown.sh b/rserve/shutdown.sh deleted file mode 100755 index dd5ecd8..0000000 --- a/rserve/shutdown.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -echo "library(Rserve) -c <- RSconnect() -RSshutdown(c)" | R --slave diff --git a/rserve/start.sh b/rserve/start.sh deleted file mode 100755 index 0979817..0000000 --- a/rserve/start.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -dir=`pwd` -R CMD /home/metrics/R/x86_64-pc-linux-gnu-library/2.11/Rserve/libs/Rserve-bin.so --no-save --RS-conf $dir/Rserv.conf >> rserve.log 2>&1 diff --git a/rserve/tables.R b/rserve/tables.R deleted file mode 100644 index 091a4de..0000000 --- a/rserve/tables.R +++ /dev/null @@ -1,55 +0,0 @@ -countrynames <- function(countries) { - sapply(countries, countryname) -} - -write_userstats <- function(start, end, node, path) { - end <- min(end, as.character(Sys.Date())) - c <- read.csv("/srv/metrics.torproject.org/web/stats/clients.csv", - stringsAsFactors = FALSE) - c <- c[c$date >= start & c$date <= end & c$country != '' & - c$transport == '' & c$version == '' & c$node == node, ] - u <- data.frame(country = c$country, users = c$clients, - stringsAsFactors = FALSE) - u <- aggregate(list(users = u$users), by = list(country = u$country), - mean) - total <- sum(u$users) - u <- u[!(u$country %in% c("zy", "??", "a1", "a2", "o1", "ap", "eu")), ] - u <- u[order(u$users, decreasing = TRUE), ] - u <- u[1:10, ] - u <- data.frame( - cc = as.character(u$country), - country = sub('the ', '', countrynames(as.character(u$country))), - abs = round(u$users), - rel = round(100 * u$users / total, 2)) - write.csv(u, path, quote = FALSE, row.names = FALSE) -} - -write_userstats_relay <- function(start, end, path) { - write_userstats(start, end, 'relay', path) -} - -write_userstats_bridge <- function(start, end, path) { - write_userstats(start, end, 'bridge', path) -} - -write_userstats_censorship_events <- function(start, end, path) { - end <- min(end, as.character(Sys.Date())) - c <- read.csv("/srv/metrics.torproject.org/web/stats/clients.csv", - stringsAsFactors = FALSE) - c <- c[c$date >= start & c$date <= end & c$country != '' & - c$transport == '' & c$version == '' & c$node == 'relay', ] - r <- data.frame(date = c$date, country = c$country, - upturn = ifelse(c$clients > c$upper, 1, 0), - downturn = ifelse(c$clients <= c$lower, 1, 0)) - r <- aggregate(r[, c("upturn", "downturn")], - by = list(country = r$country), sum) - r <- r[!(r$country %in% c("zy", "??", "a1", "a2", "o1", "ap", "eu")), ] - r <- r[order(r$downturn, r$upturn, decreasing = TRUE), ] - r <- r[1:10, ] - r <- data.frame(cc = r$country, - country = sub('the ', '', countrynames(as.character(r$country))), - downturns = r$downturn, - upturns = r$upturn) - write.csv(r, path, quote = FALSE, row.names = FALSE) -} - diff --git a/src/org/torproject/ernie/status/doctor/ConsensusHealthServlet.java b/src/org/torproject/ernie/status/doctor/ConsensusHealthServlet.java deleted file mode 100644 index 330708f..0000000 --- a/src/org/torproject/ernie/status/doctor/ConsensusHealthServlet.java +++ /dev/null @@ -1,57 +0,0 @@ -/* Copyright 2011, 2012 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.ernie.status.doctor; - -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -public class ConsensusHealthServlet extends HttpServlet { - - private static final long serialVersionUID = -5230032733057814869L; - - public void doGet(HttpServletRequest request, - HttpServletResponse response) throws IOException, - ServletException { - - /* Read file from disk and write it to response. */ - BufferedInputStream input = null; - BufferedOutputStream output = null; - try { - File f = new File("/srv/metrics.torproject.org/ernie/website/" - + "consensus-health.html"); - if (!f.exists()) { - response.sendError(HttpServletResponse.SC_NOT_FOUND); - return; - } - response.setContentType(this.getServletContext().getMimeType(f.getName())); - response.setHeader("Content-Length", String.valueOf( - f.length())); - response.setHeader("Content-Disposition", - "inline; filename="" + f.getName() + """); - input = new BufferedInputStream(new FileInputStream(f), - 1024); - output = new BufferedOutputStream(response.getOutputStream(), 1024); - byte[] buffer = new byte[1024]; - int length; - while ((length = input.read(buffer)) > 0) { - output.write(buffer, 0, length); - } - } finally { - if (output != null) { - output.close(); - } - if (input != null) { - input.close(); - } - } - } -} - diff --git a/src/org/torproject/ernie/status/exonerator/ExoneraTorServlet.java b/src/org/torproject/ernie/status/exonerator/ExoneraTorServlet.java deleted file mode 100644 index d37b9a8..0000000 --- a/src/org/torproject/ernie/status/exonerator/ExoneraTorServlet.java +++ /dev/null @@ -1,24 +0,0 @@ -/* Copyright 2011, 2012 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.ernie.status.exonerator; - -import java.io.IOException; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -public class ExoneraTorServlet extends HttpServlet { - - private static final long serialVersionUID = -6227541092325776626L; - - public void doGet(HttpServletRequest request, - HttpServletResponse response) throws IOException, ServletException { - - /* Forward the request to the JSP that does all the hard work. */ - request.getRequestDispatcher("WEB-INF/exonerator.jsp").forward( - request, response); - } -} - diff --git a/src/org/torproject/ernie/status/relaysearch/RelaySearchServlet.java b/src/org/torproject/ernie/status/relaysearch/RelaySearchServlet.java deleted file mode 100644 index b7c8291..0000000 --- a/src/org/torproject/ernie/status/relaysearch/RelaySearchServlet.java +++ /dev/null @@ -1,535 +0,0 @@ -/* Copyright 2011, 2012 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.ernie.status.relaysearch; - -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.math.BigInteger; -import java.sql.Connection; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.SortedMap; -import java.util.SortedSet; -import java.util.TimeZone; -import java.util.TreeMap; -import java.util.TreeSet; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.regex.Pattern; - -import javax.naming.Context; -import javax.naming.InitialContext; -import javax.naming.NamingException; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.sql.DataSource; - -import org.apache.commons.codec.binary.Base64; - -/** - * Web page that allows users to search for relays in the descriptor - * archives. - * - * Possible search terms for testing: - * - gabelmoo - * - gabelmoo 2010-09 - * - gabelmoo 2010-09-18 - * - gabelmoo $F2044413DAC2E02E3D6BCF4735A19BCA1DE97281 - * - gabelmoo 80.190.246 - * - gabelmoo $F2044413DAC2E02E3D6BCF4735A19BCA1DE97281 80.190.246 - * - 5898549205 dc737cc9dca16af6 79.212.74.45 - * - 5898549205 dc737cc9dca16af6 - * - 80.190.246.100 - * - $F2044413DAC2E02E3D6BCF4735A19BCA1DE97281 - * - $F2044413DAC2E02E3D6BCF4735A19BCA1DE97281 80.190.246 - * - 58985492 - * - 58985492 79.212.74.45 - */ -public class RelaySearchServlet extends HttpServlet { - - private static final long serialVersionUID = -1772662230310611805L; - - private Pattern alphaNumDotDashDollarSpacePattern = - Pattern.compile("[A-Za-z0-9\.\-$ ]+"); - - private Pattern numPattern = Pattern.compile("[0-9]+"); - - private Pattern hexPattern = Pattern.compile("[A-Fa-f0-9]+"); - - private Pattern alphaNumPattern = Pattern.compile("[A-Za-z0-9]+"); - - private SimpleDateFormat dayFormat = new SimpleDateFormat("yyyy-MM-dd"); - - private SimpleDateFormat monthFormat = new SimpleDateFormat("yyyy-MM"); - - private SimpleDateFormat dateTimeFormat = - new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - - private long minValidAfterMillis; - - private DataSource ds; - - private Logger logger; - - public void init() { - - /* Initialize logger. */ - this.logger = Logger.getLogger(RelaySearchServlet.class.toString()); - - /* Initialize date format parsers. */ - dayFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - monthFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - dateTimeFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - - /* Look up data source. */ - try { - Context cxt = new InitialContext(); - this.ds = (DataSource) cxt.lookup("java:comp/env/jdbc/tordir"); - this.logger.info("Successfully looked up data source."); - } catch (NamingException e) { - this.logger.log(Level.WARNING, "Could not look up data source", e); - } - - /* Look up first consensus in the database. */ - try { - long requestedConnection = System.currentTimeMillis(); - Connection conn = this.ds.getConnection(); - String query = "SELECT MIN(validafter) AS first FROM consensus"; - Statement statement = conn.createStatement(); - ResultSet rs = statement.executeQuery(query); - if (rs.next()) { - this.minValidAfterMillis = rs.getTimestamp(1).getTime(); - } - rs.close(); - statement.close(); - conn.close(); - this.logger.info("Returned a database connection to the pool " - + "after " + (System.currentTimeMillis() - - requestedConnection) + " millis."); - } catch (SQLException e) { - this.logger.log(Level.WARNING, "Could not look up first consensus " - + "valid-after time in the database.", e); - } - } - - public void doGet(HttpServletRequest request, - HttpServletResponse response) throws IOException, - ServletException { - - /* Read search parameter. If we don't have a search parameter, we're - * done here. */ - String searchParameter = request.getParameter("search"); - if (searchParameter == null || searchParameter.length() == 0) { - request.getRequestDispatcher("WEB-INF/relay-search.jsp").forward( - request, response); - return; - } - - /* Parse search parameter to identify what nickname, fingerprint, - * and/or IP address to search for. A valid query contains no more - * than one identifier for each of the fields. As a special case, - * there are search terms consisting of 8 to 19 hex characters that - * can be either a nickname or a fingerprint. */ - String searchNickname = ""; - String searchFingerprint = ""; - String searchIPAddress = ""; - SortedSet<String> searchDays = new TreeSet<String>(); - SortedSet<String> searchMonths = new TreeSet<String>(); - SortedSet<Long> searchDayTimestamps = new TreeSet<Long>(); - SortedSet<Long> searchMonthTimestamps = new TreeSet<Long>(); - boolean validQuery = false; - - /* Only parse search parameter if it contains nothing else than - * alphanumeric characters, dots, and spaces. */ - if (alphaNumDotDashDollarSpacePattern.matcher(searchParameter). - matches()) { - SortedSet<String> searchTerms = new TreeSet<String>(); - if (searchParameter.trim().contains(" ")) { - String[] split = searchParameter.trim().split(" "); - for (int i = 0; i < split.length; i++) { - if (split[i].length() > 0) { - searchTerms.add(split[i]); - } - } - } else { - searchTerms.add(searchParameter.trim()); - } - - /* Parse each search term separately. */ - for (String searchTerm : searchTerms) { - - /* If the search term contains a dot, it can only be an IP - * address. */ - if (searchTerm.contains(".") && !searchTerm.startsWith(".")) { - String[] octets = searchTerm.split("\."); - if (searchIPAddress.length() > 0 || octets.length < 2 || - octets.length > 4) { - validQuery = false; - break; - } - boolean invalidOctet = false; - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < octets.length; i++) { - if (!numPattern.matcher(octets[i]).matches() || - octets[i].length() > 3 || - Integer.parseInt(octets[i]) > 255) { - invalidOctet = true; - break; - } else { - sb.append("." + Integer.parseInt(octets[i])); - } - } - if (invalidOctet) { - validQuery = false; - break; - } - if (octets.length < 4) { - sb.append("."); - } - searchIPAddress = sb.toString().substring(1); - validQuery = true; - } - - /* If the search term contains hyphens, it must be a month or a - * day. */ - else if (searchTerm.contains("-") && - searchTerm.startsWith("20")) { - try { - if (searchTerm.length() == 10) { - searchDayTimestamps.add(dayFormat.parse(searchTerm). - getTime()); - searchDays.add(searchTerm); - } else if (searchTerm.length() == 7) { - searchMonthTimestamps.add(monthFormat.parse(searchTerm). - getTime()); - searchMonths.add(searchTerm); - } else { - validQuery = false; - break; - } - } catch (ParseException e) { - validQuery = false; - break; - } - } - - /* If the search term starts with a $ followed by 8 to 40 hex - * characters, it must be a fingerprint. */ - else if ((searchTerm.length() >= 9 && searchTerm.length() <= 41 && - searchTerm.startsWith("$") && - hexPattern.matcher(searchTerm.substring(1)).matches()) || - (searchTerm.length() > 19 && searchTerm.length() <= 40 && - !searchTerm.startsWith("$") && - hexPattern.matcher(searchTerm).matches())) { - if (searchFingerprint.length() > 0) { - validQuery = false; - break; - } - searchFingerprint = searchTerm.substring( - (searchTerm.startsWith("$") ? 1 : 0)); - validQuery = true; - } - - /* If the search term contains up to 19 alphanumerical characters, - * it must be a nickname. */ - else if (searchTerm.length() <= 19 && - alphaNumPattern.matcher(searchTerm).matches()) { - if (searchNickname.length() > 0) { - validQuery = false; - break; - } - searchNickname = searchTerm; - validQuery = true; - } - - /* We didn't recognize this search term. */ - else { - validQuery = false; - break; - } - } - } - - /* We only accept at most one month or three days, but not both, or - * people could accidentally keep the database busy. */ - if (searchDays.size() > 3 || searchMonths.size() > 1 || - (searchMonths.size() == 1 && searchDays.size() > 0)) { - validQuery = false; - } - - /* If the query is invalid, stop here. */ - if (!validQuery) { - request.setAttribute("invalidQuery", "Query is invalid."); - request.getRequestDispatcher("WEB-INF/relay-search.jsp"). - forward(request, response); - return; - } - - /* Look up last consensus in the database. */ - long maxValidAfterMillis = -1L; - try { - long requestedConnection = System.currentTimeMillis(); - Connection conn = this.ds.getConnection(); - String query = "SELECT MAX(validafter) AS last FROM consensus"; - Statement statement = conn.createStatement(); - ResultSet rs = statement.executeQuery(query); - if (rs.next()) { - maxValidAfterMillis = rs.getTimestamp(1).getTime(); - } - rs.close(); - statement.close(); - conn.close(); - this.logger.info("Returned a database connection to the pool " - + "after " + (System.currentTimeMillis() - - requestedConnection) + " millis."); - } catch (SQLException e) { - this.logger.log(Level.WARNING, "Could not look up last consensus " - + "valid-after time in the database.", e); - } - - /* Prepare a string that says what we're searching for. */ - List<String> recognizedSearchTerms = new ArrayList<String>(); - if (searchNickname.length() > 0) { - recognizedSearchTerms.add("nickname <b>" + searchNickname + "</b>"); - } - if (searchFingerprint.length() > 0) { - recognizedSearchTerms.add("fingerprint <b>" + searchFingerprint - + "</b>"); - } - if (searchIPAddress.length() > 0) { - recognizedSearchTerms.add("IP address <b>" + searchIPAddress - + "</b>"); - } - List<String> recognizedIntervals = new ArrayList<String>(); - for (String searchTerm : searchMonths) { - recognizedIntervals.add("in <b>" + searchTerm + "</b>"); - } - for (String searchTerm : searchDays) { - recognizedIntervals.add("on <b>" + searchTerm + "</b>"); - } - StringBuilder searchNoticeBuilder = new StringBuilder(); - if (maxValidAfterMillis > 0L) { - searchNoticeBuilder.append("Most recent consensus in database is " - + "from " + dateTimeFormat.format(maxValidAfterMillis) - + ".</p><p>"); - } - searchNoticeBuilder.append("Searching for relays with "); - if (recognizedSearchTerms.size() == 1) { - searchNoticeBuilder.append(recognizedSearchTerms.get(0)); - } else if (recognizedSearchTerms.size() == 2) { - searchNoticeBuilder.append(recognizedSearchTerms.get(0) + " and " - + recognizedSearchTerms.get(1)); - } else { - for (int i = 0; i < recognizedSearchTerms.size() - 1; i++) { - searchNoticeBuilder.append(recognizedSearchTerms.get(i) + ", "); - } - searchNoticeBuilder.append("and " + recognizedSearchTerms.get( - recognizedSearchTerms.size() - 1)); - } - if (recognizedIntervals.size() == 1) { - searchNoticeBuilder.append(" running " - + recognizedIntervals.get(0)); - } else if (recognizedIntervals.size() == 2) { - searchNoticeBuilder.append(" running " + recognizedIntervals.get(0) - + " and/or " + recognizedIntervals.get(1)); - } else if (recognizedIntervals.size() > 2) { - searchNoticeBuilder.append(" running "); - for (int i = 0; i < recognizedIntervals.size() - 1; i++) { - searchNoticeBuilder.append(recognizedIntervals.get(i) + ", "); - } - searchNoticeBuilder.append("and/or " + recognizedIntervals.get( - recognizedIntervals.size() - 1)); - } - searchNoticeBuilder.append(" ..."); - String searchNotice = searchNoticeBuilder.toString(); - request.setAttribute("searchNotice", searchNotice); - - /* Prepare the query string. */ - StringBuilder conditionBuilder = new StringBuilder(); - boolean addAnd = false; - if (searchNickname.length() > 0) { - conditionBuilder.append((addAnd ? "AND " : "") - + "LOWER(nickname) LIKE '" + searchNickname.toLowerCase() - + "%' "); - addAnd = true; - } - if (searchFingerprint.length() > 0) { - conditionBuilder.append((addAnd ? "AND " : "") - + "fingerprint LIKE '" + searchFingerprint.toLowerCase() - + "%' "); - addAnd = true; - } - if (searchIPAddress.length() > 0) { - conditionBuilder.append((addAnd ? "AND " : "") - + "address LIKE '" + searchIPAddress + "%' "); - addAnd = true; - } - List<String> timeIntervals = new ArrayList<String>(); - if (searchDayTimestamps.size() > 0 || - searchMonthTimestamps.size() > 0) { - StringBuilder timeIntervalBuilder = new StringBuilder(); - boolean addOr = false; - timeIntervalBuilder.append("AND ("); - for (long searchTimestamp : searchDayTimestamps) { - if (searchTimestamp < this.minValidAfterMillis) { - request.setAttribute("outsideInterval", "Returned search " - + "results may be incomplete, as our data only dates back " - + "to " + dateTimeFormat.format(this.minValidAfterMillis) - + ". Older archives are not available."); - } - timeIntervalBuilder.append((addOr ? "OR " : "") - + "(validafter >= '" - + dateTimeFormat.format(searchTimestamp) + "' AND " - + "validafter < '" + dateTimeFormat.format(searchTimestamp - + 24L * 60L * 60L * 1000L) + "') "); - addOr = true; - } - for (long searchTimestamp : searchMonthTimestamps) { - if (searchTimestamp < this.minValidAfterMillis) { - request.setAttribute("outsideInterval", "Returned search " - + "results may be incomplete, as our data only dates back " - + "to " + dateTimeFormat.format(this.minValidAfterMillis) - + ". Older archives are not available."); - } - Calendar firstOfNextMonth = Calendar.getInstance( - TimeZone.getTimeZone("UTC")); - firstOfNextMonth.setTimeInMillis(searchTimestamp); - firstOfNextMonth.add(Calendar.MONTH, 1); - timeIntervalBuilder.append((addOr ? "OR " : "") - + "(validafter >= '" - + dateTimeFormat.format(searchTimestamp) + "' AND " - + "validafter < '" + dateTimeFormat.format( - firstOfNextMonth.getTimeInMillis()) + "') "); - addOr = true; - } - timeIntervalBuilder.append(") "); - timeIntervals.add(timeIntervalBuilder.toString()); - } else { - timeIntervals.add("AND validafter >= '" - + dateTimeFormat.format(System.currentTimeMillis() - - 4L * 24L * 60L * 60L * 1000L) + "' "); - timeIntervals.add("AND validafter >= '" - + dateTimeFormat.format(System.currentTimeMillis() - - 30L * 24L * 60L * 60L * 1000L) + "' "); - } - List<String> queries = new ArrayList<String>(); - for (String timeInterval : timeIntervals) { - StringBuilder queryBuilder = new StringBuilder(); - queryBuilder.append("SELECT validafter, fingerprint, descriptor, " - + "rawdesc FROM statusentry WHERE validafter IN (SELECT " - + "validafter FROM statusentry WHERE "); - queryBuilder.append(conditionBuilder.toString()); - queryBuilder.append(timeInterval); - queryBuilder.append("ORDER BY validafter DESC LIMIT 31) AND "); - queryBuilder.append(conditionBuilder.toString()); - queryBuilder.append(timeInterval); - queries.add(queryBuilder.toString()); - } - - /* Actually execute the query. */ - long startedQuery = System.currentTimeMillis(); - SortedMap<String, SortedSet<String>> foundDescriptors = - new TreeMap<String, SortedSet<String>>( - Collections.reverseOrder()); - Map<String, String> rawValidAfterLines = - new HashMap<String, String>(); - Map<String, String> rawStatusEntries = new HashMap<String, String>(); - String query = null; - int matches = 0; - try { - long requestedConnection = System.currentTimeMillis(); - Connection conn = this.ds.getConnection(); - while (!queries.isEmpty()) { - query = queries.remove(0); - this.logger.info("Running query '" + query + "'."); - Statement statement = conn.createStatement(); - ResultSet rs = statement.executeQuery(query); - while (rs.next()) { - matches++; - String validAfter = rs.getTimestamp(1).toString(). - substring(0, 19); - String fingerprint = rs.getString(2); - if (!foundDescriptors.containsKey(validAfter)) { - foundDescriptors.put(validAfter, new TreeSet<String>()); - } - foundDescriptors.get(validAfter).add(validAfter + " " - + fingerprint); - if (!rawValidAfterLines.containsKey(validAfter)) { - rawValidAfterLines.put(validAfter, "<tt>valid-after " - + "<a href="https://exonerator.torproject.org/" - + "consensus?valid-after=" - + validAfter.replaceAll(":", "-").replaceAll(" ", "-") - + "" target="_blank">" + validAfter + "</a></tt><br>"); - } - byte[] rawStatusEntry = rs.getBytes(4); - String statusEntryLines = null; - try { - statusEntryLines = new String(rawStatusEntry, "US-ASCII"); - } catch (UnsupportedEncodingException e) { - /* This shouldn't happen, because we know that ASCII is - * supported. */ - } - StringBuilder rawStatusEntryBuilder = new StringBuilder(); - String[] lines = statusEntryLines.split("\n"); - for (String line : lines) { - if (line.startsWith("r ")) { - String[] parts = line.split(" "); - String descriptorBase64 = String.format("%040x", - new BigInteger(1, Base64.decodeBase64(parts[3] - + "=="))); - rawStatusEntryBuilder.append("<tt>r " + parts[1] + " " - + parts[2] + " <a href="" - + "https://exonerator.torproject.org/" - + "serverdesc?desc-id=" - + descriptorBase64 + "" target="_blank">" + parts[3] - + "</a> " + parts[4] + " " + parts[5] + " " + parts[6] - + " " + parts[7] + " " + parts[8] + "</tt><br>"); - } else { - rawStatusEntryBuilder.append("<tt>" + line + "</tt><br>"); - } - } - rawStatusEntries.put(validAfter + " " + fingerprint, - rawStatusEntryBuilder.toString()); - } - rs.close(); - statement.close(); - if (matches >= 31) { - queries.clear(); - } - } - conn.close(); - this.logger.info("Returned a database connection to the pool " - + "after " + (System.currentTimeMillis() - - requestedConnection) + " millis."); - } catch (SQLException e) { - - /* Tell the user we have a database problem. */ - response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, - "Database problem"); - return; - } - request.setAttribute("query", query); - request.setAttribute("queryTime", System.currentTimeMillis() - - startedQuery); - request.setAttribute("foundDescriptors", foundDescriptors); - request.setAttribute("rawValidAfterLines", rawValidAfterLines); - request.setAttribute("rawStatusEntries", rawStatusEntries); - request.setAttribute("matches", matches); - - /* We're done. Let the JSP do the rest. */ - request.getRequestDispatcher("WEB-INF/relay-search.jsp").forward( - request, response); - } -} - diff --git a/src/org/torproject/ernie/web/GraphsServlet.java b/src/org/torproject/ernie/web/GraphsServlet.java deleted file mode 100644 index 111aa0f..0000000 --- a/src/org/torproject/ernie/web/GraphsServlet.java +++ /dev/null @@ -1,24 +0,0 @@ -/* Copyright 2011, 2012 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.ernie.web; - -import java.io.IOException; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -public class GraphsServlet extends HttpServlet { - - private static final long serialVersionUID = 7615715032362498151L; - - public void doGet(HttpServletRequest request, - HttpServletResponse response) throws IOException, ServletException { - - /* Forward the request to the JSP that does all the hard work. */ - request.getRequestDispatcher("WEB-INF/graphs.jsp").forward(request, - response); - } -} - diff --git a/src/org/torproject/ernie/web/IndexServlet.java b/src/org/torproject/ernie/web/IndexServlet.java deleted file mode 100644 index 11aff7c..0000000 --- a/src/org/torproject/ernie/web/IndexServlet.java +++ /dev/null @@ -1,24 +0,0 @@ -/* Copyright 2011, 2012 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.ernie.web; - -import java.io.IOException; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -public class IndexServlet extends HttpServlet { - - private static final long serialVersionUID = 7871368999788994664L; - - public void doGet(HttpServletRequest request, - HttpServletResponse response) throws IOException, ServletException { - - /* Forward the request to the JSP that does all the hard work. */ - request.getRequestDispatcher("WEB-INF/index.jsp").forward(request, - response); - } -} - diff --git a/src/org/torproject/ernie/web/ResearchServlet.java b/src/org/torproject/ernie/web/ResearchServlet.java deleted file mode 100644 index c78be69..0000000 --- a/src/org/torproject/ernie/web/ResearchServlet.java +++ /dev/null @@ -1,24 +0,0 @@ -/* Copyright 2011, 2012 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.ernie.web; - -import java.io.IOException; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -public class ResearchServlet extends HttpServlet { - - private static final long serialVersionUID = -9151727188925700665L; - - public void doGet(HttpServletRequest request, - HttpServletResponse response) throws IOException, ServletException { - - /* Forward the request to the JSP that does all the hard work. */ - request.getRequestDispatcher("WEB-INF/research.jsp").forward(request, - response); - } -} - diff --git a/src/org/torproject/ernie/web/StatusServlet.java b/src/org/torproject/ernie/web/StatusServlet.java deleted file mode 100644 index 07790ec..0000000 --- a/src/org/torproject/ernie/web/StatusServlet.java +++ /dev/null @@ -1,24 +0,0 @@ -/* Copyright 2011, 2012 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.ernie.web; - -import java.io.IOException; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -public class StatusServlet extends HttpServlet { - - private static final long serialVersionUID = -7249872082399236981L; - - public void doGet(HttpServletRequest request, - HttpServletResponse response) throws IOException, ServletException { - - /* Forward the request to the JSP that does all the hard work. */ - request.getRequestDispatcher("WEB-INF/status.jsp").forward(request, - response); - } -} - diff --git a/src/org/torproject/ernie/web/graphs/BubblesServlet.java b/src/org/torproject/ernie/web/graphs/BubblesServlet.java deleted file mode 100644 index 6f66413..0000000 --- a/src/org/torproject/ernie/web/graphs/BubblesServlet.java +++ /dev/null @@ -1,24 +0,0 @@ -/* Copyright 2013 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.ernie.web.graphs; - -import java.io.IOException; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -public class BubblesServlet extends HttpServlet { - - private static final long serialVersionUID = -6011833075497881033L; - - public void doGet(HttpServletRequest request, - HttpServletResponse response) throws IOException, ServletException { - - /* Forward the request to the JSP that does all the hard work. */ - request.getRequestDispatcher("WEB-INF/bubbles.jsp").forward(request, - response); - } -} - diff --git a/src/org/torproject/ernie/web/graphs/Countries.java b/src/org/torproject/ernie/web/graphs/Countries.java deleted file mode 100644 index 93dac4c..0000000 --- a/src/org/torproject/ernie/web/graphs/Countries.java +++ /dev/null @@ -1,285 +0,0 @@ -/* Copyright 2011, 2012 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.ernie.web.graphs; - -import java.util.ArrayList; -import java.util.List; - -public class Countries { - - private static Countries instance = new Countries(); - - public static Countries getInstance() { - return Countries.instance; - } - - /* List of arrays of length 2, containing country codes at [0] and - * country names at [1], alphabetically ordered by country names. */ - private List<String[]> knownCountries; - - private Countries() { - this.knownCountries = new ArrayList<String[]>(); - this.knownCountries.add("af;Afghanistan".split(";")); - this.knownCountries.add("ax;Aland Islands".split(";")); - this.knownCountries.add("al;Albania".split(";")); - this.knownCountries.add("dz;Algeria".split(";")); - this.knownCountries.add("as;American Samoa".split(";")); - this.knownCountries.add("ad;Andorra".split(";")); - this.knownCountries.add("ao;Angola".split(";")); - this.knownCountries.add("ai;Anguilla".split(";")); - this.knownCountries.add("aq;Antarctica".split(";")); - this.knownCountries.add("ag;Antigua and Barbuda".split(";")); - this.knownCountries.add("ar;Argentina".split(";")); - this.knownCountries.add("am;Armenia".split(";")); - this.knownCountries.add("aw;Aruba".split(";")); - this.knownCountries.add("au;Australia".split(";")); - this.knownCountries.add("at;Austria".split(";")); - this.knownCountries.add("az;Azerbaijan".split(";")); - this.knownCountries.add("bs;Bahamas".split(";")); - this.knownCountries.add("bh;Bahrain".split(";")); - this.knownCountries.add("bd;Bangladesh".split(";")); - this.knownCountries.add("bb;Barbados".split(";")); - this.knownCountries.add("by;Belarus".split(";")); - this.knownCountries.add("be;Belgium".split(";")); - this.knownCountries.add("bz;Belize".split(";")); - this.knownCountries.add("bj;Benin".split(";")); - this.knownCountries.add("bm;Bermuda".split(";")); - this.knownCountries.add("bt;Bhutan".split(";")); - this.knownCountries.add("bo;Bolivia".split(";")); - this.knownCountries.add("ba;Bosnia and Herzegovina".split(";")); - this.knownCountries.add("bw;Botswana".split(";")); - this.knownCountries.add("bv;Bouvet Island".split(";")); - this.knownCountries.add("br;Brazil".split(";")); - this.knownCountries.add("io;British Indian Ocean Territory". - split(";")); - this.knownCountries.add("bn;Brunei".split(";")); - this.knownCountries.add("bg;Bulgaria".split(";")); - this.knownCountries.add("bf;Burkina Faso".split(";")); - this.knownCountries.add("mm;Burma".split(";")); - this.knownCountries.add("bi;Burundi".split(";")); - this.knownCountries.add("kh;Cambodia".split(";")); - this.knownCountries.add("cm;Cameroon".split(";")); - this.knownCountries.add("ca;Canada".split(";")); - this.knownCountries.add("cv;Cape Verde".split(";")); - this.knownCountries.add("ky;Cayman Islands".split(";")); - this.knownCountries.add("cf;Central African Republic".split(";")); - this.knownCountries.add("td;Chad".split(";")); - this.knownCountries.add("cl;Chile".split(";")); - this.knownCountries.add("cn;China".split(";")); - this.knownCountries.add("cx;Christmas Island".split(";")); - this.knownCountries.add("cc;Cocos (Keeling) Islands".split(";")); - this.knownCountries.add("co;Colombia".split(";")); - this.knownCountries.add("km;Comoros".split(";")); - this.knownCountries.add("cd;Congo, The Democratic Republic of the". - split(";")); - this.knownCountries.add("cg;Congo".split(";")); - this.knownCountries.add("ck;Cook Islands".split(";")); - this.knownCountries.add("cr;Costa Rica".split(";")); - this.knownCountries.add("ci:Côte d'Ivoire".split(":")); - this.knownCountries.add("hr;Croatia".split(";")); - this.knownCountries.add("cu;Cuba".split(";")); - this.knownCountries.add("cy;Cyprus".split(";")); - this.knownCountries.add("cz;Czech Republic".split(";")); - this.knownCountries.add("dk;Denmark".split(";")); - this.knownCountries.add("dj;Djibouti".split(";")); - this.knownCountries.add("dm;Dominica".split(";")); - this.knownCountries.add("do;Dominican Republic".split(";")); - this.knownCountries.add("ec;Ecuador".split(";")); - this.knownCountries.add("eg;Egypt".split(";")); - this.knownCountries.add("sv;El Salvador".split(";")); - this.knownCountries.add("gq;Equatorial Guinea".split(";")); - this.knownCountries.add("er;Eritrea".split(";")); - this.knownCountries.add("ee;Estonia".split(";")); - this.knownCountries.add("et;Ethiopia".split(";")); - this.knownCountries.add("fk;Falkland Islands (Malvinas)".split(";")); - this.knownCountries.add("fo;Faroe Islands".split(";")); - this.knownCountries.add("fj;Fiji".split(";")); - this.knownCountries.add("fi;Finland".split(";")); - this.knownCountries.add("fx;France, Metropolitan".split(";")); - this.knownCountries.add("fr;France".split(";")); - this.knownCountries.add("gf;French Guiana".split(";")); - this.knownCountries.add("pf;French Polynesia".split(";")); - this.knownCountries.add("tf;French Southern Territories".split(";")); - this.knownCountries.add("ga;Gabon".split(";")); - this.knownCountries.add("gm;Gambia".split(";")); - this.knownCountries.add("ge;Georgia".split(";")); - this.knownCountries.add("de;Germany".split(";")); - this.knownCountries.add("gh;Ghana".split(";")); - this.knownCountries.add("gi;Gibraltar".split(";")); - this.knownCountries.add("gr;Greece".split(";")); - this.knownCountries.add("gl;Greenland".split(";")); - this.knownCountries.add("gd;Grenada".split(";")); - this.knownCountries.add("gp;Guadeloupe".split(";")); - this.knownCountries.add("gu;Guam".split(";")); - this.knownCountries.add("gt;Guatemala".split(";")); - this.knownCountries.add("gg;Guernsey".split(";")); - this.knownCountries.add("gn;Guinea".split(";")); - this.knownCountries.add("gw;Guinea-Bissau".split(";")); - this.knownCountries.add("gy;Guyana".split(";")); - this.knownCountries.add("ht;Haiti".split(";")); - this.knownCountries.add("hm;Heard Island and McDonald Islands". - split(";")); - this.knownCountries.add("va;Vatican City".split(";")); - this.knownCountries.add("hn;Honduras".split(";")); - this.knownCountries.add("hk;Hong Kong".split(";")); - this.knownCountries.add("hu;Hungary".split(";")); - this.knownCountries.add("is;Iceland".split(";")); - this.knownCountries.add("in;India".split(";")); - this.knownCountries.add("id;Indonesia".split(";")); - this.knownCountries.add("ir;Iran".split(";")); - this.knownCountries.add("iq;Iraq".split(";")); - this.knownCountries.add("ie;Ireland".split(";")); - this.knownCountries.add("im;Isle of Man".split(";")); - this.knownCountries.add("il;Israel".split(";")); - this.knownCountries.add("it;Italy".split(";")); - this.knownCountries.add("jm;Jamaica".split(";")); - this.knownCountries.add("jp;Japan".split(";")); - this.knownCountries.add("je;Jersey".split(";")); - this.knownCountries.add("jo;Jordan".split(";")); - this.knownCountries.add("kz;Kazakhstan".split(";")); - this.knownCountries.add("ke;Kenya".split(";")); - this.knownCountries.add("ki;Kiribati".split(";")); - this.knownCountries.add("kp;North Korea".split(";")); - this.knownCountries.add("kr;Korea, Republic of".split(";")); - this.knownCountries.add("kw;Kuwait".split(";")); - this.knownCountries.add("kg;Kyrgyzstan".split(";")); - this.knownCountries.add("la;Laos".split(";")); - this.knownCountries.add("lv;Latvia".split(";")); - this.knownCountries.add("lb;Lebanon".split(";")); - this.knownCountries.add("ls;Lesotho".split(";")); - this.knownCountries.add("lr;Liberia".split(";")); - this.knownCountries.add("ly;Libya".split(";")); - this.knownCountries.add("li;Liechtenstein".split(";")); - this.knownCountries.add("lt;Lithuania".split(";")); - this.knownCountries.add("lu;Luxembourg".split(";")); - this.knownCountries.add("mo;Macau".split(";")); - this.knownCountries.add("mk;Macedonia".split(";")); - this.knownCountries.add("mg;Madagascar".split(";")); - this.knownCountries.add("mw;Malawi".split(";")); - this.knownCountries.add("my;Malaysia".split(";")); - this.knownCountries.add("mv;Maldives".split(";")); - this.knownCountries.add("ml;Mali".split(";")); - this.knownCountries.add("mt;Malta".split(";")); - this.knownCountries.add("mh;Marshall Islands".split(";")); - this.knownCountries.add("mq;Martinique".split(";")); - this.knownCountries.add("mr;Mauritania".split(";")); - this.knownCountries.add("mu;Mauritius".split(";")); - this.knownCountries.add("yt;Mayotte".split(";")); - this.knownCountries.add("mx;Mexico".split(";")); - this.knownCountries.add("fm;Micronesia, Federated States of". - split(";")); - this.knownCountries.add("md;Moldova, Republic of".split(";")); - this.knownCountries.add("mc;Monaco".split(";")); - this.knownCountries.add("mn;Mongolia".split(";")); - this.knownCountries.add("me;Montenegro".split(";")); - this.knownCountries.add("ms;Montserrat".split(";")); - this.knownCountries.add("ma;Morocco".split(";")); - this.knownCountries.add("mz;Mozambique".split(";")); - this.knownCountries.add("mm;Burma".split(";")); - this.knownCountries.add("na;Namibia".split(";")); - this.knownCountries.add("nr;Nauru".split(";")); - this.knownCountries.add("np;Nepal".split(";")); - this.knownCountries.add("an;Netherlands Antilles".split(";")); - this.knownCountries.add("nl;Netherlands".split(";")); - this.knownCountries.add("nc;New Caledonia".split(";")); - this.knownCountries.add("nz;New Zealand".split(";")); - this.knownCountries.add("ni;Nicaragua".split(";")); - this.knownCountries.add("ne;Niger".split(";")); - this.knownCountries.add("ng;Nigeria".split(";")); - this.knownCountries.add("nu;Niue".split(";")); - this.knownCountries.add("nf;Norfolk Island".split(";")); - this.knownCountries.add("mp;Northern Mariana Islands".split(";")); - this.knownCountries.add("no;Norway".split(";")); - this.knownCountries.add("om;Oman".split(";")); - this.knownCountries.add("pk;Pakistan".split(";")); - this.knownCountries.add("pw;Palau".split(";")); - this.knownCountries.add("ps;Palestinian Territory".split(";")); - this.knownCountries.add("pa;Panama".split(";")); - this.knownCountries.add("pg;Papua New Guinea".split(";")); - this.knownCountries.add("py;Paraguay".split(";")); - this.knownCountries.add("pe;Peru".split(";")); - this.knownCountries.add("ph;Philippines".split(";")); - this.knownCountries.add("pn;Pitcairn Islands".split(";")); - this.knownCountries.add("pl;Poland".split(";")); - this.knownCountries.add("pt;Portugal".split(";")); - this.knownCountries.add("pr;Puerto Rico".split(";")); - this.knownCountries.add("qa;Qatar".split(";")); - this.knownCountries.add("re;Reunion".split(";")); - this.knownCountries.add("ro;Romania".split(";")); - this.knownCountries.add("ru;Russia".split(";")); - this.knownCountries.add("rw;Rwanda".split(";")); - this.knownCountries.add("bl;Saint Bartelemey".split(";")); - this.knownCountries.add("sh;Saint Helena".split(";")); - this.knownCountries.add("kn;Saint Kitts and Nevis".split(";")); - this.knownCountries.add("lc;Saint Lucia".split(";")); - this.knownCountries.add("mf;Saint Martin".split(";")); - this.knownCountries.add("pm;Saint Pierre and Miquelon".split(";")); - this.knownCountries.add("vc;Saint Vincent and the Grenadines". - split(";")); - this.knownCountries.add("ws;Samoa".split(";")); - this.knownCountries.add("sm;San Marino".split(";")); - this.knownCountries.add("st:São Tomé and Príncipe". - split(":")); - this.knownCountries.add("sa;Saudi Arabia".split(";")); - this.knownCountries.add("sn;Senegal".split(";")); - this.knownCountries.add("rs;Serbia".split(";")); - this.knownCountries.add("sc;Seychelles".split(";")); - this.knownCountries.add("sl;Sierra Leone".split(";")); - this.knownCountries.add("sg;Singapore".split(";")); - this.knownCountries.add("sk;Slovakia".split(";")); - this.knownCountries.add("si;Slovenia".split(";")); - this.knownCountries.add("sb;Solomon Islands".split(";")); - this.knownCountries.add("so;Somalia".split(";")); - this.knownCountries.add("za;South Africa".split(";")); - this.knownCountries.add(("gs;South Georgia and the South Sandwich " - + "Islands").split(";")); - this.knownCountries.add("ss;South Sudan".split(";")); - this.knownCountries.add("es;Spain".split(";")); - this.knownCountries.add("lk;Sri Lanka".split(";")); - this.knownCountries.add("sd;Sudan".split(";")); - this.knownCountries.add("sr;Suriname".split(";")); - this.knownCountries.add("sj;Svalbard and Jan Mayen".split(";")); - this.knownCountries.add("sz;Swaziland".split(";")); - this.knownCountries.add("se;Sweden".split(";")); - this.knownCountries.add("ch;Switzerland".split(";")); - this.knownCountries.add("sy;Syrian Arab Republic".split(";")); - this.knownCountries.add("tw;Taiwan".split(";")); - this.knownCountries.add("tj;Tajikistan".split(";")); - this.knownCountries.add("tz;Tanzania, United Republic of".split(";")); - this.knownCountries.add("th;Thailand".split(";")); - this.knownCountries.add("tl;East Timor".split(";")); - this.knownCountries.add("tg;Togo".split(";")); - this.knownCountries.add("tk;Tokelau".split(";")); - this.knownCountries.add("to;Tonga".split(";")); - this.knownCountries.add("tt;Trinidad and Tobago".split(";")); - this.knownCountries.add("tn;Tunisia".split(";")); - this.knownCountries.add("tr;Turkey".split(";")); - this.knownCountries.add("tm;Turkmenistan".split(";")); - this.knownCountries.add("tc;Turks and Caicos Islands".split(";")); - this.knownCountries.add("tv;Tuvalu".split(";")); - this.knownCountries.add("ug;Uganda".split(";")); - this.knownCountries.add("ua;Ukraine".split(";")); - this.knownCountries.add("ae;United Arab Emirates".split(";")); - this.knownCountries.add("gb;United Kingdom".split(";")); - this.knownCountries.add("um;United States Minor Outlying Islands". - split(";")); - this.knownCountries.add("us;United States".split(";")); - this.knownCountries.add("uy;Uruguay".split(";")); - this.knownCountries.add("uz;Uzbekistan".split(";")); - this.knownCountries.add("vu;Vanuatu".split(";")); - this.knownCountries.add("ve;Venezuela".split(";")); - this.knownCountries.add("vn;Vietnam".split(";")); - this.knownCountries.add("vg;Virgin Islands, British".split(";")); - this.knownCountries.add("vi;Virgin Islands, U.S.".split(";")); - this.knownCountries.add("wf;Wallis and Futuna".split(";")); - this.knownCountries.add("eh;Western Sahara".split(";")); - this.knownCountries.add("ye;Yemen".split(";")); - this.knownCountries.add("zm;Zambia".split(";")); - this.knownCountries.add("zw;Zimbabwe".split(";")); - } - - public List<String[]> getCountryList() { - return this.knownCountries; - } -} - diff --git a/src/org/torproject/ernie/web/graphs/CsvServlet.java b/src/org/torproject/ernie/web/graphs/CsvServlet.java deleted file mode 100644 index 40e3bea..0000000 --- a/src/org/torproject/ernie/web/graphs/CsvServlet.java +++ /dev/null @@ -1,97 +0,0 @@ -/* Copyright 2011, 2012 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.ernie.web.graphs; - -import java.io.IOException; -import java.util.SortedSet; -import java.util.logging.Logger; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * Servlet that reads an HTTP request for a comma-separated value file, - * asks the GraphGenerator to generate this file, and returns it to the - * client. - */ -public class CsvServlet extends HttpServlet { - - private static final long serialVersionUID = 7501442926823719958L; - - private RObjectGenerator rObjectGenerator; - - /* Available CSV files. */ - private SortedSet<String> availableCsvFiles; - - private Logger logger; - - public void init() { - - /* Initialize logger. */ - this.logger = Logger.getLogger(CsvServlet.class.toString()); - - /* Get a reference to the R object generator that we need to generate - * CSV files. */ - this.rObjectGenerator = (RObjectGenerator) getServletContext(). - getAttribute("RObjectGenerator"); - this.availableCsvFiles = rObjectGenerator.getAvailableCsvFiles(); - } - - public void doGet(HttpServletRequest request, - HttpServletResponse response) throws IOException, - ServletException { - - /* Check if the directory listing was requested. */ - String requestURI = request.getRequestURI(); - if (requestURI.equals("/ernie/csv/")) { - request.setAttribute("directory", "/csv"); - request.setAttribute("extension", ".csv"); - request.setAttribute("files", this.availableCsvFiles); - request.getRequestDispatcher("/WEB-INF/dir.jsp").forward(request, - response); - return; - } - - /* Find out which CSV file was requested and make sure we know this - * CSV file type. */ - String requestedCsvFile = requestURI; - if (requestedCsvFile.endsWith(".csv")) { - requestedCsvFile = requestedCsvFile.substring(0, - requestedCsvFile.length() - ".csv".length()); - } - if (requestedCsvFile.contains("/")) { - requestedCsvFile = requestedCsvFile.substring(requestedCsvFile. - lastIndexOf("/") + 1); - } - if (!availableCsvFiles.contains(requestedCsvFile)) { - logger.info("Did not recognize requested .csv file from request " - + "URI: '" + requestURI + "'. Responding with 404 Not Found."); - response.sendError(HttpServletResponse.SC_NOT_FOUND); - return; - } - logger.fine("CSV file '" + requestedCsvFile + ".csv' requested."); - - /* Request CSV file from R object generator, which asks Rserve to - * generate it. */ - RObject csvFile = this.rObjectGenerator.generateCsv( - requestedCsvFile, true); - - /* Make sure that we have a .csv file to return. */ - if (csvFile == null) { - response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - return; - } - - /* Write CSV file to response. */ - String csvFileContent = new String(csvFile.getBytes()); - response.setContentType("text/csv"); - response.setHeader("Content-Length", String.valueOf( - csvFileContent.length())); - response.setHeader("Content-Disposition", - "inline; filename="" + requestedCsvFile + ".csv""); - response.getWriter().print(csvFileContent); - } -} - diff --git a/src/org/torproject/ernie/web/graphs/GraphImageServlet.java b/src/org/torproject/ernie/web/graphs/GraphImageServlet.java deleted file mode 100644 index b7d0b17..0000000 --- a/src/org/torproject/ernie/web/graphs/GraphImageServlet.java +++ /dev/null @@ -1,76 +0,0 @@ -/* Copyright 2011, 2012 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.ernie.web.graphs; - -import java.io.BufferedOutputStream; -import java.io.IOException; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * Servlet that reads an HTTP request for a graph image, asks the - * RObjectGenerator to generate this graph if it's not in the cache, and - * returns the image bytes to the client. - */ -public class GraphImageServlet extends HttpServlet { - - private static final long serialVersionUID = -7356818641689744288L; - - private RObjectGenerator rObjectGenerator; - - public void init() { - - /* Get a reference to the R object generator that we need to generate - * graph images. */ - this.rObjectGenerator = (RObjectGenerator) getServletContext(). - getAttribute("RObjectGenerator"); - } - - public void doGet(HttpServletRequest request, - HttpServletResponse response) throws IOException, - ServletException { - - /* Find out which graph type was requested and make sure we know this - * graph type and file type. */ - String requestedGraph = request.getRequestURI(); - String fileType = null; - if (requestedGraph.endsWith(".png") || - requestedGraph.endsWith(".pdf") || - requestedGraph.endsWith(".svg")) { - fileType = requestedGraph.substring(requestedGraph.length() - 3); - requestedGraph = requestedGraph.substring(0, requestedGraph.length() - - 4); - } - if (requestedGraph.contains("/")) { - requestedGraph = requestedGraph.substring(requestedGraph. - lastIndexOf("/") + 1); - } - - /* Request graph from R object generator, which either returns it from - * its cache or asks Rserve to generate it. */ - RObject graph = rObjectGenerator.generateGraph(requestedGraph, - fileType, request.getParameterMap(), true); - - /* Make sure that we have a graph to return. */ - if (graph == null || graph.getBytes() == null || fileType == null) { - response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - return; - } - - /* Write graph bytes to response. */ - BufferedOutputStream output = null; - response.setContentType("image/" + fileType); - response.setHeader("Content-Length", - String.valueOf(graph.getBytes().length)); - response.setHeader("Content-Disposition", - "inline; filename="" + graph.getFileName() + """); - output = new BufferedOutputStream(response.getOutputStream(), 1024); - output.write(graph.getBytes(), 0, graph.getBytes().length); - output.flush(); - output.close(); - } -} - diff --git a/src/org/torproject/ernie/web/graphs/GraphParameterChecker.java b/src/org/torproject/ernie/web/graphs/GraphParameterChecker.java deleted file mode 100644 index 74ca6f9..0000000 --- a/src/org/torproject/ernie/web/graphs/GraphParameterChecker.java +++ /dev/null @@ -1,280 +0,0 @@ -/* Copyright 2011, 2012 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.ernie.web.graphs; - -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TimeZone; -import java.util.regex.Pattern; - -/** - * Checks request parameters passed to graph-generating servlets. - */ -public class GraphParameterChecker { - - /** - * Singleton instance of this class. - */ - private static GraphParameterChecker instance = - new GraphParameterChecker(); - - /** - * Returns the singleton instance of this class. - */ - public static GraphParameterChecker getInstance() { - return instance; - } - - /* Date format for parsing start and end dates. */ - private SimpleDateFormat dateFormat; - - /* Available graphs with corresponding parameter lists. */ - private Map<String, String> availableGraphs; - - /* Known parameters and parameter values. */ - private Map<String, String> knownParameterValues; - - /** - * Initializes map with valid parameters for each of the graphs. - */ - public GraphParameterChecker() { - this.dateFormat = new SimpleDateFormat("yyyy-MM-dd"); - this.dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - - this.knownParameterValues = new HashMap<String, String>(); - this.knownParameterValues.put("flag", - "Running,Exit,Guard,Fast,Stable,HSDir"); - StringBuilder sb = new StringBuilder("all"); - for (String[] country : Countries.getInstance().getCountryList()) { - sb.append("," + country[0]); - } - this.knownParameterValues.put("country", sb.toString()); - this.knownParameterValues.put("events", "on,off,points"); - this.knownParameterValues.put("source", "all,siv,moria,torperf"); - this.knownParameterValues.put("filesize", "50kb,1mb,5mb"); - this.knownParameterValues.put("transport", - "obfs2,obfs3,websocket,<OR>,<??>"); - this.knownParameterValues.put("version", "v4,v6"); - } - - public void setAvailableGraphs(Map<String, String> availableGraphs) { - this.availableGraphs = availableGraphs; - } - - /** - * Checks request parameters for the given graph type and returns a map - * of recognized parameters, or null if the graph type doesn't exist or - * the parameters are invalid. - */ - public Map<String, String[]> checkParameters(String graphType, - Map requestParameters) { - - /* Check if the graph type exists. */ - if (graphType == null || - !this.availableGraphs.containsKey(graphType)) { - return null; - } - - /* Find out which other parameters are supported by this graph type - * and parse them if they are given. */ - Set<String> supportedGraphParameters = new HashSet<String>(Arrays. - asList(this.availableGraphs.get(graphType).split(","))); - Map<String, String[]> recognizedGraphParameters = - new HashMap<String, String[]>(); - - /* Parse start and end dates if supported by the graph type. If no end - * date is provided, set it to today. If no start date is provided, - * set it to 90 days before the end date. Make sure that start date - * precedes end date. */ - if (supportedGraphParameters.contains("start") || - supportedGraphParameters.contains("end")) { - String[] startParameter = (String[]) requestParameters.get("start"); - String[] endParameter = (String[]) requestParameters.get("end"); - long endTimestamp = System.currentTimeMillis(); - if (endParameter != null && endParameter.length > 0 && - endParameter[0].length() > 0) { - try { - endTimestamp = dateFormat.parse(endParameter[0]).getTime(); - } catch (ParseException e) { - return null; - } - if (!endParameter[0].startsWith("20")) { - return null; - } - } - endParameter = new String[] { dateFormat.format(endTimestamp) }; - long startTimestamp = endTimestamp - 90L * 24L * 60L * 60L * 1000L; - if (startParameter != null && startParameter.length > 0 && - startParameter[0].length() > 0) { - try { - startTimestamp = dateFormat.parse(startParameter[0]).getTime(); - } catch (ParseException e) { - return null; - } - if (!startParameter[0].startsWith("20")) { - return null; - } - } - startParameter = new String[] { dateFormat.format(startTimestamp) }; - if (startTimestamp > endTimestamp) { - return null; - } - recognizedGraphParameters.put("start", startParameter); - recognizedGraphParameters.put("end", endParameter); - } - - /* Parse relay flags if supported by the graph type. If no relay flags - * are passed or none of them have been recognized, use the set of all - * known flags as default. */ - if (supportedGraphParameters.contains("flag")) { - String[] flagParameters = (String[]) requestParameters.get("flag"); - List<String> knownFlags = Arrays.asList( - this.knownParameterValues.get("flag").split(",")); - if (flagParameters != null) { - for (String flag : flagParameters) { - if (flag == null || flag.length() == 0 || - !knownFlags.contains(flag)) { - return null; - } - } - } else { - flagParameters = "Running,Exit,Guard,Fast,Stable".split(","); - } - recognizedGraphParameters.put("flag", flagParameters); - } - - /* Parse country codes if supported by the graph type. If no countries - * are passed, use country code "all" (all countries) as default. */ - if (supportedGraphParameters.contains("country")) { - String[] countryParameters = (String[]) requestParameters.get( - "country"); - List<String> knownCountries = Arrays.asList( - this.knownParameterValues.get("country").split(",")); - if (countryParameters != null) { - for (String country : countryParameters) { - if (country == null || country.length() == 0 || - !knownCountries.contains(country)) { - return null; - } - } - } else { - countryParameters = new String[] { "all" }; - } - recognizedGraphParameters.put("country", countryParameters); - } - - /* Parse whether the estimated min/max range shall be displayed if - * supported by the graph type. This parameter can either be "on" or - * "off," where "off" is the default. */ - if (supportedGraphParameters.contains("events")) { - String[] eventsParameter = (String[]) requestParameters.get( - "events"); - List<String> knownRanges = Arrays.asList( - this.knownParameterValues.get("events").split(",")); - if (eventsParameter != null) { - if (eventsParameter.length != 1 || - eventsParameter[0].length() == 0 || - !knownRanges.contains(eventsParameter[0])) { - return null; - } - } else { - eventsParameter = new String[] { "off" }; - } - recognizedGraphParameters.put("events", eventsParameter); - } - - /* Parse torperf data source if supported by the graph type. Only a - * single source can be passed. If no source is passed, use "torperf" - * as default. */ - if (supportedGraphParameters.contains("source")) { - String[] sourceParameter = (String[]) requestParameters.get( - "source"); - List<String> knownSources = Arrays.asList( - this.knownParameterValues.get("source").split(",")); - if (sourceParameter != null) { - if (sourceParameter.length != 1) { - return null; - } - if (sourceParameter[0].length() == 0 || - !knownSources.contains(sourceParameter[0])) { - return null; - } - } else { - sourceParameter = new String[] { "all" }; - } - recognizedGraphParameters.put("source", sourceParameter); - } - - /* Parse torperf file size if supported by the graph type. Only a - * single file size can be passed. If no file size is passed, use - * "50kb" as default. */ - if (supportedGraphParameters.contains("filesize")) { - String[] filesizeParameter = (String[]) requestParameters.get( - "filesize"); - List<String> knownFilesizes = Arrays.asList( - this.knownParameterValues.get("filesize").split(",")); - if (filesizeParameter != null) { - if (filesizeParameter.length != 1) { - return null; - } - if (filesizeParameter[0].length() == 0 || - !knownFilesizes.contains(filesizeParameter[0])) { - return null; - } - } else { - filesizeParameter = new String[] { "50kb" }; - } - recognizedGraphParameters.put("filesize", filesizeParameter); - } - - /* Parse transports if supported by the graph type. If no transports - * are passed, use "<OR>" as default. */ - if (supportedGraphParameters.contains("transport")) { - String[] transportParameters = (String[]) requestParameters.get( - "transport"); - List<String> knownTransports = Arrays.asList( - this.knownParameterValues.get("transport").split(",")); - if (transportParameters != null) { - for (String transport : transportParameters) { - if (transport == null || transport.length() == 0 || - !knownTransports.contains(transport)) { - return null; - } - } - } else { - transportParameters = new String[] { "<OR>" }; - } - recognizedGraphParameters.put("transport", transportParameters); - } - - /* Parse versions if supported by the graph type. If no versions - * are passed, use "v4" as default. */ - if (supportedGraphParameters.contains("version")) { - String[] versionParameters = (String[]) requestParameters.get( - "version"); - List<String> knownVersions = Arrays.asList( - this.knownParameterValues.get("version").split(",")); - if (versionParameters != null) { - for (String version : versionParameters) { - if (version == null || version.length() == 0 || - !knownVersions.contains(version)) { - return null; - } - } - } else { - versionParameters = new String[] { "v4" }; - } - recognizedGraphParameters.put("version", versionParameters); - } - - /* We now have a map with all required graph parameters. Return it. */ - return recognizedGraphParameters; - } -} - diff --git a/src/org/torproject/ernie/web/graphs/GraphsSubpagesServlet.java b/src/org/torproject/ernie/web/graphs/GraphsSubpagesServlet.java deleted file mode 100644 index 94d7340..0000000 --- a/src/org/torproject/ernie/web/graphs/GraphsSubpagesServlet.java +++ /dev/null @@ -1,162 +0,0 @@ -/* Copyright 2011, 2012 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.ernie.web.graphs; - -import java.io.IOException; -import java.text.SimpleDateFormat; -import java.util.Arrays; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TimeZone; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -public class GraphsSubpagesServlet extends HttpServlet { - - private static final long serialVersionUID = -5959829347747628403L; - - /* Available graphs subpages with corresponding JSP to which requests - * are forwarded. */ - private Map<String, String> availableGraphsSubpages; - - /* Available tables on graphs subpages. */ - private Map<String, Set<String>> availableGraphsSubpageTables; - - /* Country codes and names for per-country graphs. */ - private List<String[]> knownCountries; - - /* R object generator for generating table data. */ - private RObjectGenerator rObjectGenerator; - - public GraphsSubpagesServlet() { - this.availableGraphsSubpages = new HashMap<String, String>(); - this.availableGraphsSubpages.put("network.html", - "WEB-INF/network.jsp"); - this.availableGraphsSubpages.put("fast-exits.html", - "WEB-INF/fast-exits.jsp"); - this.availableGraphsSubpages.put("users.html", "WEB-INF/users.jsp"); - this.availableGraphsSubpages.put("performance.html", - "WEB-INF/performance.jsp"); - - this.availableGraphsSubpageTables = - new HashMap<String, Set<String>>(); - this.availableGraphsSubpageTables.put("users.html", - new HashSet<String>(Arrays.asList(( - "direct-users,censorship-events,bridge-users,userstats-relay," - + "userstats-censorship-events,userstats-bridge").split(",")))); - - this.knownCountries = Countries.getInstance().getCountryList(); - } - - public void init() { - /* Get a reference to the R object generator that we need to generate - * table data. */ - this.rObjectGenerator = (RObjectGenerator) getServletContext(). - getAttribute("RObjectGenerator"); - } - - public void doGet(HttpServletRequest request, - HttpServletResponse response) throws IOException, ServletException { - - /* Find out which graph subpage was requested and look up which JSP - * handles this subpage. */ - String requestedPage = request.getRequestURI(); - if (requestedPage == null) { - response.sendError(HttpServletResponse.SC_BAD_REQUEST); - return; - } - if (requestedPage.contains("/")) { - requestedPage = requestedPage.substring(requestedPage. - lastIndexOf("/") + 1); - } - if (!availableGraphsSubpages.containsKey(requestedPage)) { - response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - return; - } - String jsp = availableGraphsSubpages.get(requestedPage); - - /* Find out which graph or table type was requested, if any. */ - String requestedGraph = request.getParameter("graph"); - String requestedTable = request.getParameter("table"); - if (requestedGraph != null) { - - /* Check if the passed parameters are valid. */ - Map<String, String[]> checkedParameters = GraphParameterChecker. - getInstance().checkParameters(requestedGraph, - request.getParameterMap()); - if (checkedParameters != null) { - - /* Set the graph's attributes to the appropriate values, so that - * we can display the correct graph and prepopulate the form. */ - StringBuilder urlBuilder = new StringBuilder(); - for (Map.Entry<String, String[]> param : - checkedParameters.entrySet()) { - request.setAttribute(requestedGraph.replaceAll("-", "_") + "_" - + param.getKey(), param.getValue()); - for (String paramValue : param.getValue()) { - urlBuilder.append("&" + param.getKey() + "=" + paramValue); - } - } - String url = "?" + urlBuilder.toString().substring(1); - request.setAttribute(requestedGraph.replaceAll("-", "_") + "_url", - url); - } - } - if (requestedTable != null) { - - /* Check if the passed parameters are valid. */ - Map<String, String[]> checkedParameters = TableParameterChecker. - getInstance().checkParameters(requestedTable, - request.getParameterMap()); - if (checkedParameters != null) { - - /* Set the table's attributes to the appropriate values, so that - * we can prepopulate the form. */ - for (Map.Entry<String, String[]> param : - checkedParameters.entrySet()) { - request.setAttribute(requestedTable.replaceAll("-", "_") + "_" - + param.getKey(), param.getValue()); - } - } - } - - /* Generate table data if the graphs subpage has any tables, - * regardless of whether a table update was requested, and add the - * table data as request attribute. */ - if (this.availableGraphsSubpageTables.containsKey(requestedPage)) { - for (String tableName : - this.availableGraphsSubpageTables.get(requestedPage)) { - List<Map<String, String>> tableData = rObjectGenerator. - generateTable(tableName, requestedTable, - request.getParameterMap(), true); - request.setAttribute(tableName.replaceAll("-", "_") - + "_tabledata", tableData); - } - } - - /* Pass list of known countries in case we want to display them. */ - request.setAttribute("countries", this.knownCountries); - - /* Pass the default start and end dates. */ - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); - dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - Date defaultEndDate = new Date(); - Date defaultStartDate = new Date(defaultEndDate.getTime() - - 90L * 24L * 60L * 60L * 1000L); - request.setAttribute("default_start_date", - dateFormat.format(defaultStartDate)); - request.setAttribute("default_end_date", - dateFormat.format(defaultEndDate)); - - /* Forward the request to the JSP that does all the hard work. */ - request.getRequestDispatcher(jsp).forward(request, response); - } -} - diff --git a/src/org/torproject/ernie/web/graphs/RObject.java b/src/org/torproject/ernie/web/graphs/RObject.java deleted file mode 100644 index cfab819..0000000 --- a/src/org/torproject/ernie/web/graphs/RObject.java +++ /dev/null @@ -1,23 +0,0 @@ -/* Copyright 2011, 2012 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.ernie.web.graphs; - -public class RObject { - private byte[] bytes; - private String fileName; - private long lastModified; - public RObject(byte[] bytes, String fileName, long lastModified) { - this.bytes = bytes; - this.fileName = fileName; - this.lastModified = lastModified; - } - public String getFileName() { - return this.fileName; - } - public byte[] getBytes() { - return this.bytes; - } - public long getLastModified() { - return this.lastModified; - } -} diff --git a/src/org/torproject/ernie/web/graphs/RObjectGenerator.java b/src/org/torproject/ernie/web/graphs/RObjectGenerator.java deleted file mode 100644 index 927b5c4..0000000 --- a/src/org/torproject/ernie/web/graphs/RObjectGenerator.java +++ /dev/null @@ -1,398 +0,0 @@ -/* Copyright 2011, 2012 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.ernie.web.graphs; - -import java.io.BufferedInputStream; -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.SortedSet; -import java.util.TreeSet; - -import javax.servlet.ServletContext; -import javax.servlet.ServletContextEvent; -import javax.servlet.ServletContextListener; - -import org.rosuda.REngine.Rserve.RConnection; -import org.rosuda.REngine.Rserve.RserveException; - -public class RObjectGenerator implements ServletContextListener { - - /* Host and port where Rserve is listening. */ - private String rserveHost; - private int rservePort; - - /* Some parameters for our cache of graph images. */ - private String cachedGraphsDirectory; - private long maxCacheAge; - - private SortedSet<String> availableCsvFiles; - private Map<String, String> availableTables; - private Map<String, String> availableGraphs; - private Set<String> availableGraphFileTypes; - - public void contextInitialized(ServletContextEvent event) { - - /* Initialize using context parameters. */ - ServletContext servletContext = event.getServletContext(); - this.rserveHost = servletContext.getInitParameter("rserveHost"); - this.rservePort = Integer.parseInt(servletContext.getInitParameter( - "rservePort")); - this.maxCacheAge = Long.parseLong(servletContext.getInitParameter( - "maxCacheAge")); - this.cachedGraphsDirectory = servletContext.getInitParameter( - "cachedGraphsDir"); - - /* Initialize map of available CSV files. */ - this.availableCsvFiles = new TreeSet<String>(); - this.availableCsvFiles.add("bandwidth"); - this.availableCsvFiles.add("bandwidth-flags"); - this.availableCsvFiles.add("bwhist-flags"); - this.availableCsvFiles.add("connbidirect"); - this.availableCsvFiles.add("cloudbridges"); - this.availableCsvFiles.add("dirbytes"); - this.availableCsvFiles.add("monthly-userstats-average"); - this.availableCsvFiles.add("monthly-userstats-peak"); - this.availableCsvFiles.add("networksize"); - this.availableCsvFiles.add("platforms"); - this.availableCsvFiles.add("relaycountries"); - this.availableCsvFiles.add("relayflags"); - this.availableCsvFiles.add("torperf"); - this.availableCsvFiles.add("torperf-failures"); - this.availableCsvFiles.add("userstats"); - this.availableCsvFiles.add("userstats-detector"); - this.availableCsvFiles.add("versions"); - - this.availableTables = new HashMap<String, String>(); - this.availableTables.put("userstats-relay", "start,end,filename"); - this.availableTables.put("userstats-bridge", "start,end,filename"); - this.availableTables.put("userstats-censorship-events", - "start,end,filename"); - TableParameterChecker.getInstance().setAvailableTables( - availableTables); - - this.availableGraphs = new HashMap<String, String>(); - this.availableGraphs.put("networksize", "start,end,filename"); - this.availableGraphs.put("cloudbridges", "start,end,filename"); - this.availableGraphs.put("relaycountries", - "start,end,country,filename"); - this.availableGraphs.put("relayflags", "start,end,flag,filename"); - this.availableGraphs.put("versions", "start,end,filename"); - this.availableGraphs.put("platforms", "start,end,filename"); - this.availableGraphs.put("bandwidth", "start,end,filename"); - this.availableGraphs.put("bandwidth-flags", "start,end,filename"); - this.availableGraphs.put("bwhist-flags", "start,end,filename"); - this.availableGraphs.put("dirbytes", "start,end,filename"); - this.availableGraphs.put("torperf", - "start,end,source,filesize,filename"); - this.availableGraphs.put("torperf-failures", - "start,end,source,filesize,filename"); - this.availableGraphs.put("connbidirect", "start,end,filename"); - this.availableGraphs.put("fast-exits", "start,end,filename"); - this.availableGraphs.put("almost-fast-exits", "start,end,filename"); - this.availableGraphs.put("userstats-relay-country", - "start,end,country,events,filename"); - this.availableGraphs.put("userstats-bridge-country", - "start,end,country,filename"); - this.availableGraphs.put("userstats-bridge-transport", - "start,end,transport,filename"); - this.availableGraphs.put("userstats-bridge-version", - "start,end,version,filename"); - this.availableGraphFileTypes = new HashSet<String>(Arrays.asList( - "png,pdf,svg".split(","))); - GraphParameterChecker.getInstance().setAvailableGraphs( - availableGraphs); - - /* Register ourself, so that servlets can use us. */ - servletContext.setAttribute("RObjectGenerator", this); - - /* Periodically generate R objects with default parameters. */ - new Thread() { - public void run() { - long lastUpdated = 0L, sleep; - while (true) { - while ((sleep = maxCacheAge * 1000L / 2L + lastUpdated - - System.currentTimeMillis()) > 0L) { - try { - Thread.sleep(sleep); - } catch (InterruptedException e) { - } - } - for (String csvFile : availableCsvFiles) { - generateCsv(csvFile, false); - } - for (String tableName : availableTables.keySet()) { - generateTable(tableName, tableName, new HashMap(), false); - } - for (String graphName : availableGraphs.keySet()) { - for (String fileType : availableGraphFileTypes) { - generateGraph(graphName, fileType, new HashMap(), false); - } - } - lastUpdated = System.currentTimeMillis(); - } - }; - }.start(); - } - - public void contextDestroyed(ServletContextEvent event) { - /* Nothing to do. */ - } - - public RObject generateGraph(String requestedGraph, String fileType, - Map parameterMap, boolean checkCache) { - Map<String, String[]> checkedParameters = GraphParameterChecker. - getInstance().checkParameters(requestedGraph, parameterMap); - if (checkedParameters == null) { - /* TODO We're going to take the blame by sending an internal server - * error to the client, but really the user is to blame. */ - return null; - } - StringBuilder rQueryBuilder = new StringBuilder("plot_" - + requestedGraph.replaceAll("-", "_") + "("), - imageFilenameBuilder = new StringBuilder(requestedGraph); - for (Map.Entry<String, String[]> parameter : - checkedParameters.entrySet()) { - String parameterName = parameter.getKey(); - String[] parameterValues = parameter.getValue(); - for (String param : parameterValues) { - imageFilenameBuilder.append("-" + param); - } - if (parameterValues.length < 2) { - rQueryBuilder.append(parameterName + " = '" + parameterValues[0] - + "', "); - } else { - rQueryBuilder.append(parameterName + " = c("); - for (int i = 0; i < parameterValues.length - 1; i++) { - rQueryBuilder.append("'" + parameterValues[i] + "', "); - } - rQueryBuilder.append("'" + parameterValues[ - parameterValues.length - 1] + "'), "); - } - } - imageFilenameBuilder.append("." + fileType); - String imageFilename = imageFilenameBuilder.toString(); - rQueryBuilder.append("path = '%s')"); - String rQuery = rQueryBuilder.toString(); - File imageFile = new File(this.cachedGraphsDirectory + "/" - + imageFilename); - return this.generateRObject(rQuery, imageFile, imageFilename, - checkCache); - } - - public SortedSet<String> getAvailableCsvFiles() { - return this.availableCsvFiles; - } - - public RObject generateCsv(String requestedCsvFile, - boolean checkCache) { - /* Prepare filename and R query string. */ - String rQuery = "export_" + requestedCsvFile.replaceAll("-", "_") - + "(path = '%s')"; - String csvFilename = requestedCsvFile + ".csv"; - - /* See if we need to generate this .csv file. */ - File csvFile = new File(this.cachedGraphsDirectory + "/" - + csvFilename); - return this.generateRObject(rQuery, csvFile, csvFilename, checkCache); - } - - public List<Map<String, String>> generateTable(String tableName, - String requestedTable, Map parameterMap, boolean checkCache) { - - Map<String, String[]> checkedParameters = null; - if (tableName.equals(requestedTable)) { - checkedParameters = TableParameterChecker. - getInstance().checkParameters(requestedTable, - parameterMap); - } else { - checkedParameters = TableParameterChecker. - getInstance().checkParameters(tableName, null); - } - if (checkedParameters == null) { - /* TODO We're going to take the blame by sending an internal server - * error to the client, but really the user is to blame. */ - return null; - } - StringBuilder rQueryBuilder = new StringBuilder("write_" - + tableName.replaceAll("-", "_") + "("), - tableFilenameBuilder = new StringBuilder(tableName); - - for (Map.Entry<String, String[]> parameter : - checkedParameters.entrySet()) { - String parameterName = parameter.getKey(); - String[] parameterValues = parameter.getValue(); - for (String param : parameterValues) { - tableFilenameBuilder.append("-" + param); - } - if (parameterValues.length < 2) { - rQueryBuilder.append(parameterName + " = '" - + parameterValues[0] + "', "); - } else { - rQueryBuilder.append(parameterName + " = c("); - for (int i = 0; i < parameterValues.length - 1; i++) { - rQueryBuilder.append("'" + parameterValues[i] + "', "); - } - rQueryBuilder.append("'" + parameterValues[ - parameterValues.length - 1] + "'), "); - } - } - tableFilenameBuilder.append(".tbl"); - String tableFilename = tableFilenameBuilder.toString(); - rQueryBuilder.append("path = '%s')"); - String rQuery = rQueryBuilder.toString(); - return this.generateTable(rQuery, tableFilename, checkCache); - } - - /* Generate table data using the given R query and filename or read - * previously generated table data from disk if it's not too old and - * return table data. */ - private List<Map<String, String>> generateTable(String rQuery, - String tableFilename, boolean checkCache) { - - /* See if we need to generate this table. */ - File tableFile = new File(this.cachedGraphsDirectory + "/" - + tableFilename); - byte[] tableBytes = this.generateRObject(rQuery, tableFile, - tableFilename, checkCache).getBytes(); - - /* Write the table content to a map. */ - List<Map<String, String>> result = null; - try { - result = new ArrayList<Map<String, String>>(); - BufferedReader br = new BufferedReader(new InputStreamReader( - new ByteArrayInputStream(tableBytes))); - String line = br.readLine(); - if (line != null) { - List<String> headers = new ArrayList<String>(Arrays.asList( - line.split(","))); - while ((line = br.readLine()) != null) { - String[] parts = line.split(","); - if (headers.size() != parts.length) { - return null; - } - Map<String, String> row = new HashMap<String, String>(); - for (int i = 0; i < headers.size(); i++) { - row.put(headers.get(i), parts[i]); - } - result.add(row); - } - } - } catch (IOException e) { - return null; - } - - /* Return table values. */ - return result; - } - - /* Generate an R object in a separate worker thread, or wait for an - * already running worker thread to finish and get its result. */ - private RObject generateRObject(String rQuery, File rObjectFile, - String fileName, boolean checkCache) { - RObjectGeneratorWorker worker = null; - synchronized (this.rObjectGeneratorThreads) { - if (this.rObjectGeneratorThreads.containsKey(rQuery)) { - worker = this.rObjectGeneratorThreads.get(rQuery); - } else { - worker = new RObjectGeneratorWorker(rQuery, rObjectFile, - fileName, checkCache); - this.rObjectGeneratorThreads.put(rQuery, worker); - worker.start(); - } - } - try { - worker.join(); - } catch (InterruptedException e) { - } - synchronized (this.rObjectGeneratorThreads) { - if (this.rObjectGeneratorThreads.containsKey(rQuery) && - this.rObjectGeneratorThreads.get(rQuery) == worker) { - this.rObjectGeneratorThreads.remove(rQuery); - } - } - return worker.getRObject(); - } - - private Map<String, RObjectGeneratorWorker> rObjectGeneratorThreads = - new HashMap<String, RObjectGeneratorWorker>(); - - private class RObjectGeneratorWorker extends Thread { - - private String rQuery; - private File rObjectFile; - private String fileName; - private boolean checkCache; - private RObject result = null; - - public RObjectGeneratorWorker(String rQuery, File rObjectFile, - String fileName, boolean checkCache) { - this.rQuery = rQuery; - this.rObjectFile = rObjectFile; - this.fileName = fileName; - this.checkCache = checkCache; - } - - public void run() { - - /* See if we need to generate this R object. */ - long now = System.currentTimeMillis(); - if (!this.checkCache || !this.rObjectFile.exists() || - this.rObjectFile.lastModified() < now - maxCacheAge * 1000L) { - - /* We do. Update the R query to contain the absolute path to the - * file to be generated, create a connection to Rserve, run the R - * query, and close the connection. The generated object will be - * on disk. */ - this.rQuery = String.format(this.rQuery, - this.rObjectFile.getAbsolutePath()); - try { - RConnection rc = new RConnection(rserveHost, rservePort); - rc.eval(this.rQuery); - rc.close(); - } catch (RserveException e) { - return; - } - - /* Check that we really just generated the R object. */ - if (!this.rObjectFile.exists() || this.rObjectFile.lastModified() - < now - maxCacheAge * 1000L) { - return; - } - } - - /* Read the R object from disk and write it to a byte array. */ - long lastModified = this.rObjectFile.lastModified(); - try { - BufferedInputStream bis = new BufferedInputStream( - new FileInputStream(this.rObjectFile), 1024); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - byte[] buffer = new byte[1024]; - int length; - while ((length = bis.read(buffer)) > 0) { - baos.write(buffer, 0, length); - } - bis.close(); - this.result = new RObject(baos.toByteArray(), this.fileName, - lastModified); - } catch (IOException e) { - return; - } - } - - public RObject getRObject() { - return this.result; - } - } -} diff --git a/src/org/torproject/ernie/web/graphs/TableParameterChecker.java b/src/org/torproject/ernie/web/graphs/TableParameterChecker.java deleted file mode 100644 index 02a0c85..0000000 --- a/src/org/torproject/ernie/web/graphs/TableParameterChecker.java +++ /dev/null @@ -1,120 +0,0 @@ -/* Copyright 2011, 2012 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.ernie.web.graphs; - -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.TimeZone; - -/** - * Checks request parameters passed to generate tables. - */ -public class TableParameterChecker { - - /** - * Singleton instance of this class. - */ - private static TableParameterChecker instance = - new TableParameterChecker(); - - /** - * Returns the singleton instance of this class. - */ - public static TableParameterChecker getInstance() { - return instance; - } - - /* Date format for parsing start and end dates. */ - private SimpleDateFormat dateFormat; - - /* Available tables with corresponding parameter lists. */ - private Map<String, String> availableTables; - - /** - * Initializes map with valid parameters for each of the graphs. - */ - public TableParameterChecker() { - this.dateFormat = new SimpleDateFormat("yyyy-MM-dd"); - this.dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - } - - public void setAvailableTables(Map<String, String> availableTables) { - this.availableTables = availableTables; - } - - /** - * Checks request parameters for the given table type and returns a map - * of recognized parameters, or null if the table type doesn't exist or - * the parameters are invalid. - */ - public Map<String, String[]> checkParameters(String tableType, - Map requestParameters) { - - /* Check if the table type exists. */ - if (tableType == null || - !this.availableTables.containsKey(tableType)) { - return null; - } - - /* Find out which other parameters are supported by this table type - * and parse them if they are given. */ - Set<String> supportedTableParameters = new HashSet<String>(Arrays. - asList(this.availableTables.get(tableType).split(","))); - Map<String, String[]> recognizedTableParameters = - new HashMap<String, String[]>(); - - /* Parse start and end dates if supported by the table type. If no end - * date is provided, set it to today. If no start date is provided, - * set it to 90 days before the end date. Make sure that start date - * precedes end date. */ - if (supportedTableParameters.contains("start") || - supportedTableParameters.contains("end")) { - String[] startParameter = null; - String[] endParameter = null; - if (requestParameters != null) { - startParameter = (String[]) requestParameters.get("start"); - endParameter = (String[]) requestParameters.get("end"); - } - long endTimestamp = System.currentTimeMillis(); - if (endParameter != null && endParameter.length > 0 && - endParameter[0].length() > 0) { - try { - endTimestamp = dateFormat.parse(endParameter[0]).getTime(); - } catch (ParseException e) { - return null; - } - if (!endParameter[0].startsWith("20")) { - return null; - } - } - endParameter = new String[] { dateFormat.format(endTimestamp) }; - long startTimestamp = endTimestamp - 90L * 24L * 60L * 60L * 1000L; - if (startParameter != null && startParameter.length > 0 && - startParameter[0].length() > 0) { - try { - startTimestamp = dateFormat.parse(startParameter[0]).getTime(); - } catch (ParseException e) { - return null; - } - if (!startParameter[0].startsWith("20")) { - return null; - } - } - startParameter = new String[] { dateFormat.format(startTimestamp) }; - if (startTimestamp > endTimestamp) { - return null; - } - recognizedTableParameters.put("start", startParameter); - recognizedTableParameters.put("end", endParameter); - } - - /* We now have a map with all required table parameters. Return it. */ - return recognizedTableParameters; - } -} - diff --git a/src/org/torproject/ernie/web/research/ResearchDataServlet.java b/src/org/torproject/ernie/web/research/ResearchDataServlet.java deleted file mode 100644 index 6c24e0d..0000000 --- a/src/org/torproject/ernie/web/research/ResearchDataServlet.java +++ /dev/null @@ -1,260 +0,0 @@ -/* Copyright 2011, 2012 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.ernie.web.research; - -import java.io.File; -import java.io.IOException; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.SortedMap; -import java.util.TreeMap; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * Controller servlet for the Data page. Prepares the various lists of - * downloadable metrics data files by parsing a file with URLs on other - * servers and looking at a local directory with files served by local - * Apache HTTP server. The file with URLs on other servers may contain - * comment lines starting with #. Recognizes metrics data file types from - * the file names. - */ -public class ResearchDataServlet extends HttpServlet { - - private static final long serialVersionUID = -5168280373350515577L; - - public void doGet(HttpServletRequest request, - HttpServletResponse response) throws IOException, ServletException { - - /* Read local directory with files served by the local Apache HTTP - * server and add the URLs to the list. */ - List<String> dataFileUrls = new ArrayList<String>(); - String localDataDir = getServletConfig().getInitParameter( - "localDataDir"); - if (localDataDir != null) { - try { - File localDataDirFile = new File(localDataDir); - if (localDataDirFile.exists() && localDataDirFile.isDirectory()) { - for (File localDataFile : localDataDirFile.listFiles()) { - if (!localDataFile.isDirectory()) { - dataFileUrls.add("/data/" + localDataFile.getName()); - } - } - } - } catch (SecurityException e) { - /* We're not permitted to read the directory with metrics data - * files. Ignore. */ - } - } - - /* Prepare data structures that we're going to pass to the JSP. All - * data structures are (nested) maps with the map keys being used for - * displaying the files in tables and map values being 2-element - * arrays containing the file url and optional signature file. */ - SortedMap<Date, Map<String, String[]>> relayDescriptors = - new TreeMap<Date, Map<String, String[]>>( - java.util.Collections.reverseOrder()); - String[] certs = new String[2]; - SortedMap<Date, String[]> bridgeDescriptors = - new TreeMap<Date, String[]>(java.util.Collections.reverseOrder()); - String[] relayStatistics = new String[2]; - SortedMap<Date, String[]> torperfTarballs = - new TreeMap<Date, String[]>(java.util.Collections.reverseOrder()); - SortedMap<String, Map<String, String[]>> torperfData = - new TreeMap<String, Map<String, String[]>>(); - SortedMap<Date, String[]> exitLists = - new TreeMap<Date, String[]>(java.util.Collections.reverseOrder()); - SortedMap<Date, String[]> torperfExperiments = - new TreeMap<Date, String[]>(); - SortedMap<Date, String[]> bridgePoolAssignments = - new TreeMap<Date, String[]>(java.util.Collections.reverseOrder()); - - /* Prepare rewriting Torperf sources. */ - Map<String, String> torperfSources = new HashMap<String, String>(); - torperfSources.put("torperffast", "torperf, fastest"); - torperfSources.put("torperffastratio", "torperf, best ratio"); - torperfSources.put("torperfslow", "torperf, slowest"); - torperfSources.put("torperfslowratio", "torperf, worst ratio"); - - /* Go through the file list, decide for each file what metrics data - * type it is, and put it in the appropriate map. */ - SimpleDateFormat monthFormat = new SimpleDateFormat("yyyy-MM"); - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); - List<String> torperfFilesizes = Arrays.asList("50kb,1mb,5mb". - split(",")); - for (String url : dataFileUrls) { - if (!url.contains("/")) { - continue; - } - String filename = url.substring(url.lastIndexOf("/") + 1); - - /* URL contains relay descriptors. */ - if (filename.startsWith("tor-20") || - filename.startsWith("statuses-20") || - filename.startsWith("server-descriptors-20") || - filename.startsWith("extra-infos-20") || - filename.startsWith("votes-20") || - filename.startsWith("consensuses-20")) { - String type = filename.substring(0, filename.indexOf("-20")); - String yearMonth = filename.substring(filename.indexOf("20")); - yearMonth = yearMonth.substring(0, 7); - Date month = null; - try { - month = monthFormat.parse(yearMonth); - } catch (ParseException e) { - /* Ignore this URL. */ - continue; - } - int index = filename.endsWith(".asc") ? 1 : 0; - if (!relayDescriptors.containsKey(month)) { - relayDescriptors.put(month, new HashMap<String, String[]>()); - } - if (!relayDescriptors.get(month).containsKey(type)) { - relayDescriptors.get(month).put(type, new String[2]); - } - relayDescriptors.get(month).get(type)[index] = url; - - /* URL contains v3 certificates. */ - } else if (filename.startsWith("certs.tar")) { - int index = filename.endsWith(".asc") ? 1 : 0; - certs[index] = url; - - /* URL contains bridge descriptors. */ - } else if (filename.startsWith("bridge-descriptors-20")) { - String yearMonth = filename.substring(filename.indexOf("20")); - yearMonth = yearMonth.substring(0, 7); - Date month = null; - try { - month = monthFormat.parse(yearMonth); - } catch (ParseException e) { - /* Ignore this URL. */ - continue; - } - int index = filename.endsWith(".asc") ? 1 : 0; - if (!bridgeDescriptors.containsKey(month)) { - bridgeDescriptors.put(month, new String[2]); - } - bridgeDescriptors.get(month)[index] = url; - - /* URL contains relay statistics. */ - } else if (filename.startsWith("relay-statistics.tar.bz2")) { - int index = filename.endsWith(".asc") ? 1 : 0; - relayStatistics[index] = url; - - /* URL contains Torperf tarball. */ - } else if (filename.startsWith("torperf-20")) { - String yearMonth = filename.substring(filename.indexOf("20")); - yearMonth = yearMonth.substring(0, 7); - Date month = null; - try { - month = monthFormat.parse(yearMonth); - } catch (ParseException e) { - /* Ignore this URL. */ - continue; - } - if (!torperfTarballs.containsKey(month)) { - torperfTarballs.put(month, new String[2]); - } - torperfTarballs.get(month)[0] = url; - - /* URL contains Torperf data file. */ - } else if (filename.endsWith("b.data") || - filename.endsWith("b.extradata")) { - boolean isExtraData = filename.endsWith("b.extradata"); - String[] parts = filename.split("-"); - if (parts.length != 2) { - continue; - } - String source = parts[0]; - if (torperfSources.containsKey(source)) { - source = torperfSources.get(source); - } - String filesize = parts[1]; - filesize = filesize.substring(0, filesize.length() - - (isExtraData ? 10 : 5)); - if (!torperfFilesizes.contains(filesize)) { - continue; - } - if (!torperfData.containsKey(source)) { - torperfData.put(source, new HashMap<String, String[]>()); - } - if (!torperfData.get(source).containsKey(filesize)) { - torperfData.get(source).put(filesize, new String[2]); - } - torperfData.get(source).get(filesize)[isExtraData ? 1 : 0] = url; - - /* URL contains Torperf experiment tarball. */ - } else if (filename.startsWith("torperf-experiment-20")) { - String dateString = filename.substring(filename.indexOf("20")); - dateString = dateString.substring(0, 10); - Date date = null; - try { - date = dateFormat.parse(dateString); - } catch (ParseException e) { - /* Ignore this URL. */ - continue; - } - if (!torperfExperiments.containsKey(date)) { - torperfExperiments.put(date, new String[2]); - } - torperfExperiments.get(date)[0] = url; - - /* URL contains exit list. */ - } else if (filename.startsWith("exit-list-20")) { - String yearMonth = filename.substring(filename.indexOf("20")); - yearMonth = yearMonth.substring(0, 7); - Date month = null; - try { - month = monthFormat.parse(yearMonth); - } catch (ParseException e) { - /* Ignore this URL. */ - continue; - } - if (!exitLists.containsKey(month)) { - exitLists.put(month, new String[2]); - } - exitLists.get(month)[0] = url; - - /* URL contains bridge pool assignments. */ - } else if (filename.startsWith("bridge-pool-assignments-20")) { - String yearMonth = filename.substring(filename.indexOf("20")); - yearMonth = yearMonth.substring(0, 7); - Date month = null; - try { - month = monthFormat.parse(yearMonth); - } catch (ParseException e) { - /* Ignore this URL. */ - continue; - } - if (!bridgePoolAssignments.containsKey(month)) { - bridgePoolAssignments.put(month, new String[2]); - } - bridgePoolAssignments.get(month)[0] = url; - } - } - - /* Add the maps to the request and forward it to the JSP to display - * the page. */ - request.setAttribute("relayDescriptors", relayDescriptors); - request.setAttribute("certs", certs); - request.setAttribute("bridgeDescriptors", bridgeDescriptors); - request.setAttribute("relayStatistics", relayStatistics); - request.setAttribute("torperfData", torperfData); - request.setAttribute("exitLists", exitLists); - request.setAttribute("torperfTarballs", torperfTarballs); - request.setAttribute("torperfExperiments", torperfExperiments); - request.setAttribute("bridgePoolAssignments", bridgePoolAssignments); - request.getRequestDispatcher("WEB-INF/data.jsp").forward(request, - response); - } -} - diff --git a/src/org/torproject/ernie/web/research/ResearchFormatsServlet.java b/src/org/torproject/ernie/web/research/ResearchFormatsServlet.java deleted file mode 100644 index 3b70ca3..0000000 --- a/src/org/torproject/ernie/web/research/ResearchFormatsServlet.java +++ /dev/null @@ -1,24 +0,0 @@ -/* Copyright 2011, 2012 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.ernie.web.research; - -import java.io.IOException; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -public class ResearchFormatsServlet extends HttpServlet { - - private static final long serialVersionUID = 5666493868675314116L; - - public void doGet(HttpServletRequest request, - HttpServletResponse response) throws IOException, ServletException { - - /* Forward the request to the JSP that does all the hard work. */ - request.getRequestDispatcher("WEB-INF/formats.jsp").forward(request, - response); - } -} - diff --git a/src/org/torproject/ernie/web/research/ResearchPapersServlet.java b/src/org/torproject/ernie/web/research/ResearchPapersServlet.java deleted file mode 100644 index a63eef0..0000000 --- a/src/org/torproject/ernie/web/research/ResearchPapersServlet.java +++ /dev/null @@ -1,24 +0,0 @@ -/* Copyright 2011, 2012 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.ernie.web.research; - -import java.io.IOException; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -public class ResearchPapersServlet extends HttpServlet { - - private static final long serialVersionUID = -8135459207158536268L; - - public void doGet(HttpServletRequest request, - HttpServletResponse response) throws IOException, ServletException { - - /* Forward the request to the JSP that does all the hard work. */ - request.getRequestDispatcher("WEB-INF/papers.jsp").forward(request, - response); - } -} - diff --git a/src/org/torproject/ernie/web/research/ResearchStatsServlet.java b/src/org/torproject/ernie/web/research/ResearchStatsServlet.java deleted file mode 100644 index 2ca93a3..0000000 --- a/src/org/torproject/ernie/web/research/ResearchStatsServlet.java +++ /dev/null @@ -1,132 +0,0 @@ -/* Copyright 2013 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.ernie.web.research; - -import java.io.BufferedInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.util.SortedSet; -import java.util.TreeSet; - -import javax.servlet.ServletConfig; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -public class ResearchStatsServlet extends HttpServlet { - - private static final long serialVersionUID = 3346710354297653810L; - - private File statsDir; - - private SortedSet<String> availableStatisticsFiles; - - public void init(ServletConfig config) throws ServletException { - super.init(config); - this.statsDir = new File(config.getInitParameter("statsDir")); - this.availableStatisticsFiles = new TreeSet<String>(); - this.availableStatisticsFiles.add("servers"); - this.availableStatisticsFiles.add("bandwidth"); - this.availableStatisticsFiles.add("fast-exits"); - this.availableStatisticsFiles.add("clients"); - this.availableStatisticsFiles.add("torperf"); - this.availableStatisticsFiles.add("connbidirect"); - } - - public long getLastModified(HttpServletRequest request) { - File statsFile = this.determineStatsFile(request); - if (statsFile == null || !statsFile.exists()) { - return -1L; - } else { - return statsFile.lastModified(); - } - } - - public void doGet(HttpServletRequest request, - HttpServletResponse response) throws IOException, ServletException { - String requestURI = request.getRequestURI(); - if (requestURI.equals("/ernie/stats/")) { - this.writeDirectoryListing(request, response); - } else if (requestURI.equals("/ernie/stats.html")) { - this.writeStatisticsPage(request, response); - } else { - File statsFile = this.determineStatsFile(request); - if (statsFile == null) { - response.sendError(HttpServletResponse.SC_NOT_FOUND); - return; - } else if (!this.writeStatsFile(statsFile, response)) { - response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - } - } - } - - private void writeDirectoryListing(HttpServletRequest request, - HttpServletResponse response) throws IOException, ServletException { - request.setAttribute("directory", "/stats"); - request.setAttribute("extension", ".csv"); - request.setAttribute("files", this.availableStatisticsFiles); - request.getRequestDispatcher("/WEB-INF/dir.jsp").forward(request, - response); - } - - private void writeStatisticsPage(HttpServletRequest request, - HttpServletResponse response) throws IOException, ServletException { - request.getRequestDispatcher("/WEB-INF/stats.jsp").forward(request, - response); - } - - private File determineStatsFile(HttpServletRequest request) { - String requestedStatsFile = request.getRequestURI(); - if (requestedStatsFile.equals("/ernie/stats/") || - requestedStatsFile.equals("/ernie/stats.html")) { - return null; - } - if (requestedStatsFile.endsWith(".csv")) { - requestedStatsFile = requestedStatsFile.substring(0, - requestedStatsFile.length() - ".csv".length()); - } - if (requestedStatsFile.contains("/")) { - requestedStatsFile = requestedStatsFile.substring( - requestedStatsFile.lastIndexOf("/") + 1); - } - if (!availableStatisticsFiles.contains(requestedStatsFile)) { - return null; - } else { - return new File(this.statsDir, requestedStatsFile + ".csv"); - } - } - - private boolean writeStatsFile(File statsFile, - HttpServletResponse response) throws IOException, ServletException { - if (!statsFile.exists()) { - return false; - } - byte[] statsFileBytes; - try { - BufferedInputStream bis = new BufferedInputStream( - new FileInputStream(statsFile), 1024); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - byte[] buffer = new byte[1024]; - int length; - while ((length = bis.read(buffer)) > 0) { - baos.write(buffer, 0, length); - } - bis.close(); - statsFileBytes = baos.toByteArray(); - } catch (IOException e) { - return false; - } - String statsFileContent = new String(statsFileBytes); - response.setContentType("text/csv"); - response.setHeader("Content-Length", String.valueOf( - statsFileContent.length())); - response.setHeader("Content-Disposition", - "inline; filename=\"" + statsFile.getName() + "\""); - response.getWriter().print(statsFileContent); - return true; - } -} - diff --git a/src/org/torproject/ernie/web/research/ResearchToolsServlet.java b/src/org/torproject/ernie/web/research/ResearchToolsServlet.java deleted file mode 100644 index 173a1da..0000000 --- a/src/org/torproject/ernie/web/research/ResearchToolsServlet.java +++ /dev/null @@ -1,24 +0,0 @@ -/* Copyright 2011, 2012 The Tor Project - * See LICENSE for licensing information */ -package org.torproject.ernie.web.research; - -import java.io.IOException; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -public class ResearchToolsServlet extends HttpServlet { - - private static final long serialVersionUID = -3344204426180358872L; - - public void doGet(HttpServletRequest request, - HttpServletResponse response) throws IOException, ServletException { - - /* Forward the request to the JSP that does all the hard work. */ - request.getRequestDispatcher("WEB-INF/tools.jsp").forward(request, - response); - } -} - diff --git a/web/WEB-INF/banner.jsp b/web/WEB-INF/banner.jsp deleted file mode 100644 index 6bff272..0000000 --- a/web/WEB-INF/banner.jsp +++ /dev/null @@ -1,81 +0,0 @@ -<table class="banner" border="0" cellpadding="0" cellspacing="0" summary=""> -<tr> - <td class="banner-left"> - <a href="/index.html"> - <img src="/images/top-left.png" alt="Click to go to home page" - width="193" height="79"></a></td> - <td class="banner-middle"> - <% String currentPage = request.getRequestURI(); %> - <a <% if (currentPage.endsWith("index.jsp")) { - %>class="current"<%} else {%>href="/index.html"<%}%>>Home</a> - <a <% if (currentPage.endsWith("graphs.jsp")) { - %>class="current"<%} else {%>href="/graphs.html"<%}%>>Graphs</a> - <a <% if (currentPage.endsWith("research.jsp")) { - %>class="current"<%} else {%>href="/research.html"<%}%>>Research</a> - <a <% if (currentPage.endsWith("status.jsp")) { - %>class="current"<%} else {%>href="/status.html"<%}%>>Status</a> - <%if (currentPage.endsWith("graphs.jsp") || - currentPage.endsWith("network.jsp") || - currentPage.endsWith("bubbles.jsp") || - currentPage.endsWith("fast-exits.jsp") || - currentPage.endsWith("users.jsp") || - currentPage.endsWith("performance.jsp")) { - %><br> - <font size="2"> - <a <%if (currentPage.endsWith("network.jsp")){ - %>class="current"<%} else {%>href="/network.html"<%} - %>>Network</a> - <a <%if (currentPage.endsWith("fast-exits.jsp")){ - %>class="current"<%} else {%>href="/fast-exits.html"<%} - %>>Fast Exits</a> - <a <%if (currentPage.endsWith("bubbles.jsp")){ - %>class="current"<%} else {%>href="/bubbles.html"<%} - %>>Bubbles</a> - <a <%if (currentPage.endsWith("users.jsp")) { - %>class="current"<%} else {%>href="/users.html"<%} - %>>Users</a> - <a <%if (currentPage.endsWith("performance.jsp")) { - %>class="current"<%} else {%>href="/performance.html"<%} - %>>Performance</a> - </font> - <%} else if (currentPage.endsWith("status.jsp") || - currentPage.endsWith("exonerator.jsp") || - currentPage.endsWith("relay-search.jsp") || - currentPage.endsWith("consensus-health.jsp")) { - %><br> - <font size="2"> - <a <%if (currentPage.endsWith("exonerator.jsp")){ - %>class="current"<%} else {%>href="/exonerator.html"<%} - %>>ExoneraTor</a> - <a <%if (currentPage.endsWith("relay-search.jsp")){ - %>class="current"<%} else {%>href="/relay-search.html"<%} - %>>Relay Search</a> - <a <%if (currentPage.endsWith("consensus-health.jsp")){ - %>class="current"<%} else {%>href="/consensus-health.html"<%} - %>>Consensus Health</a> - </font> - <%} else if (currentPage.endsWith("research.jsp") || - currentPage.endsWith("data.jsp") || - currentPage.endsWith("formats.jsp") || - currentPage.endsWith("tools.jsp") || - currentPage.endsWith("stats.jsp")) { - %><br> - <font size="2"> - <a <%if (currentPage.endsWith("data.jsp")) { - %>class="current"<%} else {%> href="/data.html"<%} - %>>Data</a> - <a <%if (currentPage.endsWith("formats.jsp")) { - %>class="current"<%} else {%> href="/formats.html"<%} - %>>Formats</a> - <a <%if (currentPage.endsWith("tools.jsp")) { - %>class="current"<%} else {%> href="/tools.html"<%} - %>>Tools</a> - <a <%if (currentPage.endsWith("stats.jsp")) { - %>class="current"<%} else {%> href="/stats.html"<%} - %>>Statistics</a> - </font> - <%}%> - </td> - <td class="banner-right"></td> -</tr> -</table> diff --git a/web/WEB-INF/bubbles.jsp b/web/WEB-INF/bubbles.jsp deleted file mode 100644 index 59b3025..0000000 --- a/web/WEB-INF/bubbles.jsp +++ /dev/null @@ -1,40 +0,0 @@ -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> -<html> -<head> - <title>Tor Metrics Portal: Network bubble graphs</title> - <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> - <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet"> - <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon"> - <script src="/js/d3.min.js"></script> - <script src="/js/bubbles.js"></script> -</head> -<body> - <div class="center"> - <%@ include file="banner.jsp"%> - <div class="main-column"> - <p> - All relays: - <a href="#no-group" onclick="make_bubble_graph('no-group');">No group</a> | - <a href="#as" onclick="make_bubble_graph('as');">Autonomous Systems</a> | - <a href="#contact" onclick="make_bubble_graph('contact');">Contact</a> | - <a href="#country" onclick="make_bubble_graph('country');">Country</a> | - <a href="#network-family" onclick="make_bubble_graph('network-family');">Network family (/16)</a> - </p> - <p> - Exits only: - <a href="#no-group-exits-only" onclick="make_bubble_graph('no-group-exits-only');">No group</a> | - <a href="#as-exits-only" onclick="make_bubble_graph('as-exits-only');">Autonomous Systems</a> | - <a href="#contact-exits-only" onclick="make_bubble_graph('contact-exits-only');">Contact</a> | - <a href="#country-exits-only" onclick="make_bubble_graph('country-exits-only');">Country</a> | - <a href="#network-family-exits-only" onclick="make_bubble_graph('network-family-exits-only');">Network family (/16)</a> - </p> - <script>make_bubble_graph();</script> - <noscript>Sorry, you need to turn on JavaScript.</script> - </div> - </div> - <div class="bottom" id="bottom"> - <%@ include file="footer.jsp"%> - </div> -</body> -</html> diff --git a/web/WEB-INF/data.jsp b/web/WEB-INF/data.jsp deleted file mode 100644 index 06176db..0000000 --- a/web/WEB-INF/data.jsp +++ /dev/null @@ -1,286 +0,0 @@ -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> -<fmt:setLocale value="en_US"/> -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> -<html> -<head> - <title>Tor Metrics Portal: Data</title> - <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> - <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet"> - <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon"> -</head> -<body> - <div class="center"> - <%@ include file="banner.jsp"%> - <div class="main-column"> - <h2>Tor Metrics Portal: Data</h2> - <br> - <p>One of the main goals of the Tor Metrics Project is to make all - gathered data available to the public. This approach enables - privacy researchers to perform their own analyses using real data - on the Tor network, and it acts as a safeguard to not gather data - that are too sensitive to publish. The following data are - available (see the <a href="tools.html">Tools</a> section for - details on processing the files):</p> - <ul> - <li><a href="#relaydesc">Relay descriptor archives</a></li> - <li><a href="#bridgedesc">Bridge descriptor archives</a></li> - <li><a href="#bridgeassignments">Bridge pool assignments</a></li> - <li><a href="#performance">Performance data</a></li> - <li><a href="#exitlist">Exit lists</a></li> - </ul> - <p>The tarballs listed on this page and the raw files that were - published on the last three days are also available via - "rsync metrics.torproject.org::".</p> - <br> - <a name="relaydesc"></a> - <h3><a href="#relaydesc" class="anchor">Relay descriptor - archives</a></h3> - <br> - <p>The relay descriptor archives contain all documents that the - directory authorities make available about the network of relays. - These documents include network statuses, server (relay) - descriptors, and extra-info descriptors. - The data formats are described <a href="formats.html">here</a>.</p> - <table width="100%" border="0" cellpadding="5" cellspacing="0" summary=""> - <c:forEach var="item" items="${relayDescriptors}" > - <fmt:formatDate var="longDate" pattern="MMMM yyyy" - value="${item.key}"/> - <tr> - <td>${longDate}</td> - <td> - <c:if test="${item.value['tor'] ne null}" > - <a href="${item.value['tor'][0]}">v1 directories</a> - <c:if test="${item.value['tor'][1] ne null}"> - (<a href="${item.value['tor'][1]}">sig</a>) - </c:if> - </c:if> - </td> - <td> - <c:if test="${item.value['statuses'] ne null}" > - <a href="${item.value['statuses'][0]}">v2 statuses</a> - <c:if test="${item.value['statuses'][1] ne null}"> - (<a href="${item.value['statuses'][1]}">sig</a>) - </c:if> - </c:if> - </td> - <td> - <c:if test="${item.value['server-descriptors'] ne null}" > - <a href="${item.value['server-descriptors'][0]}">server descriptors</a> - <c:if test="${item.value['server-descriptors'][1] ne null}"> - (<a href="${item.value['server-descriptors'][1]}">sig</a>) - </c:if> - </c:if> - </td> - <td> - <c:if test="${item.value['extra-infos'] ne null}" > - <a href="${item.value['extra-infos'][0]}">extra-infos</a> - <c:if test="${item.value['extra-infos'][1] ne null}"> - (<a href="${item.value['extra-infos'][1]}">sig</a>) - </c:if> - </c:if> - </td> - <td> - <c:if test="${item.value['votes'] ne null}" > - <a href="${item.value['votes'][0]}">v3 votes</a> - <c:if test="${item.value['votes'][1] ne null}"> - (<a href="${item.value['votes'][1]}">sig</a>) - </c:if> - </c:if> - </td> - <td> - <c:if test="${item.value['consensuses'] ne null}" > - <a href="${item.value['consensuses'][0]}">v3 consensuses</a> - <c:if test="${item.value['consensuses'][1] ne null}"> - (<a href="${item.value['consensuses'][1]}">sig</a>) - </c:if> - </c:if> - </td> - </tr> - </c:forEach> - </table> - <c:if test="${certs[0] ne null}"> - <br> - <p>In order to verify the v3 votes and v3 consensuses, download - the tarball of <a href="${certs[0]}">v3 certificates</a> - <c:if test="${certs[1] ne null}"> - (<a href="${certs[1]}">sig</a>) - </c:if> - which is updated whenever new v3 certificates become available.</p> - </c:if> - <c:if test="${relayStatistics[0] ne null}"> - <br> - <p>Some of the relays are configured to gather statistics on the - number of requests or connecting clients, the number of - processed cells per queue, or the number of exiting bytes per - port. Relays running version 0.2.2.4-alpha or higher can include - these statistics in extra-info descriptors, so that they are - included in the relay descriptor archives. This - <a href="${relayStatistics[0]}">archive</a> - <c:if test="${relayStatistics[1] ne null}"> - (<a href="${relayStatistics[1]}">sig</a>) - </c:if> - contains the statistics produced by relays running earlier - versions.</p> - </c:if> - <br> - <a name="bridgedesc"></a> - <h3><a href="#bridgedesc" class="anchor">Bridge descriptor - archives</a></h3> - <br> - <p>The bridge descriptor archives contain similar documents as the - relay descriptor archives, but for the non-public bridges. The - descriptors have been sanitized before publication to remove all - information that could otherwise be used to locate bridges. The - files below contain all documents of a given month, including - bridge network statuses, bridge server descriptors, and bridge - extra-info descriptors. The sanitizing process is described - <a href="formats.html#bridgedesc">here</a>.</p> - <table width="100%" border="0" cellpadding="5" cellspacing="0" summary=""> - <c:forEach var="item" items="${bridgeDescriptors}" > - <fmt:formatDate var="longDate" pattern="MMMM yyyy" - value="${item.key}"/> - <tr> - <td> - <a href="${item.value[0]}">${longDate}</a> - <c:if test="${item.value[1] ne null}"> - (<a href="${item.value[1]}">sig</a>) - </c:if> - </td> - </tr> - </c:forEach> - </table> - <p></p> - <br> - <a name="bridgeassignments"></a> - <h3><a href="#bridgeassignments" class="anchor">Bridge pool - assignments</a></h3> - <br> - <p>BridgeDB periodically dumps the list of running bridges with - information about the rings, subrings, and file buckets to which - they are assigned to a local file. We are archiving sanitized - versions of these files here to analyze how the pool assignment - affects a bridge's usage.</p> - The data format and sanitizing process is described - <a href="formats.html#bridgepool">here</a>.</p> - <table width="100%" border="0" cellpadding="5" cellspacing="0" summary=""> - <c:forEach var="item" items="${bridgePoolAssignments}" > - <fmt:formatDate var="longDate" pattern="MMMM yyyy" - value="${item.key}"/> - <tr> - <td> - <a href="${item.value[0]}">${longDate}</a> - </td> - </tr> - </c:forEach> - </table> - <br> - <a name="performance"></a> - <h3><a href="#performance" class="anchor">Performance - data</a></h3> - <br> - <p>We are continuously measuring the performance of the Tor - network by periodically requesting files of different sizes and - recording the time needed to do so. These measurements take place - on moria, siv, and torperf and use an unmodified Tor client. - The files below contain the output of the torperf application. - The data format is described - <a href="formats.html#torperf">here</a>.</p> - <table width="100%" border="0" cellpadding="5" cellspacing="0" summary=""> - <c:forEach var="item" items="${torperfTarballs}" > - <fmt:formatDate var="longDate" pattern="MMMM yyyy" - value="${item.key}"/> - <tr> - <td> - <a href="${item.value[0]}">${longDate}</a> - </td> - </tr> - </c:forEach> - </table> - <br> - <p>The output above is the result of combining torperf request - data with information about used paths. - The raw files are also available below.</p> - <table width="100%" border="0" cellpadding="5" cellspacing="0" summary=""> - <c:forEach var="item" items="${torperfData}" > - <tr> - <td>${item.key}</td> - <td> - <c:if test="${item.value['50kb'] ne null}" > - <c:if test="${item.value['50kb'][0] ne null}" > - <a href="${item.value['50kb'][0]}">50 KiB requests</a> - </c:if> - <c:if test="${item.value['50kb'][1] ne null}" > - <a href="${item.value['50kb'][1]}">50 KiB path info</a> - </c:if> - </c:if> - </td> - <td> - <c:if test="${item.value['1mb'] ne null}" > - <c:if test="${item.value['1mb'][0] ne null}" > - <a href="${item.value['1mb'][0]}">1 MiB requests</a> - </c:if> - <c:if test="${item.value['1mb'][1] ne null}" > - <a href="${item.value['1mb'][1]}">1 MiB path info</a> - </c:if> - </c:if> - </td> - <td> - <c:if test="${item.value['5mb'] ne null}" > - <c:if test="${item.value['5mb'][0] ne null}" > - <a href="${item.value['5mb'][0]}">5 MiB requests</a> - </c:if> - <c:if test="${item.value['5mb'][1] ne null}" > - <a href="${item.value['5mb'][1]}">5 MiB path info</a> - </c:if> - </c:if> - </td> - </tr> - </c:forEach> - </table> - <br> - <p>We further conducted additional experiments with Torperf in the - past by modifying the guard node selection strategies or circuit - build timeouts. The modified guard node selection strategies are - to pick guard nodes from sets of the a) absolute fastest, b) - absolute slowest, c) best rated vs. advertised ratio or d) worst - rated vs. advertised ratio nodes. The ratio mechanisms provide a - way to select the nodes that the bandwidth authorities think stand - out in their measurement. Experiments are listed by the date when - they ended. Details about the experiment setup are contained in a - README file in the tarballs.</p> - <table width="100%" border="0" cellpadding="5" cellspacing="0" summary=""> - <c:forEach var="item" items="${torperfExperiments}" > - <fmt:formatDate var="endDate" pattern="MMMM dd, yyyy" - value="${item.key}"/> - <tr><td><a href="${item.value[0]}">${endDate}</a></td></tr> - </c:forEach> - </table> - <br> - <a name="exitlist"></a> - <h3><a href="#exitlist" class="anchor">Exit lists</a></h3> - <br> - <p>We are archiving the bulk exit lists used by - <a href="https://check.torproject.org/">Tor Check</a> - containing the IP addresses that exit relays exit from. - The data format is described in - <a href="https://www.torproject.org/tordnsel/exitlist-spec.txt">exitlist-spec.txt</a> - and <a href="formats.html#exitlist">here</a>.</p> - <table width="100%" border="0" cellpadding="5" cellspacing="0" summary=""> - <c:forEach var="item" items="${exitLists}" > - <fmt:formatDate var="longDate" pattern="MMMM yyyy" - value="${item.key}"/> - <tr> - <td> - <a href="${item.value[0]}">${longDate}</a> - </td> - </tr> - </c:forEach> - </table> - </div> - </div> - <div class="bottom" id="bottom"> - <%@ include file="footer.jsp"%> - </div> -</body> -</html> diff --git a/web/WEB-INF/dir.jsp b/web/WEB-INF/dir.jsp deleted file mode 100644 index 1ac1819..0000000 --- a/web/WEB-INF/dir.jsp +++ /dev/null @@ -1,25 +0,0 @@ -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> -<html> -<head> - <title>Tor Metrics Portal: Index of ${directory}</title> - <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> - <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet"> - <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon"> -</head> -<body> - <div class="center"> - <%@ include file="banner.jsp"%> - <div class="main-column"> - <h2>Tor Metrics Portal: Index of ${directory}</h2> - <br> - <c:forEach var="file" items="${files}" > - <a href="${directory}/${file}${extension}">${file}${extension}</a><br> - </c:forEach> - </div> - </div> - <div class="bottom" id="bottom"> - <%@ include file="footer.jsp"%> - </div> -</body> -</html> diff --git a/web/WEB-INF/error.jsp b/web/WEB-INF/error.jsp deleted file mode 100644 index 9c5150e..0000000 --- a/web/WEB-INF/error.jsp +++ /dev/null @@ -1,77 +0,0 @@ -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ page isErrorPage="true" %> -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> -<html> -<head> - <title>Tor Metrics Portal: Error</title> - <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> - <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet"> - <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon"> -</head> -<body> - <div class="center"> - <%@ include file="banner.jsp"%> - <div class="main-column"> -<h2>Tor Metrics Portal: Error</h2> -<br> -<p> -Oops! Something went wrong here! We encountered a -<b> -<c:choose> -<c:when test="${pageContext.errorData.statusCode eq 400}"> -400 Bad Request -</c:when> -<c:when test="${pageContext.errorData.statusCode eq 404}"> -404 Not Found -</c:when> -<c:when test="${pageContext.errorData.statusCode eq 500}"> -500 Internal Server Error -</c:when> -<c:when test="${not empty pageContext.errorData.throwable}"> -${pageContext.exception} -</c:when> -<c:otherwise> -Unknown Error -</c:otherwise> -</c:choose> -</b> -when processing your request!</p> - -<p> -Maybe you find what you're looking for on our sitemap: -<ul> -<li><a href="index.html">Home</a></li> -<li><a href="graphs.html">Graphs</a> -<ul> -<li><a href="network.html">Network</a></li> -<li><a href="fast-exits.html">Fast Exits</a></li> -<li><a href="bubbles.html">Bubbles</a></li> -<li><a href="users.html">Users</a></li> -<li><a href="performance.html">Performance</a></li> -</ul></li> -<li><a href="research.html">Research</a> -<ul> -<li><a href="data.html">Data</a></li> -<li><a href="formats.html">Formats</a></li> -<li><a href="tools.html">Tools</a></li> -<li><a href="stats.html">Statistics</a></li> -</ul></li> -<li><a href="status.html">Status</a> -<ul> -<li><a href="exonerator.html">ExoneraTor</a></li> -<li><a href="relay-search.html">Relay Search</a></li> -<li><a href="consensus-health.html">Consensus Health</a></li> -</ul></li> -</ul> -</p> - -<p>If this problem persists, please -<a href="mailto:tor-assistants@torproject.org">let us know</a>!</p> - - </div> - </div> - <div class="bottom" id="bottom"> - <%@ include file="footer.jsp"%> - </div> -</body> -</html> diff --git a/web/WEB-INF/exonerator.jsp b/web/WEB-INF/exonerator.jsp deleted file mode 100644 index 0eefe99..0000000 --- a/web/WEB-INF/exonerator.jsp +++ /dev/null @@ -1,45 +0,0 @@ -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> -<html> -<head> - <title>Tor Metrics Portal: ExoneraTor</title> - <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> - <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet"> - <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon"> -</head> -<body> - <div class="center"> - <%@ include file="banner.jsp"%> - <div class="main-column" style="margin:5; Padding:0;"> - <h2>ExoneraTor</h2> - <h3>or: a website that tells you whether a given IP address was a Tor - relay</h3> - <br> - <p>Just because you see an Internet connection from a particular IP - address does not mean you know <i>who</i> originated the traffic. Tor - anonymizes Internet traffic by "<a href="https://www.torproject.org/about/overview#thesolution">onion - routing</a>," sending packets through a series of encrypted hops - before they reach their destination. Therefore, if you see traffic - from a Tor node, you may be seeing traffic that originated from - someone using Tor, rather than from the node operator itself. The Tor - Project and Tor node operators have no records of the traffic that - passes over the network, but we do maintain current and historical - records of which IP addresses are part of the Tor network.</p> - <br> - <p>ExoneraTor tells you whether there was a Tor relay running on a - given IP address at a given time. ExoneraTor can further indicate - whether this relay permitted exiting to a given server and/or TCP - port. ExoneraTor learns these facts by parsing the public relay lists - and relay descriptors that are collected from the Tor directory - authorities and the exit lists collected by TorDNSEL. By inputting an - IP address and time, you can determine whether that IP was then a part - of the Tor network.</p> - <br> - <p><font color="red"><b>Notice:</b> This service has moved to: - <a href="https://exonerator.torproject.org/">https://exonerator.torproject.org/</a></font></p> - </div> - </div> - <div class="bottom" id="bottom"> - <%@ include file="footer.jsp"%> - </div> -</body> -</html> diff --git a/web/WEB-INF/fast-exits.jsp b/web/WEB-INF/fast-exits.jsp deleted file mode 100644 index 903c88c..0000000 --- a/web/WEB-INF/fast-exits.jsp +++ /dev/null @@ -1,81 +0,0 @@ -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> -<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> -<html> -<head> - <title>Tor Metrics Portal: Fast Exits</title> - <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> - <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet"> - <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon"> -</head> -<body> - <div class="center"> - <%@ include file="banner.jsp"%> - <div class="main-column"> -<h2>Tor Metrics Portal: Fast Exits</h2> -<br> -<p>This page summarizes progress in operating 125 fast exits, which is a -deliverable for -<a href="https://trac.torproject.org/projects/tor/wiki/org/sponsors/SponsorJ">SponsorJ</a>.</p> -<br> - -<a name="fastexits"></a> -<h3><a href="#fastexits" class="anchor">Relays meeting the fast-exit -requirements</a></h3> -<br> -<img src="fast-exits.png${fast_exits_url}" - width="576" height="432" alt="Fast exits graph"> -<form action="fast-exits.html#fastexits"> - <div class="formrow"> - <input type="hidden" name="graph" value="fast-exits"> - <p> - <label>Start date (yyyy-mm-dd):</label> - <input type="text" name="start" size="10" - value="<c:choose><c:when test="${fn:length(fast_exits_start) == 0}">${default_start_date}</c:when><c:otherwise>${fast_exits_start[0]}</c:otherwise></c:choose>"> - <label>End date (yyyy-mm-dd):</label> - <input type="text" name="end" size="10" - value="<c:choose><c:when test="${fn:length(fast_exits_end) == 0}">${default_end_date}</c:when><c:otherwise>${fast_exits_end[0]}</c:otherwise></c:choose>"> - </p><p> - <input class="submit" type="submit" value="Update graph"> - </p> - </div> -</form> -<p>Download graph as -<a href="fast-exits.pdf${fast_exits_url}">PDF</a> or -<a href="fast-exits.svg${fast_exits_url}">SVG</a>.</p> -<br> - -<a name="almostfastexits"></a> -<h3><a href="#almostfastexits" class="anchor">Relays almost meeting the -fast-exit requirements</a></h3> -<br> -<img src="almost-fast-exits.png${almost_fast_exits_url}" - width="576" height="432" alt="Almost fast exits graph"> -<form action="fast-exits.html#almostfastexits"> - <div class="formrow"> - <input type="hidden" name="graph" value="almost-fast-exits"> - <p> - <label>Start date (yyyy-mm-dd):</label> - <input type="text" name="start" size="10" - value="<c:choose><c:when test="${fn:length(almost_fast_exits_start) == 0}">${default_start_date}</c:when><c:otherwise>${almost_fast_exits_start[0]}</c:otherwise></c:choose>"> - <label>End date (yyyy-mm-dd):</label> - <input type="text" name="end" size="10" - value="<c:choose><c:when test="${fn:length(almost_fast_exits_end) == 0}">${default_end_date}</c:when><c:otherwise>${almost_fast_exits_end[0]}</c:otherwise></c:choose>"> - </p><p> - <input class="submit" type="submit" value="Update graph"> - </p> - </div> -</form> -<p>Download graph as -<a href="almost-fast-exits.pdf${almost_fast_exits_url}">PDF</a> or -<a href="almost-fast-exits.svg${almost_fast_exits_url}">SVG</a>.</p> -<br> - - </div> - </div> - <div class="bottom" id="bottom"> - <%@ include file="footer.jsp"%> - </div> -</body> -</html> diff --git a/web/WEB-INF/footer.jsp b/web/WEB-INF/footer.jsp deleted file mode 100644 index cab9e7f..0000000 --- a/web/WEB-INF/footer.jsp +++ /dev/null @@ -1,13 +0,0 @@ - <p>This material is supported in part by the National Science - Foundation under Grant No. CNS-0959138. Any opinions, - finding, and conclusions or recommendations expressed in this - material are those of the author(s) and do not necessarily reflect - the views of the National Science Foundation.</p> - <p>"Tor" and the "Onion Logo" are <a href="https://www.torproject.org/docs/trademark-faq.html.en">registered trademarks</a> of The Tor Project, Inc.</p> - <p>Data on this site is freely available under a - <a href="http://creativecommons.org/publicdomain/zero/1.0/">CC0 no - copyright declaration</a>: To the extent possible under law, the Tor - Project has waived all copyright and related or neighboring rights - in the data. Graphs are licensed under a - <a href="http://creativecommons.org/licenses/by/3.0/us/">Creative - Commons Attribution 3.0 United States License</a>.</p> diff --git a/web/WEB-INF/formats.jsp b/web/WEB-INF/formats.jsp deleted file mode 100644 index 4d297b2..0000000 --- a/web/WEB-INF/formats.jsp +++ /dev/null @@ -1,986 +0,0 @@ -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> -<html> -<head> - <title>Tor Metrics Portal: Data Formats</title> - <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> - <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet"> - <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon"> -</head> -<body> -<div class="center"> -<%@ include file="banner.jsp"%> -<div class="main-column"> -<h2>Tor Metrics Portal: Data Formats</h2> -<br> - -<p> -Statistical analysis in the Tor network can be performed using various -kinds of data. -This page gives an overview of three major data sources for -statistics in the Tor network:</p> - -<ol> -<li>First, we recap measuring the Tor network from public directory -information (<a href="https://research.torproject.org/techreports/metrics-2009-08-07.pdf">PDF</a>) -by describing the data format of -<a href="#serverdesc">server descriptors and network statuses</a>, -and we explain the sanitzation process of (non-public) -<a href="#bridgedesc">bridge directory information</a>.</li> -<li>Second, we describe the numerous aggregate statistics that relays -publish about their usage (<a href="http://freehaven.net/anonbib/cache/wecsr10measuring-tor.pdf">PDF</a>), -including -<a href="#bytehist">byte histories</a>, -<a href="#dirreqstats">directory request statistics</a>, -<a href="#entrystats">connecting client statistics</a>, -<a href="#bridgestats">bridge user statistics</a>, -<a href="#cellstats">cell-queue statistics</a>, -<a href="#exitstats">exit-port statistics</a>, and -<a href="#bidistats">bidirectional connection use</a>.</li> -<li>Third, we delineate the output of various Tor services like -<a href="#bridgepool">BridgeDB</a>, or -<a href="#exitlist">Tor Check</a> as well as specific measurement tools like -<a href="#torperf">Torperf</a>.</li> -</ol> - -<p> -All data described on this page are available for download on the -<a href="data.html">data</a> page. -This page is based on a technical report -(<a href="https://research.torproject.org/techreports/data-2011-03-14.pdf">PDF</a>) -and is very likely more recent than the report. -</p> -<hr> -<br> - -<a name="descriptortypes"></a> -<h3><a href="#descriptortypes" class="anchor">Descriptor types</a></h3> -<br> -<p> -Any file containing descriptors described on this page may contain meta -data in its first text line using the format -<tt>@type $descriptortype $major.$minor</tt>. -Any tool that processes these descriptors may parse files without meta -data or with an unknown descriptor type at its own risk, can safely parse -files with known descriptor type and same major version number, and should -not parse files with known descriptor type and higher major version -number. -</p> - -<p> -The following descriptor types and versions are known. -Gray entries are deprecated, black entries are recent: -</p> - -<ul> -<li><tt>@type server-descriptor 1.0</tt></li> -<li><tt>@type microdescriptor 1.0</tt></li> -<li><tt>@type extra-info 1.0</tt></li> -<li><tt>@type directory 1.0</tt></li> -<li><tt>@type network-status-2 1.0</tt></li> -<li><tt>@type dir-key-certificate-3 1.0</tt></li> -<li><tt>@type network-status-consensus-3 1.0</tt></li> -<li><tt>@type network-status-vote-3 1.0</tt></li> -<li><tt>@type network-status-microdesc-consensus-3 1.0</tt></li> -<li><tt>@type bridge-network-status 1.0</tt></li> -<li><tt>@type bridge-server-descriptor 1.0</tt></li> -<li><font color="gray"><tt>@type bridge-extra-info 1.0</tt></font></li> -<li><font color="gray"><tt>@type bridge-extra-info 1.1</tt> contain - sanitized <tt>transport</tt> lines</font></li> -<li><tt>@type bridge-extra-info 1.2</tt> contain <tt>ntor-onion-key</tt> - lines</li> -<li><tt>@type torperf 1.0</tt></li> -<li><tt>@type bridge-pool-assignment 1.0</tt></li> -<li><tt>@type tordnsel 1.0</tt></li> -</ul> - -<hr> -<br> - -<a name="serverdesc"></a> -<h3><a href="#serverdesc" class="anchor">Server descriptors and network -statuses</a></h3> -<br> -<p> -Relays in the Tor network report their capabilities by publishing server -descriptors to the directory authorities. -The directory authorities confirm reachability of relays and assign flags -to help clients make good path selections. -Every hour, the directory authorities publish a network status consensus -with all known running relays at the time. -Both server descriptors and network statuses constitute a solid data basis -for statistical analysis in the Tor network. -We described the approach to measure the Tor network from public directory -information in the HotPETS 2009 paper -(<a href="https://research.torproject.org/techreports/metrics-2009-08-07.pdf">PDF</a>) -and provide interactive -<a href="graphs.html">graphs</a> on the metrics -website. -We briefly describe the most interesting pieces of the -two descriptor formats that can be used for statistics. -</p> - -<p> -The server descriptors published by relays at least once every 18 hours -contain the necessary information for clients to build circuits using a -given relay. -These server descriptors can also be useful for statistical analysis of -the Tor network infrastructure. -</p> - -<p> -We assume that the majority of server descriptors are correct. -But when performing statistical analysis on server descriptors, one has to -keep in mind that only a small subset of the information written to server -descriptors is confirmed by the trusted directory authorities. -In theory, relays can provide false information in their server -descriptors, even though the incentive to do so is probably low. -</p> - -<blockquote> -<p> -<i>Server descriptor published by relay <tt>blutmagie</tt> (without -cryptographic keys and hashes):</i> -</p> -<p> -<tt>router blutmagie 192.251.226.206 443 0 80</tt><br> -<tt>platform Tor 0.2.2.20-alpha on Linux x86_64</tt><br> -<tt>opt protocols Link 1 2 Circuit 1</tt><br> -<tt>published 2010-12-27 14:35:27</tt><br> -<tt>opt fingerprint 6297 B13A 687B 521A 59C6 BD79 188A 2501 EC03 A065</tt><br> -<tt>uptime 445412</tt><br> -<tt>bandwidth 14336000 18432000 15905178</tt><br> -<tt>opt extra-info-digest 5C1D5D6F8B243304079BC15CD96C7FCCB88322D4</tt><br> -<tt>opt caches-extra-info</tt><br> -<tt>onion-key</tt><br> -<tt>[...]</tt><br> -<tt>signing-key</tt><br> -<tt>[...]</tt><br> -<tt>family $66CA87E164F1CFCE8C3BB5C095217A28578B8BAF - $67EC84376D9C4C467DCE8621AACA109160B5264E - $7B698D327F1695590408FED95CDEE1565774D136</tt><br> -<tt>opt hidden-service-dir</tt><br> -<tt>contact abuse@blutmagie.de</tt><br> -<tt>reject 0.0.0.0/8:*</tt><br> -<tt>reject 169.254.0.0/16:*</tt><br> -<tt>reject 127.0.0.0/8:*</tt><br> -<tt>reject 192.168.0.0/16:*</tt><br> -<tt>reject 10.0.0.0/8:*</tt><br> -<tt>reject 172.16.0.0/12:*</tt><br> -<tt>reject 192.251.226.206:*</tt><br> -<tt>reject *:25</tt><br> -<tt>reject *:119</tt><br> -<tt>reject *:135-139</tt><br> -<tt>reject *:445</tt><br> -<tt>reject *:465</tt><br> -<tt>reject *:563</tt><br> -<tt>reject *:587</tt><br> -<tt>reject *:1214</tt><br> -<tt>reject *:4661-4666</tt><br> -<tt>reject *:6346-6429</tt><br> -<tt>reject *:6660-6999</tt><br> -<tt>accept *:*</tt><br> -<tt>router-signature</tt><br> -<tt>[...]</tt><br> -</p> -</blockquote> - -<p> -The document above shows an example server descriptor. -The following data fields in server descriptors may be relevant to -statistical analysis: -</p> - -<ul> -<li><b>IP address and ports:</b> Relays provide their IP address -and ports where they accept requests to build circuits and directory -requests. -These data fields are contained in the first line of a server descriptor -starting with <tt>router</tt>. -Note that in rare cases, the IP address provided here can be different -from the IP address used for exiting to the Internet. -The latter can be found in the exit lists produced by Tor Check as -described in the <a href="#exitlist">Tor Check exit lists</a> section below. -</li> -<li><b>Operating system and Tor software version:</b> Relays include -their operating system and Tor software version in their server -descriptors in the <tt>platform</tt> line. -While this information is very likely correct in most cases, a few relay -operators may try to impede hacking attempts by providing false platform -strings.</li> -<li><b>Uptime:</b> Relays include the number of seconds since the -last restart in their server descriptor in the <tt>uptime</tt> line.</li> -<li><b>Own measured bandwidth:</b> Relays report the bandwidth that -they are willing to provide on average and for short periods of time. -Relays also perform periodic bandwidth self-tests and report their actual -available bandwidth. -The latter was used by clients to weight relays in the path selection -algorithm and was sometimes subject to manipulation by malicious relays. -All three bandwidth values can be found in a server descriptor's -<tt>bandwidth</tt> line. -With the introduction of -<a href="http://gitweb.torproject.org/torflow.git/">bandwidth scanners</a>, the self-reported relay -bandwidth in server descriptors has become less -relevant.</li> -<li><b>Relay family:</b> Some relay operators who run more than one -relay organize their relays in relay families, so that clients don't pick -more than one of these relays for a single circuit. -Each relay belonging to a relay family lists the members of that family -either by nickname or fingerprint in its server descriptor in the -<tt>family</tt> line.</li> -<li><b>Exit policy:</b> Relays define their exit policy by including -firewall-like rules which outgoing connections they reject or accept in -the <tt>reject</tt> and <tt>accept</tt> lines.</li> -</ul> - -<p> -These are just a subset of the fields in a server descriptor that seem -relevant for statistical analysis. -For a complete list of fields in server descriptors, see the <a href="https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt">directory -protocol specification</a>. -</p> - -<p> -Every hour, the directory authorities publish a new network status that -contains a list of all running relays. -The directory authorities confirm reachability of the contained relays and -assign flags based on the relays' characteristics. -The entries in a network status reference the last published server -descriptor of a relay. -</p> - -<p> -The network statuses are relevant for statistical analysis, because they -constitute trusted snapshots of the Tor network. -Anyone can publish as many server descriptors as they want, but only the -directory authorities can confirm that a relay was running at a given -time. -Most statistics on the Tor network infrastructure rely on network statuses -and possibly combine them with the referenced server descriptors. -The document below shows the network status entry referencing -the server descriptor above. -In addition to the reachability information, network statuses contain the -following fields that may be relevant for statistical analysis: -</p> - -<blockquote> -<p> -<i>Network status entry of relay <tt>blutmagie</tt>:</i> -</p> -<p> -<tt>r blutmagie YpexOmh7UhpZxr15GIolAewDoGU - lFY7WmD/yvVFp9drmZzNeTxZ6dw 2010-12-27 14:35:27 192.251.226.206 - 443 80</tt><br> -<tt>s Exit Fast Guard HSDir Named Running Stable V2Dir Valid</tt><br> -<tt>v Tor 0.2.2.20-alpha</tt><br> -<tt>w Bandwidth=30800</tt><br> -<tt>p reject 25,119,135-139,445,465,563,587,1214,4661-4666,6346-6429,6660-6999</tt><br> -</p> -</blockquote> - -<ul> -<li><b>Relay flags:</b> The directory authorities assign flags to -relays based on their characteristics to the line starting with <tt>s</tt>. -Examples are the <tt>Exit</tt> flag if a relay permits exiting to the -Internet and the <tt>Guard</tt> flag if a relay is stable enough to be -picked as guard node</li> -<li><b>Relay version:</b> The directory authorities include the -version part of the platform string written to server descriptors in the -network status in the line starting with <tt>v</tt>.</li> -<li><b>Bandwidth weights:</b> The network status contains a bandwidth -weight for every relay in the lines with <tt>w</tt> that clients shall use -for weighting relays in their path selection algorithm. -This bandwidth weight is either the self-reported bandwidth of the relay -or the bandwidth measured by the bandwidth scanners.</li> -<li><b>Exit policy summary:</b> Every entry in a network status -contains a summary version of a relay's exit policy in the line starting -with <tt>p</tt>. -This summary is a list of accepted or rejected ports for exit to most IP -addresses.</li> -</ul> -<hr> -<br> - -<a name="bridgedesc"></a> -<h3><a href="#bridgedesc" class="anchor">Sanitized bridge -descriptors</a></h3> -<br> -Bridges in the Tor network publish server descriptors to the bridge -authority which in turn generates a bridge network status. -We cannot, however, make the bridge server descriptors and bridge network -statuses available for statistical analysis as we do with the relay server -descriptors and relay network statuses. -The problem is that bridge server descriptors and network statuses contain -bridge IP addresses and other sensitive information that shall not be made -publicly available. -We therefore sanitize bridge descriptors by removing all potentially -identifying information and publish sanitized versions of the descriptors. -The processing steps for sanitizing bridge descriptors are as follows: - -<ol> -<li><b>Replace the bridge identity with its SHA1 value:</b> Clients -can request a bridge's current descriptor by sending its identity string -to the bridge authority. -This is a feature to make bridges on dynamic IP addresses useful. -Therefore, the original identities (and anything that could be used to -derive them) need to be removed from the descriptors. -The bridge identity is replaced with its SHA1 hash value. -The idea is to have a consistent replacement that remains stable over -months or even years (without keeping a secret for a keyed hash function).</li> -<li><b>Remove all cryptographic keys and signatures:</b> It would be -straightforward to learn about the bridge identity from the bridge's -public key. -Replacing keys by newly generated ones seemed to be unnecessary (and would -involve keeping a state over months/years), so that all cryptographic -objects have simply been removed.</li> -<li><b>Replace IP address with IP address hash:</b> Of course, IP -addresses need to be sanitized, too. -<ul><li>IPv4 addresses are replaced with <tt>10.x.x.x</tt> with -<tt>x.x.x</tt> being the 3 byte output of -<tt>H(IP address | bridge identity | secret)[:3]</tt>. -The input <tt>IP address</tt> is the 4-byte long binary representation of -the bridge's current IP address. -The <tt>bridge identity</tt> is the 20-byte long binary representation of -the bridge's long-term identity fingerprint. -The <tt>secret</tt> is a 31-byte long secure random string that changes once -per month for all descriptors and statuses published in that month. -<tt>H()</tt> is SHA-256. -The <tt>[:3]</tt> operator means that we pick the 3 most significant bytes -of the result.</li> -<li>IPv6 addresses are replaced with <tt>[fd9f:2e19:3bcf::xx:xxxx]</tt> -with <tt>xx:xxxx</tt> being the hex-formatted 3 byte output of a similar -hash function as described for IPv4 addresses. -The only differences are that the input <tt>IP address</tt> is 16 bytes -long and the <tt>secret</tt> is only 19 bytes long.</li></ul> -<li><b>Replace contact information:</b> If there is contact -information in a descriptor, the contact line is changed to -<tt>somebody</tt>.</li> -<li><b>Remove pluggable transport addresses and arguments:</b> Bridges may -provide transports in addition to the onion-routing protocol and include -information about these transports in their extra-info descriptors for -BridgeDB. In that case, any IP addresses, TCP ports, or additional -arguments are removed, only leaving in the supported transport names.</li> -</ol> - -<p> -Apart from these processing steps, sanitized bridge server descriptors -follow the same format as relay server descriptors. -The same applies to sanitized bridge extra-info descriptors. -Sanitized bridge network statuses are similar to version 2 relay network -statuses, but with only a <tt>published</tt> line in the header and -without any lines in the footer. -</p> - -<p> -The two documents below show an example bridge server -descriptor that is referenced from a bridge network status. -For more details about this process, see the -<a href="https://gitweb.torproject.org/metrics-db.git">metrics data processor</a> software. -</p> - -<blockquote> -<p> -<i>Sanitized bridge server descriptor:</i> -</p> -<p> -<tt>@type bridge-server-descriptor 1.0</tt><br> -<tt>router Hawthorne 10.175.105.22 443 0 0</tt><br> -<tt>platform Tor 0.2.2.19-alpha (git-1988927edecce4c7) on Linux i686</tt><br> -<tt>opt protocols Link 1 2 Circuit 1</tt><br> -<tt>published 2010-12-27 18:55:01</tt><br> -<tt>opt fingerprint A5FA 7F38 B02A 415E 72FE 614C 64A1 E5A9 2BA9 9BBD</tt><br> -<tt>uptime 2347112</tt><br> -<tt>bandwidth 5242880 10485760 1016594</tt><br> -<tt>opt extra-info-digest E729BCB5E06A5657A73151B55354EB003D2BAE0F</tt><br> -<tt>opt hidden-service-dir</tt><br> -<tt>contact somebody</tt><br> -<tt>reject *:*</tt><br> -<tt>router-digest 46DFDBE7B67B7C90A1962B0B5AA4526FAF406979</tt><br> -</p> -</blockquote> - -<blockquote> -<p> -<i>Sanitized bridge network status:</i> -</p> -<p> -<tt>@type bridge-network-status 1.0</tt><br> -<tt>published 2010-12-27 22:07:03</tt><br> -<tt>[...status entries...]</tt><br> -<tt>r Hawthorne pfp/OLAqQV5y/mFMZKHlqSupm70 Rt/b57Z7fJChlisLWqRSb69AaXk - 2010-12-27 18:55:01 10.175.105.22 443 0</tt><br> -<tt>s Fast Guard HSDir Running Stable Valid</tt><br> -<tt>[...status entries...]</tt><br> -</p> -</blockquote> -<hr> -<br> - -<a name="bytehist"></a> -<h3><a href="#bytehist" class="anchor">Byte histories</a></h3> -<br> -<p> -Relays include aggregate statistics in their descriptors that they upload -to the directory authorities. -These aggregate statistics are contained in extra-info descriptors that -are published in companion with server descriptors. -Extra-info descriptors are not required for clients to build circuits. -An extra-info descriptor belonging to a server descriptor is referenced by -its SHA1 hash value. -</p> - -<p> -Byte histories were the first statistical data that relays published about -their usage. -Relays report the number of written and read bytes in 15-minute intervals -throughout the last 24 hours. -The extra-info descriptor in the document below contains the byte -histories in the two lines starting with <tt>write-history</tt> and -<tt>read-history</tt>. -More details about these statistics can be found in the <a href="https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt">directory protocol -specification</a>. -</p> - -<blockquote> -<p> -<i>Extra-info descriptor published by relay <tt>blutmagie</tt> -(without cryptographic signature and with long lines being truncated):</i> -</p> -<p> -<tt>extra-info blutmagie 6297B13A687B521A59C6BD79188A2501EC03A065</tt><br> -<tt>published 2010-12-27 14:35:27</tt><br> -<tt>write-history 2010-12-27 14:34:05 (900 s) 12902389760,12902402048,12859373568,12894131200,[...]</tt><br> -<tt>read-history 2010-12-27 14:34:05 (900 s) 12770249728,12833485824,12661140480,12872439808,[...]</tt><br> -<tt>dirreq-write-history 2010-12-27 14:26:13 (900 s) 51731456,60808192,56740864,54948864,[...]</tt><br> -<tt>dirreq-read-history 2010-12-27 14:26:13 (900 s) 4747264,4767744,4511744,4752384,[...]</tt><br> -<tt>dirreq-stats-end 2010-12-27 10:51:09 (86400 s)</tt><br> -<tt>dirreq-v3-ips us=2000,de=1344,fr=744,kr=712,[...]</tt><br> -<tt>dirreq-v2-ips ??=8,au=8,cn=8,cz=8,[...]</tt><br> -<tt>dirreq-v3-reqs us=2368,de=1680,kr=1048,fr=800,[...]</tt><br> -<tt>dirreq-v2-reqs id=48,??=8,au=8,cn=8,[...]</tt><br> -<tt>dirreq-v3-resp ok=12504,not-enough-sigs=0,unavailable=0,not-found=0,not-modified=0,busy=128</tt><br> -<tt>dirreq-v2-resp ok=64,unavailable=0,not-found=8,not-modified=0,busy=8</tt><br> -<tt>dirreq-v2-share 1.03%</tt><br> -<tt>dirreq-v3-share 1.03%</tt><br> -<tt>dirreq-v3-direct-dl complete=316,timeout=4,running=0,min=4649,d1=36436,d2=68056,q1=76600,d3=87891,d4=131294,md=173579,d6=229695,d7=294528,q3=332053,d8=376301,d9=530252,max=2129698</tt><br> -<tt>dirreq-v2-direct-dl complete=16,timeout=52,running=0,min=9769,d1=9769,d2=9844,q1=9981,d3=9981,d4=27297,md=33640,d6=60814,d7=205884,q3=205884,d8=361137,d9=628256,max=956009</tt><br> -<tt>dirreq-v3-tunneled-dl complete=12088,timeout=92,running=4,min=534,d1=31351,d2=49166,q1=58490,d3=70774,d4=88192,md=109778,d6=152389,d7=203435,q3=246377,d8=323837,d9=559237,max=26601000</tt><br> -<tt>dirreq-v2-tunneled-dl complete=0,timeout=0,running=0</tt><br> -<tt>entry-stats-end 2010-12-27 10:51:09 (86400 s)</tt><br> -<tt>entry-ips de=11024,us=10672,ir=5936,fr=5040,[...]</tt><br> -<tt>exit-stats-end 2010-12-27 10:51:09 (86400 s)</tt><br> -<tt>exit-kibibytes-written 80=6758009,443=498987,4000=227483,5004=1182656,11000=22767,19371=1428809,31551=8212,41500=965584,51413=3772428,56424=1912605,other=175227777</tt><br> -<tt>exit-kibibytes-read 80=197075167,443=5954607,4000=1660990,5004=1808563,11000=1893893,19371=130360,31551=7588414,41500=756287,51413=2994144,56424=1646509,other=288412366</tt><br> -<tt>exit-streams-opened 80=5095484,443=359256,4000=4508,5004=22288,11000=124,19371=24,31551=40,41500=96,51413=16840,56424=28,other=1970964</tt><br> -</p> -</blockquote> -<hr> -<br> - -<a name="dirreqstats"></a> -<h3><a href="#dirreqstats" class="anchor">Directory requests</a></h3> -<br> -<p> -The directory authorities and directory mirrors report statistical data -about processed directory requests. -Starting with Tor version 0.2.2.15-alpha, all directories report the -number of written and read bytes for answering directory requests. -The format is similar to the format of byte histories as described in the -previous section. -The relevant lines are <tt>dirreq-write-history</tt> and -<tt>dirreq-read-history</tt> in the document listed in the -<a href="#bytehist">Byte histories</a> section above. -These two lines contain the subset of total read and written bytes that -the directory mirror spent on responding to any kind of directory request, -including network statuses, server descriptors, extra-info descriptors, -authority certificates, etc. -</p> - -<p> -The directories further report statistics on answering directory requests -for network statuses only. -For Tor versions before 0.2.3.x, relay operators had to manually enable -these statistics, which is why only a few directories report them. -The lines starting with <tt>dirreq-v3-</tt> all belong to the directory -request statistics (the lines starting with <tt>dirreq-v2-</tt> report -similar statistics for version 2 of the directory protocol which is -deprecated at the time of writing this report). -The following fields may be relevant for statistical analysis: -</p> - -<ul> -<li><b>Unique IP addresses:</b> The numbers in <tt>dirreq-v3-ips</tt> -denote the unique IP addresses of clients requesting network statuses by -country.</li> -<li><b>Network status requests:</b> The numbers in -<tt>dirreq-v3-reqs</tt> constitute the total network status requests by -country.</li> -<li><b>Request share:</b> The percentage in <tt>dirreq-v3-share</tt> is -an estimate of the share of directory requests that the reporting relay -expects to see in the Tor network. -In a tech report (<a href="https://research.torproject.org/techreports/countingusers-2010-11-30.pdf">PDF</a>) -we found that this estimate isn't very useful -for statistical analysis because of the different approaches that clients -take to select directory mirrors. -The fraction of written directory bytes (<tt>dirreq-write-history</tt>) can -be used to derive a better metric for the share of directory requests.</li> -<li><b>Network status responses:</b> The directories also report -whether they could provide the requested network status to clients in -<tt>dirreq-v3-resp</tt>. -This information was mostly used to diagnose error rates in version 2 of -the directory protocol where a lot of directories replied to network -status requests with <tt>503 Busy</tt>. -In version 3 of the directory protocol, most responses contain the status -code <tt>200 OK</tt>.</li> -<li><b>Network status download times:</b> The line -<tt>dirreq-v3-direct-dl</tt> contains statistics on the download of network -statuses via the relay's directory port. -The line <tt>dirreq-v3-tunneled-dl</tt> contains similar statistics on -downloads via a 1-hop circuit between client and directory (which is the -common approach in version 3 of the directory protocol). -Relays report how many requests have been completed, have timed out, and -are still running at the end of a 24-hour time interval as well as the -minimum, maximum, median, quartiles, and deciles of download times.</li> -</ul> - -<p> -More details about these statistics can be found in the <a href="https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt">directory protocol -specification</a>. -</p> -<hr> -<br> - -<a name="entrystats"></a> -<h3><a href="#entrystats" class="anchor">Connecting clients</a></h3> -<br> -<p> -Relays can be configured to report per-country statistics on directly -connecting clients. -This metric includes clients connecting to a relay in order to build -circuits and clients creating a 1-hop circuit to request directory -information. -In practice, the latter number outweighs the former number. -The <tt>entry-ips</tt> line in the document listed in the -<a href="#bytehist">Byte histories</a> section above -shows the number -of unique IP addresses connecting to the relay by country. -More details about these statistics can be found in the <a href="https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt">directory protocol -specification</a>. -</p> -<hr> -<br> - -<a name="bridgestats"></a> -<h3><a href="#bridgestats" class="anchor">Bridge users</a></h3> -<br> -<p> -Bridges report statistics on connecting bridge clients in their extra-info -descriptors. -The document below shows a bridge extra-info descriptor -with the bridge user statistics in the <tt>bridge-ips</tt> line. - -<blockquote> -<p> -<i>Sanitized bridge extra-info descriptor:</i> -</p> -<p> -<tt>extra-info Unnamed A5FA7F38B02A415E72FE614C64A1E5A92BA99BBD</tt><br> -<tt>published 2010-12-27 18:55:01</tt><br> -<tt>write-history 2010-12-27 18:43:50 (900 s) 151712768,176698368,180030464,163150848,[...]</tt><br> -<tt>read-history 2010-12-27 18:43:50 (900 s) 148109312,172274688,172168192,161094656,[...]</tt><br> -<tt>bridge-stats-end 2010-12-27 14:56:29 (86400 s)</tt><br> -<tt>bridge-ips sa=48,us=40,de=32,ir=32,[...]</tt><br> -</p> -</blockquote> - -<p> -Bridges running Tor version 0.2.2.3-alpha or earlier report bridge users -in a similar line starting with <tt>geoip-client-origins</tt>. -The reason for switching to <tt>bridge-ips</tt> was that the measurement -interval in <tt>geoip-client-origins</tt> had a variable length, whereas the -measurement interval in 0.2.2.4-alpha and later is set to exactly -24 hours. -In order to clearly distinguish the new measurement intervals from the old -ones, the new keywords have been introduced. -More details about these statistics can be found in the <a href="https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt">directory protocol -specification</a>. -</p> -<hr> -<br> - -<a name="cellstats"></a> -<h3><a href="#cellstats" class="anchor">Cell-queue statistics</a></h3> -<br> -<p> -Relays can be configured to report aggregate statistics on their cell -queues. -These statistics include average processed cells, average number of queued -cells, and average time that cells spend in circuits. -Circuits are split into deciles based on the number of processed cells. -The statistics are provided for circuit deciles from loudest to quietest -circuits. -The document below shows the cell statistics contained in an -extra-info descriptor by relay <tt>gabelmoo</tt>. -An early analysis of cell-queue statistics can be found in a tech report -(<a href="https://research.torproject.org/techreports/bufferstats-2009-08-25.pdf">PDF</a>). -More details about these statistics can be found in the <a href="https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt">directory protocol -specification</a>. -</p> - -<blockquote> -<p> -<i>Cell statistics in extra-info descriptor by relay <tt>gabelmoo</tt>:</i> -</p> -<p> -<tt>cell-stats-end 2010-12-27 09:59:50 (86400 s)</tt><br> -<tt>cell-processed-cells 4563,153,42,15,7,7,6,5,4,2</tt><br> -<tt>cell-queued-cells 9.39,0.98,0.09,0.01,0.00,0.00,0.00,0.01,0.00, - 0.01</tt><br> -<tt>cell-time-in-queue 2248,807,277,92,49,22,52,55,81,148</tt><br> -<tt>cell-circuits-per-decile 7233</tt><br> -</p> -</blockquote> -<hr> -<br> - -<a name="exitstats"></a> -<h3><a href="#exitstats" class="anchor">Exit-port statistics</a></h3> -<br> -<p> -Exit relays running Tor version 0.2.1.1-alpha or higher can be configured -to report aggregate statistics on exiting connections. -These relays report the number of opened streams, written and read bytes -by exiting port. -Until version 0.2.2.19-alpha, relays reported all ports exceeding a -threshold of 0.01 % of all written and read exit bytes. -Starting with version 0.2.2.20-alpha, relays only report the top 10 ports -in exit-port statistics in order not to exceed the maximum extra-info -descriptor length of 50 KB. -The document listed in the -<a href="#bytehist">Byte histories</a> section above contains -exit-port statistics in the lines starting with <tt>exit-</tt>. -More details about these statistics can be found in the <a href="https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt">directory protocol -specification</a>. -</p> -<hr> -<br> - -<a name="bidistats"></a> -<h3><a href="#bidistats" class="anchor">Bidirectional connection -use</a></h3> -<br> -<p> -Relays running Tor version 0.2.3.1-alpha or higher can be configured to -report what fraction of connections is used uni- or bi-directionally. -Every 10 seconds, relays determine for every connection whether they read -and wrote less than a threshold of 20 KiB. -Connections below this threshold are labeled as "Below Threshold". -For the remaining connections, relays report whether they read/wrote at -least 10 times as many bytes as they wrote/read. -If so, they classify a connection as "Mostly reading" or "Mostly -writing," respectively. -All other connections are classified as "Both reading and writing." -After classifying connections, read and write counters are reset for the -next 10-second interval. -Statistics are aggregated over 24 hours. -The document below shows the bidirectional connection use -statistics in an extra-info descriptor by relay <tt>zweifaltigkeit</tt>. -The four numbers denote the number of connections "Below threshold," -"Mostly reading," "Mostly writing," and "Both reading and writing." -More details about these statistics can be found in the <a href="https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt">directory protocol -specification</a>. -</p> - -<blockquote> -<p> -<i>Bidirectional connection use statistic in extra-info descriptor -by relay <tt>zweifaltigkeit</tt>:</i> -</p> -<p> -<tt>conn-bi-direct 2010-12-28 15:55:11 (86400 s) 387465,45285,55361,81786</tt> -</p> -</blockquote> -<hr> -<br> - -<a name="torperf"></a> -<h3><a href="#torperf" class="anchor">Torperf output files</a></h3> -<br> -<p> -Torperf is a little tool that measures Tor's performance as users -experience it. -Torperf uses a trivial SOCKS client to download files of various sizes -over the Tor network and notes how long substeps take. -Torperf can be -<a href="https://metrics.torproject.org/tools.html">downloaded</a> -from the metrics -website. -A Torperf results file contains a single line per Torperf run with -<tt>key=value</tt> pairs. -Such a result line is sufficient to learn about 1) the Tor and Torperf -configuration, 2) measurement results, and 3) additional information that -might help explain the results. -Known keys are explained below. -</p> -<ul> -<li>Configuration -<ul> -<li><tt>SOURCE:</tt> Configured name of the data source; required.</li> -<li><tt>FILESIZE:</tt> Configured file size in bytes; required.</li> -<li>Other meta data describing the Tor or Torperf configuration, e.g., -GUARD for a custom guard choice; optional.</li> -</ul> -<li>Measurement results -<ul> -<li><tt>START:</tt> Time when the connection process starts; -required.</li> -<li><tt>SOCKET:</tt> Time when the socket was created; required.</li> -<li><tt>CONNECT:</tt> Time when the socket was connected; required.</li> -<li><tt>NEGOTIATE:</tt> Time when SOCKS 5 authentication methods have been -negotiated; required.</li> -<li><tt>REQUEST:</tt> Time when the SOCKS request was sent; required.</li> -<li><tt>RESPONSE:</tt> Time when the SOCKS response was received; -required.</li> -<li><tt>DATAREQUEST:</tt> Time when the HTTP request was written; -required.</li> -<li><tt>DATARESPONSE:</tt> Time when the first response was received; -required.</li> -<li><tt>DATACOMPLETE:</tt> Time when the payload was complete; -required.</li> -<li><tt>WRITEBYTES:</tt> Total number of bytes written; required.</li> -<li><tt>READBYTES:</tt> Total number of bytes read; required.</li> -<li><tt>DIDTIMEOUT:</tt> 1 if the request timed out, 0 otherwise; -optional.</li> -<li><tt>DATAPERCx:</tt> Time when x% of expected bytes were read for -x = { 10, 20, 30, 40, 50, 60, 70, 80, 90 }; optional.</li> -<li>Other measurement results, e.g., START_RENDCIRC, GOT_INTROCIRC, etc. -for hidden-service measurements; optional.</li> -</ul> -<li>Additional information -<ul> -<li><tt>LAUNCH:</tt> Time when the circuit was launched; optional.</li> -<li><tt>USED_AT:</tt> Time when this circuit was used; optional.</li> -<li><tt>PATH:</tt> List of relays in the circuit, separated by commas; -optional.</li> -<li><tt>BUILDTIMES:</tt> List of times when circuit hops were built, -separated by commas; optional.</li> -<li><tt>TIMEOUT:</tt> Circuit build timeout that the Tor client used when -building this circuit; optional.</li> -<li><tt>QUANTILE:</tt> Circuit build time quantile that the Tor client -uses to determine its circuit-build timeout; optional.</li> -<li><tt>CIRC_ID:</tt> Circuit identifier of the circuit used for this -measurement; optional.</li> -<li><tt>USED_BY:</tt> Stream identifier of the stream used for this -measurement; optional.</li> -<li>Other fields containing additional information; optional.</li> -</ul> -</ul> - -<blockquote> -<p> -<i>Torperf <tt>.tpf</tt> output lines for a single request to download a -50 KiB file (reformatted):</i> -</p> - -<p> -<tt>BUILDTIMES=1.16901898384,1.86555600166,2.13295292854</tt><br> -<tt>CIRC_ID=9878</tt><br> -<tt>CONNECT=1338357901.42</tt><br> -<tt>DATACOMPLETE=1338357902.91</tt><br> -<tt>DATAPERC10=1338357902.48</tt><br> -<tt>DATAPERC20=1338357902.48</tt><br> -<tt>DATAPERC30=1338357902.61</tt><br> -<tt>DATAPERC40=1338357902.64</tt><br> -<tt>DATAPERC50=1338357902.65</tt><br> -<tt>DATAPERC60=1338357902.74</tt><br> -<tt>DATAPERC70=1338357902.74</tt><br> -<tt>DATAPERC80=1338357902.75</tt><br> -<tt>DATAPERC90=1338357902.79</tt><br> -<tt>DATAREQUEST=1338357901.83</tt><br> -<tt>DATARESPONSE=1338357902.25</tt><br> -<tt>DIDTIMEOUT=0</tt><br> -<tt>FILESIZE=51200</tt><br> -<tt>LAUNCH=1338357661.74</tt><br> -<tt>NEGOTIATE=1338357901.42</tt><br> -<tt>PATH=$980D326017CEF4CBBF4089FBABE767DC83D059AF,$03545609092A24C71CCAD2F4523F5CCC6714F159,$CAC3CF7154AE9C656C4096DC38B4EFA145905654</tt><br> -<tt>QUANTILE=0.800000</tt><br> -<tt>READBYTES=51442</tt><br> -<tt>REQUEST=1338357901.42</tt><br> -<tt>RESPONSE=1338357901.83</tt><br> -<tt>SOCKET=1338357901.42</tt><br> -<tt>SOURCE=torperf</tt><br> -<tt>START=1338357901.42</tt><br> -<tt>TIMEOUT=5049</tt><br> -<tt>USED_AT=1338357902.91</tt><br> -<tt>USED_BY=18869</tt><br> -<tt>WRITEBYTES=75</tt><br> -</p> -</blockquote> -<br> - -<p> -Torperf can produce two output files: <tt>.data</tt> and -<tt>.extradata</tt>. -The <tt>.data</tt> file contains timestamps for request substeps and the -byte summaries for downloading a test file via Tor. -The document below shows an example output of a Torperf run. -The timestamps are seconds and microseconds since 1970-01-01 -00:00:00.000000. -Torperf can be configured to write <tt>.extradata</tt> files by attaching -a Tor controller and writing certain controller events to disk. -The format of a <tt>.extradata</tt> line is similar to the combined format -as specified above, except that it can only contain "Additional -information" keywords. -</p> - -<blockquote> -<p> -<i>Torperf <tt>.data</tt> and <tt>.extradata</tt> output lines for a -single request to download a 50 KiB file (reformatted and annotated with -comments):</i> -</p> - -<p> -<tt># Timestamps and byte summaries contained in .data files:</tt><br> -<tt>1338357901 422336 # Connection process started</tt><br> -<tt>1338357901 422346 # After socket is created</tt><br> -<tt>1338357901 422521 # After socket is connected</tt><br> -<tt>1338357901 422604 # After authentication methods are negotiated (SOCKS 5 only)</tt><br> -<tt>1338357901 423550 # After SOCKS request is sent</tt><br> -<tt>1338357901 839639 # After SOCKS response is received</tt><br> -<tt>1338357901 839849 # After HTTP request is written</tt><br> -<tt>1338357902 258157 # After first response is received</tt><br> -<tt>1338357902 914263 # After payload is complete</tt><br> -<tt>75 # Written bytes</tt><br> -<tt>51442 # Read bytes</tt><br> -<tt>0 # Timeout (optional field)</tt><br> -<tt>1338357902 481591 # After 10% of expected bytes are read (optional field)</tt><br> -<tt>1338357902 482719 # After 20% of expected bytes are read (optional field)</tt><br> -<tt>1338357902 613169 # After 30% of expected bytes are read (optional field)</tt><br> -<tt>1338357902 647108 # After 40% of expected bytes are read (optional field)</tt><br> -<tt>1338357902 651764 # After 50% of expected bytes are read (optional field)</tt><br> -<tt>1338357902 743705 # After 60% of expected bytes are read (optional field)</tt><br> -<tt>1338357902 743876 # After 70% of expected bytes are read (optional field)</tt><br> -<tt>1338357902 757475 # After 80% of expected bytes are read (optional field)</tt><br> -<tt>1338357902 795100 # After 90% of expected bytes are read (optional field)</tt><br> -</p> - -<p> -<tt># Path information contained in .extradata files:</tt><br> -<tt>CIRC_ID=9878</tt><br> -<tt>LAUNCH=1338357661.74</tt><br> -<tt>PATH=$980D326017CEF4CBBF4089FBABE767DC83D059AF,$03545609092A24C71CCAD2F4523F5CCC6714F159,$CAC3CF7154AE9C656C4096DC38B4EFA145905654</tt><br> -<tt>BUILDTIMES=1.16901898384,1.86555600166,2.13295292854</tt><br> -<tt>USED_AT=1338357902.91</tt><br> -<tt>USED_BY=18869</tt><br> -<tt>TIMEOUT=5049</tt><br> -<tt>QUANTILE=0.800000</tt><br> -</p> -</blockquote> -<hr> -<br> - -<a name="bridgepool"></a> -<h3><a href="#bridgepool" class="anchor">BridgeDB pool assignment -files</a></h3> -<br> -<p> -BridgeDB is the software that receives bridge network statuses containing -the information which bridges are running from the bridge authority, -assigns these bridges to persistent distribution rings, and hands them out -to bridge users. -BridgeDB periodically dumps the list of running bridges with information -about the rings, subrings, and file buckets to which they are assigned to -a local file. -The sanitized versions of these lists containing SHA-1 hashes of bridge -fingerprints instead of the original fingerprints are available for -statistical analysis. -</p> - -<blockquote> -<p> -<i>BridgeDB pool assignment file from March 13, 2011:</i> -</p> -<p> -<tt>bridge-pool-assignment 2011-03-13 14:38:03</tt><br> -<tt>00b834117566035736fc6bd4ece950eace8e057a unallocated</tt><br> -<tt>00e923e7a8d87d28954fee7503e480f3a03ce4ee email port=443 flag=stable</tt><br> -<tt>0103bb5b00ad3102b2dbafe9ce709a0a7c1060e4 https ring=2 port=443 flag=stable</tt><br> -<tt>[...]</tt><br> -</p> -</blockquote> - -<p> -The document above shows a BridgeDB pool assignment file -from March 13, 2011. -Every such file begins with a line containing the timestamp when BridgeDB -wrote this file. -Subsequent lines always start with the SHA-1 hash of a bridge fingerprint, -followed by ring, subring, and/or file bucket information. -There are currently three distributor ring types in BridgeDB: -</p> - -<ol> -<li><b>unallocated:</b> These bridges are not distributed by BridgeDB, -but are either reserved for manual distribution or are written to file -buckets for distribution via an external tool. -If a bridge in the <tt>unallocated</tt> ring is assigned to a file bucket, -this is noted by <tt>bucket=$bucketname</tt>.</li> -<li><b>email:</b> These bridges are distributed via an e-mail -autoresponder. Bridges can be assigned to subrings by their OR port or -relay flag which is defined by <tt>port=$port</tt> and/or <tt>flag=$flag</tt>. -</li> -<li><b>https:</b> These bridges are distributed via https server. -There are multiple https rings to further distribute bridges by IP address -ranges, which is denoted by <tt>ring=$ring</tt>. -Bridges in the <tt>https</tt> ring can also be assigned to subrings by -OR port or relay flag which is defined by <tt>port=$port</tt> and/or -<tt>flag=$flag</tt>.</li> -</ol> -<hr> -<br> - -<a name="exitlist"> -<h3><a href="#exitlist" class="anchor">Tor Check exit lists</a></h3> -<br> -<p> -<a href="https://www.torproject.org/tordnsel/dist/">TorDNSEL</a> is an -implementation of the active testing, DNS-based exit list -for Tor exit -nodes. -Tor Check makes the list of known exits and corresponding exit IP -addresses available in a specific format. -The document below shows an entry of the exit list written on -December 28, 2010 at 15:21:44 UTC. -This entry means that the relay with fingerprint <tt>63BA..</tt> which -published a descriptor at 07:35:55 and was contained in a version 2 -network status from 08:10:11 uses two different IP addresses for exiting. -The first address <tt>91.102.152.236</tt> was found in a test performed at -07:10:30. -When looking at the corresponding server descriptor, one finds that this -is also the IP address on which the relay accepts connections from inside -the Tor network. -A second test performed at 10:35:30 reveals that the relay also uses IP -address <tt>91.102.152.227</tt> for exiting. -</p> - -<blockquote> -<p> -<i>Exit list entry written on December 28, 2010 at 15:21:44 UTC:</i> -</p> -<p> -<tt>ExitNode 63BA28370F543D175173E414D5450590D73E22DC</tt><br> -<tt>Published 2010-12-28 07:35:55</tt><br> -<tt>LastStatus 2010-12-28 08:10:11</tt><br> -<tt>ExitAddress 91.102.152.236 2010-12-28 07:10:30</tt><br> -<tt>ExitAddress 91.102.152.227 2010-12-28 10:35:30</tt><br> -</p> -</blockquote> - -</div> -</div> -<div class="bottom" id="bottom"> -<%@ include file="footer.jsp"%> -</div> -</body> -</html> - diff --git a/web/WEB-INF/graphs.jsp b/web/WEB-INF/graphs.jsp deleted file mode 100644 index 58c122c..0000000 --- a/web/WEB-INF/graphs.jsp +++ /dev/null @@ -1,34 +0,0 @@ -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> -<html> -<head> - <title>Tor Metrics Portal: Graphs</title> - <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> - <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet"> - <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon"> -</head> -<body> - <div class="center"> - <%@ include file="banner.jsp"%> - <div class="main-column"> - <h2>Tor Metrics Portal: Graphs</h2> - <br> - <p>The graphs on this page visualize a small portion of the data - gathered in the Tor Metrics Project. The following graphs are - available:</p> - <ul> - <li>The <a href="network.html">Network page</a> has numerous - statistics on the network of relays and bridges.</li> - <li>The <a href="users.html">Users page</a> attempts to estimate - the number of users in the network.</li> - <li>There are active and passive performance measurements of the - Tor network available on the - <a href="performance.html">Performance page</a>.</li> - </ul> - <br> - </div> - </div> - <div class="bottom" id="bottom"> - <%@ include file="footer.jsp"%> - </div> -</body> -</html> diff --git a/web/WEB-INF/index.jsp b/web/WEB-INF/index.jsp deleted file mode 100644 index 9accc2b..0000000 --- a/web/WEB-INF/index.jsp +++ /dev/null @@ -1,40 +0,0 @@ -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> -<html> -<head> - <title>Tor Metrics Portal</title> - <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> - <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet"> - <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon"> -</head> -<body> - <div class="center"> - <%@ include file="banner.jsp"%> - <div class="main-column"> - <h2>Tor Metrics Portal</h2> - <br> - <p>The Tor Metrics Portal aggregates all kinds of interesting - data about the Tor network and visualizes them in graphs and - reports. This portal also provides easy access to the underlying - data and documentation for performing own analyses based on these - data. Find out more here:</p> - <ul> - <li>View daily updated <a href="graphs.html">graphs</a> on - estimated client numbers, on network performance, and other - statistics on the Tor network</li> - <li>Read <a href="http://freehaven.net/anonbib/">papers</a> and - <a href="https://research.torproject.org/techreports.html">technical - reports</a> - on the measurement techniques and results of statistical - analysis of metrics data</li> - <li>Download the <a href="data.html">data</a> that is behind the - graphs and reports to make your own evaluations</li> - <li>Try out the <a href="tools.html">tools</a> to parse and - evaluate the metrics data</li> - </ul> - </div> - </div> - <div class="bottom" id="bottom"> - <%@ include file="footer.jsp"%> - </div> -</body> -</html> diff --git a/web/WEB-INF/network.jsp b/web/WEB-INF/network.jsp deleted file mode 100644 index e0b297a..0000000 --- a/web/WEB-INF/network.jsp +++ /dev/null @@ -1,305 +0,0 @@ -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> -<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> -<html> -<head> - <title>Tor Metrics Portal: Network</title> - <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> - <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet"> - <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon"> -</head> -<body> - <div class="center"> - <%@ include file="banner.jsp"%> - <div class="main-column"> -<h2>Tor Metrics Portal: Network</h2> -<br> -<a name="networksize"></a> -<h3><a href="#networksize" class="anchor">Relays and bridges in the -network</a></h3> -<br> -<p>The following graph shows the average daily number of relays and -bridges in the network.</p> -<img src="networksize.png${networksize_url}" - width="576" height="360" alt="Network size graph"> -<form action="network.html#networksize"> - <div class="formrow"> - <input type="hidden" name="graph" value="networksize"> - <p> - <label>Start date (yyyy-mm-dd):</label> - <input type="text" name="start" size="10" - value="<c:choose><c:when test="${fn:length(networksize_start) == 0}">${default_start_date}</c:when><c:otherwise>${networksize_start[0]}</c:otherwise></c:choose>"> - <label>End date (yyyy-mm-dd):</label> - <input type="text" name="end" size="10" - value="<c:choose><c:when test="${fn:length(networksize_end) == 0}">${default_end_date}</c:when><c:otherwise>${networksize_end[0]}</c:otherwise></c:choose>"> - </p><p> - <input class="submit" type="submit" value="Update graph"> - </p> - </div> -</form> -<p>Download graph as -<a href="networksize.pdf${networksize_url}">PDF</a> or -<a href="networksize.svg${networksize_url}">SVG</a>.</p> -<p><a href="stats/servers.csv">CSV</a> file containing all data.</p> -<br> - -<a name="relayflags"></a> -<h3><a href="#relayflags" class="anchor">Relays with Exit, Fast, Guard, -Stable, and HSDir flags</a></h3> -<br> -<p>The directory authorities assign certain flags to relays that clients -use for their path selection decisions. The following graph shows the -average number of relays with these flags assigned.</p> -<img src="relayflags.png${relayflags_url}" - width="576" height="360" alt="Relay flags graph"> -<form action="network.html#relayflags"> - <div class="formrow"> - <input type="hidden" name="graph" value="relayflags"> - <p> - <label>Start date (yyyy-mm-dd):</label> - <input type="text" name="start" size="10" - value="<c:choose><c:when test="${fn:length(relayflags_start) == 0}">${default_start_date}</c:when><c:otherwise>${relayflags_start[0]}</c:otherwise></c:choose>"> - <label>End date (yyyy-mm-dd):</label> - <input type="text" name="end" size="10" - value="<c:choose><c:when test="${fn:length(relayflags_end) == 0}">${default_end_date}</c:when><c:otherwise>${relayflags_end[0]}</c:otherwise></c:choose>"> - </p><p> - <label>Relay flags: </label> - <input type="checkbox" name="flag" value="Running"<c:if test="${fn:length(relayflags_flag) == 0 or fn:contains(fn:join(relayflags_flag, ','), 'Running')}"> checked</c:if>> Running - <input type="checkbox" name="flag" value="Exit"<c:if test="${fn:length(relayflags_flag) == 0 or fn:contains(fn:join(relayflags_flag, ','), 'Exit')}"> checked</c:if>> Exit - <input type="checkbox" name="flag" value="Fast"<c:if test="${fn:length(relayflags_flag) == 0 or fn:contains(fn:join(relayflags_flag, ','), 'Fast')}"> checked</c:if>> Fast - <input type="checkbox" name="flag" value="Guard"<c:if test="${fn:length(relayflags_flag) == 0 or fn:contains(fn:join(relayflags_flag, ','), 'Guard')}"> checked</c:if>> Guard - <input type="checkbox" name="flag" value="Stable"<c:if test="${fn:length(relayflags_flag) == 0 or fn:contains(fn:join(relayflags_flag, ','), 'Stable')}"> checked</c:if>> Stable - <input type="checkbox" name="flag" value="HSDir"<c:if test="${fn:length(relayflags_flag) > 0 and fn:contains(fn:join(relayflags_flag, ','), 'HSDir')}"> checked</c:if>> HSDir - </p><p> - <input class="submit" type="submit" value="Update graph"> - </p> - </div> -</form> -<p>Download graph as -<a href="relayflags.pdf${relayflags_url}">PDF</a> or -<a href="relayflags.svg${relayflags_url}">SVG</a>.</p> -<p><a href="stats/servers.csv">CSV</a> file containing all data.</p> -<br> - -<a name="versions"></a> -<h3><a href="#versions" class="anchor">Relays by version</a></h3> -<br> -<p>Relays report the Tor version that they are running to the directory -authorities. See the -<a href="https://www.torproject.org/download/download.html.en">download -page</a> and -<a href="https://gitweb.torproject.org/tor.git/blob/HEAD:/ChangeLog">ChangeLog file</a> -to find out which Tor versions are stable and unstable. -The following graph shows the number of relays by version.</p> -<img src="versions.png${versions_url}" - width="576" height="360" alt="Relay versions graph"> -<form action="network.html#versions"> - <div class="formrow"> - <input type="hidden" name="graph" value="versions"> - <p> - <label>Start date (yyyy-mm-dd):</label> - <input type="text" name="start" size="10" - value="<c:choose><c:when test="${fn:length(versions_start) == 0}">${default_start_date}</c:when><c:otherwise>${versions_start[0]}</c:otherwise></c:choose>"> - <label>End date (yyyy-mm-dd):</label> - <input type="text" name="end" size="10" - value="<c:choose><c:when test="${fn:length(versions_end) == 0}">${default_end_date}</c:when><c:otherwise>${versions_end[0]}</c:otherwise></c:choose>"> - </p><p> - <input class="submit" type="submit" value="Update graph"> - </p> - </div> -</form> -<p>Download graph as -<a href="versions.pdf${versions_url}">PDF</a> or -<a href="versions.svg${versions_url}">SVG</a>.</p> -<p><a href="stats/servers.csv">CSV</a> file containing all data.</p> -<br> - -<a name="platforms"></a> -<h3><a href="#platforms" class="anchor">Relays by platform</a></h3> -<br> -<p>Relays report the operating system they are running to the directory -authorities. The following graph shows the number of relays by -platform.</p> -<img src="platforms.png${platforms_url}" - width="576" height="360" alt="Relay platforms graph"> -<form action="network.html#platforms"> - <div class="formrow"> - <input type="hidden" name="graph" value="platforms"> - <p> - <label>Start date (yyyy-mm-dd):</label> - <input type="text" name="start" size="10" - value="<c:choose><c:when test="${fn:length(platforms_start) == 0}">${default_start_date}</c:when><c:otherwise>${platforms_start[0]}</c:otherwise></c:choose>"> - <label>End date (yyyy-mm-dd):</label> - <input type="text" name="end" size="10" - value="<c:choose><c:when test="${fn:length(platforms_end) == 0}">${default_end_date}</c:when><c:otherwise>${platforms_end[0]}</c:otherwise></c:choose>"> - </p><p> - <input class="submit" type="submit" value="Update graph"> - </p> - </div> -</form> -<p>Download graph as -<a href="platforms.pdf${platforms_url}">PDF</a> or -<a href="platforms.svg${platforms_url}">SVG</a>.</p> -<p><a href="stats/servers.csv">CSV</a> file containing all data.</p> -<br> - -<a name="cloudbridges"></a> -<h3><a href="#cloudbridges" class="anchor">Tor Cloud bridges</a></h3> -<br> -<p>The following graph shows the average daily number of -<a href="http://cloud.torproject.org/">Tor Cloud</a> bridges in the -network.</p> -<img src="cloudbridges.png${cloudbridges_url}" - width="576" height="360" alt="Tor Cloud bridges graph"> -<form action="network.html#cloudbridges"> - <div class="formrow"> - <input type="hidden" name="graph" value="cloudbridges"> - <p> - <label>Start date (yyyy-mm-dd):</label> - <input type="text" name="start" size="10" - value="<c:choose><c:when test="${fn:length(cloudbridges_start) == 0}">${default_start_date}</c:when><c:otherwise>${cloudbridges_start[0]}</c:otherwise></c:choose>"> - <label>End date (yyyy-mm-dd):</label> - <input type="text" name="end" size="10" - value="<c:choose><c:when test="${fn:length(cloudbridges_end) == 0}">${default_end_date}</c:when><c:otherwise>${cloudbridges_end[0]}</c:otherwise></c:choose>"> - </p><p> - <input class="submit" type="submit" value="Update graph"> - </p> - </div> -</form> -<p>Download graph as -<a href="cloudbridges.pdf${cloudbridges_url}">PDF</a> or -<a href="cloudbridges.svg${cloudbridges_url}">SVG</a>.</p> -<p><a href="stats/servers.csv">CSV</a> file containing all data.</p> -<br> - -<a name="bandwidth"></a> -<h3><a href="#bandwidth" class="anchor">Total relay bandwidth in the -network</a></h3> -<br> -<p>Relays report how much bandwidth they are willing to contribute and how -many bytes they have read and written in the past 24 hours. The following -graph shows total advertised bandwidth and bandwidth history of all relays -in the network.</p> -<img src="bandwidth.png${bandwidth_url}" - width="576" height="360" alt="Relay bandwidth graph"> -<form action="network.html#bandwidth"> - <div class="formrow"> - <input type="hidden" name="graph" value="bandwidth"> - <p> - <label>Start date (yyyy-mm-dd):</label> - <input type="text" name="start" size="10" - value="<c:choose><c:when test="${fn:length(bandwidth_start) == 0}">${default_start_date}</c:when><c:otherwise>${bandwidth_start[0]}</c:otherwise></c:choose>"> - <label>End date (yyyy-mm-dd):</label> - <input type="text" name="end" size="10" - value="<c:choose><c:when test="${fn:length(bandwidth_end) == 0}">${default_end_date}</c:when><c:otherwise>${bandwidth_end[0]}</c:otherwise></c:choose>"> - </p><p> - <input class="submit" type="submit" value="Update graph"> - </p> - </div> -</form> -<p>Download graph as -<a href="bandwidth.pdf${bandwidth_url}">PDF</a> or -<a href="bandwidth.svg${bandwidth_url}">SVG</a>.</p> -<p><a href="stats/bandwidth.csv">CSV</a> file containing all data.</p> -<br> - -<a name="bwhist-flags"></a> -<h3><a href="#bwhist-flags" class="anchor">Relay bandwidth by Exit and/or -Guard flags</a></h3> -<br> -<p>The following graph shows the relay bandwidth of all relays with the -Exit and/or Guard flags assigned by the directory authorities.</p> -<img src="bwhist-flags.png${bwhist_flags_url}" - width="576" height="360" alt="Relay bandwidth by flags graph"> -<form action="network.html#bwhist-flags"> - <div class="formrow"> - <input type="hidden" name="graph" value="bwhist-flags"> - <p> - <label>Start date (yyyy-mm-dd):</label> - <input type="text" name="start" size="10" - value="<c:choose><c:when test="${fn:length(bwhist_flags_start) == 0}">${default_start_date}</c:when><c:otherwise>${bwhist_flags_start[0]}</c:otherwise></c:choose>"> - <label>End date (yyyy-mm-dd):</label> - <input type="text" name="end" size="10" - value="<c:choose><c:when test="${fn:length(bwhist_flags_end) == 0}">${default_end_date}</c:when><c:otherwise>${bwhist_flags_end[0]}</c:otherwise></c:choose>"> - </p><p> - <input class="submit" type="submit" value="Update graph"> - </p> - </div> -</form> -<p>Download graph as -<a href="bwhist-flags.pdf${bwhist_flags_url}">PDF</a> or -<a href="bwhist-flags.svg${bwhist_flags_url}">SVG</a>.</p> -<p><a href="stats/bandwidth.csv">CSV</a> file containing all data.</p> -<br> - -<a name="bandwidth-flags"></a> -<h3><a href="#bandwidth-flags" class="anchor">Advertised bandwidth and -bandwidth history by relay flags</a></h3> -<br> -<p>The following graph shows the advertised bandwidth and bandwidth -history of all relays with the Exit and/or Guard flags assigned by the -directory authorities. -Note that these sets possibly overlap with relays having both Exit and -Guard flag.</p> -<img src="bandwidth-flags.png${bandwidth_flags_url}" - width="576" height="360" alt="Advertised bandwidth and bandwidth history by relay flags graph"> -<form action="network.html#bandwidth-flags"> - <div class="formrow"> - <input type="hidden" name="graph" value="bandwidth-flags"> - <p> - <label>Start date (yyyy-mm-dd):</label> - <input type="text" name="start" size="10" - value="<c:choose><c:when test="${fn:length(bandwidth_flags_start) == 0}">${default_start_date}</c:when><c:otherwise>${bandwidth_flags_start[0]}</c:otherwise></c:choose>"> - <label>End date (yyyy-mm-dd):</label> - <input type="text" name="end" size="10" - value="<c:choose><c:when test="${fn:length(bandwidth_flags_end) == 0}">${default_end_date}</c:when><c:otherwise>${bandwidth_flags_end[0]}</c:otherwise></c:choose>"> - </p><p> - <input class="submit" type="submit" value="Update graph"> - </p> - </div> -</form> -<p>Download graph as -<a href="bandwidth-flags.pdf${bandwidth_flags_url}">PDF</a> or -<a href="bandwidth-flags.svg${bandwidth_flags_url}">SVG</a>.</p> -<p><a href="stats/bandwidth.csv">CSV</a> file containing all data.</p> -<br> - -<a name="dirbytes"></a> -<h3><a href="#dirbytes" class="anchor">Number of bytes spent on answering -directory requests</a></h3> -<br> -<p>Relays running on 0.2.2.15-alpha or higher report the number of bytes -they spend on answering directory requests. The following graph shows -total written and read bytes as well as written and read dir bytes. The -dir bytes are extrapolated from those relays who report them to reflect -the number of written and read dir bytes by all relays.</p> -<img src="dirbytes.png${dirbytes_url}" - width="576" height="360" alt="Dir bytes graph"> -<form action="network.html#dirbytes"> - <div class="formrow"> - <input type="hidden" name="graph" value="dirbytes"> - <p> - <label>Start date (yyyy-mm-dd):</label> - <input type="text" name="start" size="10" - value="<c:choose><c:when test="${fn:length(dirbytes_start) == 0}">${default_start_date}</c:when><c:otherwise>${dirbytes_start[0]}</c:otherwise></c:choose>"> - <label>End date (yyyy-mm-dd):</label> - <input type="text" name="end" size="10" - value="<c:choose><c:when test="${fn:length(dirbytes_end) == 0}">${default_end_date}</c:when><c:otherwise>${dirbytes_end[0]}</c:otherwise></c:choose>"> - </p><p> - <input class="submit" type="submit" value="Update graph"> - </p> - </div> -</form> -<p>Download graph as -<a href="dirbytes.pdf${dirbytes_url}">PDF</a> or -<a href="dirbytes.svg${dirbytes_url}">SVG</a>.</p> -<p><a href="stats/bandwidth.csv">CSV</a> file containing all data.</p> -<br> - </div> - </div> - <div class="bottom" id="bottom"> - <%@ include file="footer.jsp"%> - </div> -</body> -</html> diff --git a/web/WEB-INF/papers.jsp b/web/WEB-INF/papers.jsp deleted file mode 100644 index 1a48491..0000000 --- a/web/WEB-INF/papers.jsp +++ /dev/null @@ -1,81 +0,0 @@ -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> -<html> -<head> - <title>Tor Metrics Portal: Papers</title> - <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> - <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet"> - <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon"> -</head> -<body> - <div class="center"> - <%@ include file="banner.jsp"%> - <div class="main-column"> - <h2>Tor Metrics Portal: Papers</h2> - <br> - <p>The <a href="#papers">papers</a>, - <a href="#techreports">technical reports</a>, and - <a href="#blogposts">blog posts</a> listed on this page originate - from, are based on, or are related to work performed in the Tor - Metrics Project.</p> - <p><font color="red">This page will be removed soon. - Its main purpose was to list metrics-related tech reports, but - those have now moved - <a href="https://research.torproject.org/techreports.html">here</a>. - </font></p> - <br> - <a name="papers"></a> - <h3><a href="#papers" class="anchor">Papers</a></h3> - <br> - These papers summarize some of the results of of the Tor Metrics - Project and have been accepted for publication at academic - conferences or workshops. - <ul> - <li>Karsten Loesing, Steven J. Murdoch, Roger Dingledine. A Case - Study on Measuring Statistical Data in the Tor Anonymity - Network. Workshop on Ethics in - Computer Security Research (WECSR 2010), Tenerife, Spain, - January 2010. (<a href="http://freehaven.net/anonbib/cache/wecsr10measuring-tor.pdf">PDF</a>)</li> - <li>Karsten Loesing. Measuring the Tor Network from Public - Directory Information. 2nd Hot Topics in Privacy Enhancing - Technologies (HotPETs 2009), Seattle, WA, USA, August 2009. - (<a href="https://research.torproject.org/techreports/metrics-2009-08-07.pdf">PDF</a>)</li> - </ul> - <br> - <a name="techreports"></a> - <h3><a href="#techreports" class="anchor">Technical - reports</a></h3> - <br> - <p> - Some of the - <a href="https://research.torproject.org/techreports.html">Tor - Technical Reports</a> have been the first place to - publish novel kinds of statistics on the Tor network. Some, but - not all, of the results contained in those technical reports have - been included in the <a href="#papers">papers</a> above or in the - daily updated <a href="graphs.html">graphs</a>. - </p> - <br> - <a name="blogposts"></a> - <h3><a href="#blogposts" class="anchor">Blog posts</a></h3> - <br> - The following blog posts are either the results of metrics - research or describe new interesting research questions that can - (partly) be answered with metrics data. - <ul> - <li>Research problems: Ten ways to discover Tor bridges - (<a href="https://blog.torproject.org/blog/research-problems-ten-ways-discover-tor-bridges">link</a>, - October 31, 2011).</li> - <li>Research problem: better guard rotation parameters - (<a href="https://blog.torproject.org/blog/research-problem-better-guard-rotation-parameters">link</a>, - August 20, 2011).</li> - <li>Research problem: measuring the safety of the Tor network - (<a href="https://blog.torproject.org/blog/research-problem-measuring-safety-tor-network">link</a>, - February 5, 2011).</li> - </ul> - </div> - </div> - <div class="bottom" id="bottom"> - <%@ include file="footer.jsp"%> - </div> -</body> -</html> diff --git a/web/WEB-INF/performance.jsp b/web/WEB-INF/performance.jsp deleted file mode 100644 index e220b59..0000000 --- a/web/WEB-INF/performance.jsp +++ /dev/null @@ -1,150 +0,0 @@ -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> -<html> -<head> - <title>Tor Metrics Portal: Performance</title> - <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> - <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet"> - <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon"> -</head> -<body> - <div class="center"> - <%@ include file="banner.jsp"%> - <div class="main-column"> -<h2>Tor Metrics Portal: Performance</h2> -<br> -<a name="torperf"></a> -<h3><a href="#torperf" class="anchor">Time to download files over -Tor</a></h3> -<br> -<p>The following graphs show the performance of the Tor network as -experienced by its users. The graphs contain the average (median) time to -request files of three different sizes over Tor as well as first and third -quartile of request times. Medians and quartiles are calculated multiple -times per day for completed days only, resulting in a delay of 1 to 1.5 -days before changes to network performance become visible in the -graph.</p> -<img src="torperf.png${torperf_url}" - width="576" height="360" alt="Torperf graph"> -<form action="performance.html#torperf"> - <div class="formrow"> - <input type="hidden" name="graph" value="torperf"> - <p> - <label>Start date (yyyy-mm-dd):</label> - <input type="text" name="start" size="10" - value="<c:choose><c:when test="${fn:length(torperf_start) == 0}">${default_start_date}</c:when><c:otherwise>${torperf_start[0]}</c:otherwise></c:choose>"> - <label>End date (yyyy-mm-dd):</label> - <input type="text" name="end" size="10" - value="<c:choose><c:when test="${fn:length(torperf_end) == 0}">${default_end_date}</c:when><c:otherwise>${torperf_end[0]}</c:otherwise></c:choose>"> - </p><p> - Source: - <input type="radio" name="source" value="all"<c:if test="${fn:length(torperf_source) == 0 or torperf_source[0] eq 'all'}"> checked</c:if>> all - <input type="radio" name="source" value="torperf"<c:if test="${torperf_source[0] eq 'torperf'}"> checked</c:if>> torperf - <input type="radio" name="source" value="moria"<c:if test="${torperf_source[0] eq 'moria'}"> checked</c:if>> moria - <input type="radio" name="source" value="siv"<c:if test="${torperf_source[0] eq 'siv'}"> checked</c:if>> siv - </p><p> - <label>File size: </label> - <input type="radio" name="filesize" value="50kb"<c:if test="${fn:length(torperf_filesize) == 0 or torperf_filesize[0] eq '50kb'}"> checked</c:if>> 50 KiB - <input type="radio" name="filesize" value="1mb"<c:if test="${torperf_filesize[0] eq '1mb'}"> checked</c:if>> 1 MiB - <input type="radio" name="filesize" value="5mb"<c:if test="${torperf_filesize[0] eq '5mb'}"> checked</c:if>> 5 MiB - </p><p> - <input class="submit" type="submit" value="Update graph"> - </p> - </div> -</form> -<p>Download graph as -<a href="torperf.pdf${torperf_url}">PDF</a> or -<a href="torperf.svg${torperf_url}">SVG</a>.</p> -<p><a href="stats/torperf.csv">CSV</a> file containing all data.</p> - -<br> -<a name="torperf-failures"></a> -<h3><a href="#torperf-failures" class="anchor">Timeouts and failures of -downloading files over Tor</a></h3> -<br> -<p>The following graphs show the fraction of timeouts and failures of -downloading files over Tor as experienced by users. -A timeout occurs when a 50 KiB (1 MiB, 5 MiB) download does not complete -within 4:55 minutes (29:55 minutes, 59:55 minutes). -A failure occurs when the download completes, but the response is smaller -than 50 KiB (1 MiB, 5 MiB).</p> -<img src="torperf-failures.png${torperf_failures_url}" - width="576" height="360" alt="Torperf failures graph"> -<form action="performance.html#torperf-failures"> - <div class="formrow"> - <input type="hidden" name="graph" value="torperf-failures"> - <p> - <label>Start date (yyyy-mm-dd):</label> - <input type="text" name="start" size="10" - value="<c:choose><c:when test="${fn:length(torperf_failures_start) == 0}">${default_start_date}</c:when><c:otherwise>${torperf_failures_start[0]}</c:otherwise></c:choose>"> - <label>End date (yyyy-mm-dd):</label> - <input type="text" name="end" size="10" - value="<c:choose><c:when test="${fn:length(torperf_failures_end) == 0}">${default_end_date}</c:when><c:otherwise>${torperf_failures_end[0]}</c:otherwise></c:choose>"> - </p><p> - Source: - <input type="radio" name="source" value="all"<c:if test="${fn:length(torperf_failures_source) == 0 or torperf_failures_source[0] eq 'all'}"> checked</c:if>> all - <input type="radio" name="source" value="torperf"<c:if test="${torperf_failures_source[0] eq 'torperf'}"> checked</c:if>> torperf - <input type="radio" name="source" value="moria"<c:if test="${torperf_failures_source[0] eq 'moria'}"> checked</c:if>> moria - <input type="radio" name="source" value="siv"<c:if test="${torperf_failures_source[0] eq 'siv'}"> checked</c:if>> siv - </p><p> - <label>File size: </label> - <input type="radio" name="filesize" value="50kb"<c:if test="${fn:length(torperf_failures_filesize) == 0 or torperf_failures_filesize[0] eq '50kb'}"> checked</c:if>> 50 KiB - <input type="radio" name="filesize" value="1mb"<c:if test="${torperf_failures_filesize[0] eq '1mb'}"> checked</c:if>> 1 MiB - <input type="radio" name="filesize" value="5mb"<c:if test="${torperf_failures_filesize[0] eq '5mb'}"> checked</c:if>> 5 MiB - </p><p> - <input class="submit" type="submit" value="Update graph"> - </p> - </div> -</form> -<p>Download graph as -<a href="torperf-failures.pdf${torperf_failures_url}">PDF</a> or -<a href="torperf-failures.svg${torperf_failures_url}">SVG</a>.</p> -<p><a href="stats/torperf.csv">CSV</a> file containing all data.</p> - -<br> -<a name="connbidirect"></a> -<h3><a href="#connbidirect" class="anchor">Fraction of connections used -uni-/bidirectionally</a></h3> -<br> -<p>The following graph shows the fraction of connections that is used -uni- or bi-directionally. Every 10 seconds, relays determine for every -connection whether they read and wrote less than a threshold of 20 KiB. -Connections below this threshold are excluded from these statistics. For -the remaining connections, relays report whether they read/wrote at least -10 times as many bytes as they wrote/read. If so, they classify a -connection as "Mostly reading" or "Mostly writing," respectively. All -other connections are classified as "Both reading and writing." After -classifying connections, read and write counters are reset for the next -10-second interval. Statistics are aggregated over 24 hours.</p> -<img src="connbidirect.png${connbidirect_url}" - width="576" height="360" - alt="Fraction of direct connections used uni-/bidirectionally"> -<form action="performance.html#connbidirect"> - <div class="formrow"> - <input type="hidden" name="graph" value="connbidirect"> - <p> - <label>Start date (yyyy-mm-dd):</label> - <input type="text" name="start" size="10" - value="<c:choose><c:when test="${fn:length(connbidirect_start) == 0}">${default_start_date}</c:when><c:otherwise>${connbidirect_start[0]}</c:otherwise></c:choose>"> - <label>End date (yyyy-mm-dd):</label> - <input type="text" name="end" size="10" - value="<c:choose><c:when test="${fn:length(connbidirect_end) == 0}">${default_end_date}</c:when><c:otherwise>${connbidirect_end[0]}</c:otherwise></c:choose>"> - </p><p> - <input class="submit" type="submit" value="Update graph"> - </p> - </div> -</form> -<p>Download graph as -<a href="connbidirect.pdf${connbidirect_url}">PDF</a> or -<a href="connbidirect.svg${connbidirect_url}">SVG</a>.</p> -<p><a href="stats/connbidirect.csv">CSV</a> file containing all data.</p> -<br> - - </div> - </div> - <div class="bottom" id="bottom"> - <%@ include file="footer.jsp"%> - </div> -</body> -</html> diff --git a/web/WEB-INF/relay-search.jsp b/web/WEB-INF/relay-search.jsp deleted file mode 100644 index 90fdd86..0000000 --- a/web/WEB-INF/relay-search.jsp +++ /dev/null @@ -1,85 +0,0 @@ -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> -<html> -<head> - <title>Tor Metrics Portal: Relay Search</title> - <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> - <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet"> - <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon"> -</head> -<body> - <div class="center"> - <%@ include file="banner.jsp"%> - <div class="main-column"> - <h2>Tor Metrics Portal: Relay Search</h2> - <p>Search for a relay in the relay descriptor archive by typing - (part of) a <b>nickname</b>, <b>$-prefixed fingerprint</b>, or <b>IP - address</b> and optionally a <b>month (yyyy-mm)</b> or up to three - <b>days (yyyy-mm-dd)</b> in the following search field and - clicking Search. The search will stop after 30 hits or, unless you - provide a month or a day, after parsing the last 30 days of relay - lists.</p> - <br> - <form action="relay-search.html"> - <table> - <tr> - <td><input type="text" name="search" - value="<c:out value="${param.search}"/>"></td> - <td><input type="submit" value="Search"></td> - </tr> - </table> - </form> - <br> - <c:if test="${not empty invalidQuery}"> - <p>Sorry, I didn't understand your query. Please provide a - nickname (e.g., "gabelmoo"), at least the first 8 hex characters - of a fingerprint prefixed by $ (e.g., "$F2044413"), or at least - the first two octets of an IPv4 address in dotted-decimal notation - (e.g., "80.190"). You can also provide at most three months or - days in ISO 8601 format (e.g., "2010-09" or "2010-09-17").</p> - </c:if> - <c:if test="${not empty outsideInterval}"> - <p>${outsideInterval}</p> - </c:if> - <c:if test="${not empty searchNotice}"> - <p>${searchNotice}</p> - </c:if> - <c:if test="${not empty query}"> - <!-- ${query} --> - </c:if> - <c:if test="${not empty queryTime}"> - <c:forEach var="consensus" items="${foundDescriptors}"> - ${rawValidAfterLines[consensus.key]} - <c:forEach var="statusentry" items="${consensus.value}"> - ${rawStatusEntries[statusentry]} - </c:forEach> - <br> - </c:forEach> - <p>Found - <c:choose> - <c:when test="${matches > 30}"> - more than 30 relays (displaying only those in the last - consensuses) - </c:when> - <c:otherwise> - ${matches} relays - </c:otherwise> - </c:choose> - in <fmt:formatNumber value="${queryTime / 1000}" pattern="#.###"/> - seconds.</p> - <c:if test="${queryTime > 10000}"> - <p>In theory, search time should not exceed 10 seconds. The - query was '${query}'. If this or similar searches remain slow, - please <a href="mailto:tor-assistants@torproject.org">let us - know</a>!</p> - </c:if> - </c:if> - </div> - </div> - <div class="bottom" id="bottom"> - <%@ include file="footer.jsp"%> - </div> -</body> -</html> - diff --git a/web/WEB-INF/research.jsp b/web/WEB-INF/research.jsp deleted file mode 100644 index b0a7277..0000000 --- a/web/WEB-INF/research.jsp +++ /dev/null @@ -1,32 +0,0 @@ -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> -<html> -<head> - <title>Tor Metrics Portal: Research</title> - <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> - <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet"> - <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon"> -</head> -<body> - <div class="center"> - <%@ include file="banner.jsp"%> - <div class="main-column"> - <h2>Tor Metrics Portal: Research</h2> - <br> - <p>The Tor Metrics Project aims at supporting privacy enhancing - technologies research by making gathered network - <a href="data.html">data</a>, and - <a href="tools.html">tools</a> - for processing these data available to the public. - Some results from analyzing these data can be found in - <a href="https://research.torproject.org/techreports.html">Tor - Tech Reports</a>. If you are - missing anything for your Tor-related research or want to share - your research results with others, please - <a href="mailto:tor-assistants@torproject.org">let us know</a>! - </div> - </div> - <div class="bottom" id="bottom"> - <%@ include file="footer.jsp"%> - </div> -</body> -</html> diff --git a/web/WEB-INF/stats.jsp b/web/WEB-INF/stats.jsp deleted file mode 100644 index ccc2540..0000000 --- a/web/WEB-INF/stats.jsp +++ /dev/null @@ -1,313 +0,0 @@ -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> -<html> -<head> - <title>Tor Metrics Portal: Statistics</title> - <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> - <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet"> - <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon"> -</head> -<body> - <div class="center"> - <%@ include file="banner.jsp"%> - <div class="main-column"> -<h2>Tor Metrics Portal: Statistics</h2> -<br> - -<p>The metrics portal aggregates large amounts of Tor network -<a href="data.html">data</a> and visualizes results in customizable -<a href="graphs.html">graphs</a> and tables. -All aggregated data are also available for download, so that people can -easily plot their own graphs or even develop a prettier metrics website -without writing their own data aggregation code. -Data formats of aggregate statistics are specified below.</p> -<hr> -<br> - -<a name="servers"></a> -<h3><a href="#servers" class="anchor">Number of relays and -bridges</a></h3> -<br> -<p>Statistics file <a href="stats/servers.csv">servers.csv</a> contains -the average number of relays and bridges in the Tor network. -All averages are calculated per day by evaluating the relay and bridge -lists published by the directory authorities. -Statistics include subsets of relays or bridges by relay flag (only -relays), country code (only relays, only until February 2013), Tor -software version (only relays), operating system (only relays), and EC2 -cloud (only bridges). -The statistics file contains the following columns:</p> - -<ul> -<li><b>date:</b> UTC date (YYYY-MM-DD) when relays or bridges have been -listed as running.</li> -<li><b>flag:</b> Relay flag assigned by the directory authorities. -Examples are <b>"Exit"</b>, <b>"Guard"</b>, <b>"Fast"</b>, -<b>"Stable"</b>, and <b>"HSDir"</b>. -Relays can have none, some, or all these relay flags assigned. -Relays that don't have the <b>"Running"</b> flag are not included in these -statistics regardless of their other flags. -If this column contains the empty string, all running relays are included, -regardless of assigned flags. -There are no statistics on the number of bridges by relay flag.</li> -<li><b>country:</b> Two-letter lower-case country code as found in a GeoIP -database by resolving the relay's first onion-routing IP address, or -<b>"??"</b> if an IP addresses could not be resolved. -If this column contains the empty string, all running relays are included, -regardless of their resolved country code. -Statistics on relays by country code are only available until January 31, -2013. -There are no statistics on the number of bridges by country code.</li> -<li><b>version:</b> First three dotted numbers of the Tor software version -as reported by the relay. -An example is <b>"0.2.5"</b>. -If this column contains the empty string, all running relays are included, -regardless of the Tor software version they run. -There are no statistics on the number of bridges by Tor software -version.</li> -<li><b>platform:</b> Operating system as reported by the relay. -Examples are <b>"Linux"</b>, <b>"Darwin"</b> (Mac OS X), <b>"FreeBSD"</b>, -<b>"Windows"</b>, and <b>"Other"</b>. -If this column contains the empty string, all running relays are included, -regardless of the operating system they run on. -There are no statistics on the number of bridges by operating system.</li> -<li><b>ec2bridge:</b> Whether bridges are running in the EC2 cloud or not. -More precisely, bridges in the EC2 cloud running an image provided by Tor -by default set their nickname to <b>"ec2bridger"</b> plus 8 random hex -characters. -This column either contains <b>"t"</b> for bridges matching this naming -scheme, or the empty string for all bridges regardless of their nickname. -There are no statistics on the number of relays running in the EC2 -cloud.</li> -<li><b>relays:</b> The average number of relays matching the criteria in -the previous columns. -If the values in previous columns are specific to bridges only, this -column contains the empty string.</li> -<li><b>bridges:</b> The average number of bridges matching the criteria in -the previous columns. -If the values in previous columns are specific to relays only, this column -contains the empty string.</li> -</ul> -<hr> -<br> - -<a name="bandwidth"></a> -<h3><a href="#bandwidth" class="anchor">Bandwidth provided and consumed by -relays</a></h3> -<br> -<p>Statistics on bandwidth provided and consumed by relays are contained -in file <a href="stats/bandwidth.csv">bandwidth.csv</a>. -This file contains three different bandwidth metrics: -(1) bandwidth that relays are capable to provide and bandwidth that relays -report to have consumed, either (2) for any traffic, or (3) only traffic -from serving directory data. -Relays providing bandwidth statistics are categorized by having the -<b>"Exit"</b> and <b>"Guard"</b> relay flag, having both, or not having -either. -The statistics file contains the following columns:</p> - -<ul> -<li><b>date:</b> UTC date (YYYY-MM-DD) that relays reported bandwidth data -for.</li> -<li><b>isexit:</b> Whether relays included in this line have the -<b>"Exit"</b> relay flag or not, which can be <b>"t"</b> or <b>"f"</b>. -If this column contains the empty string, bandwidth data from all running -relays are included, regardless of assigned relay flags.</li> -<li><b>isguard:</b> Whether relays included in this line have the -<b>"Guard"</b> relay flag or not, which can be <b>"t"</b> or <b>"f"</b>. -If this column contains the empty string, bandwidth data from all running -relays are included, regardless of assigned relay flags.</li> -<li><b>advbw:</b> Total advertised bandwidth in bytes per second that -relays are capable to provide.</li> -<li><b>bwread:</b> Total bandwidth in bytes per second that relays have -read. -This metric includes any kind of traffic.</li> -<li><b>bwwrite:</b> Similar to <b>bwread</b>, but for traffic written by -relays.</li> -<li><b>dirread:</b> Bandwidth in bytes per second that relays have read -when serving directory data. -Not all relays report how many bytes they read when serving directory data -which is why this value is an estimate from the available data. -This metric is not available for subsets of relays with certain relay -flags, so that this column will contain the empty string if either -<b>isexit</b> or <b>isguard</b> is non-empty.</li> -<li><b>dirwrite:</b> Similar to <b>dirread</b>, but for traffic written by -relays when serving directory data.</li> -</ul> -<hr> -<br> - -<a name="fast-exits"></a> -<h3><a href="#fast-exits" class="anchor">Relays meeting or almost meeting -fast-exit requirements</a></h3> -<br> -<p>Statistics file <a href="stats/fast-exits.csv">fast-exits.csv</a> -contains the number of relays meeting or almost meeting fast-exit -requirements. -These requirements originate from a Tor sponsor contract and are defined as -follows: -a Tor relay is fast if it has at least 95 Mbit/s configured bandwidth -rate, at least 5000 KB/s advertised bandwidth capacity, and permits -exiting to ports 80, 443, 554, and 1755; furthermore, there may be at most -2 relays per /24 network in the set of fast exits. -Similarly, an almost fast exit is one that almost meets the fast-exit -requirements, but fails at least one of them. -In particular, an almost fast exit is one that has at least 80 Mbit/s -configured bandwidth rate, at least 2000 KB/s advertised bandwidth -capacity, and permits exiting to ports 80 and 443; also, if there are more -than 2 relays per /24 network meeting fast-exit requirements, all but two -are considered almost fast. -The statistics file contains the following columns:</p> - -<ul> -<li><b>date:</b> UTC date (YYYY-MM-DD) when relays have been listed as -running.</li> -<li><b>fastnum:</b> Average number of relays matching fast-exit -requirements.</li> -<li><b>almostnum:</b> Average number of relays almost matching -fast-exit requirements.</li> -<li><b>fastprob:</b> Total exit probability of all relays matching -fast-exit requirements.</li> -<li><b>almostprob:</b> Total exit probability of all relays almost -matching fast-exit requirements.</li> -</li> -</ul> -<hr> -<br> - -<a name="clients"></a> -<h3><a href="#clients" class="anchor">Estimated number of clients in the -Tor network</a></h3> -<br> -<p>Statistics file <a href="stats/clients.csv">clients.csv</a> contains -estimates on the number of clients in the Tor network. -These estimates are based on the number of directory requests counted on -directory mirrors and bridges. -Statistics are available for clients connecting directly to the Tor -network and clients connecting via bridges. -For relays, there exist statistics on the number of clients by country, -and for bridges, statistics are available by country, by transport, and by -IP version. -Statistics further include expected client numbers from past observations -which can be used to detect censorship or release of censorship. -The statistics file contains the following columns:</p> - -<ul> -<li><b>date:</b> UTC date (YYYY-MM-DD) for which client numbers are -estimated.</li> -<li><b>node:</b> The node type to which clients connect first, which can -be either <b>"relay"</b> or <b>"bridge"</b>.</li> -<li><b>country:</b> Two-letter lower-case country code as found in a GeoIP -database by resolving clients' IP addresses, or <b>"??"</b> if client IP -addresses could not be resolved. -If this column contains the empty string, all clients are included, -regardless of their country code.</li> -<li><b>transport:</b> Transport name used by clients to connect to the Tor -network using bridges. -Examples are <b>"obfs2"</b>, <b>"obfs3"</b>, <b>"websocket"</b>, or -<b>"<OR>"</b> (original onion routing protocol). -If this column contains the empty string, all clients are included, -regardless of their transport. -There are no statistics on the number of clients by transport that connect -to the Tor network via relays.</li> -<li><b>version:</b> IP version used by clients to connect to the Tor -network using bridges. -Examples are <b>"v4"</b> and <b>"v6"</b>. -If this column contains the empty string, all clients are included, -regardless of their IP version. -There are no statistics on the number of clients by IP version that connect -directly to the Tor network using relays.</li> -<li><b>lower:</b> Lower number of expected clients under the assumption -that there has been no censorship event. -If this column contains the empty string, there are no expectations on the -number of clients.</li> -<li><b>upper:</b> Upper number of expected clients under the assumption -that there has been no release of censorship. -If this column contains the empty string, there are no expectations on the -number of clients.</li> -<li><b>clients:</b> Estimated number of clients.</li> -<li><b>frac:</b> Fraction of relays or bridges in percent that the -estimate is based on. -The higher this value, the more reliable is the estimate. -Values above 50 can be considered reliable enough for most purposes, -lower values should be handled with more care.</li> -</ul> -<hr> -<br> - -<a name="torperf"></a> -<h3><a href="#torperf" class="anchor">Performance of downloading static -files over Tor</a></h3> -<br> -<p>Statistics file <a href="stats/torperf.csv">torperf.csv</a> contains -aggregate statistics on download performance over time. -These statistics come from the Torperf service that periodically downloads -static files over Tor. -The statistics file contains the following columns:</p> - -<ul> -<li><b>date:</b> UTC date (YYYY-MM-DD) when download performance was -measured.</li> -<li><b>size:</b> Size of the downloaded file in bytes.</li> -<li><b>source:</b> Name of the Torperf service performing measurements. -If this column contains the empty string, all measurements are included, -regardless of which Torperf service performed them. -Examples are <b>"moria"</b>, <b>"siv"</b>, and <b>"torperf"</b>.</li> -<li><b>q1:</b> First quartile of time until receiving the last byte in -milliseconds.</li> -<li><b>md:</b> Median of time until receiving the last byte in -milliseconds.</li> -<li><b>q3:</b> Third quartile of time until receiving the last byte in -milliseconds.</li> -<li><b>timeouts:</b> Number of timeouts that occurred when attempting to -download the static file over Tor.</li> -<li><b>failures:</b> Number of failures that occurred when attempting to -download the static file over Tor.</li> -<li><b>requests:</b> Total number of requests made to download the static -file over Tor.</li> -</ul> -<hr> -<br> - -<a name="connbidirect"></a> -<h3><a href="#connbidirect" class="anchor">Fraction of connections used -uni-/bidirectionally</a></h3> -<br> -<p>Statistics file <a href="stats/connbidirect.csv">connbidirect.csv</a> -contains statistics on the fraction of connections that is used uni- or -bidirectionally. -Every 10 seconds, relays determine for every connection whether they read -and wrote less than a threshold of 20 KiB. -For the remaining connections, relays report whether they read/wrote at -least 10 times as many bytes as they wrote/read. -If so, they classify a connection as "mostly reading" or "mostly writing," -respectively. -All other connections are classified as "both reading and writing." -After classifying connections, read and write counters are reset for the -next 10-second interval. -Statistics are aggregated over 24 hours. -The statistics file contains the following columns:</p> - -<ul> -<li><b>date:</b> UTC date (YYYY-MM-DD) for which statistics on -uni-/bidirectional connection usage were reported.</li> -<li><b>source:</b> Fingerprint of the relay reporting statistics.</li> -<li><b>below:</b> Number of 10-second intervals of connections with less -than 20 KiB read and written data.</li> -<li><b>read:</b> Number of 10-second intervals of connections with 10 -times as many read bytes as written bytes.</li> -<li><b>write:</b> Number of 10-second intervals of connections with 10 -times as many written bytes as read bytes.</li> -<li><b>both:</b> Number of 10-second intervals of connections with less -than 10 times as many written or read bytes as in the other -direction.</li> -</ul> - </div> - </div> - <div class="bottom" id="bottom"> - <%@ include file="footer.jsp"%> - </div> -</body> -</html> -</body> -</html> - diff --git a/web/WEB-INF/status.jsp b/web/WEB-INF/status.jsp deleted file mode 100644 index d6b565e..0000000 --- a/web/WEB-INF/status.jsp +++ /dev/null @@ -1,30 +0,0 @@ -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> -<html> -<head> - <title>Tor Metrics Portal: Status</title> - <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> - <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet"> - <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon"> -</head> -<body> - <div class="center"> - <%@ include file="banner.jsp"%> - <div class="main-column"> - <h2>Tor Metrics Portal: Status</h2> - <br> - <p>The network data collected by the Tor Metrics Project can be - used to analyze the Tor network status from a few years ago until - an hour ago. There are currently two applications for this data: - The <a href="exonerator.html">ExoneraTor</a> tells you whether - some IP address was a Tor relay at a given time, the - <a href="relay-search.html">Relay Search</a> lets you search the - descriptor archive for a relay, and the - <a href="consensus-health.html">Consensus Health</a> summarizes - information about the latest network consensus voting process. - </div> - </div> - <div class="bottom" id="bottom"> - <%@ include file="footer.jsp"%> - </div> -</body> -</html> diff --git a/web/WEB-INF/tools.jsp b/web/WEB-INF/tools.jsp deleted file mode 100644 index 8929d74..0000000 --- a/web/WEB-INF/tools.jsp +++ /dev/null @@ -1,110 +0,0 @@ -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> -<html> -<head> - <title>Tor Metrics Portal: Tools</title> - <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> - <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet"> - <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon"> -</head> -<body> - <div class="center"> - <%@ include file="banner.jsp"%> - <div class="main-column"> - <h2>Tor Metrics Portal: Tools</h2> - <br> - <p>The metrics website and related websites depend on a collection - of services that measure, archive, process, and finally present Tor - network data. - This page gives an overview of these services.</p> - <img src="/images/tor-metrics-overview.png"/> - <br> - <a name="measure"></a> - <h3><a href="#measure" class="anchor">Measuring Tor network - data</a></h3> - <br> - <p>Tor network data is measured at various places: - <ul> - <li><a href="https://gitweb.torproject.org/torperf.git">Torperf</a> - is a set of utilities for testing Tor performance from a client - perspective.</li> - <li><a href="https://gitweb.torproject.org/tor.git">tor</a> - relays and bridges gather aggregate usage statistics and publish - descriptors containing data about Tor network structure and - usage.</li> - <li><a href="https://gitweb.torproject.org/tordnsel.git">TorDNSEL</a> - is a Tor DNS-based exit list that runs periodic checks whether - relays use different IP addresses for exiting to the Internet - than they use to register in the Tor network.</li> - <li><a href="https://gitweb.torproject.org/bridgedb.git">BridgeDB</a> - assigns bridges to distributors and gives them out via HTTPS or - email.</li> - </ul> - <br> - <a name="archive"></a> - <h3><a href="#archive" class="anchor">Archiving Tor network - data</a></h3> - <br> - <p>All Tor network data is downloaded, possibly sanitized, and - then archived by a single tool:</p> - <ul> - <li><a href="https://gitweb.torproject.org/metrics-db.git">metrics-db</a> - contains five components for archiving relay descriptors, bridge - descriptors, Torperf results, TorDNSEL exit lists, and BridgeDB - pool assignments.</li> - </ul> - <br> - <a name="process"></a> - <h3><a href="#process" class="anchor">Processing Tor network - data</a></h3> - <br> - <p>In some cases, processing and presenting Tor network data is - separated for maximum flexibility. - In particular, there are currently two main tools that process Tor - network data and write an intermediate data format, but don't - directly present results:</p> - <ul> - <li><a href="https://gitweb.torproject.org/metrics-web.git">metrics-web</a> - is the software behind this website, including aggregation code - that produces statistics files.</li> - <li><a href="https://gitweb.torproject.org/metrics-tasks.git/tree/HEAD:/task-6498">task-6498</a> - is a submodule of metric-web that - aggregates data to visualize fast exits in the Tor network.</li> - <li><a href="https://gitweb.torproject.org/metrics-tasks.git/tree/HEAD:/task-8462">task-8462</a> - is another submodule of metric-web that - estimates daily users from reported directory request - statistics.</li> - <li><a href="https://gitweb.torproject.org/onionoo.git">Onionoo</a> - provides Tor network status information in JSON format via a - RESTful web service.</li> - </ul> - <br> - <a name="present"></a> - <h3><a href="#present" class="anchor">Presenting Tor network - data</a></h3> - <br> - <p>There are a few websites and additional tools presenting Tor - network data: - <ul> - <li><a href="https://gitweb.torproject.org/metrics-web.git">metrics-web</a> - also contains the code that presents aggregate statistics on - this website.</li> - <li><a href="https://gitweb.torproject.org/exonerator.git">ExoneraTor</a> - is a website that tells you whether a given IP address was a Tor - relay.</li> - <li><a href="https://gitweb.torproject.org/atlas.git">Atlas</a> - is a web application to discover relays that uses Onionoo as its - data back-end.</li> - <li><a href="https://gitweb.torproject.org/globe.git">Globe</a> - is a Tor relay and bridge explorer that also uses Onionoo as its - data back-end.</li> - <li><a href="https://gitweb.torproject.org/compass.git">Compass</a> - is a web application that uses Onionoo's data to display - information about fast exits in the Tor network.</li> - </ul> - </div> - </div> - <div class="bottom" id="bottom"> - <%@ include file="footer.jsp"%> - </div> -</body> -</html> diff --git a/web/WEB-INF/users.jsp b/web/WEB-INF/users.jsp deleted file mode 100644 index ec9f200..0000000 --- a/web/WEB-INF/users.jsp +++ /dev/null @@ -1,390 +0,0 @@ -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> -<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> -<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> -<html> -<head> - <title>Tor Metrics Portal: Users</title> - <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> - <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet"> - <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon"> -</head> -<body> - <div class="center"> - <%@ include file="banner.jsp"%> - <div class="main-column"> -<h2>Tor Metrics Portal: Users</h2> -<br> - -<a name="userstats-relay-country"></a> -<p><b>Direct users by country:</b></p> - -<img src="userstats-relay-country.png${userstats_relay_country_url}" - width="576" height="360" alt="Direct users by country graph"> -<form action="users.html#userstats-relay-country"> - <div class="formrow"> - <input type="hidden" name="graph" value="userstats-relay-country"> - <p> - <label>Start date (yyyy-mm-dd):</label> - <input type="text" name="start" size="10" - value="<c:choose><c:when test="${fn:length(userstats_relay_country_start) == 0}">${default_start_date}</c:when><c:otherwise>${userstats_relay_country_start[0]}</c:otherwise></c:choose>"> - <label>End date (yyyy-mm-dd):</label> - <input type="text" name="end" size="10" - value="<c:choose><c:when test="${fn:length(userstats_relay_country_end) == 0}">${default_end_date}</c:when><c:otherwise>${userstats_relay_country_end[0]}</c:otherwise></c:choose>"> - </p><p> - Source: <select name="country"> - <option value="all"<c:if test="${userstats_relay_country_country[0] eq 'all'}"> selected</c:if>>All users</option> - <c:forEach var="country" items="${countries}" > - <option value="${country[0]}"<c:if test="${userstats_relay_country_country[0] eq country[0]}"> selected</c:if>>${country[1]}</option> - </c:forEach> - </select> - </p><p> - Show possible censorship events if available (<a - href="http://research.torproject.org/techreports/detector-2011-09-09.pdf">BETA</a>) - <select name="events"> - <option value="off">Off</option> - <option value="on"<c:if test="${userstats_relay_country_events[0] eq 'on'}"> selected</c:if>>On: both points and expected range</option> - <option value="points"<c:if test="${userstats_relay_country_events[0] eq 'points'}"> selected</c:if>>On: points only, no expected range</option> - </select> - </p><p> - <input class="submit" type="submit" value="Update graph"> - </p> - </div> -</form> -<p>Download graph as -<a href="userstats-relay-country.pdf${userstats_relay_country_url}">PDF</a> or -<a href="userstats-relay-country.svg${userstats_relay_country_url}">SVG</a>.</p> -<hr> -<a name="userstats-relay-table"></a> -<p><b>Top-10 countries by directly connecting users:</b></p> -<form action="users.html#userstats-relay-table"> - <div class="formrow"> - <input type="hidden" name="table" value="userstats-relay"> - <p> - <label>Start date (yyyy-mm-dd):</label> - <input type="text" name="start" size="10" - value="<c:choose><c:when test="${fn:length(userstats_relay_start) == 0}">${default_start_date}</c:when><c:otherwise>${userstats_relay_start[0]}</c:otherwise></c:choose>"> - <label>End date (yyyy-mm-dd):</label> - <input type="text" name="end" size="10" - value="<c:choose><c:when test="${fn:length(userstats_relay_end) == 0}">${default_end_date}</c:when><c:otherwise>${userstats_relay_end[0]}</c:otherwise></c:choose>"> - </p><p> - <input class="submit" type="submit" value="Update table"> - </p> - </div> -</form> -<br> -<table> - <tr> - <th>Country</th> - <th>Mean daily users</th> - </tr> - <c:forEach var="row" items="${userstats_relay_tabledata}"> - <tr> - <td><a href="users.html?graph=userstats-relay-country&country=${row['cc']}#userstats-relay-country">${row['country']}</a> </td> - <td>${row['abs']} (<fmt:formatNumber type="number" minFractionDigits="2" value="${row['rel']}" /> %)</td> - </tr> - </c:forEach> -</table> -<hr> -<a name="userstats-censorship-events"></a> -<p><b>Top-10 countries by possible censorship events (<a - href="http://research.torproject.org/techreports/detector-2011-09-09.pdf">BETA</a>):</b></p> -<form action="users.html#userstats-censorship-events"> - <div class="formrow"> - <input type="hidden" name="table" value="userstats-censorship-events"> - <p> - <label>Start date (yyyy-mm-dd):</label> - <input type="text" name="start" size="10" - value="<c:choose><c:when test="${fn:length(userstats_censorship_events_start) == 0}">${default_start_date}</c:when><c:otherwise>${userstats_censorship_events_start[0]}</c:otherwise></c:choose>"> - <label>End date (yyyy-mm-dd):</label> - <input type="text" name="end" size="10" - value="<c:choose><c:when test="${fn:length(userstats_censorship_events_end) == 0}">${default_end_date}</c:when><c:otherwise>${userstats_censorship_events_end[0]}</c:otherwise></c:choose>"> - </p><p> - <input class="submit" type="submit" value="Update table"> - </p> - </div> -</form> -<br> -<table> - <tr> - <th>Country</th> - <th>Downturns</th> - <th>Upturns</th> - </tr> - <c:forEach var="row" items="${userstats_censorship_events_tabledata}"> - <tr> - <td><a href="users.html?graph=userstats-relay-country&country=${row['cc']}&events=on#userstats-relay-country">${row['country']}</a> </td> - <td>${row['downturns']}</td> - <td>${row['upturns']}</td> - </tr> - </c:forEach> -</table> -<hr> - -<a name="userstats-bridge-country"></a> -<p><b>Bridge users by country:</b></p> - -<img src="userstats-bridge-country.png${userstats_bridge_country_url}" - width="576" height="360" alt="Bridge users by country graph"> -<form action="users.html#userstats-bridge-country"> - <div class="formrow"> - <input type="hidden" name="graph" value="userstats-bridge-country"> - <p> - <label>Start date (yyyy-mm-dd):</label> - <input type="text" name="start" size="10" - value="<c:choose><c:when test="${fn:length(userstats_bridge_country_start) == 0}">${default_start_date}</c:when><c:otherwise>${userstats_bridge_country_start[0]}</c:otherwise></c:choose>"> - <label>End date (yyyy-mm-dd):</label> - <input type="text" name="end" size="10" - value="<c:choose><c:when test="${fn:length(userstats_bridge_country_end) == 0}">${default_end_date}</c:when><c:otherwise>${userstats_bridge_country_end[0]}</c:otherwise></c:choose>"> - </p><p> - Source: <select name="country"> - <option value="all"<c:if test="${userstats_bridge_country_country[0] eq 'all'}"> selected</c:if>>All users</option> - <c:forEach var="country" items="${countries}" > - <option value="${country[0]}"<c:if test="${userstats_bridge_country_country[0] eq country[0]}"> selected</c:if>>${country[1]}</option> - </c:forEach> - </select> - </p><p> - <input class="submit" type="submit" value="Update graph"> - </p> - </div> -</form> -<p>Download graph as -<a href="userstats-bridge-country.pdf${userstats_bridge_country_url}">PDF</a> or -<a href="userstats-bridge-country.svg${userstats_bridge_country_url}">SVG</a>.</p> -<hr> -<a name="userstats-bridge-table"></a> -<p><b>Top-10 countries by bridge users:</b></p> -<form action="users.html#userstats-bridge-table"> - <div class="formrow"> - <input type="hidden" name="table" value="userstats-bridge"> - <p> - <label>Start date (yyyy-mm-dd):</label> - <input type="text" name="start" size="10" - value="<c:choose><c:when test="${fn:length(userstats_bridge_start) == 0}">${default_start_date}</c:when><c:otherwise>${userstats_bridge_start[0]}</c:otherwise></c:choose>"> - <label>End date (yyyy-mm-dd):</label> - <input type="text" name="end" size="10" - value="<c:choose><c:when test="${fn:length(userstats_bridge_end) == 0}">${default_end_date}</c:when><c:otherwise>${userstats_bridge_end[0]}</c:otherwise></c:choose>"> - </p><p> - <input class="submit" type="submit" value="Update table"> - </p> - </div> -</form> -<br> -<table> - <tr> - <th>Country</th> - <th>Mean daily users</th> - </tr> - <c:forEach var="row" items="${userstats_bridge_tabledata}"> - <tr> - <td><a href="users.html?graph=userstats-bridge-country&country=${row['cc']}#userstats-bridge-country">${row['country']}</a> </td> - <td>${row['abs']} (<fmt:formatNumber type="number" minFractionDigits="2" value="${row['rel']}" /> %)</td> - </tr> - </c:forEach> -</table> -<hr> - -<a name="userstats-bridge-transport"></a> -<p><b>Bridge users by transport:</b></p> - -<img src="userstats-bridge-transport.png${userstats_bridge_transport_url}" - width="576" height="360" alt="Bridge users by transport graph"> -<form action="users.html#userstats-bridge-transport"> - <div class="formrow"> - <input type="hidden" name="graph" value="userstats-bridge-transport"> - <p> - <label>Start date (yyyy-mm-dd):</label> - <input type="text" name="start" size="10" - value="<c:choose><c:when test="${fn:length(userstats_bridge_transport_start) == 0}">${default_start_date}</c:when><c:otherwise>${userstats_bridge_transport_start[0]}</c:otherwise></c:choose>"> - <label>End date (yyyy-mm-dd):</label> - <input type="text" name="end" size="10" - value="<c:choose><c:when test="${fn:length(userstats_bridge_transport_end) == 0}">${default_end_date}</c:when><c:otherwise>${userstats_bridge_transport_end[0]}</c:otherwise></c:choose>"> - </p><p> - Source: <select name="transport"> - <option value="<OR>"<c:if test="${userstats_bridge_transport_transport[0] eq '<OR>'}"> selected</c:if>>Default OR protocol</option> - <option value="obfs2"<c:if test="${userstats_bridge_transport_transport[0] eq 'obfs2'}"> selected</c:if>>obfs2</option> - <option value="obfs3"<c:if test="${userstats_bridge_transport_transport[0] eq 'obfs3'}"> selected</c:if>>obfs3</option> - <option value="websocket"<c:if test="${userstats_bridge_transport_transport[0] eq 'websocket'}"> selected</c:if>>Flash proxy/websocket</option> - <option value="<??>"<c:if test="${userstats_bridge_transport_transport[0] eq '<??>'}"> selected</c:if>>Unknown transport</option> - </select> - </p><p> - <input class="submit" type="submit" value="Update graph"> - </p> - </div> -</form> -<p>Download graph as -<a href="userstats-bridge-transport.pdf${userstats_bridge_transport_url}">PDF</a> or -<a href="userstats-bridge-transport.svg${userstats_bridge_transport_url}">SVG</a>.</p> -<hr> - -<a name="userstats-bridge-version"></a> -<p><b>Bridge users by IP version:</b></p> - -<img src="userstats-bridge-version.png${userstats_bridge_version_url}" - width="576" height="360" alt="Bridge users by IP version graph"> -<form action="users.html#userstats-bridge-version"> - <div class="formrow"> - <input type="hidden" name="graph" value="userstats-bridge-version"> - <p> - <label>Start date (yyyy-mm-dd):</label> - <input type="text" name="start" size="10" - value="<c:choose><c:when test="${fn:length(userstats_bridge_version_start) == 0}">${default_start_date}</c:when><c:otherwise>${userstats_bridge_version_start[0]}</c:otherwise></c:choose>"> - <label>End date (yyyy-mm-dd):</label> - <input type="text" name="end" size="10" - value="<c:choose><c:when test="${fn:length(userstats_bridge_version_end) == 0}">${default_end_date}</c:when><c:otherwise>${userstats_bridge_version_end[0]}</c:otherwise></c:choose>"> - </p><p> - Source: <select name="version"> - <option value="v4"<c:if test="${userstats_bridge_version_version[0] eq 'v4'}"> selected</c:if>>IPv4</option> - <option value="v6"<c:if test="${userstats_bridge_version_version[0] eq 'v6'}"> selected</c:if>>IPv6</option> - </select> - </p><p> - <input class="submit" type="submit" value="Update graph"> - </p> - </div> -</form> -<p>Download graph as -<a href="userstats-bridge-version.pdf${userstats_bridge_version_url}">PDF</a> or -<a href="userstats-bridge-version.svg${userstats_bridge_version_url}">SVG</a>.</p> -<hr> - -<p><a href="stats/clients.csv">CSV</a> file containing new user -estimates.</p> -<br> - -<hr> -<a name="questions-and-answers"></a> -<p><b>Questions and answers</b></p> -<p> -Q: How is it even possible to count users in an anonymity network?<br/> -A: We actually don't count users, but we count requests to the directories -that clients make periodically to update their list of relays and estimate -user numbers indirectly from there. -</p> -<p> -Q: Do all directories report these directory request numbers?<br/> -A: No, but we can see what fraction of directories reported them, and then -we can extrapolate the total number in the network. -</p> - -<p> -Q: How do you get from these directory requests to user numbers?<br/> -A: We put in the assumption that the average client makes 10 such requests -per day. A tor client that is connected 24/7 makes about 15 requests per -day, but not all clients are connected 24/7, so we picked the number 10 -for the average client. We simply divide directory requests by 10 and -consider the result as the number of users. -</p> - -<p> -Q: So, are these distinct users per day, average number of users connected -over the day, or what?<br/> -A: Average number of users connected over the day. We can't say how many -distinct users there are. -</p> - -<p> -Q: Are these tor clients or users? What if there's more than one user -behind a tor client?<br/> -A: Then we count those users as one. We really count clients, but it's -more intuitive for most people to think of users, that's why we say users -and not clients. -</p> - -<p> -Q: What if a user runs tor on a laptop and changes their IP address a few -times per day? Don't you overcount that user?<br/> -A: No, because that user updates their list of relays as often as a user -that doesn't change IP address over the day. -</p> - -<p> -Q: How do you know which countries users come from?<br/> -A: The directories resolve IP addresses to country codes and report these -numbers in aggregate form. This is one of the reasons why tor ships with -a GeoIP database. -</p> - -<p> -Q: Why are there so few bridge users that are not using the default OR -protocol or that are using IPv6?<br/> -A: Very few bridges report data on transports or IP versions yet, and by -default we consider requests to use the default OR protocol and IPv4. -Once more bridges report these data, the numbers will become more -accurate. -</p> - -<p> -Q: Why do the graphs end 2 days in the past and not today?<br/> -A: Relays and bridges report some of the data in 24-hour intervals which -may end at any time of the day. And after such an interval is over relays -and bridges might take another 18 hours to report the data. We cut off -the last two days from the graphs, because we want to avoid that the last -data point in a graph indicates a recent trend change which is in fact -just an artifact of the algorithm. -</p> - -<p> -Q: But I noticed that the last data point went up/down a bit since I last -looked a few hours ago. Why is that?<br/> -A: You're an excellent observer! The reason is that we publish user -numbers once we're confident enough that they won't change significantly -anymore. But it's always possible that a directory reports data a few -hours after we were confident enough, but which then slightly changed the -graph. -</p> - -<p> -Q: Why are no numbers available before September 2011?<br/> -A: We do have descriptor archives from before that time, but those -descriptors didn't contain all the data we use to estimate user numbers. -We do have older user numbers from an earlier estimation approach -<a href="/data/old-user-number-estimates.tar.gz">here</a>, but we believe -the current approach is more accurate. -</p> - -<p> -Q: Why do you believe the current approach to estimate user numbers is -more accurate?<br/> -A: For direct users, we include all directories which we didn't do in the -old approach. We also use histories that only contain bytes written to -answer directory requests, which is more precise than using general byte -histories. -</p> - -<p> -Q: And what about the advantage of the current approach over the old one -when it comes to bridge users?<br/> -A: Oh, that's a whole different story. We wrote a 13 page long -<a href="https://research.torproject.org/techreports/counting-daily-bridge-users-2012-10-24.pdf">technical -report</a> explaining the reasons for retiring the old approach. -tl;dr: in the old approach we measured the wrong thing, and now we measure -the right thing. -</p> - -<p> -Q: Are the data and the source code for estimating these user numbers -available?<br/> -A: Sure, <a href="/data.html">data</a> and -<a href="https://gitweb.torproject.org/metrics-tasks.git/tree/HEAD:/task-8462">source -code</a> are publicly available. -</p> - -<p> -Q: What are these red and blue dots indicating possible censorship -events?<br/> -A: We run an anomaly-based censorship-detection system that looks at -estimated user numbers over a series of days and predicts the user number -in the next days. If the actual number is higher or lower, this might -indicate a possible censorship event or release of censorship. For more -details, see our -<a href="https://research.torproject.org/techreports/detector-2011-09-09.pdf">technical -report</a>. -</p> - - </div> - </div> - <div class="bottom" id="bottom"> - <%@ include file="footer.jsp"%> - </div> -</body> -</html> diff --git a/web/css/stylesheet-ltr.css b/web/css/stylesheet-ltr.css deleted file mode 100644 index ce0c54e..0000000 --- a/web/css/stylesheet-ltr.css +++ /dev/null @@ -1,161 +0,0 @@ -body { - background-color: white; - margin-top: 0px; - font-family: Arial, Helvetica, sans-serif; - font-size: 1em; - font-style: normal; - color: #000000; - padding-top: 0px; -} - -/* images */ - -img { - border: 0; -} - - -li { - margin: .2em .2em .2em 1em; -} - -/* this centers the page */ - -.center { - text-align: center; - background-color: white; - margin: 0px auto 0 auto; - width: 85%; -} - -.center table { - margin-left: auto; - margin-right: auto; - text-align: left; -} - -div.bottom { - font-size: 0.8em; - margin-top: 0.5cm; - margin-left: 1em; - margin-right: 1em; - text-align: center; -} - -/* The main column (left text) */ - -div.main-column { - padding: 15px 0 10px 10px; - text-indent: 0pt; - font-size: 1em; - direction: ltr; - text-align: left; -} - -/* formatting styles */ - -h2 { - font-size: 1.4em; - margin-bottom: 0em; - font-weight: bold; - margin-top: 0; -} - -h3 { - font-size: 1.2em; - margin-bottom: 0em; - font-weight: bold; - margin-top: 0; -} - -p { - margin-top: 0; - margin-bottom: 1em; -} - -a:link { - color: blue; - font-size: 1em; -} - -a:visited { - color: purple; - font-size: 1em; -} - -a.anchor { - font-size: 1em; - color: black; - font-weight: bold; - text-decoration: none; -} - -td { - vertical-align: top; -} - -/* the banner */ - -table.banner { - width: 100%; - height: 79px; - margin-left: auto; - margin-right: auto; -} - -td.banner-left { - /* This is done with an <img> in the HTML so it can be clickable - background-image: url("images/top-left.png"); - background-repeat: no-repeat; */ - width: 193px; -} - -td.banner-middle { - background-color: #00802B; - background-image: url("/images/top-middle.png"); - background-repeat: repeat-x; - vertical-align: bottom; - padding-bottom: 10px; - color: white; - font-weight: bold; - font-size: 1.2em; -} - -td.banner-middle a, td.banner-middle a:visited { - margin-right: 5px; - color: white; - font-weight: bold; - font-size: 1em; -} - -td.banner-middle a:hover { - color: #FF7F00; - font-weight: bold; - font-size: 1em; -} - -td.banner-right { - background-image: url("/images/top-right.png"); - background-repeat: no-repeat; - width: 15px; - background-position: right; - padding-top: 8px; -} - -.banner-middle a.current { - text-decoration: none; - color: #FF7F00; - font-weight: bold; - font-size: 1em; - width: auto; - left: -50px; -} - -hr { - background-color:#002200; - color:#666666; - font-size:1px; - height:1px; - line-height:0; - margin:15px 0 5px; -} diff --git a/web/images/favicon.ico b/web/images/favicon.ico deleted file mode 100644 index 48060b1..0000000 Binary files a/web/images/favicon.ico and /dev/null differ diff --git a/web/images/top-left.png b/web/images/top-left.png deleted file mode 100644 index 9927355..0000000 Binary files a/web/images/top-left.png and /dev/null differ diff --git a/web/images/top-middle.png b/web/images/top-middle.png deleted file mode 100644 index 1035dc5..0000000 Binary files a/web/images/top-middle.png and /dev/null differ diff --git a/web/images/top-right.png b/web/images/top-right.png deleted file mode 100644 index 54623ef..0000000 Binary files a/web/images/top-right.png and /dev/null differ diff --git a/web/images/tor-metrics-overview.png b/web/images/tor-metrics-overview.png deleted file mode 100644 index ee2bc07..0000000 Binary files a/web/images/tor-metrics-overview.png and /dev/null differ diff --git a/web/js/bubbles.js b/web/js/bubbles.js deleted file mode 100644 index 8030e20..0000000 --- a/web/js/bubbles.js +++ /dev/null @@ -1,288 +0,0 @@ -function get_prefix(relay) { return /^[0-9]+.[0-9]+./.exec(relay.or_addresses[0]); } - -var graphs = { - 'default': { - extra_fields: [], - group: undefined, - group_id_func: function(relay) { return undefined; }, - group_name_func: function(relay) { return 'Relays'; }, - }, 'as': { - extra_fields: ['as_number', 'as_name'], - group: "autonomous systems", - group_id_func: function(relay) { return relay.as_number; }, - group_name_func: function(relay) { return relay.as_name; }, - }, 'contact': { - extra_fields: ['contact'], - group: "contact infos", - group_id_func: function(relay) { return relay.contact; }, - group_name_func: function(relay) { return relay.contact; }, - }, 'country': { - extra_fields: ['country', 'country_name'], - group: "countries", - group_id_func: function(relay) { return relay.country; }, - group_name_func: function(relay) { return relay.country_name; }, - }, 'network-family': { - extra_fields: ['or_addresses'], - group: "network families (/16)", - group_id_func: function(relay) { return get_prefix(relay); }, - group_name_func: function(relay) { return get_prefix(relay) + "0.0/16"; }, - }, -}; - -function make_bubble_graph(graph_name) { - var onionoo_url = "https://onionoo.torproject.org/details?type=relay&running=true&field..."; - 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%22,xhtml:%22http://www.w3.org/1999/xhtml%22,xlink... 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@torproject.org">let us know</a>!</p> + + </div> + </div> + <div class="bottom" id="bottom"> + <%@ include file="footer.jsp"%> + </div> +</body> +</html> diff --git a/website/web/WEB-INF/exonerator.jsp b/website/web/WEB-INF/exonerator.jsp new file mode 100644 index 0000000..0eefe99 --- /dev/null +++ b/website/web/WEB-INF/exonerator.jsp @@ -0,0 +1,45 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> +<html> +<head> + <title>Tor Metrics Portal: ExoneraTor</title> + <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> + <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet"> + <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon"> +</head> +<body> + <div class="center"> + <%@ include file="banner.jsp"%> + <div class="main-column" style="margin:5; Padding:0;"> + <h2>ExoneraTor</h2> + <h3>or: a website that tells you whether a given IP address was a Tor + relay</h3> + <br> + <p>Just because you see an Internet connection from a particular IP + address does not mean you know <i>who</i> originated the traffic. Tor + anonymizes Internet traffic by "<a href="https://www.torproject.org/about/overview#thesolution">onion + routing</a>," sending packets through a series of encrypted hops + before they reach their destination. Therefore, if you see traffic + from a Tor node, you may be seeing traffic that originated from + someone using Tor, rather than from the node operator itself. The Tor + Project and Tor node operators have no records of the traffic that + passes over the network, but we do maintain current and historical + records of which IP addresses are part of the Tor network.</p> + <br> + <p>ExoneraTor tells you whether there was a Tor relay running on a + given IP address at a given time. ExoneraTor can further indicate + whether this relay permitted exiting to a given server and/or TCP + port. ExoneraTor learns these facts by parsing the public relay lists + and relay descriptors that are collected from the Tor directory + authorities and the exit lists collected by TorDNSEL. By inputting an + IP address and time, you can determine whether that IP was then a part + of the Tor network.</p> + <br> + <p><font color="red"><b>Notice:</b> This service has moved to: + <a href="https://exonerator.torproject.org/">https://exonerator.torproject.org/</a></font></p> + </div> + </div> + <div class="bottom" id="bottom"> + <%@ include file="footer.jsp"%> + </div> +</body> +</html> diff --git a/website/web/WEB-INF/fast-exits.jsp b/website/web/WEB-INF/fast-exits.jsp new file mode 100644 index 0000000..903c88c --- /dev/null +++ b/website/web/WEB-INF/fast-exits.jsp @@ -0,0 +1,81 @@ +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> +<html> +<head> + <title>Tor Metrics Portal: Fast Exits</title> + <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> + <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet"> + <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon"> +</head> +<body> + <div class="center"> + <%@ include file="banner.jsp"%> + <div class="main-column"> +<h2>Tor Metrics Portal: Fast Exits</h2> +<br> +<p>This page summarizes progress in operating 125 fast exits, which is a +deliverable for +<a href="https://trac.torproject.org/projects/tor/wiki/org/sponsors/SponsorJ">SponsorJ</a>.</p> +<br> + +<a name="fastexits"></a> +<h3><a href="#fastexits" class="anchor">Relays meeting the fast-exit +requirements</a></h3> +<br> +<img src="fast-exits.png${fast_exits_url}" + width="576" height="432" alt="Fast exits graph"> +<form action="fast-exits.html#fastexits"> + <div class="formrow"> + <input type="hidden" name="graph" value="fast-exits"> + <p> + <label>Start date (yyyy-mm-dd):</label> + <input type="text" name="start" size="10" + value="<c:choose><c:when test="${fn:length(fast_exits_start) == 0}">${default_start_date}</c:when><c:otherwise>${fast_exits_start[0]}</c:otherwise></c:choose>"> + <label>End date (yyyy-mm-dd):</label> + <input type="text" name="end" size="10" + value="<c:choose><c:when test="${fn:length(fast_exits_end) == 0}">${default_end_date}</c:when><c:otherwise>${fast_exits_end[0]}</c:otherwise></c:choose>"> + </p><p> + <input class="submit" type="submit" value="Update graph"> + </p> + </div> +</form> +<p>Download graph as +<a href="fast-exits.pdf${fast_exits_url}">PDF</a> or +<a href="fast-exits.svg${fast_exits_url}">SVG</a>.</p> +<br> + +<a name="almostfastexits"></a> +<h3><a href="#almostfastexits" class="anchor">Relays almost meeting the +fast-exit requirements</a></h3> +<br> +<img src="almost-fast-exits.png${almost_fast_exits_url}" + width="576" height="432" alt="Almost fast exits graph"> +<form action="fast-exits.html#almostfastexits"> + <div class="formrow"> + <input type="hidden" name="graph" value="almost-fast-exits"> + <p> + <label>Start date (yyyy-mm-dd):</label> + <input type="text" name="start" size="10" + value="<c:choose><c:when test="${fn:length(almost_fast_exits_start) == 0}">${default_start_date}</c:when><c:otherwise>${almost_fast_exits_start[0]}</c:otherwise></c:choose>"> + <label>End date (yyyy-mm-dd):</label> + <input type="text" name="end" size="10" + value="<c:choose><c:when test="${fn:length(almost_fast_exits_end) == 0}">${default_end_date}</c:when><c:otherwise>${almost_fast_exits_end[0]}</c:otherwise></c:choose>"> + </p><p> + <input class="submit" type="submit" value="Update graph"> + </p> + </div> +</form> +<p>Download graph as +<a href="almost-fast-exits.pdf${almost_fast_exits_url}">PDF</a> or +<a href="almost-fast-exits.svg${almost_fast_exits_url}">SVG</a>.</p> +<br> + + </div> + </div> + <div class="bottom" id="bottom"> + <%@ include file="footer.jsp"%> + </div> +</body> +</html> diff --git a/website/web/WEB-INF/footer.jsp b/website/web/WEB-INF/footer.jsp new file mode 100644 index 0000000..cab9e7f --- /dev/null +++ b/website/web/WEB-INF/footer.jsp @@ -0,0 +1,13 @@ + <p>This material is supported in part by the National Science + Foundation under Grant No. CNS-0959138. Any opinions, + finding, and conclusions or recommendations expressed in this + material are those of the author(s) and do not necessarily reflect + the views of the National Science Foundation.</p> + <p>"Tor" and the "Onion Logo" are <a href="https://www.torproject.org/docs/trademark-faq.html.en">registered trademarks</a> of The Tor Project, Inc.</p> + <p>Data on this site is freely available under a + <a href="http://creativecommons.org/publicdomain/zero/1.0/">CC0 no + copyright declaration</a>: To the extent possible under law, the Tor + Project has waived all copyright and related or neighboring rights + in the data. Graphs are licensed under a + <a href="http://creativecommons.org/licenses/by/3.0/us/">Creative + Commons Attribution 3.0 United States License</a>.</p> diff --git a/website/web/WEB-INF/formats.jsp b/website/web/WEB-INF/formats.jsp new file mode 100644 index 0000000..4d297b2 --- /dev/null +++ b/website/web/WEB-INF/formats.jsp @@ -0,0 +1,986 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> +<html> +<head> + <title>Tor Metrics Portal: Data Formats</title> + <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> + <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet"> + <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon"> +</head> +<body> +<div class="center"> +<%@ include file="banner.jsp"%> +<div class="main-column"> +<h2>Tor Metrics Portal: Data Formats</h2> +<br> + +<p> +Statistical analysis in the Tor network can be performed using various +kinds of data. +This page gives an overview of three major data sources for +statistics in the Tor network:</p> + +<ol> +<li>First, we recap measuring the Tor network from public directory +information (<a href="https://research.torproject.org/techreports/metrics-2009-08-07.pdf">PDF</a>) +by describing the data format of +<a href="#serverdesc">server descriptors and network statuses</a>, +and we explain the sanitzation process of (non-public) +<a href="#bridgedesc">bridge directory information</a>.</li> +<li>Second, we describe the numerous aggregate statistics that relays +publish about their usage (<a href="http://freehaven.net/anonbib/cache/wecsr10measuring-tor.pdf">PDF</a>), +including +<a href="#bytehist">byte histories</a>, +<a href="#dirreqstats">directory request statistics</a>, +<a href="#entrystats">connecting client statistics</a>, +<a href="#bridgestats">bridge user statistics</a>, +<a href="#cellstats">cell-queue statistics</a>, +<a href="#exitstats">exit-port statistics</a>, and +<a href="#bidistats">bidirectional connection use</a>.</li> +<li>Third, we delineate the output of various Tor services like +<a href="#bridgepool">BridgeDB</a>, or +<a href="#exitlist">Tor Check</a> as well as specific measurement tools like +<a href="#torperf">Torperf</a>.</li> +</ol> + +<p> +All data described on this page are available for download on the +<a href="data.html">data</a> page. +This page is based on a technical report +(<a href="https://research.torproject.org/techreports/data-2011-03-14.pdf">PDF</a>) +and is very likely more recent than the report. +</p> +<hr> +<br> + +<a name="descriptortypes"></a> +<h3><a href="#descriptortypes" class="anchor">Descriptor types</a></h3> +<br> +<p> +Any file containing descriptors described on this page may contain meta +data in its first text line using the format +<tt>@type $descriptortype $major.$minor</tt>. +Any tool that processes these descriptors may parse files without meta +data or with an unknown descriptor type at its own risk, can safely parse +files with known descriptor type and same major version number, and should +not parse files with known descriptor type and higher major version +number. +</p> + +<p> +The following descriptor types and versions are known. +Gray entries are deprecated, black entries are recent: +</p> + +<ul> +<li><tt>@type server-descriptor 1.0</tt></li> +<li><tt>@type microdescriptor 1.0</tt></li> +<li><tt>@type extra-info 1.0</tt></li> +<li><tt>@type directory 1.0</tt></li> +<li><tt>@type network-status-2 1.0</tt></li> +<li><tt>@type dir-key-certificate-3 1.0</tt></li> +<li><tt>@type network-status-consensus-3 1.0</tt></li> +<li><tt>@type network-status-vote-3 1.0</tt></li> +<li><tt>@type network-status-microdesc-consensus-3 1.0</tt></li> +<li><tt>@type bridge-network-status 1.0</tt></li> +<li><tt>@type bridge-server-descriptor 1.0</tt></li> +<li><font color="gray"><tt>@type bridge-extra-info 1.0</tt></font></li> +<li><font color="gray"><tt>@type bridge-extra-info 1.1</tt> contain + sanitized <tt>transport</tt> lines</font></li> +<li><tt>@type bridge-extra-info 1.2</tt> contain <tt>ntor-onion-key</tt> + lines</li> +<li><tt>@type torperf 1.0</tt></li> +<li><tt>@type bridge-pool-assignment 1.0</tt></li> +<li><tt>@type tordnsel 1.0</tt></li> +</ul> + +<hr> +<br> + +<a name="serverdesc"></a> +<h3><a href="#serverdesc" class="anchor">Server descriptors and network +statuses</a></h3> +<br> +<p> +Relays in the Tor network report their capabilities by publishing server +descriptors to the directory authorities. +The directory authorities confirm reachability of relays and assign flags +to help clients make good path selections. +Every hour, the directory authorities publish a network status consensus +with all known running relays at the time. +Both server descriptors and network statuses constitute a solid data basis +for statistical analysis in the Tor network. +We described the approach to measure the Tor network from public directory +information in the HotPETS 2009 paper +(<a href="https://research.torproject.org/techreports/metrics-2009-08-07.pdf">PDF</a>) +and provide interactive +<a href="graphs.html">graphs</a> on the metrics +website. +We briefly describe the most interesting pieces of the +two descriptor formats that can be used for statistics. +</p> + +<p> +The server descriptors published by relays at least once every 18 hours +contain the necessary information for clients to build circuits using a +given relay. +These server descriptors can also be useful for statistical analysis of +the Tor network infrastructure. +</p> + +<p> +We assume that the majority of server descriptors are correct. +But when performing statistical analysis on server descriptors, one has to +keep in mind that only a small subset of the information written to server +descriptors is confirmed by the trusted directory authorities. +In theory, relays can provide false information in their server +descriptors, even though the incentive to do so is probably low. +</p> + +<blockquote> +<p> +<i>Server descriptor published by relay <tt>blutmagie</tt> (without +cryptographic keys and hashes):</i> +</p> +<p> +<tt>router blutmagie 192.251.226.206 443 0 80</tt><br> +<tt>platform Tor 0.2.2.20-alpha on Linux x86_64</tt><br> +<tt>opt protocols Link 1 2 Circuit 1</tt><br> +<tt>published 2010-12-27 14:35:27</tt><br> +<tt>opt fingerprint 6297 B13A 687B 521A 59C6 BD79 188A 2501 EC03 A065</tt><br> +<tt>uptime 445412</tt><br> +<tt>bandwidth 14336000 18432000 15905178</tt><br> +<tt>opt extra-info-digest 5C1D5D6F8B243304079BC15CD96C7FCCB88322D4</tt><br> +<tt>opt caches-extra-info</tt><br> +<tt>onion-key</tt><br> +<tt>[...]</tt><br> +<tt>signing-key</tt><br> +<tt>[...]</tt><br> +<tt>family $66CA87E164F1CFCE8C3BB5C095217A28578B8BAF + $67EC84376D9C4C467DCE8621AACA109160B5264E + $7B698D327F1695590408FED95CDEE1565774D136</tt><br> +<tt>opt hidden-service-dir</tt><br> +<tt>contact abuse@blutmagie.de</tt><br> +<tt>reject 0.0.0.0/8:*</tt><br> +<tt>reject 169.254.0.0/16:*</tt><br> +<tt>reject 127.0.0.0/8:*</tt><br> +<tt>reject 192.168.0.0/16:*</tt><br> +<tt>reject 10.0.0.0/8:*</tt><br> +<tt>reject 172.16.0.0/12:*</tt><br> +<tt>reject 192.251.226.206:*</tt><br> +<tt>reject *:25</tt><br> +<tt>reject *:119</tt><br> +<tt>reject *:135-139</tt><br> +<tt>reject *:445</tt><br> +<tt>reject *:465</tt><br> +<tt>reject *:563</tt><br> +<tt>reject *:587</tt><br> +<tt>reject *:1214</tt><br> +<tt>reject *:4661-4666</tt><br> +<tt>reject *:6346-6429</tt><br> +<tt>reject *:6660-6999</tt><br> +<tt>accept *:*</tt><br> +<tt>router-signature</tt><br> +<tt>[...]</tt><br> +</p> +</blockquote> + +<p> +The document above shows an example server descriptor. +The following data fields in server descriptors may be relevant to +statistical analysis: +</p> + +<ul> +<li><b>IP address and ports:</b> Relays provide their IP address +and ports where they accept requests to build circuits and directory +requests. +These data fields are contained in the first line of a server descriptor +starting with <tt>router</tt>. +Note that in rare cases, the IP address provided here can be different +from the IP address used for exiting to the Internet. +The latter can be found in the exit lists produced by Tor Check as +described in the <a href="#exitlist">Tor Check exit lists</a> section below. +</li> +<li><b>Operating system and Tor software version:</b> Relays include +their operating system and Tor software version in their server +descriptors in the <tt>platform</tt> line. +While this information is very likely correct in most cases, a few relay +operators may try to impede hacking attempts by providing false platform +strings.</li> +<li><b>Uptime:</b> Relays include the number of seconds since the +last restart in their server descriptor in the <tt>uptime</tt> line.</li> +<li><b>Own measured bandwidth:</b> Relays report the bandwidth that +they are willing to provide on average and for short periods of time. +Relays also perform periodic bandwidth self-tests and report their actual +available bandwidth. +The latter was used by clients to weight relays in the path selection +algorithm and was sometimes subject to manipulation by malicious relays. +All three bandwidth values can be found in a server descriptor's +<tt>bandwidth</tt> line. +With the introduction of +<a href="http://gitweb.torproject.org/torflow.git/">bandwidth scanners</a>, the self-reported relay +bandwidth in server descriptors has become less +relevant.</li> +<li><b>Relay family:</b> Some relay operators who run more than one +relay organize their relays in relay families, so that clients don't pick +more than one of these relays for a single circuit. +Each relay belonging to a relay family lists the members of that family +either by nickname or fingerprint in its server descriptor in the +<tt>family</tt> line.</li> +<li><b>Exit policy:</b> Relays define their exit policy by including +firewall-like rules which outgoing connections they reject or accept in +the <tt>reject</tt> and <tt>accept</tt> lines.</li> +</ul> + +<p> +These are just a subset of the fields in a server descriptor that seem +relevant for statistical analysis. +For a complete list of fields in server descriptors, see the <a href="https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt">directory +protocol specification</a>. +</p> + +<p> +Every hour, the directory authorities publish a new network status that +contains a list of all running relays. +The directory authorities confirm reachability of the contained relays and +assign flags based on the relays' characteristics. +The entries in a network status reference the last published server +descriptor of a relay. +</p> + +<p> +The network statuses are relevant for statistical analysis, because they +constitute trusted snapshots of the Tor network. +Anyone can publish as many server descriptors as they want, but only the +directory authorities can confirm that a relay was running at a given +time. +Most statistics on the Tor network infrastructure rely on network statuses +and possibly combine them with the referenced server descriptors. +The document below shows the network status entry referencing +the server descriptor above. +In addition to the reachability information, network statuses contain the +following fields that may be relevant for statistical analysis: +</p> + +<blockquote> +<p> +<i>Network status entry of relay <tt>blutmagie</tt>:</i> +</p> +<p> +<tt>r blutmagie YpexOmh7UhpZxr15GIolAewDoGU + lFY7WmD/yvVFp9drmZzNeTxZ6dw 2010-12-27 14:35:27 192.251.226.206 + 443 80</tt><br> +<tt>s Exit Fast Guard HSDir Named Running Stable V2Dir Valid</tt><br> +<tt>v Tor 0.2.2.20-alpha</tt><br> +<tt>w Bandwidth=30800</tt><br> +<tt>p reject 25,119,135-139,445,465,563,587,1214,4661-4666,6346-6429,6660-6999</tt><br> +</p> +</blockquote> + +<ul> +<li><b>Relay flags:</b> The directory authorities assign flags to +relays based on their characteristics to the line starting with <tt>s</tt>. +Examples are the <tt>Exit</tt> flag if a relay permits exiting to the +Internet and the <tt>Guard</tt> flag if a relay is stable enough to be +picked as guard node</li> +<li><b>Relay version:</b> The directory authorities include the +version part of the platform string written to server descriptors in the +network status in the line starting with <tt>v</tt>.</li> +<li><b>Bandwidth weights:</b> The network status contains a bandwidth +weight for every relay in the lines with <tt>w</tt> that clients shall use +for weighting relays in their path selection algorithm. +This bandwidth weight is either the self-reported bandwidth of the relay +or the bandwidth measured by the bandwidth scanners.</li> +<li><b>Exit policy summary:</b> Every entry in a network status +contains a summary version of a relay's exit policy in the line starting +with <tt>p</tt>. +This summary is a list of accepted or rejected ports for exit to most IP +addresses.</li> +</ul> +<hr> +<br> + +<a name="bridgedesc"></a> +<h3><a href="#bridgedesc" class="anchor">Sanitized bridge +descriptors</a></h3> +<br> +Bridges in the Tor network publish server descriptors to the bridge +authority which in turn generates a bridge network status. +We cannot, however, make the bridge server descriptors and bridge network +statuses available for statistical analysis as we do with the relay server +descriptors and relay network statuses. +The problem is that bridge server descriptors and network statuses contain +bridge IP addresses and other sensitive information that shall not be made +publicly available. +We therefore sanitize bridge descriptors by removing all potentially +identifying information and publish sanitized versions of the descriptors. +The processing steps for sanitizing bridge descriptors are as follows: + +<ol> +<li><b>Replace the bridge identity with its SHA1 value:</b> Clients +can request a bridge's current descriptor by sending its identity string +to the bridge authority. +This is a feature to make bridges on dynamic IP addresses useful. +Therefore, the original identities (and anything that could be used to +derive them) need to be removed from the descriptors. +The bridge identity is replaced with its SHA1 hash value. +The idea is to have a consistent replacement that remains stable over +months or even years (without keeping a secret for a keyed hash function).</li> +<li><b>Remove all cryptographic keys and signatures:</b> It would be +straightforward to learn about the bridge identity from the bridge's +public key. +Replacing keys by newly generated ones seemed to be unnecessary (and would +involve keeping a state over months/years), so that all cryptographic +objects have simply been removed.</li> +<li><b>Replace IP address with IP address hash:</b> Of course, IP +addresses need to be sanitized, too. +<ul><li>IPv4 addresses are replaced with <tt>10.x.x.x</tt> with +<tt>x.x.x</tt> being the 3 byte output of +<tt>H(IP address | bridge identity | secret)[:3]</tt>. +The input <tt>IP address</tt> is the 4-byte long binary representation of +the bridge's current IP address. +The <tt>bridge identity</tt> is the 20-byte long binary representation of +the bridge's long-term identity fingerprint. +The <tt>secret</tt> is a 31-byte long secure random string that changes once +per month for all descriptors and statuses published in that month. +<tt>H()</tt> is SHA-256. +The <tt>[:3]</tt> operator means that we pick the 3 most significant bytes +of the result.</li> +<li>IPv6 addresses are replaced with <tt>[fd9f:2e19:3bcf::xx:xxxx]</tt> +with <tt>xx:xxxx</tt> being the hex-formatted 3 byte output of a similar +hash function as described for IPv4 addresses. +The only differences are that the input <tt>IP address</tt> is 16 bytes +long and the <tt>secret</tt> is only 19 bytes long.</li></ul> +<li><b>Replace contact information:</b> If there is contact +information in a descriptor, the contact line is changed to +<tt>somebody</tt>.</li> +<li><b>Remove pluggable transport addresses and arguments:</b> Bridges may +provide transports in addition to the onion-routing protocol and include +information about these transports in their extra-info descriptors for +BridgeDB. In that case, any IP addresses, TCP ports, or additional +arguments are removed, only leaving in the supported transport names.</li> +</ol> + +<p> +Apart from these processing steps, sanitized bridge server descriptors +follow the same format as relay server descriptors. +The same applies to sanitized bridge extra-info descriptors. +Sanitized bridge network statuses are similar to version 2 relay network +statuses, but with only a <tt>published</tt> line in the header and +without any lines in the footer. +</p> + +<p> +The two documents below show an example bridge server +descriptor that is referenced from a bridge network status. +For more details about this process, see the +<a href="https://gitweb.torproject.org/metrics-db.git">metrics data processor</a> software. +</p> + +<blockquote> +<p> +<i>Sanitized bridge server descriptor:</i> +</p> +<p> +<tt>@type bridge-server-descriptor 1.0</tt><br> +<tt>router Hawthorne 10.175.105.22 443 0 0</tt><br> +<tt>platform Tor 0.2.2.19-alpha (git-1988927edecce4c7) on Linux i686</tt><br> +<tt>opt protocols Link 1 2 Circuit 1</tt><br> +<tt>published 2010-12-27 18:55:01</tt><br> +<tt>opt fingerprint A5FA 7F38 B02A 415E 72FE 614C 64A1 E5A9 2BA9 9BBD</tt><br> +<tt>uptime 2347112</tt><br> +<tt>bandwidth 5242880 10485760 1016594</tt><br> +<tt>opt extra-info-digest E729BCB5E06A5657A73151B55354EB003D2BAE0F</tt><br> +<tt>opt hidden-service-dir</tt><br> +<tt>contact somebody</tt><br> +<tt>reject *:*</tt><br> +<tt>router-digest 46DFDBE7B67B7C90A1962B0B5AA4526FAF406979</tt><br> +</p> +</blockquote> + +<blockquote> +<p> +<i>Sanitized bridge network status:</i> +</p> +<p> +<tt>@type bridge-network-status 1.0</tt><br> +<tt>published 2010-12-27 22:07:03</tt><br> +<tt>[...status entries...]</tt><br> +<tt>r Hawthorne pfp/OLAqQV5y/mFMZKHlqSupm70 Rt/b57Z7fJChlisLWqRSb69AaXk + 2010-12-27 18:55:01 10.175.105.22 443 0</tt><br> +<tt>s Fast Guard HSDir Running Stable Valid</tt><br> +<tt>[...status entries...]</tt><br> +</p> +</blockquote> +<hr> +<br> + +<a name="bytehist"></a> +<h3><a href="#bytehist" class="anchor">Byte histories</a></h3> +<br> +<p> +Relays include aggregate statistics in their descriptors that they upload +to the directory authorities. +These aggregate statistics are contained in extra-info descriptors that +are published in companion with server descriptors. +Extra-info descriptors are not required for clients to build circuits. +An extra-info descriptor belonging to a server descriptor is referenced by +its SHA1 hash value. +</p> + +<p> +Byte histories were the first statistical data that relays published about +their usage. +Relays report the number of written and read bytes in 15-minute intervals +throughout the last 24 hours. +The extra-info descriptor in the document below contains the byte +histories in the two lines starting with <tt>write-history</tt> and +<tt>read-history</tt>. +More details about these statistics can be found in the <a href="https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt">directory protocol +specification</a>. +</p> + +<blockquote> +<p> +<i>Extra-info descriptor published by relay <tt>blutmagie</tt> +(without cryptographic signature and with long lines being truncated):</i> +</p> +<p> +<tt>extra-info blutmagie 6297B13A687B521A59C6BD79188A2501EC03A065</tt><br> +<tt>published 2010-12-27 14:35:27</tt><br> +<tt>write-history 2010-12-27 14:34:05 (900 s) 12902389760,12902402048,12859373568,12894131200,[...]</tt><br> +<tt>read-history 2010-12-27 14:34:05 (900 s) 12770249728,12833485824,12661140480,12872439808,[...]</tt><br> +<tt>dirreq-write-history 2010-12-27 14:26:13 (900 s) 51731456,60808192,56740864,54948864,[...]</tt><br> +<tt>dirreq-read-history 2010-12-27 14:26:13 (900 s) 4747264,4767744,4511744,4752384,[...]</tt><br> +<tt>dirreq-stats-end 2010-12-27 10:51:09 (86400 s)</tt><br> +<tt>dirreq-v3-ips us=2000,de=1344,fr=744,kr=712,[...]</tt><br> +<tt>dirreq-v2-ips ??=8,au=8,cn=8,cz=8,[...]</tt><br> +<tt>dirreq-v3-reqs us=2368,de=1680,kr=1048,fr=800,[...]</tt><br> +<tt>dirreq-v2-reqs id=48,??=8,au=8,cn=8,[...]</tt><br> +<tt>dirreq-v3-resp ok=12504,not-enough-sigs=0,unavailable=0,not-found=0,not-modified=0,busy=128</tt><br> +<tt>dirreq-v2-resp ok=64,unavailable=0,not-found=8,not-modified=0,busy=8</tt><br> +<tt>dirreq-v2-share 1.03%</tt><br> +<tt>dirreq-v3-share 1.03%</tt><br> +<tt>dirreq-v3-direct-dl complete=316,timeout=4,running=0,min=4649,d1=36436,d2=68056,q1=76600,d3=87891,d4=131294,md=173579,d6=229695,d7=294528,q3=332053,d8=376301,d9=530252,max=2129698</tt><br> +<tt>dirreq-v2-direct-dl complete=16,timeout=52,running=0,min=9769,d1=9769,d2=9844,q1=9981,d3=9981,d4=27297,md=33640,d6=60814,d7=205884,q3=205884,d8=361137,d9=628256,max=956009</tt><br> +<tt>dirreq-v3-tunneled-dl complete=12088,timeout=92,running=4,min=534,d1=31351,d2=49166,q1=58490,d3=70774,d4=88192,md=109778,d6=152389,d7=203435,q3=246377,d8=323837,d9=559237,max=26601000</tt><br> +<tt>dirreq-v2-tunneled-dl complete=0,timeout=0,running=0</tt><br> +<tt>entry-stats-end 2010-12-27 10:51:09 (86400 s)</tt><br> +<tt>entry-ips de=11024,us=10672,ir=5936,fr=5040,[...]</tt><br> +<tt>exit-stats-end 2010-12-27 10:51:09 (86400 s)</tt><br> +<tt>exit-kibibytes-written 80=6758009,443=498987,4000=227483,5004=1182656,11000=22767,19371=1428809,31551=8212,41500=965584,51413=3772428,56424=1912605,other=175227777</tt><br> +<tt>exit-kibibytes-read 80=197075167,443=5954607,4000=1660990,5004=1808563,11000=1893893,19371=130360,31551=7588414,41500=756287,51413=2994144,56424=1646509,other=288412366</tt><br> +<tt>exit-streams-opened 80=5095484,443=359256,4000=4508,5004=22288,11000=124,19371=24,31551=40,41500=96,51413=16840,56424=28,other=1970964</tt><br> +</p> +</blockquote> +<hr> +<br> + +<a name="dirreqstats"></a> +<h3><a href="#dirreqstats" class="anchor">Directory requests</a></h3> +<br> +<p> +The directory authorities and directory mirrors report statistical data +about processed directory requests. +Starting with Tor version 0.2.2.15-alpha, all directories report the +number of written and read bytes for answering directory requests. +The format is similar to the format of byte histories as described in the +previous section. +The relevant lines are <tt>dirreq-write-history</tt> and +<tt>dirreq-read-history</tt> in the document listed in the +<a href="#bytehist">Byte histories</a> section above. +These two lines contain the subset of total read and written bytes that +the directory mirror spent on responding to any kind of directory request, +including network statuses, server descriptors, extra-info descriptors, +authority certificates, etc. +</p> + +<p> +The directories further report statistics on answering directory requests +for network statuses only. +For Tor versions before 0.2.3.x, relay operators had to manually enable +these statistics, which is why only a few directories report them. +The lines starting with <tt>dirreq-v3-</tt> all belong to the directory +request statistics (the lines starting with <tt>dirreq-v2-</tt> report +similar statistics for version 2 of the directory protocol which is +deprecated at the time of writing this report). +The following fields may be relevant for statistical analysis: +</p> + +<ul> +<li><b>Unique IP addresses:</b> The numbers in <tt>dirreq-v3-ips</tt> +denote the unique IP addresses of clients requesting network statuses by +country.</li> +<li><b>Network status requests:</b> The numbers in +<tt>dirreq-v3-reqs</tt> constitute the total network status requests by +country.</li> +<li><b>Request share:</b> The percentage in <tt>dirreq-v3-share</tt> is +an estimate of the share of directory requests that the reporting relay +expects to see in the Tor network. +In a tech report (<a href="https://research.torproject.org/techreports/countingusers-2010-11-30.pdf">PDF</a>) +we found that this estimate isn't very useful +for statistical analysis because of the different approaches that clients +take to select directory mirrors. +The fraction of written directory bytes (<tt>dirreq-write-history</tt>) can +be used to derive a better metric for the share of directory requests.</li> +<li><b>Network status responses:</b> The directories also report +whether they could provide the requested network status to clients in +<tt>dirreq-v3-resp</tt>. +This information was mostly used to diagnose error rates in version 2 of +the directory protocol where a lot of directories replied to network +status requests with <tt>503 Busy</tt>. +In version 3 of the directory protocol, most responses contain the status +code <tt>200 OK</tt>.</li> +<li><b>Network status download times:</b> The line +<tt>dirreq-v3-direct-dl</tt> contains statistics on the download of network +statuses via the relay's directory port. +The line <tt>dirreq-v3-tunneled-dl</tt> contains similar statistics on +downloads via a 1-hop circuit between client and directory (which is the +common approach in version 3 of the directory protocol). +Relays report how many requests have been completed, have timed out, and +are still running at the end of a 24-hour time interval as well as the +minimum, maximum, median, quartiles, and deciles of download times.</li> +</ul> + +<p> +More details about these statistics can be found in the <a href="https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt">directory protocol +specification</a>. +</p> +<hr> +<br> + +<a name="entrystats"></a> +<h3><a href="#entrystats" class="anchor">Connecting clients</a></h3> +<br> +<p> +Relays can be configured to report per-country statistics on directly +connecting clients. +This metric includes clients connecting to a relay in order to build +circuits and clients creating a 1-hop circuit to request directory +information. +In practice, the latter number outweighs the former number. +The <tt>entry-ips</tt> line in the document listed in the +<a href="#bytehist">Byte histories</a> section above +shows the number +of unique IP addresses connecting to the relay by country. +More details about these statistics can be found in the <a href="https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt">directory protocol +specification</a>. +</p> +<hr> +<br> + +<a name="bridgestats"></a> +<h3><a href="#bridgestats" class="anchor">Bridge users</a></h3> +<br> +<p> +Bridges report statistics on connecting bridge clients in their extra-info +descriptors. +The document below shows a bridge extra-info descriptor +with the bridge user statistics in the <tt>bridge-ips</tt> line. + +<blockquote> +<p> +<i>Sanitized bridge extra-info descriptor:</i> +</p> +<p> +<tt>extra-info Unnamed A5FA7F38B02A415E72FE614C64A1E5A92BA99BBD</tt><br> +<tt>published 2010-12-27 18:55:01</tt><br> +<tt>write-history 2010-12-27 18:43:50 (900 s) 151712768,176698368,180030464,163150848,[...]</tt><br> +<tt>read-history 2010-12-27 18:43:50 (900 s) 148109312,172274688,172168192,161094656,[...]</tt><br> +<tt>bridge-stats-end 2010-12-27 14:56:29 (86400 s)</tt><br> +<tt>bridge-ips sa=48,us=40,de=32,ir=32,[...]</tt><br> +</p> +</blockquote> + +<p> +Bridges running Tor version 0.2.2.3-alpha or earlier report bridge users +in a similar line starting with <tt>geoip-client-origins</tt>. +The reason for switching to <tt>bridge-ips</tt> was that the measurement +interval in <tt>geoip-client-origins</tt> had a variable length, whereas the +measurement interval in 0.2.2.4-alpha and later is set to exactly +24 hours. +In order to clearly distinguish the new measurement intervals from the old +ones, the new keywords have been introduced. +More details about these statistics can be found in the <a href="https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt">directory protocol +specification</a>. +</p> +<hr> +<br> + +<a name="cellstats"></a> +<h3><a href="#cellstats" class="anchor">Cell-queue statistics</a></h3> +<br> +<p> +Relays can be configured to report aggregate statistics on their cell +queues. +These statistics include average processed cells, average number of queued +cells, and average time that cells spend in circuits. +Circuits are split into deciles based on the number of processed cells. +The statistics are provided for circuit deciles from loudest to quietest +circuits. +The document below shows the cell statistics contained in an +extra-info descriptor by relay <tt>gabelmoo</tt>. +An early analysis of cell-queue statistics can be found in a tech report +(<a href="https://research.torproject.org/techreports/bufferstats-2009-08-25.pdf">PDF</a>). +More details about these statistics can be found in the <a href="https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt">directory protocol +specification</a>. +</p> + +<blockquote> +<p> +<i>Cell statistics in extra-info descriptor by relay <tt>gabelmoo</tt>:</i> +</p> +<p> +<tt>cell-stats-end 2010-12-27 09:59:50 (86400 s)</tt><br> +<tt>cell-processed-cells 4563,153,42,15,7,7,6,5,4,2</tt><br> +<tt>cell-queued-cells 9.39,0.98,0.09,0.01,0.00,0.00,0.00,0.01,0.00, + 0.01</tt><br> +<tt>cell-time-in-queue 2248,807,277,92,49,22,52,55,81,148</tt><br> +<tt>cell-circuits-per-decile 7233</tt><br> +</p> +</blockquote> +<hr> +<br> + +<a name="exitstats"></a> +<h3><a href="#exitstats" class="anchor">Exit-port statistics</a></h3> +<br> +<p> +Exit relays running Tor version 0.2.1.1-alpha or higher can be configured +to report aggregate statistics on exiting connections. +These relays report the number of opened streams, written and read bytes +by exiting port. +Until version 0.2.2.19-alpha, relays reported all ports exceeding a +threshold of 0.01 % of all written and read exit bytes. +Starting with version 0.2.2.20-alpha, relays only report the top 10 ports +in exit-port statistics in order not to exceed the maximum extra-info +descriptor length of 50 KB. +The document listed in the +<a href="#bytehist">Byte histories</a> section above contains +exit-port statistics in the lines starting with <tt>exit-</tt>. +More details about these statistics can be found in the <a href="https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt">directory protocol +specification</a>. +</p> +<hr> +<br> + +<a name="bidistats"></a> +<h3><a href="#bidistats" class="anchor">Bidirectional connection +use</a></h3> +<br> +<p> +Relays running Tor version 0.2.3.1-alpha or higher can be configured to +report what fraction of connections is used uni- or bi-directionally. +Every 10 seconds, relays determine for every connection whether they read +and wrote less than a threshold of 20 KiB. +Connections below this threshold are labeled as "Below Threshold". +For the remaining connections, relays report whether they read/wrote at +least 10 times as many bytes as they wrote/read. +If so, they classify a connection as "Mostly reading" or "Mostly +writing," respectively. +All other connections are classified as "Both reading and writing." +After classifying connections, read and write counters are reset for the +next 10-second interval. +Statistics are aggregated over 24 hours. +The document below shows the bidirectional connection use +statistics in an extra-info descriptor by relay <tt>zweifaltigkeit</tt>. +The four numbers denote the number of connections "Below threshold," +"Mostly reading," "Mostly writing," and "Both reading and writing." +More details about these statistics can be found in the <a href="https://gitweb.torproject.org/torspec.git/blob/HEAD:/dir-spec.txt">directory protocol +specification</a>. +</p> + +<blockquote> +<p> +<i>Bidirectional connection use statistic in extra-info descriptor +by relay <tt>zweifaltigkeit</tt>:</i> +</p> +<p> +<tt>conn-bi-direct 2010-12-28 15:55:11 (86400 s) 387465,45285,55361,81786</tt> +</p> +</blockquote> +<hr> +<br> + +<a name="torperf"></a> +<h3><a href="#torperf" class="anchor">Torperf output files</a></h3> +<br> +<p> +Torperf is a little tool that measures Tor's performance as users +experience it. +Torperf uses a trivial SOCKS client to download files of various sizes +over the Tor network and notes how long substeps take. +Torperf can be +<a href="https://metrics.torproject.org/tools.html">downloaded</a> +from the metrics +website. +A Torperf results file contains a single line per Torperf run with +<tt>key=value</tt> pairs. +Such a result line is sufficient to learn about 1) the Tor and Torperf +configuration, 2) measurement results, and 3) additional information that +might help explain the results. +Known keys are explained below. +</p> +<ul> +<li>Configuration +<ul> +<li><tt>SOURCE:</tt> Configured name of the data source; required.</li> +<li><tt>FILESIZE:</tt> Configured file size in bytes; required.</li> +<li>Other meta data describing the Tor or Torperf configuration, e.g., +GUARD for a custom guard choice; optional.</li> +</ul> +<li>Measurement results +<ul> +<li><tt>START:</tt> Time when the connection process starts; +required.</li> +<li><tt>SOCKET:</tt> Time when the socket was created; required.</li> +<li><tt>CONNECT:</tt> Time when the socket was connected; required.</li> +<li><tt>NEGOTIATE:</tt> Time when SOCKS 5 authentication methods have been +negotiated; required.</li> +<li><tt>REQUEST:</tt> Time when the SOCKS request was sent; required.</li> +<li><tt>RESPONSE:</tt> Time when the SOCKS response was received; +required.</li> +<li><tt>DATAREQUEST:</tt> Time when the HTTP request was written; +required.</li> +<li><tt>DATARESPONSE:</tt> Time when the first response was received; +required.</li> +<li><tt>DATACOMPLETE:</tt> Time when the payload was complete; +required.</li> +<li><tt>WRITEBYTES:</tt> Total number of bytes written; required.</li> +<li><tt>READBYTES:</tt> Total number of bytes read; required.</li> +<li><tt>DIDTIMEOUT:</tt> 1 if the request timed out, 0 otherwise; +optional.</li> +<li><tt>DATAPERCx:</tt> Time when x% of expected bytes were read for +x = { 10, 20, 30, 40, 50, 60, 70, 80, 90 }; optional.</li> +<li>Other measurement results, e.g., START_RENDCIRC, GOT_INTROCIRC, etc. +for hidden-service measurements; optional.</li> +</ul> +<li>Additional information +<ul> +<li><tt>LAUNCH:</tt> Time when the circuit was launched; optional.</li> +<li><tt>USED_AT:</tt> Time when this circuit was used; optional.</li> +<li><tt>PATH:</tt> List of relays in the circuit, separated by commas; +optional.</li> +<li><tt>BUILDTIMES:</tt> List of times when circuit hops were built, +separated by commas; optional.</li> +<li><tt>TIMEOUT:</tt> Circuit build timeout that the Tor client used when +building this circuit; optional.</li> +<li><tt>QUANTILE:</tt> Circuit build time quantile that the Tor client +uses to determine its circuit-build timeout; optional.</li> +<li><tt>CIRC_ID:</tt> Circuit identifier of the circuit used for this +measurement; optional.</li> +<li><tt>USED_BY:</tt> Stream identifier of the stream used for this +measurement; optional.</li> +<li>Other fields containing additional information; optional.</li> +</ul> +</ul> + +<blockquote> +<p> +<i>Torperf <tt>.tpf</tt> output lines for a single request to download a +50 KiB file (reformatted):</i> +</p> + +<p> +<tt>BUILDTIMES=1.16901898384,1.86555600166,2.13295292854</tt><br> +<tt>CIRC_ID=9878</tt><br> +<tt>CONNECT=1338357901.42</tt><br> +<tt>DATACOMPLETE=1338357902.91</tt><br> +<tt>DATAPERC10=1338357902.48</tt><br> +<tt>DATAPERC20=1338357902.48</tt><br> +<tt>DATAPERC30=1338357902.61</tt><br> +<tt>DATAPERC40=1338357902.64</tt><br> +<tt>DATAPERC50=1338357902.65</tt><br> +<tt>DATAPERC60=1338357902.74</tt><br> +<tt>DATAPERC70=1338357902.74</tt><br> +<tt>DATAPERC80=1338357902.75</tt><br> +<tt>DATAPERC90=1338357902.79</tt><br> +<tt>DATAREQUEST=1338357901.83</tt><br> +<tt>DATARESPONSE=1338357902.25</tt><br> +<tt>DIDTIMEOUT=0</tt><br> +<tt>FILESIZE=51200</tt><br> +<tt>LAUNCH=1338357661.74</tt><br> +<tt>NEGOTIATE=1338357901.42</tt><br> +<tt>PATH=$980D326017CEF4CBBF4089FBABE767DC83D059AF,$03545609092A24C71CCAD2F4523F5CCC6714F159,$CAC3CF7154AE9C656C4096DC38B4EFA145905654</tt><br> +<tt>QUANTILE=0.800000</tt><br> +<tt>READBYTES=51442</tt><br> +<tt>REQUEST=1338357901.42</tt><br> +<tt>RESPONSE=1338357901.83</tt><br> +<tt>SOCKET=1338357901.42</tt><br> +<tt>SOURCE=torperf</tt><br> +<tt>START=1338357901.42</tt><br> +<tt>TIMEOUT=5049</tt><br> +<tt>USED_AT=1338357902.91</tt><br> +<tt>USED_BY=18869</tt><br> +<tt>WRITEBYTES=75</tt><br> +</p> +</blockquote> +<br> + +<p> +Torperf can produce two output files: <tt>.data</tt> and +<tt>.extradata</tt>. +The <tt>.data</tt> file contains timestamps for request substeps and the +byte summaries for downloading a test file via Tor. +The document below shows an example output of a Torperf run. +The timestamps are seconds and microseconds since 1970-01-01 +00:00:00.000000. +Torperf can be configured to write <tt>.extradata</tt> files by attaching +a Tor controller and writing certain controller events to disk. +The format of a <tt>.extradata</tt> line is similar to the combined format +as specified above, except that it can only contain "Additional +information" keywords. +</p> + +<blockquote> +<p> +<i>Torperf <tt>.data</tt> and <tt>.extradata</tt> output lines for a +single request to download a 50 KiB file (reformatted and annotated with +comments):</i> +</p> + +<p> +<tt># Timestamps and byte summaries contained in .data files:</tt><br> +<tt>1338357901 422336 # Connection process started</tt><br> +<tt>1338357901 422346 # After socket is created</tt><br> +<tt>1338357901 422521 # After socket is connected</tt><br> +<tt>1338357901 422604 # After authentication methods are negotiated (SOCKS 5 only)</tt><br> +<tt>1338357901 423550 # After SOCKS request is sent</tt><br> +<tt>1338357901 839639 # After SOCKS response is received</tt><br> +<tt>1338357901 839849 # After HTTP request is written</tt><br> +<tt>1338357902 258157 # After first response is received</tt><br> +<tt>1338357902 914263 # After payload is complete</tt><br> +<tt>75 # Written bytes</tt><br> +<tt>51442 # Read bytes</tt><br> +<tt>0 # Timeout (optional field)</tt><br> +<tt>1338357902 481591 # After 10% of expected bytes are read (optional field)</tt><br> +<tt>1338357902 482719 # After 20% of expected bytes are read (optional field)</tt><br> +<tt>1338357902 613169 # After 30% of expected bytes are read (optional field)</tt><br> +<tt>1338357902 647108 # After 40% of expected bytes are read (optional field)</tt><br> +<tt>1338357902 651764 # After 50% of expected bytes are read (optional field)</tt><br> +<tt>1338357902 743705 # After 60% of expected bytes are read (optional field)</tt><br> +<tt>1338357902 743876 # After 70% of expected bytes are read (optional field)</tt><br> +<tt>1338357902 757475 # After 80% of expected bytes are read (optional field)</tt><br> +<tt>1338357902 795100 # After 90% of expected bytes are read (optional field)</tt><br> +</p> + +<p> +<tt># Path information contained in .extradata files:</tt><br> +<tt>CIRC_ID=9878</tt><br> +<tt>LAUNCH=1338357661.74</tt><br> +<tt>PATH=$980D326017CEF4CBBF4089FBABE767DC83D059AF,$03545609092A24C71CCAD2F4523F5CCC6714F159,$CAC3CF7154AE9C656C4096DC38B4EFA145905654</tt><br> +<tt>BUILDTIMES=1.16901898384,1.86555600166,2.13295292854</tt><br> +<tt>USED_AT=1338357902.91</tt><br> +<tt>USED_BY=18869</tt><br> +<tt>TIMEOUT=5049</tt><br> +<tt>QUANTILE=0.800000</tt><br> +</p> +</blockquote> +<hr> +<br> + +<a name="bridgepool"></a> +<h3><a href="#bridgepool" class="anchor">BridgeDB pool assignment +files</a></h3> +<br> +<p> +BridgeDB is the software that receives bridge network statuses containing +the information which bridges are running from the bridge authority, +assigns these bridges to persistent distribution rings, and hands them out +to bridge users. +BridgeDB periodically dumps the list of running bridges with information +about the rings, subrings, and file buckets to which they are assigned to +a local file. +The sanitized versions of these lists containing SHA-1 hashes of bridge +fingerprints instead of the original fingerprints are available for +statistical analysis. +</p> + +<blockquote> +<p> +<i>BridgeDB pool assignment file from March 13, 2011:</i> +</p> +<p> +<tt>bridge-pool-assignment 2011-03-13 14:38:03</tt><br> +<tt>00b834117566035736fc6bd4ece950eace8e057a unallocated</tt><br> +<tt>00e923e7a8d87d28954fee7503e480f3a03ce4ee email port=443 flag=stable</tt><br> +<tt>0103bb5b00ad3102b2dbafe9ce709a0a7c1060e4 https ring=2 port=443 flag=stable</tt><br> +<tt>[...]</tt><br> +</p> +</blockquote> + +<p> +The document above shows a BridgeDB pool assignment file +from March 13, 2011. +Every such file begins with a line containing the timestamp when BridgeDB +wrote this file. +Subsequent lines always start with the SHA-1 hash of a bridge fingerprint, +followed by ring, subring, and/or file bucket information. +There are currently three distributor ring types in BridgeDB: +</p> + +<ol> +<li><b>unallocated:</b> These bridges are not distributed by BridgeDB, +but are either reserved for manual distribution or are written to file +buckets for distribution via an external tool. +If a bridge in the <tt>unallocated</tt> ring is assigned to a file bucket, +this is noted by <tt>bucket=$bucketname</tt>.</li> +<li><b>email:</b> These bridges are distributed via an e-mail +autoresponder. Bridges can be assigned to subrings by their OR port or +relay flag which is defined by <tt>port=$port</tt> and/or <tt>flag=$flag</tt>. +</li> +<li><b>https:</b> These bridges are distributed via https server. +There are multiple https rings to further distribute bridges by IP address +ranges, which is denoted by <tt>ring=$ring</tt>. +Bridges in the <tt>https</tt> ring can also be assigned to subrings by +OR port or relay flag which is defined by <tt>port=$port</tt> and/or +<tt>flag=$flag</tt>.</li> +</ol> +<hr> +<br> + +<a name="exitlist"> +<h3><a href="#exitlist" class="anchor">Tor Check exit lists</a></h3> +<br> +<p> +<a href="https://www.torproject.org/tordnsel/dist/">TorDNSEL</a> is an +implementation of the active testing, DNS-based exit list +for Tor exit +nodes. +Tor Check makes the list of known exits and corresponding exit IP +addresses available in a specific format. +The document below shows an entry of the exit list written on +December 28, 2010 at 15:21:44 UTC. +This entry means that the relay with fingerprint <tt>63BA..</tt> which +published a descriptor at 07:35:55 and was contained in a version 2 +network status from 08:10:11 uses two different IP addresses for exiting. +The first address <tt>91.102.152.236</tt> was found in a test performed at +07:10:30. +When looking at the corresponding server descriptor, one finds that this +is also the IP address on which the relay accepts connections from inside +the Tor network. +A second test performed at 10:35:30 reveals that the relay also uses IP +address <tt>91.102.152.227</tt> for exiting. +</p> + +<blockquote> +<p> +<i>Exit list entry written on December 28, 2010 at 15:21:44 UTC:</i> +</p> +<p> +<tt>ExitNode 63BA28370F543D175173E414D5450590D73E22DC</tt><br> +<tt>Published 2010-12-28 07:35:55</tt><br> +<tt>LastStatus 2010-12-28 08:10:11</tt><br> +<tt>ExitAddress 91.102.152.236 2010-12-28 07:10:30</tt><br> +<tt>ExitAddress 91.102.152.227 2010-12-28 10:35:30</tt><br> +</p> +</blockquote> + +</div> +</div> +<div class="bottom" id="bottom"> +<%@ include file="footer.jsp"%> +</div> +</body> +</html> + diff --git a/website/web/WEB-INF/graphs.jsp b/website/web/WEB-INF/graphs.jsp new file mode 100644 index 0000000..58c122c --- /dev/null +++ b/website/web/WEB-INF/graphs.jsp @@ -0,0 +1,34 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> +<html> +<head> + <title>Tor Metrics Portal: Graphs</title> + <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> + <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet"> + <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon"> +</head> +<body> + <div class="center"> + <%@ include file="banner.jsp"%> + <div class="main-column"> + <h2>Tor Metrics Portal: Graphs</h2> + <br> + <p>The graphs on this page visualize a small portion of the data + gathered in the Tor Metrics Project. The following graphs are + available:</p> + <ul> + <li>The <a href="network.html">Network page</a> has numerous + statistics on the network of relays and bridges.</li> + <li>The <a href="users.html">Users page</a> attempts to estimate + the number of users in the network.</li> + <li>There are active and passive performance measurements of the + Tor network available on the + <a href="performance.html">Performance page</a>.</li> + </ul> + <br> + </div> + </div> + <div class="bottom" id="bottom"> + <%@ include file="footer.jsp"%> + </div> +</body> +</html> diff --git a/website/web/WEB-INF/index.jsp b/website/web/WEB-INF/index.jsp new file mode 100644 index 0000000..9accc2b --- /dev/null +++ b/website/web/WEB-INF/index.jsp @@ -0,0 +1,40 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> +<html> +<head> + <title>Tor Metrics Portal</title> + <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> + <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet"> + <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon"> +</head> +<body> + <div class="center"> + <%@ include file="banner.jsp"%> + <div class="main-column"> + <h2>Tor Metrics Portal</h2> + <br> + <p>The Tor Metrics Portal aggregates all kinds of interesting + data about the Tor network and visualizes them in graphs and + reports. This portal also provides easy access to the underlying + data and documentation for performing own analyses based on these + data. Find out more here:</p> + <ul> + <li>View daily updated <a href="graphs.html">graphs</a> on + estimated client numbers, on network performance, and other + statistics on the Tor network</li> + <li>Read <a href="http://freehaven.net/anonbib/">papers</a> and + <a href="https://research.torproject.org/techreports.html">technical + reports</a> + on the measurement techniques and results of statistical + analysis of metrics data</li> + <li>Download the <a href="data.html">data</a> that is behind the + graphs and reports to make your own evaluations</li> + <li>Try out the <a href="tools.html">tools</a> to parse and + evaluate the metrics data</li> + </ul> + </div> + </div> + <div class="bottom" id="bottom"> + <%@ include file="footer.jsp"%> + </div> +</body> +</html> diff --git a/website/web/WEB-INF/network.jsp b/website/web/WEB-INF/network.jsp new file mode 100644 index 0000000..e0b297a --- /dev/null +++ b/website/web/WEB-INF/network.jsp @@ -0,0 +1,305 @@ +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> +<html> +<head> + <title>Tor Metrics Portal: Network</title> + <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> + <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet"> + <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon"> +</head> +<body> + <div class="center"> + <%@ include file="banner.jsp"%> + <div class="main-column"> +<h2>Tor Metrics Portal: Network</h2> +<br> +<a name="networksize"></a> +<h3><a href="#networksize" class="anchor">Relays and bridges in the +network</a></h3> +<br> +<p>The following graph shows the average daily number of relays and +bridges in the network.</p> +<img src="networksize.png${networksize_url}" + width="576" height="360" alt="Network size graph"> +<form action="network.html#networksize"> + <div class="formrow"> + <input type="hidden" name="graph" value="networksize"> + <p> + <label>Start date (yyyy-mm-dd):</label> + <input type="text" name="start" size="10" + value="<c:choose><c:when test="${fn:length(networksize_start) == 0}">${default_start_date}</c:when><c:otherwise>${networksize_start[0]}</c:otherwise></c:choose>"> + <label>End date (yyyy-mm-dd):</label> + <input type="text" name="end" size="10" + value="<c:choose><c:when test="${fn:length(networksize_end) == 0}">${default_end_date}</c:when><c:otherwise>${networksize_end[0]}</c:otherwise></c:choose>"> + </p><p> + <input class="submit" type="submit" value="Update graph"> + </p> + </div> +</form> +<p>Download graph as +<a href="networksize.pdf${networksize_url}">PDF</a> or +<a href="networksize.svg${networksize_url}">SVG</a>.</p> +<p><a href="stats/servers.csv">CSV</a> file containing all data.</p> +<br> + +<a name="relayflags"></a> +<h3><a href="#relayflags" class="anchor">Relays with Exit, Fast, Guard, +Stable, and HSDir flags</a></h3> +<br> +<p>The directory authorities assign certain flags to relays that clients +use for their path selection decisions. The following graph shows the +average number of relays with these flags assigned.</p> +<img src="relayflags.png${relayflags_url}" + width="576" height="360" alt="Relay flags graph"> +<form action="network.html#relayflags"> + <div class="formrow"> + <input type="hidden" name="graph" value="relayflags"> + <p> + <label>Start date (yyyy-mm-dd):</label> + <input type="text" name="start" size="10" + value="<c:choose><c:when test="${fn:length(relayflags_start) == 0}">${default_start_date}</c:when><c:otherwise>${relayflags_start[0]}</c:otherwise></c:choose>"> + <label>End date (yyyy-mm-dd):</label> + <input type="text" name="end" size="10" + value="<c:choose><c:when test="${fn:length(relayflags_end) == 0}">${default_end_date}</c:when><c:otherwise>${relayflags_end[0]}</c:otherwise></c:choose>"> + </p><p> + <label>Relay flags: </label> + <input type="checkbox" name="flag" value="Running"<c:if test="${fn:length(relayflags_flag) == 0 or fn:contains(fn:join(relayflags_flag, ','), 'Running')}"> checked</c:if>> Running + <input type="checkbox" name="flag" value="Exit"<c:if test="${fn:length(relayflags_flag) == 0 or fn:contains(fn:join(relayflags_flag, ','), 'Exit')}"> checked</c:if>> Exit + <input type="checkbox" name="flag" value="Fast"<c:if test="${fn:length(relayflags_flag) == 0 or fn:contains(fn:join(relayflags_flag, ','), 'Fast')}"> checked</c:if>> Fast + <input type="checkbox" name="flag" value="Guard"<c:if test="${fn:length(relayflags_flag) == 0 or fn:contains(fn:join(relayflags_flag, ','), 'Guard')}"> checked</c:if>> Guard + <input type="checkbox" name="flag" value="Stable"<c:if test="${fn:length(relayflags_flag) == 0 or fn:contains(fn:join(relayflags_flag, ','), 'Stable')}"> checked</c:if>> Stable + <input type="checkbox" name="flag" value="HSDir"<c:if test="${fn:length(relayflags_flag) > 0 and fn:contains(fn:join(relayflags_flag, ','), 'HSDir')}"> checked</c:if>> HSDir + </p><p> + <input class="submit" type="submit" value="Update graph"> + </p> + </div> +</form> +<p>Download graph as +<a href="relayflags.pdf${relayflags_url}">PDF</a> or +<a href="relayflags.svg${relayflags_url}">SVG</a>.</p> +<p><a href="stats/servers.csv">CSV</a> file containing all data.</p> +<br> + +<a name="versions"></a> +<h3><a href="#versions" class="anchor">Relays by version</a></h3> +<br> +<p>Relays report the Tor version that they are running to the directory +authorities. See the +<a href="https://www.torproject.org/download/download.html.en">download +page</a> and +<a href="https://gitweb.torproject.org/tor.git/blob/HEAD:/ChangeLog">ChangeLog file</a> +to find out which Tor versions are stable and unstable. +The following graph shows the number of relays by version.</p> +<img src="versions.png${versions_url}" + width="576" height="360" alt="Relay versions graph"> +<form action="network.html#versions"> + <div class="formrow"> + <input type="hidden" name="graph" value="versions"> + <p> + <label>Start date (yyyy-mm-dd):</label> + <input type="text" name="start" size="10" + value="<c:choose><c:when test="${fn:length(versions_start) == 0}">${default_start_date}</c:when><c:otherwise>${versions_start[0]}</c:otherwise></c:choose>"> + <label>End date (yyyy-mm-dd):</label> + <input type="text" name="end" size="10" + value="<c:choose><c:when test="${fn:length(versions_end) == 0}">${default_end_date}</c:when><c:otherwise>${versions_end[0]}</c:otherwise></c:choose>"> + </p><p> + <input class="submit" type="submit" value="Update graph"> + </p> + </div> +</form> +<p>Download graph as +<a href="versions.pdf${versions_url}">PDF</a> or +<a href="versions.svg${versions_url}">SVG</a>.</p> +<p><a href="stats/servers.csv">CSV</a> file containing all data.</p> +<br> + +<a name="platforms"></a> +<h3><a href="#platforms" class="anchor">Relays by platform</a></h3> +<br> +<p>Relays report the operating system they are running to the directory +authorities. The following graph shows the number of relays by +platform.</p> +<img src="platforms.png${platforms_url}" + width="576" height="360" alt="Relay platforms graph"> +<form action="network.html#platforms"> + <div class="formrow"> + <input type="hidden" name="graph" value="platforms"> + <p> + <label>Start date (yyyy-mm-dd):</label> + <input type="text" name="start" size="10" + value="<c:choose><c:when test="${fn:length(platforms_start) == 0}">${default_start_date}</c:when><c:otherwise>${platforms_start[0]}</c:otherwise></c:choose>"> + <label>End date (yyyy-mm-dd):</label> + <input type="text" name="end" size="10" + value="<c:choose><c:when test="${fn:length(platforms_end) == 0}">${default_end_date}</c:when><c:otherwise>${platforms_end[0]}</c:otherwise></c:choose>"> + </p><p> + <input class="submit" type="submit" value="Update graph"> + </p> + </div> +</form> +<p>Download graph as +<a href="platforms.pdf${platforms_url}">PDF</a> or +<a href="platforms.svg${platforms_url}">SVG</a>.</p> +<p><a href="stats/servers.csv">CSV</a> file containing all data.</p> +<br> + +<a name="cloudbridges"></a> +<h3><a href="#cloudbridges" class="anchor">Tor Cloud bridges</a></h3> +<br> +<p>The following graph shows the average daily number of +<a href="http://cloud.torproject.org/">Tor Cloud</a> bridges in the +network.</p> +<img src="cloudbridges.png${cloudbridges_url}" + width="576" height="360" alt="Tor Cloud bridges graph"> +<form action="network.html#cloudbridges"> + <div class="formrow"> + <input type="hidden" name="graph" value="cloudbridges"> + <p> + <label>Start date (yyyy-mm-dd):</label> + <input type="text" name="start" size="10" + value="<c:choose><c:when test="${fn:length(cloudbridges_start) == 0}">${default_start_date}</c:when><c:otherwise>${cloudbridges_start[0]}</c:otherwise></c:choose>"> + <label>End date (yyyy-mm-dd):</label> + <input type="text" name="end" size="10" + value="<c:choose><c:when test="${fn:length(cloudbridges_end) == 0}">${default_end_date}</c:when><c:otherwise>${cloudbridges_end[0]}</c:otherwise></c:choose>"> + </p><p> + <input class="submit" type="submit" value="Update graph"> + </p> + </div> +</form> +<p>Download graph as +<a href="cloudbridges.pdf${cloudbridges_url}">PDF</a> or +<a href="cloudbridges.svg${cloudbridges_url}">SVG</a>.</p> +<p><a href="stats/servers.csv">CSV</a> file containing all data.</p> +<br> + +<a name="bandwidth"></a> +<h3><a href="#bandwidth" class="anchor">Total relay bandwidth in the +network</a></h3> +<br> +<p>Relays report how much bandwidth they are willing to contribute and how +many bytes they have read and written in the past 24 hours. The following +graph shows total advertised bandwidth and bandwidth history of all relays +in the network.</p> +<img src="bandwidth.png${bandwidth_url}" + width="576" height="360" alt="Relay bandwidth graph"> +<form action="network.html#bandwidth"> + <div class="formrow"> + <input type="hidden" name="graph" value="bandwidth"> + <p> + <label>Start date (yyyy-mm-dd):</label> + <input type="text" name="start" size="10" + value="<c:choose><c:when test="${fn:length(bandwidth_start) == 0}">${default_start_date}</c:when><c:otherwise>${bandwidth_start[0]}</c:otherwise></c:choose>"> + <label>End date (yyyy-mm-dd):</label> + <input type="text" name="end" size="10" + value="<c:choose><c:when test="${fn:length(bandwidth_end) == 0}">${default_end_date}</c:when><c:otherwise>${bandwidth_end[0]}</c:otherwise></c:choose>"> + </p><p> + <input class="submit" type="submit" value="Update graph"> + </p> + </div> +</form> +<p>Download graph as +<a href="bandwidth.pdf${bandwidth_url}">PDF</a> or +<a href="bandwidth.svg${bandwidth_url}">SVG</a>.</p> +<p><a href="stats/bandwidth.csv">CSV</a> file containing all data.</p> +<br> + +<a name="bwhist-flags"></a> +<h3><a href="#bwhist-flags" class="anchor">Relay bandwidth by Exit and/or +Guard flags</a></h3> +<br> +<p>The following graph shows the relay bandwidth of all relays with the +Exit and/or Guard flags assigned by the directory authorities.</p> +<img src="bwhist-flags.png${bwhist_flags_url}" + width="576" height="360" alt="Relay bandwidth by flags graph"> +<form action="network.html#bwhist-flags"> + <div class="formrow"> + <input type="hidden" name="graph" value="bwhist-flags"> + <p> + <label>Start date (yyyy-mm-dd):</label> + <input type="text" name="start" size="10" + value="<c:choose><c:when test="${fn:length(bwhist_flags_start) == 0}">${default_start_date}</c:when><c:otherwise>${bwhist_flags_start[0]}</c:otherwise></c:choose>"> + <label>End date (yyyy-mm-dd):</label> + <input type="text" name="end" size="10" + value="<c:choose><c:when test="${fn:length(bwhist_flags_end) == 0}">${default_end_date}</c:when><c:otherwise>${bwhist_flags_end[0]}</c:otherwise></c:choose>"> + </p><p> + <input class="submit" type="submit" value="Update graph"> + </p> + </div> +</form> +<p>Download graph as +<a href="bwhist-flags.pdf${bwhist_flags_url}">PDF</a> or +<a href="bwhist-flags.svg${bwhist_flags_url}">SVG</a>.</p> +<p><a href="stats/bandwidth.csv">CSV</a> file containing all data.</p> +<br> + +<a name="bandwidth-flags"></a> +<h3><a href="#bandwidth-flags" class="anchor">Advertised bandwidth and +bandwidth history by relay flags</a></h3> +<br> +<p>The following graph shows the advertised bandwidth and bandwidth +history of all relays with the Exit and/or Guard flags assigned by the +directory authorities. +Note that these sets possibly overlap with relays having both Exit and +Guard flag.</p> +<img src="bandwidth-flags.png${bandwidth_flags_url}" + width="576" height="360" alt="Advertised bandwidth and bandwidth history by relay flags graph"> +<form action="network.html#bandwidth-flags"> + <div class="formrow"> + <input type="hidden" name="graph" value="bandwidth-flags"> + <p> + <label>Start date (yyyy-mm-dd):</label> + <input type="text" name="start" size="10" + value="<c:choose><c:when test="${fn:length(bandwidth_flags_start) == 0}">${default_start_date}</c:when><c:otherwise>${bandwidth_flags_start[0]}</c:otherwise></c:choose>"> + <label>End date (yyyy-mm-dd):</label> + <input type="text" name="end" size="10" + value="<c:choose><c:when test="${fn:length(bandwidth_flags_end) == 0}">${default_end_date}</c:when><c:otherwise>${bandwidth_flags_end[0]}</c:otherwise></c:choose>"> + </p><p> + <input class="submit" type="submit" value="Update graph"> + </p> + </div> +</form> +<p>Download graph as +<a href="bandwidth-flags.pdf${bandwidth_flags_url}">PDF</a> or +<a href="bandwidth-flags.svg${bandwidth_flags_url}">SVG</a>.</p> +<p><a href="stats/bandwidth.csv">CSV</a> file containing all data.</p> +<br> + +<a name="dirbytes"></a> +<h3><a href="#dirbytes" class="anchor">Number of bytes spent on answering +directory requests</a></h3> +<br> +<p>Relays running on 0.2.2.15-alpha or higher report the number of bytes +they spend on answering directory requests. The following graph shows +total written and read bytes as well as written and read dir bytes. The +dir bytes are extrapolated from those relays who report them to reflect +the number of written and read dir bytes by all relays.</p> +<img src="dirbytes.png${dirbytes_url}" + width="576" height="360" alt="Dir bytes graph"> +<form action="network.html#dirbytes"> + <div class="formrow"> + <input type="hidden" name="graph" value="dirbytes"> + <p> + <label>Start date (yyyy-mm-dd):</label> + <input type="text" name="start" size="10" + value="<c:choose><c:when test="${fn:length(dirbytes_start) == 0}">${default_start_date}</c:when><c:otherwise>${dirbytes_start[0]}</c:otherwise></c:choose>"> + <label>End date (yyyy-mm-dd):</label> + <input type="text" name="end" size="10" + value="<c:choose><c:when test="${fn:length(dirbytes_end) == 0}">${default_end_date}</c:when><c:otherwise>${dirbytes_end[0]}</c:otherwise></c:choose>"> + </p><p> + <input class="submit" type="submit" value="Update graph"> + </p> + </div> +</form> +<p>Download graph as +<a href="dirbytes.pdf${dirbytes_url}">PDF</a> or +<a href="dirbytes.svg${dirbytes_url}">SVG</a>.</p> +<p><a href="stats/bandwidth.csv">CSV</a> file containing all data.</p> +<br> + </div> + </div> + <div class="bottom" id="bottom"> + <%@ include file="footer.jsp"%> + </div> +</body> +</html> diff --git a/website/web/WEB-INF/papers.jsp b/website/web/WEB-INF/papers.jsp new file mode 100644 index 0000000..1a48491 --- /dev/null +++ b/website/web/WEB-INF/papers.jsp @@ -0,0 +1,81 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> +<html> +<head> + <title>Tor Metrics Portal: Papers</title> + <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> + <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet"> + <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon"> +</head> +<body> + <div class="center"> + <%@ include file="banner.jsp"%> + <div class="main-column"> + <h2>Tor Metrics Portal: Papers</h2> + <br> + <p>The <a href="#papers">papers</a>, + <a href="#techreports">technical reports</a>, and + <a href="#blogposts">blog posts</a> listed on this page originate + from, are based on, or are related to work performed in the Tor + Metrics Project.</p> + <p><font color="red">This page will be removed soon. + Its main purpose was to list metrics-related tech reports, but + those have now moved + <a href="https://research.torproject.org/techreports.html">here</a>. + </font></p> + <br> + <a name="papers"></a> + <h3><a href="#papers" class="anchor">Papers</a></h3> + <br> + These papers summarize some of the results of of the Tor Metrics + Project and have been accepted for publication at academic + conferences or workshops. + <ul> + <li>Karsten Loesing, Steven J. Murdoch, Roger Dingledine. A Case + Study on Measuring Statistical Data in the Tor Anonymity + Network. Workshop on Ethics in + Computer Security Research (WECSR 2010), Tenerife, Spain, + January 2010. (<a href="http://freehaven.net/anonbib/cache/wecsr10measuring-tor.pdf">PDF</a>)</li> + <li>Karsten Loesing. Measuring the Tor Network from Public + Directory Information. 2nd Hot Topics in Privacy Enhancing + Technologies (HotPETs 2009), Seattle, WA, USA, August 2009. + (<a href="https://research.torproject.org/techreports/metrics-2009-08-07.pdf">PDF</a>)</li> + </ul> + <br> + <a name="techreports"></a> + <h3><a href="#techreports" class="anchor">Technical + reports</a></h3> + <br> + <p> + Some of the + <a href="https://research.torproject.org/techreports.html">Tor + Technical Reports</a> have been the first place to + publish novel kinds of statistics on the Tor network. Some, but + not all, of the results contained in those technical reports have + been included in the <a href="#papers">papers</a> above or in the + daily updated <a href="graphs.html">graphs</a>. + </p> + <br> + <a name="blogposts"></a> + <h3><a href="#blogposts" class="anchor">Blog posts</a></h3> + <br> + The following blog posts are either the results of metrics + research or describe new interesting research questions that can + (partly) be answered with metrics data. + <ul> + <li>Research problems: Ten ways to discover Tor bridges + (<a href="https://blog.torproject.org/blog/research-problems-ten-ways-discover-tor-bridges">link</a>, + October 31, 2011).</li> + <li>Research problem: better guard rotation parameters + (<a href="https://blog.torproject.org/blog/research-problem-better-guard-rotation-parameters">link</a>, + August 20, 2011).</li> + <li>Research problem: measuring the safety of the Tor network + (<a href="https://blog.torproject.org/blog/research-problem-measuring-safety-tor-network">link</a>, + February 5, 2011).</li> + </ul> + </div> + </div> + <div class="bottom" id="bottom"> + <%@ include file="footer.jsp"%> + </div> +</body> +</html> diff --git a/website/web/WEB-INF/performance.jsp b/website/web/WEB-INF/performance.jsp new file mode 100644 index 0000000..e220b59 --- /dev/null +++ b/website/web/WEB-INF/performance.jsp @@ -0,0 +1,150 @@ +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> +<html> +<head> + <title>Tor Metrics Portal: Performance</title> + <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> + <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet"> + <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon"> +</head> +<body> + <div class="center"> + <%@ include file="banner.jsp"%> + <div class="main-column"> +<h2>Tor Metrics Portal: Performance</h2> +<br> +<a name="torperf"></a> +<h3><a href="#torperf" class="anchor">Time to download files over +Tor</a></h3> +<br> +<p>The following graphs show the performance of the Tor network as +experienced by its users. The graphs contain the average (median) time to +request files of three different sizes over Tor as well as first and third +quartile of request times. Medians and quartiles are calculated multiple +times per day for completed days only, resulting in a delay of 1 to 1.5 +days before changes to network performance become visible in the +graph.</p> +<img src="torperf.png${torperf_url}" + width="576" height="360" alt="Torperf graph"> +<form action="performance.html#torperf"> + <div class="formrow"> + <input type="hidden" name="graph" value="torperf"> + <p> + <label>Start date (yyyy-mm-dd):</label> + <input type="text" name="start" size="10" + value="<c:choose><c:when test="${fn:length(torperf_start) == 0}">${default_start_date}</c:when><c:otherwise>${torperf_start[0]}</c:otherwise></c:choose>"> + <label>End date (yyyy-mm-dd):</label> + <input type="text" name="end" size="10" + value="<c:choose><c:when test="${fn:length(torperf_end) == 0}">${default_end_date}</c:when><c:otherwise>${torperf_end[0]}</c:otherwise></c:choose>"> + </p><p> + Source: + <input type="radio" name="source" value="all"<c:if test="${fn:length(torperf_source) == 0 or torperf_source[0] eq 'all'}"> checked</c:if>> all + <input type="radio" name="source" value="torperf"<c:if test="${torperf_source[0] eq 'torperf'}"> checked</c:if>> torperf + <input type="radio" name="source" value="moria"<c:if test="${torperf_source[0] eq 'moria'}"> checked</c:if>> moria + <input type="radio" name="source" value="siv"<c:if test="${torperf_source[0] eq 'siv'}"> checked</c:if>> siv + </p><p> + <label>File size: </label> + <input type="radio" name="filesize" value="50kb"<c:if test="${fn:length(torperf_filesize) == 0 or torperf_filesize[0] eq '50kb'}"> checked</c:if>> 50 KiB + <input type="radio" name="filesize" value="1mb"<c:if test="${torperf_filesize[0] eq '1mb'}"> checked</c:if>> 1 MiB + <input type="radio" name="filesize" value="5mb"<c:if test="${torperf_filesize[0] eq '5mb'}"> checked</c:if>> 5 MiB + </p><p> + <input class="submit" type="submit" value="Update graph"> + </p> + </div> +</form> +<p>Download graph as +<a href="torperf.pdf${torperf_url}">PDF</a> or +<a href="torperf.svg${torperf_url}">SVG</a>.</p> +<p><a href="stats/torperf.csv">CSV</a> file containing all data.</p> + +<br> +<a name="torperf-failures"></a> +<h3><a href="#torperf-failures" class="anchor">Timeouts and failures of +downloading files over Tor</a></h3> +<br> +<p>The following graphs show the fraction of timeouts and failures of +downloading files over Tor as experienced by users. +A timeout occurs when a 50 KiB (1 MiB, 5 MiB) download does not complete +within 4:55 minutes (29:55 minutes, 59:55 minutes). +A failure occurs when the download completes, but the response is smaller +than 50 KiB (1 MiB, 5 MiB).</p> +<img src="torperf-failures.png${torperf_failures_url}" + width="576" height="360" alt="Torperf failures graph"> +<form action="performance.html#torperf-failures"> + <div class="formrow"> + <input type="hidden" name="graph" value="torperf-failures"> + <p> + <label>Start date (yyyy-mm-dd):</label> + <input type="text" name="start" size="10" + value="<c:choose><c:when test="${fn:length(torperf_failures_start) == 0}">${default_start_date}</c:when><c:otherwise>${torperf_failures_start[0]}</c:otherwise></c:choose>"> + <label>End date (yyyy-mm-dd):</label> + <input type="text" name="end" size="10" + value="<c:choose><c:when test="${fn:length(torperf_failures_end) == 0}">${default_end_date}</c:when><c:otherwise>${torperf_failures_end[0]}</c:otherwise></c:choose>"> + </p><p> + Source: + <input type="radio" name="source" value="all"<c:if test="${fn:length(torperf_failures_source) == 0 or torperf_failures_source[0] eq 'all'}"> checked</c:if>> all + <input type="radio" name="source" value="torperf"<c:if test="${torperf_failures_source[0] eq 'torperf'}"> checked</c:if>> torperf + <input type="radio" name="source" value="moria"<c:if test="${torperf_failures_source[0] eq 'moria'}"> checked</c:if>> moria + <input type="radio" name="source" value="siv"<c:if test="${torperf_failures_source[0] eq 'siv'}"> checked</c:if>> siv + </p><p> + <label>File size: </label> + <input type="radio" name="filesize" value="50kb"<c:if test="${fn:length(torperf_failures_filesize) == 0 or torperf_failures_filesize[0] eq '50kb'}"> checked</c:if>> 50 KiB + <input type="radio" name="filesize" value="1mb"<c:if test="${torperf_failures_filesize[0] eq '1mb'}"> checked</c:if>> 1 MiB + <input type="radio" name="filesize" value="5mb"<c:if test="${torperf_failures_filesize[0] eq '5mb'}"> checked</c:if>> 5 MiB + </p><p> + <input class="submit" type="submit" value="Update graph"> + </p> + </div> +</form> +<p>Download graph as +<a href="torperf-failures.pdf${torperf_failures_url}">PDF</a> or +<a href="torperf-failures.svg${torperf_failures_url}">SVG</a>.</p> +<p><a href="stats/torperf.csv">CSV</a> file containing all data.</p> + +<br> +<a name="connbidirect"></a> +<h3><a href="#connbidirect" class="anchor">Fraction of connections used +uni-/bidirectionally</a></h3> +<br> +<p>The following graph shows the fraction of connections that is used +uni- or bi-directionally. Every 10 seconds, relays determine for every +connection whether they read and wrote less than a threshold of 20 KiB. +Connections below this threshold are excluded from these statistics. For +the remaining connections, relays report whether they read/wrote at least +10 times as many bytes as they wrote/read. If so, they classify a +connection as "Mostly reading" or "Mostly writing," respectively. All +other connections are classified as "Both reading and writing." After +classifying connections, read and write counters are reset for the next +10-second interval. Statistics are aggregated over 24 hours.</p> +<img src="connbidirect.png${connbidirect_url}" + width="576" height="360" + alt="Fraction of direct connections used uni-/bidirectionally"> +<form action="performance.html#connbidirect"> + <div class="formrow"> + <input type="hidden" name="graph" value="connbidirect"> + <p> + <label>Start date (yyyy-mm-dd):</label> + <input type="text" name="start" size="10" + value="<c:choose><c:when test="${fn:length(connbidirect_start) == 0}">${default_start_date}</c:when><c:otherwise>${connbidirect_start[0]}</c:otherwise></c:choose>"> + <label>End date (yyyy-mm-dd):</label> + <input type="text" name="end" size="10" + value="<c:choose><c:when test="${fn:length(connbidirect_end) == 0}">${default_end_date}</c:when><c:otherwise>${connbidirect_end[0]}</c:otherwise></c:choose>"> + </p><p> + <input class="submit" type="submit" value="Update graph"> + </p> + </div> +</form> +<p>Download graph as +<a href="connbidirect.pdf${connbidirect_url}">PDF</a> or +<a href="connbidirect.svg${connbidirect_url}">SVG</a>.</p> +<p><a href="stats/connbidirect.csv">CSV</a> file containing all data.</p> +<br> + + </div> + </div> + <div class="bottom" id="bottom"> + <%@ include file="footer.jsp"%> + </div> +</body> +</html> diff --git a/website/web/WEB-INF/relay-search.jsp b/website/web/WEB-INF/relay-search.jsp new file mode 100644 index 0000000..90fdd86 --- /dev/null +++ b/website/web/WEB-INF/relay-search.jsp @@ -0,0 +1,85 @@ +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> +<html> +<head> + <title>Tor Metrics Portal: Relay Search</title> + <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> + <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet"> + <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon"> +</head> +<body> + <div class="center"> + <%@ include file="banner.jsp"%> + <div class="main-column"> + <h2>Tor Metrics Portal: Relay Search</h2> + <p>Search for a relay in the relay descriptor archive by typing + (part of) a <b>nickname</b>, <b>$-prefixed fingerprint</b>, or <b>IP + address</b> and optionally a <b>month (yyyy-mm)</b> or up to three + <b>days (yyyy-mm-dd)</b> in the following search field and + clicking Search. The search will stop after 30 hits or, unless you + provide a month or a day, after parsing the last 30 days of relay + lists.</p> + <br> + <form action="relay-search.html"> + <table> + <tr> + <td><input type="text" name="search" + value="<c:out value="${param.search}"/>"></td> + <td><input type="submit" value="Search"></td> + </tr> + </table> + </form> + <br> + <c:if test="${not empty invalidQuery}"> + <p>Sorry, I didn't understand your query. Please provide a + nickname (e.g., "gabelmoo"), at least the first 8 hex characters + of a fingerprint prefixed by $ (e.g., "$F2044413"), or at least + the first two octets of an IPv4 address in dotted-decimal notation + (e.g., "80.190"). You can also provide at most three months or + days in ISO 8601 format (e.g., "2010-09" or "2010-09-17").</p> + </c:if> + <c:if test="${not empty outsideInterval}"> + <p>${outsideInterval}</p> + </c:if> + <c:if test="${not empty searchNotice}"> + <p>${searchNotice}</p> + </c:if> + <c:if test="${not empty query}"> + <!-- ${query} --> + </c:if> + <c:if test="${not empty queryTime}"> + <c:forEach var="consensus" items="${foundDescriptors}"> + ${rawValidAfterLines[consensus.key]} + <c:forEach var="statusentry" items="${consensus.value}"> + ${rawStatusEntries[statusentry]} + </c:forEach> + <br> + </c:forEach> + <p>Found + <c:choose> + <c:when test="${matches > 30}"> + more than 30 relays (displaying only those in the last + consensuses) + </c:when> + <c:otherwise> + ${matches} relays + </c:otherwise> + </c:choose> + in <fmt:formatNumber value="${queryTime / 1000}" pattern="#.###"/> + seconds.</p> + <c:if test="${queryTime > 10000}"> + <p>In theory, search time should not exceed 10 seconds. The + query was '${query}'. If this or similar searches remain slow, + please <a href="mailto:tor-assistants@torproject.org">let us + know</a>!</p> + </c:if> + </c:if> + </div> + </div> + <div class="bottom" id="bottom"> + <%@ include file="footer.jsp"%> + </div> +</body> +</html> + diff --git a/website/web/WEB-INF/research.jsp b/website/web/WEB-INF/research.jsp new file mode 100644 index 0000000..b0a7277 --- /dev/null +++ b/website/web/WEB-INF/research.jsp @@ -0,0 +1,32 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> +<html> +<head> + <title>Tor Metrics Portal: Research</title> + <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> + <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet"> + <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon"> +</head> +<body> + <div class="center"> + <%@ include file="banner.jsp"%> + <div class="main-column"> + <h2>Tor Metrics Portal: Research</h2> + <br> + <p>The Tor Metrics Project aims at supporting privacy enhancing + technologies research by making gathered network + <a href="data.html">data</a>, and + <a href="tools.html">tools</a> + for processing these data available to the public. + Some results from analyzing these data can be found in + <a href="https://research.torproject.org/techreports.html">Tor + Tech Reports</a>. If you are + missing anything for your Tor-related research or want to share + your research results with others, please + <a href="mailto:tor-assistants@torproject.org">let us know</a>! + </div> + </div> + <div class="bottom" id="bottom"> + <%@ include file="footer.jsp"%> + </div> +</body> +</html> diff --git a/website/web/WEB-INF/stats.jsp b/website/web/WEB-INF/stats.jsp new file mode 100644 index 0000000..ccc2540 --- /dev/null +++ b/website/web/WEB-INF/stats.jsp @@ -0,0 +1,313 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> +<html> +<head> + <title>Tor Metrics Portal: Statistics</title> + <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> + <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet"> + <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon"> +</head> +<body> + <div class="center"> + <%@ include file="banner.jsp"%> + <div class="main-column"> +<h2>Tor Metrics Portal: Statistics</h2> +<br> + +<p>The metrics portal aggregates large amounts of Tor network +<a href="data.html">data</a> and visualizes results in customizable +<a href="graphs.html">graphs</a> and tables. +All aggregated data are also available for download, so that people can +easily plot their own graphs or even develop a prettier metrics website +without writing their own data aggregation code. +Data formats of aggregate statistics are specified below.</p> +<hr> +<br> + +<a name="servers"></a> +<h3><a href="#servers" class="anchor">Number of relays and +bridges</a></h3> +<br> +<p>Statistics file <a href="stats/servers.csv">servers.csv</a> contains +the average number of relays and bridges in the Tor network. +All averages are calculated per day by evaluating the relay and bridge +lists published by the directory authorities. +Statistics include subsets of relays or bridges by relay flag (only +relays), country code (only relays, only until February 2013), Tor +software version (only relays), operating system (only relays), and EC2 +cloud (only bridges). +The statistics file contains the following columns:</p> + +<ul> +<li><b>date:</b> UTC date (YYYY-MM-DD) when relays or bridges have been +listed as running.</li> +<li><b>flag:</b> Relay flag assigned by the directory authorities. +Examples are <b>"Exit"</b>, <b>"Guard"</b>, <b>"Fast"</b>, +<b>"Stable"</b>, and <b>"HSDir"</b>. +Relays can have none, some, or all these relay flags assigned. +Relays that don't have the <b>"Running"</b> flag are not included in these +statistics regardless of their other flags. +If this column contains the empty string, all running relays are included, +regardless of assigned flags. +There are no statistics on the number of bridges by relay flag.</li> +<li><b>country:</b> Two-letter lower-case country code as found in a GeoIP +database by resolving the relay's first onion-routing IP address, or +<b>"??"</b> if an IP addresses could not be resolved. +If this column contains the empty string, all running relays are included, +regardless of their resolved country code. +Statistics on relays by country code are only available until January 31, +2013. +There are no statistics on the number of bridges by country code.</li> +<li><b>version:</b> First three dotted numbers of the Tor software version +as reported by the relay. +An example is <b>"0.2.5"</b>. +If this column contains the empty string, all running relays are included, +regardless of the Tor software version they run. +There are no statistics on the number of bridges by Tor software +version.</li> +<li><b>platform:</b> Operating system as reported by the relay. +Examples are <b>"Linux"</b>, <b>"Darwin"</b> (Mac OS X), <b>"FreeBSD"</b>, +<b>"Windows"</b>, and <b>"Other"</b>. +If this column contains the empty string, all running relays are included, +regardless of the operating system they run on. +There are no statistics on the number of bridges by operating system.</li> +<li><b>ec2bridge:</b> Whether bridges are running in the EC2 cloud or not. +More precisely, bridges in the EC2 cloud running an image provided by Tor +by default set their nickname to <b>"ec2bridger"</b> plus 8 random hex +characters. +This column either contains <b>"t"</b> for bridges matching this naming +scheme, or the empty string for all bridges regardless of their nickname. +There are no statistics on the number of relays running in the EC2 +cloud.</li> +<li><b>relays:</b> The average number of relays matching the criteria in +the previous columns. +If the values in previous columns are specific to bridges only, this +column contains the empty string.</li> +<li><b>bridges:</b> The average number of bridges matching the criteria in +the previous columns. +If the values in previous columns are specific to relays only, this column +contains the empty string.</li> +</ul> +<hr> +<br> + +<a name="bandwidth"></a> +<h3><a href="#bandwidth" class="anchor">Bandwidth provided and consumed by +relays</a></h3> +<br> +<p>Statistics on bandwidth provided and consumed by relays are contained +in file <a href="stats/bandwidth.csv">bandwidth.csv</a>. +This file contains three different bandwidth metrics: +(1) bandwidth that relays are capable to provide and bandwidth that relays +report to have consumed, either (2) for any traffic, or (3) only traffic +from serving directory data. +Relays providing bandwidth statistics are categorized by having the +<b>"Exit"</b> and <b>"Guard"</b> relay flag, having both, or not having +either. +The statistics file contains the following columns:</p> + +<ul> +<li><b>date:</b> UTC date (YYYY-MM-DD) that relays reported bandwidth data +for.</li> +<li><b>isexit:</b> Whether relays included in this line have the +<b>"Exit"</b> relay flag or not, which can be <b>"t"</b> or <b>"f"</b>. +If this column contains the empty string, bandwidth data from all running +relays are included, regardless of assigned relay flags.</li> +<li><b>isguard:</b> Whether relays included in this line have the +<b>"Guard"</b> relay flag or not, which can be <b>"t"</b> or <b>"f"</b>. +If this column contains the empty string, bandwidth data from all running +relays are included, regardless of assigned relay flags.</li> +<li><b>advbw:</b> Total advertised bandwidth in bytes per second that +relays are capable to provide.</li> +<li><b>bwread:</b> Total bandwidth in bytes per second that relays have +read. +This metric includes any kind of traffic.</li> +<li><b>bwwrite:</b> Similar to <b>bwread</b>, but for traffic written by +relays.</li> +<li><b>dirread:</b> Bandwidth in bytes per second that relays have read +when serving directory data. +Not all relays report how many bytes they read when serving directory data +which is why this value is an estimate from the available data. +This metric is not available for subsets of relays with certain relay +flags, so that this column will contain the empty string if either +<b>isexit</b> or <b>isguard</b> is non-empty.</li> +<li><b>dirwrite:</b> Similar to <b>dirread</b>, but for traffic written by +relays when serving directory data.</li> +</ul> +<hr> +<br> + +<a name="fast-exits"></a> +<h3><a href="#fast-exits" class="anchor">Relays meeting or almost meeting +fast-exit requirements</a></h3> +<br> +<p>Statistics file <a href="stats/fast-exits.csv">fast-exits.csv</a> +contains the number of relays meeting or almost meeting fast-exit +requirements. +These requirements originate from a Tor sponsor contract and are defined as +follows: +a Tor relay is fast if it has at least 95 Mbit/s configured bandwidth +rate, at least 5000 KB/s advertised bandwidth capacity, and permits +exiting to ports 80, 443, 554, and 1755; furthermore, there may be at most +2 relays per /24 network in the set of fast exits. +Similarly, an almost fast exit is one that almost meets the fast-exit +requirements, but fails at least one of them. +In particular, an almost fast exit is one that has at least 80 Mbit/s +configured bandwidth rate, at least 2000 KB/s advertised bandwidth +capacity, and permits exiting to ports 80 and 443; also, if there are more +than 2 relays per /24 network meeting fast-exit requirements, all but two +are considered almost fast. +The statistics file contains the following columns:</p> + +<ul> +<li><b>date:</b> UTC date (YYYY-MM-DD) when relays have been listed as +running.</li> +<li><b>fastnum:</b> Average number of relays matching fast-exit +requirements.</li> +<li><b>almostnum:</b> Average number of relays almost matching +fast-exit requirements.</li> +<li><b>fastprob:</b> Total exit probability of all relays matching +fast-exit requirements.</li> +<li><b>almostprob:</b> Total exit probability of all relays almost +matching fast-exit requirements.</li> +</li> +</ul> +<hr> +<br> + +<a name="clients"></a> +<h3><a href="#clients" class="anchor">Estimated number of clients in the +Tor network</a></h3> +<br> +<p>Statistics file <a href="stats/clients.csv">clients.csv</a> contains +estimates on the number of clients in the Tor network. +These estimates are based on the number of directory requests counted on +directory mirrors and bridges. +Statistics are available for clients connecting directly to the Tor +network and clients connecting via bridges. +For relays, there exist statistics on the number of clients by country, +and for bridges, statistics are available by country, by transport, and by +IP version. +Statistics further include expected client numbers from past observations +which can be used to detect censorship or release of censorship. +The statistics file contains the following columns:</p> + +<ul> +<li><b>date:</b> UTC date (YYYY-MM-DD) for which client numbers are +estimated.</li> +<li><b>node:</b> The node type to which clients connect first, which can +be either <b>"relay"</b> or <b>"bridge"</b>.</li> +<li><b>country:</b> Two-letter lower-case country code as found in a GeoIP +database by resolving clients' IP addresses, or <b>"??"</b> if client IP +addresses could not be resolved. +If this column contains the empty string, all clients are included, +regardless of their country code.</li> +<li><b>transport:</b> Transport name used by clients to connect to the Tor +network using bridges. +Examples are <b>"obfs2"</b>, <b>"obfs3"</b>, <b>"websocket"</b>, or +<b>"<OR>"</b> (original onion routing protocol). +If this column contains the empty string, all clients are included, +regardless of their transport. +There are no statistics on the number of clients by transport that connect +to the Tor network via relays.</li> +<li><b>version:</b> IP version used by clients to connect to the Tor +network using bridges. +Examples are <b>"v4"</b> and <b>"v6"</b>. +If this column contains the empty string, all clients are included, +regardless of their IP version. +There are no statistics on the number of clients by IP version that connect +directly to the Tor network using relays.</li> +<li><b>lower:</b> Lower number of expected clients under the assumption +that there has been no censorship event. +If this column contains the empty string, there are no expectations on the +number of clients.</li> +<li><b>upper:</b> Upper number of expected clients under the assumption +that there has been no release of censorship. +If this column contains the empty string, there are no expectations on the +number of clients.</li> +<li><b>clients:</b> Estimated number of clients.</li> +<li><b>frac:</b> Fraction of relays or bridges in percent that the +estimate is based on. +The higher this value, the more reliable is the estimate. +Values above 50 can be considered reliable enough for most purposes, +lower values should be handled with more care.</li> +</ul> +<hr> +<br> + +<a name="torperf"></a> +<h3><a href="#torperf" class="anchor">Performance of downloading static +files over Tor</a></h3> +<br> +<p>Statistics file <a href="stats/torperf.csv">torperf.csv</a> contains +aggregate statistics on download performance over time. +These statistics come from the Torperf service that periodically downloads +static files over Tor. +The statistics file contains the following columns:</p> + +<ul> +<li><b>date:</b> UTC date (YYYY-MM-DD) when download performance was +measured.</li> +<li><b>size:</b> Size of the downloaded file in bytes.</li> +<li><b>source:</b> Name of the Torperf service performing measurements. +If this column contains the empty string, all measurements are included, +regardless of which Torperf service performed them. +Examples are <b>"moria"</b>, <b>"siv"</b>, and <b>"torperf"</b>.</li> +<li><b>q1:</b> First quartile of time until receiving the last byte in +milliseconds.</li> +<li><b>md:</b> Median of time until receiving the last byte in +milliseconds.</li> +<li><b>q3:</b> Third quartile of time until receiving the last byte in +milliseconds.</li> +<li><b>timeouts:</b> Number of timeouts that occurred when attempting to +download the static file over Tor.</li> +<li><b>failures:</b> Number of failures that occurred when attempting to +download the static file over Tor.</li> +<li><b>requests:</b> Total number of requests made to download the static +file over Tor.</li> +</ul> +<hr> +<br> + +<a name="connbidirect"></a> +<h3><a href="#connbidirect" class="anchor">Fraction of connections used +uni-/bidirectionally</a></h3> +<br> +<p>Statistics file <a href="stats/connbidirect.csv">connbidirect.csv</a> +contains statistics on the fraction of connections that is used uni- or +bidirectionally. +Every 10 seconds, relays determine for every connection whether they read +and wrote less than a threshold of 20 KiB. +For the remaining connections, relays report whether they read/wrote at +least 10 times as many bytes as they wrote/read. +If so, they classify a connection as "mostly reading" or "mostly writing," +respectively. +All other connections are classified as "both reading and writing." +After classifying connections, read and write counters are reset for the +next 10-second interval. +Statistics are aggregated over 24 hours. +The statistics file contains the following columns:</p> + +<ul> +<li><b>date:</b> UTC date (YYYY-MM-DD) for which statistics on +uni-/bidirectional connection usage were reported.</li> +<li><b>source:</b> Fingerprint of the relay reporting statistics.</li> +<li><b>below:</b> Number of 10-second intervals of connections with less +than 20 KiB read and written data.</li> +<li><b>read:</b> Number of 10-second intervals of connections with 10 +times as many read bytes as written bytes.</li> +<li><b>write:</b> Number of 10-second intervals of connections with 10 +times as many written bytes as read bytes.</li> +<li><b>both:</b> Number of 10-second intervals of connections with less +than 10 times as many written or read bytes as in the other +direction.</li> +</ul> + </div> + </div> + <div class="bottom" id="bottom"> + <%@ include file="footer.jsp"%> + </div> +</body> +</html> +</body> +</html> + diff --git a/website/web/WEB-INF/status.jsp b/website/web/WEB-INF/status.jsp new file mode 100644 index 0000000..d6b565e --- /dev/null +++ b/website/web/WEB-INF/status.jsp @@ -0,0 +1,30 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> +<html> +<head> + <title>Tor Metrics Portal: Status</title> + <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> + <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet"> + <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon"> +</head> +<body> + <div class="center"> + <%@ include file="banner.jsp"%> + <div class="main-column"> + <h2>Tor Metrics Portal: Status</h2> + <br> + <p>The network data collected by the Tor Metrics Project can be + used to analyze the Tor network status from a few years ago until + an hour ago. There are currently two applications for this data: + The <a href="exonerator.html">ExoneraTor</a> tells you whether + some IP address was a Tor relay at a given time, the + <a href="relay-search.html">Relay Search</a> lets you search the + descriptor archive for a relay, and the + <a href="consensus-health.html">Consensus Health</a> summarizes + information about the latest network consensus voting process. + </div> + </div> + <div class="bottom" id="bottom"> + <%@ include file="footer.jsp"%> + </div> +</body> +</html> diff --git a/website/web/WEB-INF/tools.jsp b/website/web/WEB-INF/tools.jsp new file mode 100644 index 0000000..8929d74 --- /dev/null +++ b/website/web/WEB-INF/tools.jsp @@ -0,0 +1,110 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> +<html> +<head> + <title>Tor Metrics Portal: Tools</title> + <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> + <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet"> + <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon"> +</head> +<body> + <div class="center"> + <%@ include file="banner.jsp"%> + <div class="main-column"> + <h2>Tor Metrics Portal: Tools</h2> + <br> + <p>The metrics website and related websites depend on a collection + of services that measure, archive, process, and finally present Tor + network data. + This page gives an overview of these services.</p> + <img src="/images/tor-metrics-overview.png"/> + <br> + <a name="measure"></a> + <h3><a href="#measure" class="anchor">Measuring Tor network + data</a></h3> + <br> + <p>Tor network data is measured at various places: + <ul> + <li><a href="https://gitweb.torproject.org/torperf.git">Torperf</a> + is a set of utilities for testing Tor performance from a client + perspective.</li> + <li><a href="https://gitweb.torproject.org/tor.git">tor</a> + relays and bridges gather aggregate usage statistics and publish + descriptors containing data about Tor network structure and + usage.</li> + <li><a href="https://gitweb.torproject.org/tordnsel.git">TorDNSEL</a> + is a Tor DNS-based exit list that runs periodic checks whether + relays use different IP addresses for exiting to the Internet + than they use to register in the Tor network.</li> + <li><a href="https://gitweb.torproject.org/bridgedb.git">BridgeDB</a> + assigns bridges to distributors and gives them out via HTTPS or + email.</li> + </ul> + <br> + <a name="archive"></a> + <h3><a href="#archive" class="anchor">Archiving Tor network + data</a></h3> + <br> + <p>All Tor network data is downloaded, possibly sanitized, and + then archived by a single tool:</p> + <ul> + <li><a href="https://gitweb.torproject.org/metrics-db.git">metrics-db</a> + contains five components for archiving relay descriptors, bridge + descriptors, Torperf results, TorDNSEL exit lists, and BridgeDB + pool assignments.</li> + </ul> + <br> + <a name="process"></a> + <h3><a href="#process" class="anchor">Processing Tor network + data</a></h3> + <br> + <p>In some cases, processing and presenting Tor network data is + separated for maximum flexibility. + In particular, there are currently two main tools that process Tor + network data and write an intermediate data format, but don't + directly present results:</p> + <ul> + <li><a href="https://gitweb.torproject.org/metrics-web.git">metrics-web</a> + is the software behind this website, including aggregation code + that produces statistics files.</li> + <li><a href="https://gitweb.torproject.org/metrics-tasks.git/tree/HEAD:/task-6498">task-6498</a> + is a submodule of metric-web that + aggregates data to visualize fast exits in the Tor network.</li> + <li><a href="https://gitweb.torproject.org/metrics-tasks.git/tree/HEAD:/task-8462">task-8462</a> + is another submodule of metric-web that + estimates daily users from reported directory request + statistics.</li> + <li><a href="https://gitweb.torproject.org/onionoo.git">Onionoo</a> + provides Tor network status information in JSON format via a + RESTful web service.</li> + </ul> + <br> + <a name="present"></a> + <h3><a href="#present" class="anchor">Presenting Tor network + data</a></h3> + <br> + <p>There are a few websites and additional tools presenting Tor + network data: + <ul> + <li><a href="https://gitweb.torproject.org/metrics-web.git">metrics-web</a> + also contains the code that presents aggregate statistics on + this website.</li> + <li><a href="https://gitweb.torproject.org/exonerator.git">ExoneraTor</a> + is a website that tells you whether a given IP address was a Tor + relay.</li> + <li><a href="https://gitweb.torproject.org/atlas.git">Atlas</a> + is a web application to discover relays that uses Onionoo as its + data back-end.</li> + <li><a href="https://gitweb.torproject.org/globe.git">Globe</a> + is a Tor relay and bridge explorer that also uses Onionoo as its + data back-end.</li> + <li><a href="https://gitweb.torproject.org/compass.git">Compass</a> + is a web application that uses Onionoo's data to display + information about fast exits in the Tor network.</li> + </ul> + </div> + </div> + <div class="bottom" id="bottom"> + <%@ include file="footer.jsp"%> + </div> +</body> +</html> diff --git a/website/web/WEB-INF/users.jsp b/website/web/WEB-INF/users.jsp new file mode 100644 index 0000000..ec9f200 --- /dev/null +++ b/website/web/WEB-INF/users.jsp @@ -0,0 +1,390 @@ +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> +<html> +<head> + <title>Tor Metrics Portal: Users</title> + <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"> + <link href="/css/stylesheet-ltr.css" type="text/css" rel="stylesheet"> + <link href="/images/favicon.ico" type="image/x-icon" rel="shortcut icon"> +</head> +<body> + <div class="center"> + <%@ include file="banner.jsp"%> + <div class="main-column"> +<h2>Tor Metrics Portal: Users</h2> +<br> + +<a name="userstats-relay-country"></a> +<p><b>Direct users by country:</b></p> + +<img src="userstats-relay-country.png${userstats_relay_country_url}" + width="576" height="360" alt="Direct users by country graph"> +<form action="users.html#userstats-relay-country"> + <div class="formrow"> + <input type="hidden" name="graph" value="userstats-relay-country"> + <p> + <label>Start date (yyyy-mm-dd):</label> + <input type="text" name="start" size="10" + value="<c:choose><c:when test="${fn:length(userstats_relay_country_start) == 0}">${default_start_date}</c:when><c:otherwise>${userstats_relay_country_start[0]}</c:otherwise></c:choose>"> + <label>End date (yyyy-mm-dd):</label> + <input type="text" name="end" size="10" + value="<c:choose><c:when test="${fn:length(userstats_relay_country_end) == 0}">${default_end_date}</c:when><c:otherwise>${userstats_relay_country_end[0]}</c:otherwise></c:choose>"> + </p><p> + Source: <select name="country"> + <option value="all"<c:if test="${userstats_relay_country_country[0] eq 'all'}"> selected</c:if>>All users</option> + <c:forEach var="country" items="${countries}" > + <option value="${country[0]}"<c:if test="${userstats_relay_country_country[0] eq country[0]}"> selected</c:if>>${country[1]}</option> + </c:forEach> + </select> + </p><p> + Show possible censorship events if available (<a + href="http://research.torproject.org/techreports/detector-2011-09-09.pdf">BETA</a>) + <select name="events"> + <option value="off">Off</option> + <option value="on"<c:if test="${userstats_relay_country_events[0] eq 'on'}"> selected</c:if>>On: both points and expected range</option> + <option value="points"<c:if test="${userstats_relay_country_events[0] eq 'points'}"> selected</c:if>>On: points only, no expected range</option> + </select> + </p><p> + <input class="submit" type="submit" value="Update graph"> + </p> + </div> +</form> +<p>Download graph as +<a href="userstats-relay-country.pdf${userstats_relay_country_url}">PDF</a> or +<a href="userstats-relay-country.svg${userstats_relay_country_url}">SVG</a>.</p> +<hr> +<a name="userstats-relay-table"></a> +<p><b>Top-10 countries by directly connecting users:</b></p> +<form action="users.html#userstats-relay-table"> + <div class="formrow"> + <input type="hidden" name="table" value="userstats-relay"> + <p> + <label>Start date (yyyy-mm-dd):</label> + <input type="text" name="start" size="10" + value="<c:choose><c:when test="${fn:length(userstats_relay_start) == 0}">${default_start_date}</c:when><c:otherwise>${userstats_relay_start[0]}</c:otherwise></c:choose>"> + <label>End date (yyyy-mm-dd):</label> + <input type="text" name="end" size="10" + value="<c:choose><c:when test="${fn:length(userstats_relay_end) == 0}">${default_end_date}</c:when><c:otherwise>${userstats_relay_end[0]}</c:otherwise></c:choose>"> + </p><p> + <input class="submit" type="submit" value="Update table"> + </p> + </div> +</form> +<br> +<table> + <tr> + <th>Country</th> + <th>Mean daily users</th> + </tr> + <c:forEach var="row" items="${userstats_relay_tabledata}"> + <tr> + <td><a href="users.html?graph=userstats-relay-country&country=${row['cc']}#userstats-relay-country">${row['country']}</a> </td> + <td>${row['abs']} (<fmt:formatNumber type="number" minFractionDigits="2" value="${row['rel']}" /> %)</td> + </tr> + </c:forEach> +</table> +<hr> +<a name="userstats-censorship-events"></a> +<p><b>Top-10 countries by possible censorship events (<a + href="http://research.torproject.org/techreports/detector-2011-09-09.pdf">BETA</a>):</b></p> +<form action="users.html#userstats-censorship-events"> + <div class="formrow"> + <input type="hidden" name="table" value="userstats-censorship-events"> + <p> + <label>Start date (yyyy-mm-dd):</label> + <input type="text" name="start" size="10" + value="<c:choose><c:when test="${fn:length(userstats_censorship_events_start) == 0}">${default_start_date}</c:when><c:otherwise>${userstats_censorship_events_start[0]}</c:otherwise></c:choose>"> + <label>End date (yyyy-mm-dd):</label> + <input type="text" name="end" size="10" + value="<c:choose><c:when test="${fn:length(userstats_censorship_events_end) == 0}">${default_end_date}</c:when><c:otherwise>${userstats_censorship_events_end[0]}</c:otherwise></c:choose>"> + </p><p> + <input class="submit" type="submit" value="Update table"> + </p> + </div> +</form> +<br> +<table> + <tr> + <th>Country</th> + <th>Downturns</th> + <th>Upturns</th> + </tr> + <c:forEach var="row" items="${userstats_censorship_events_tabledata}"> + <tr> + <td><a href="users.html?graph=userstats-relay-country&country=${row['cc']}&events=on#userstats-relay-country">${row['country']}</a> </td> + <td>${row['downturns']}</td> + <td>${row['upturns']}</td> + </tr> + </c:forEach> +</table> +<hr> + +<a name="userstats-bridge-country"></a> +<p><b>Bridge users by country:</b></p> + +<img src="userstats-bridge-country.png${userstats_bridge_country_url}" + width="576" height="360" alt="Bridge users by country graph"> +<form action="users.html#userstats-bridge-country"> + <div class="formrow"> + <input type="hidden" name="graph" value="userstats-bridge-country"> + <p> + <label>Start date (yyyy-mm-dd):</label> + <input type="text" name="start" size="10" + value="<c:choose><c:when test="${fn:length(userstats_bridge_country_start) == 0}">${default_start_date}</c:when><c:otherwise>${userstats_bridge_country_start[0]}</c:otherwise></c:choose>"> + <label>End date (yyyy-mm-dd):</label> + <input type="text" name="end" size="10" + value="<c:choose><c:when test="${fn:length(userstats_bridge_country_end) == 0}">${default_end_date}</c:when><c:otherwise>${userstats_bridge_country_end[0]}</c:otherwise></c:choose>"> + </p><p> + Source: <select name="country"> + <option value="all"<c:if test="${userstats_bridge_country_country[0] eq 'all'}"> selected</c:if>>All users</option> + <c:forEach var="country" items="${countries}" > + <option value="${country[0]}"<c:if test="${userstats_bridge_country_country[0] eq country[0]}"> selected</c:if>>${country[1]}</option> + </c:forEach> + </select> + </p><p> + <input class="submit" type="submit" value="Update graph"> + </p> + </div> +</form> +<p>Download graph as +<a href="userstats-bridge-country.pdf${userstats_bridge_country_url}">PDF</a> or +<a href="userstats-bridge-country.svg${userstats_bridge_country_url}">SVG</a>.</p> +<hr> +<a name="userstats-bridge-table"></a> +<p><b>Top-10 countries by bridge users:</b></p> +<form action="users.html#userstats-bridge-table"> + <div class="formrow"> + <input type="hidden" name="table" value="userstats-bridge"> + <p> + <label>Start date (yyyy-mm-dd):</label> + <input type="text" name="start" size="10" + value="<c:choose><c:when test="${fn:length(userstats_bridge_start) == 0}">${default_start_date}</c:when><c:otherwise>${userstats_bridge_start[0]}</c:otherwise></c:choose>"> + <label>End date (yyyy-mm-dd):</label> + <input type="text" name="end" size="10" + value="<c:choose><c:when test="${fn:length(userstats_bridge_end) == 0}">${default_end_date}</c:when><c:otherwise>${userstats_bridge_end[0]}</c:otherwise></c:choose>"> + </p><p> + <input class="submit" type="submit" value="Update table"> + </p> + </div> +</form> +<br> +<table> + <tr> + <th>Country</th> + <th>Mean daily users</th> + </tr> + <c:forEach var="row" items="${userstats_bridge_tabledata}"> + <tr> + <td><a href="users.html?graph=userstats-bridge-country&country=${row['cc']}#userstats-bridge-country">${row['country']}</a> </td> + <td>${row['abs']} (<fmt:formatNumber type="number" minFractionDigits="2" value="${row['rel']}" /> %)</td> + </tr> + </c:forEach> +</table> +<hr> + +<a name="userstats-bridge-transport"></a> +<p><b>Bridge users by transport:</b></p> + +<img src="userstats-bridge-transport.png${userstats_bridge_transport_url}" + width="576" height="360" alt="Bridge users by transport graph"> +<form action="users.html#userstats-bridge-transport"> + <div class="formrow"> + <input type="hidden" name="graph" value="userstats-bridge-transport"> + <p> + <label>Start date (yyyy-mm-dd):</label> + <input type="text" name="start" size="10" + value="<c:choose><c:when test="${fn:length(userstats_bridge_transport_start) == 0}">${default_start_date}</c:when><c:otherwise>${userstats_bridge_transport_start[0]}</c:otherwise></c:choose>"> + <label>End date (yyyy-mm-dd):</label> + <input type="text" name="end" size="10" + value="<c:choose><c:when test="${fn:length(userstats_bridge_transport_end) == 0}">${default_end_date}</c:when><c:otherwise>${userstats_bridge_transport_end[0]}</c:otherwise></c:choose>"> + </p><p> + Source: <select name="transport"> + <option value="<OR>"<c:if test="${userstats_bridge_transport_transport[0] eq '<OR>'}"> selected</c:if>>Default OR protocol</option> + <option value="obfs2"<c:if test="${userstats_bridge_transport_transport[0] eq 'obfs2'}"> selected</c:if>>obfs2</option> + <option value="obfs3"<c:if test="${userstats_bridge_transport_transport[0] eq 'obfs3'}"> selected</c:if>>obfs3</option> + <option value="websocket"<c:if test="${userstats_bridge_transport_transport[0] eq 'websocket'}"> selected</c:if>>Flash proxy/websocket</option> + <option value="<??>"<c:if test="${userstats_bridge_transport_transport[0] eq '<??>'}"> selected</c:if>>Unknown transport</option> + </select> + </p><p> + <input class="submit" type="submit" value="Update graph"> + </p> + </div> +</form> +<p>Download graph as +<a href="userstats-bridge-transport.pdf${userstats_bridge_transport_url}">PDF</a> or +<a href="userstats-bridge-transport.svg${userstats_bridge_transport_url}">SVG</a>.</p> +<hr> + +<a name="userstats-bridge-version"></a> +<p><b>Bridge users by IP version:</b></p> + +<img src="userstats-bridge-version.png${userstats_bridge_version_url}" + width="576" height="360" alt="Bridge users by IP version graph"> +<form action="users.html#userstats-bridge-version"> + <div class="formrow"> + <input type="hidden" name="graph" value="userstats-bridge-version"> + <p> + <label>Start date (yyyy-mm-dd):</label> + <input type="text" name="start" size="10" + value="<c:choose><c:when test="${fn:length(userstats_bridge_version_start) == 0}">${default_start_date}</c:when><c:otherwise>${userstats_bridge_version_start[0]}</c:otherwise></c:choose>"> + <label>End date (yyyy-mm-dd):</label> + <input type="text" name="end" size="10" + value="<c:choose><c:when test="${fn:length(userstats_bridge_version_end) == 0}">${default_end_date}</c:when><c:otherwise>${userstats_bridge_version_end[0]}</c:otherwise></c:choose>"> + </p><p> + Source: <select name="version"> + <option value="v4"<c:if test="${userstats_bridge_version_version[0] eq 'v4'}"> selected</c:if>>IPv4</option> + <option value="v6"<c:if test="${userstats_bridge_version_version[0] eq 'v6'}"> selected</c:if>>IPv6</option> + </select> + </p><p> + <input class="submit" type="submit" value="Update graph"> + </p> + </div> +</form> +<p>Download graph as +<a href="userstats-bridge-version.pdf${userstats_bridge_version_url}">PDF</a> or +<a href="userstats-bridge-version.svg${userstats_bridge_version_url}">SVG</a>.</p> +<hr> + +<p><a href="stats/clients.csv">CSV</a> file containing new user +estimates.</p> +<br> + +<hr> +<a name="questions-and-answers"></a> +<p><b>Questions and answers</b></p> +<p> +Q: How is it even possible to count users in an anonymity network?<br/> +A: We actually don't count users, but we count requests to the directories +that clients make periodically to update their list of relays and estimate +user numbers indirectly from there. +</p> +<p> +Q: Do all directories report these directory request numbers?<br/> +A: No, but we can see what fraction of directories reported them, and then +we can extrapolate the total number in the network. +</p> + +<p> +Q: How do you get from these directory requests to user numbers?<br/> +A: We put in the assumption that the average client makes 10 such requests +per day. A tor client that is connected 24/7 makes about 15 requests per +day, but not all clients are connected 24/7, so we picked the number 10 +for the average client. We simply divide directory requests by 10 and +consider the result as the number of users. +</p> + +<p> +Q: So, are these distinct users per day, average number of users connected +over the day, or what?<br/> +A: Average number of users connected over the day. We can't say how many +distinct users there are. +</p> + +<p> +Q: Are these tor clients or users? What if there's more than one user +behind a tor client?<br/> +A: Then we count those users as one. We really count clients, but it's +more intuitive for most people to think of users, that's why we say users +and not clients. +</p> + +<p> +Q: What if a user runs tor on a laptop and changes their IP address a few +times per day? Don't you overcount that user?<br/> +A: No, because that user updates their list of relays as often as a user +that doesn't change IP address over the day. +</p> + +<p> +Q: How do you know which countries users come from?<br/> +A: The directories resolve IP addresses to country codes and report these +numbers in aggregate form. This is one of the reasons why tor ships with +a GeoIP database. +</p> + +<p> +Q: Why are there so few bridge users that are not using the default OR +protocol or that are using IPv6?<br/> +A: Very few bridges report data on transports or IP versions yet, and by +default we consider requests to use the default OR protocol and IPv4. +Once more bridges report these data, the numbers will become more +accurate. +</p> + +<p> +Q: Why do the graphs end 2 days in the past and not today?<br/> +A: Relays and bridges report some of the data in 24-hour intervals which +may end at any time of the day. And after such an interval is over relays +and bridges might take another 18 hours to report the data. We cut off +the last two days from the graphs, because we want to avoid that the last +data point in a graph indicates a recent trend change which is in fact +just an artifact of the algorithm. +</p> + +<p> +Q: But I noticed that the last data point went up/down a bit since I last +looked a few hours ago. Why is that?<br/> +A: You're an excellent observer! The reason is that we publish user +numbers once we're confident enough that they won't change significantly +anymore. But it's always possible that a directory reports data a few +hours after we were confident enough, but which then slightly changed the +graph. +</p> + +<p> +Q: Why are no numbers available before September 2011?<br/> +A: We do have descriptor archives from before that time, but those +descriptors didn't contain all the data we use to estimate user numbers. +We do have older user numbers from an earlier estimation approach +<a href="/data/old-user-number-estimates.tar.gz">here</a>, but we believe +the current approach is more accurate. +</p> + +<p> +Q: Why do you believe the current approach to estimate user numbers is +more accurate?<br/> +A: For direct users, we include all directories which we didn't do in the +old approach. We also use histories that only contain bytes written to +answer directory requests, which is more precise than using general byte +histories. +</p> + +<p> +Q: And what about the advantage of the current approach over the old one +when it comes to bridge users?<br/> +A: Oh, that's a whole different story. We wrote a 13 page long +<a href="https://research.torproject.org/techreports/counting-daily-bridge-users-2012-10-24.pdf">technical +report</a> explaining the reasons for retiring the old approach. +tl;dr: in the old approach we measured the wrong thing, and now we measure +the right thing. +</p> + +<p> +Q: Are the data and the source code for estimating these user numbers +available?<br/> +A: Sure, <a href="/data.html">data</a> and +<a href="https://gitweb.torproject.org/metrics-tasks.git/tree/HEAD:/task-8462">source +code</a> are publicly available. +</p> + +<p> +Q: What are these red and blue dots indicating possible censorship +events?<br/> +A: We run an anomaly-based censorship-detection system that looks at +estimated user numbers over a series of days and predicts the user number +in the next days. If the actual number is higher or lower, this might +indicate a possible censorship event or release of censorship. For more +details, see our +<a href="https://research.torproject.org/techreports/detector-2011-09-09.pdf">technical +report</a>. +</p> + + </div> + </div> + <div class="bottom" id="bottom"> + <%@ include file="footer.jsp"%> + </div> +</body> +</html> diff --git a/website/web/css/stylesheet-ltr.css b/website/web/css/stylesheet-ltr.css new file mode 100644 index 0000000..ce0c54e --- /dev/null +++ b/website/web/css/stylesheet-ltr.css @@ -0,0 +1,161 @@ +body { + background-color: white; + margin-top: 0px; + font-family: Arial, Helvetica, sans-serif; + font-size: 1em; + font-style: normal; + color: #000000; + padding-top: 0px; +} + +/* images */ + +img { + border: 0; +} + + +li { + margin: .2em .2em .2em 1em; +} + +/* this centers the page */ + +.center { + text-align: center; + background-color: white; + margin: 0px auto 0 auto; + width: 85%; +} + +.center table { + margin-left: auto; + margin-right: auto; + text-align: left; +} + +div.bottom { + font-size: 0.8em; + margin-top: 0.5cm; + margin-left: 1em; + margin-right: 1em; + text-align: center; +} + +/* The main column (left text) */ + +div.main-column { + padding: 15px 0 10px 10px; + text-indent: 0pt; + font-size: 1em; + direction: ltr; + text-align: left; +} + +/* formatting styles */ + +h2 { + font-size: 1.4em; + margin-bottom: 0em; + font-weight: bold; + margin-top: 0; +} + +h3 { + font-size: 1.2em; + margin-bottom: 0em; + font-weight: bold; + margin-top: 0; +} + +p { + margin-top: 0; + margin-bottom: 1em; +} + +a:link { + color: blue; + font-size: 1em; +} + +a:visited { + color: purple; + font-size: 1em; +} + +a.anchor { + font-size: 1em; + color: black; + font-weight: bold; + text-decoration: none; +} + +td { + vertical-align: top; +} + +/* the banner */ + +table.banner { + width: 100%; + height: 79px; + margin-left: auto; + margin-right: auto; +} + +td.banner-left { + /* This is done with an <img> in the HTML so it can be clickable + background-image: url("images/top-left.png"); + background-repeat: no-repeat; */ + width: 193px; +} + +td.banner-middle { + background-color: #00802B; + background-image: url("/images/top-middle.png"); + background-repeat: repeat-x; + vertical-align: bottom; + padding-bottom: 10px; + color: white; + font-weight: bold; + font-size: 1.2em; +} + +td.banner-middle a, td.banner-middle a:visited { + margin-right: 5px; + color: white; + font-weight: bold; + font-size: 1em; +} + +td.banner-middle a:hover { + color: #FF7F00; + font-weight: bold; + font-size: 1em; +} + +td.banner-right { + background-image: url("/images/top-right.png"); + background-repeat: no-repeat; + width: 15px; + background-position: right; + padding-top: 8px; +} + +.banner-middle a.current { + text-decoration: none; + color: #FF7F00; + font-weight: bold; + font-size: 1em; + width: auto; + left: -50px; +} + +hr { + background-color:#002200; + color:#666666; + font-size:1px; + height:1px; + line-height:0; + margin:15px 0 5px; +} diff --git a/website/web/images/favicon.ico b/website/web/images/favicon.ico new file mode 100644 index 0000000..48060b1 Binary files /dev/null and b/website/web/images/favicon.ico differ diff --git a/website/web/images/top-left.png b/website/web/images/top-left.png new file mode 100644 index 0000000..9927355 Binary files /dev/null and b/website/web/images/top-left.png differ diff --git a/website/web/images/top-middle.png b/website/web/images/top-middle.png new file mode 100644 index 0000000..1035dc5 Binary files /dev/null and b/website/web/images/top-middle.png differ diff --git a/website/web/images/top-right.png b/website/web/images/top-right.png new file mode 100644 index 0000000..54623ef Binary files /dev/null and b/website/web/images/top-right.png differ diff --git a/website/web/images/tor-metrics-overview.png b/website/web/images/tor-metrics-overview.png new file mode 100644 index 0000000..ee2bc07 Binary files /dev/null and b/website/web/images/tor-metrics-overview.png differ diff --git a/website/web/js/bubbles.js b/website/web/js/bubbles.js new file mode 100644 index 0000000..8030e20 --- /dev/null +++ b/website/web/js/bubbles.js @@ -0,0 +1,288 @@ +function get_prefix(relay) { return /^[0-9]+.[0-9]+./.exec(relay.or_addresses[0]); } + +var graphs = { + 'default': { + extra_fields: [], + group: undefined, + group_id_func: function(relay) { return undefined; }, + group_name_func: function(relay) { return 'Relays'; }, + }, 'as': { + extra_fields: ['as_number', 'as_name'], + group: "autonomous systems", + group_id_func: function(relay) { return relay.as_number; }, + group_name_func: function(relay) { return relay.as_name; }, + }, 'contact': { + extra_fields: ['contact'], + group: "contact infos", + group_id_func: function(relay) { return relay.contact; }, + group_name_func: function(relay) { return relay.contact; }, + }, 'country': { + extra_fields: ['country', 'country_name'], + group: "countries", + group_id_func: function(relay) { return relay.country; }, + group_name_func: function(relay) { return relay.country_name; }, + }, 'network-family': { + extra_fields: ['or_addresses'], + group: "network families (/16)", + group_id_func: function(relay) { return get_prefix(relay); }, + group_name_func: function(relay) { return get_prefix(relay) + "0.0/16"; }, + }, +}; + +function make_bubble_graph(graph_name) { + var onionoo_url = "https://onionoo.torproject.org/details?type=relay&running=true&field..."; + 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%22,xhtml:%22http://www.w3.org/1999/xhtml%22,xlink... 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@lists.torproject.org