diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..d77702bd --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,17 @@ +{ + "cSpell.words": [ + "Electo", + "FPTP", + "Unsplit", + "Voteline", + "altlink", + "bettercount", + "endcomment", + "frontrunners", + "misinformative", + "ncasenmare", + "scorefamily", + "themself", + "yesno" + ] +} \ No newline at end of file diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..6873ace5 --- /dev/null +++ b/Gemfile @@ -0,0 +1,34 @@ +source "https://rubygems.org" + +# Hello! This is where you manage which Jekyll version is used to run. +# When you want to use a different version, change it below, save the +# file and run `bundle install`. Run Jekyll with `bundle exec`, like so: +# +# bundle exec jekyll serve +# +# This will help ensure the proper Jekyll version is running. +# Happy Jekylling! +# gem "jekyll", "~> 3.8.7" + +# This is the default theme for new Jekyll sites. You may change this to anything you like. +# gem "minima", "~> 2.0" + +# If you want to use GitHub Pages, remove the "gem "jekyll"" above and +# uncomment the line below. To upgrade, run `bundle update github-pages`. +gem "github-pages", "~> 206", group: :jekyll_plugins + +# If you have any plugins, put them here! +group :jekyll_plugins do + gem "jekyll-feed", "~> 0.6" +end + +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +# and associated library. +install_if -> { RUBY_PLATFORM =~ %r!mingw|mswin|java! } do + gem "tzinfo", "~> 1.2" + gem "tzinfo-data" +end + +# Performance-booster for watching directories on Windows +gem "wdm", "~> 0.1.0", :install_if => Gem.win_platform? + diff --git a/README.md b/README.md index 30aa08a5..567750de 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ -![](http://i.imgur.com/8ZzgDiV.png) +![](http://paretoman.github.io/ballot/social/thumbnail.png) -**TO BUILD A BETTER BALLOT** -an interactive guide to alternative voting systems +**Smart Voting Simulator** +An Explorable Guide to Group Decision Making -**[play/read it here](http://ncase.me/ballot)** \ No newline at end of file +**[play/read it here](http://paretoman.github.io/ballot)** + +[modify it here](http://paretoman.github.io/ballot/modify) \ No newline at end of file diff --git a/_config.yml b/_config.yml new file mode 100644 index 00000000..f5db32ae --- /dev/null +++ b/_config.yml @@ -0,0 +1,57 @@ +# Welcome to Jekyll! +# +# This config file is meant for settings that affect your whole blog, values +# which you are expected to set up once and rarely edit after that. If you find +# yourself editing this file very often, consider using Jekyll's data files +# feature for the data you need to update frequently. +# +# For technical reasons, this file is *NOT* reloaded automatically when you use +# 'bundle exec jekyll serve'. If you change this file, please restart the server process. + +# Site settings +# These are used to personalize your new site. If you look in the HTML files, +# you will see them accessed via {{ site.title }}, {{ site.email }}, and so on. +# You can create any custom variable you would like, and they will be accessible +# in the templates via {{ site.myvariable }}. + +title: Smart Voting Simulator +email: smartvotingsimulator1@gmail.com +description: >- # this means to ignore newlines until "baseurl:" + A simulator for voting methods - so you can understand how voting methods work + for making group decisions. The best voting methods are responsive to all parts + of the group and can even work when voters use strategies. +baseurl: "/ballot" # the subpath of your site, e.g. /blog +url: "https://paretoman.github.io" # the base hostname & protocol for your site, e.g. http://example.com +twitter_username: paretoman1 +github_username: paretoman +author: + name: Paretoman + email: smartvotingsimulator1@gmail.com +minima: + social_links: + github: Paretoman + twitter: Paretoman1 + youtube_channel: UCrwpT8YjxePjH0RxSCwbTlQ + youtube_channel_name: Paretoman +google_analytics: UA-172540609-1 +github: [metadata] # make warning go away # https://github.com/github/pages-gem/issues/399 +thumbnail: "/social/thumbnail.png" + +# # Build settings +markdown: kramdown +# theme: minima +plugins: + - jekyll-feed + +# Exclude from processing. +# The following items will not be processed, by default. Create a custom list +# to override the default setting. +exclude: + - Gemfile + - Gemfile.lock + - node_modules + - vendor/bundle/ + - vendor/cache/ + - vendor/gems/ + - vendor/ruby/ + - dev/ \ No newline at end of file diff --git a/_config_make_easy.yml b/_config_make_easy.yml new file mode 100644 index 00000000..47da3e27 --- /dev/null +++ b/_config_make_easy.yml @@ -0,0 +1 @@ +baseurl: "/" # the subpath of your site, e.g. /blog \ No newline at end of file diff --git a/_data/navigation.yml b/_data/navigation.yml new file mode 100644 index 00000000..69199fee --- /dev/null +++ b/_data/navigation.yml @@ -0,0 +1,32 @@ +- name: Home + link: . +- name: Basics + link: basics +- name: Common Ground + link: commonground +- name: Condorcet + link: condorcet +- name: Approval + link: approval +- name: STAR + link: star +- name: Instant Runoff + link: irv +- name: Single Transferable Vote + link: stv +- name: Proportional Methods + link: proportional +- name: Primaries + link: primaries +- name: Original + link: original +- name: Modify + link: modify +- name: Test Runs + link: testRuns +- name: Blog + link: blog +- name: Links + link: links +- name: Sandbox + link: sandbox/ \ No newline at end of file diff --git a/_includes/analytics.html b/_includes/analytics.html new file mode 100644 index 00000000..abd13d66 --- /dev/null +++ b/_includes/analytics.html @@ -0,0 +1,12 @@ + + {% if jekyll.environment == 'production' and site.google_analytics %} + + + +{% endif %} \ No newline at end of file diff --git a/_includes/banner.html b/_includes/banner.html new file mode 100644 index 00000000..370020d9 --- /dev/null +++ b/_includes/banner.html @@ -0,0 +1,22 @@ + + + {% if page.byline %} + {% assign do_byline = true %} + {% endif %} + {% capture byline_calc %} + {% if page.date %} + {% assign do_byline = true %} + By Paretoman, {{ page.date | date: '%B %d, %Y' }} + {% else %} + {{ page.byline }} + {% endif %} + {% endcapture %} + + \ No newline at end of file diff --git a/_includes/card.html b/_includes/card.html new file mode 100644 index 00000000..8ec4ec11 --- /dev/null +++ b/_includes/card.html @@ -0,0 +1,13 @@ +
+ +
+ +
+ {{ include.title }} + + {{ include.description }} + +
+
+
+
\ No newline at end of file diff --git a/_includes/css-1.html b/_includes/css-1.html new file mode 100644 index 00000000..4a039463 --- /dev/null +++ b/_includes/css-1.html @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/_includes/css-index.html b/_includes/css-index.html new file mode 100644 index 00000000..fc61c04b --- /dev/null +++ b/_includes/css-index.html @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/_includes/css-original.html b/_includes/css-original.html new file mode 100644 index 00000000..ebd9e731 --- /dev/null +++ b/_includes/css-original.html @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/_includes/css-sandbox-original.html b/_includes/css-sandbox-original.html new file mode 100644 index 00000000..d29dd47b --- /dev/null +++ b/_includes/css-sandbox-original.html @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/_includes/edit.html b/_includes/edit.html new file mode 100644 index 00000000..e595f170 --- /dev/null +++ b/_includes/edit.html @@ -0,0 +1,3 @@ + +

Edit this page.

+ \ No newline at end of file diff --git a/_includes/footer.html b/_includes/footer.html new file mode 100644 index 00000000..bd0183fa --- /dev/null +++ b/_includes/footer.html @@ -0,0 +1,37 @@ + diff --git a/_includes/gif-show.html b/_includes/gif-show.html new file mode 100644 index 00000000..9e44873c --- /dev/null +++ b/_includes/gif-show.html @@ -0,0 +1,7 @@ + +
+
Demonstrate
+
+
+ +
\ No newline at end of file diff --git a/_includes/head-1.html b/_includes/head-1.html new file mode 100644 index 00000000..98eff334 --- /dev/null +++ b/_includes/head-1.html @@ -0,0 +1,20 @@ + + + + {% include meta-minima.html %} + {{ page.title }} + {% include meta-1.html %} + {% include meta-viewport.html %} + {% include meta-share.html title=page.title description=page.description twuser=page.twuser %} + + + + + {% include css-index.html %} + {% include css-1.html %} + + {% include js-viewport.html %} + + {% include js-1.html %} + + {% include analytics.html %} \ No newline at end of file diff --git a/_includes/head-original.html b/_includes/head-original.html new file mode 100644 index 00000000..d546b34b --- /dev/null +++ b/_includes/head-original.html @@ -0,0 +1,14 @@ + + + {{ page.title }} + {% include meta-1.html %} + {% include meta-share-original.html title=page.title description=page.description twuser=page.twuser %} + + + + + {% include css-original.html %} + + {% include js-original.html %} + + {% include analytics.html %} \ No newline at end of file diff --git a/_includes/header.html b/_includes/header.html new file mode 100644 index 00000000..2f1e594f --- /dev/null +++ b/_includes/header.html @@ -0,0 +1,46 @@ + diff --git a/_includes/js-1.html b/_includes/js-1.html new file mode 100644 index 00000000..291a5f84 --- /dev/null +++ b/_includes/js-1.html @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_includes/js-model.html b/_includes/js-model.html new file mode 100644 index 00000000..a02115e4 --- /dev/null +++ b/_includes/js-model.html @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_includes/js-original.html b/_includes/js-original.html new file mode 100644 index 00000000..c5468b43 --- /dev/null +++ b/_includes/js-original.html @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_includes/js-viewport.html b/_includes/js-viewport.html new file mode 100644 index 00000000..690b6607 --- /dev/null +++ b/_includes/js-viewport.html @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/_includes/letters.html b/_includes/letters.html new file mode 100644 index 00000000..8931a9e6 --- /dev/null +++ b/_includes/letters.html @@ -0,0 +1,6 @@ +{% capture A %}A{% endcapture %} +{% capture B %}B{% endcapture %} +{% capture C %}C{% endcapture %} +{% capture D %}D{% endcapture %} +{% capture E %}E{% endcapture %} +{% capture F %}F{% endcapture %} diff --git a/_includes/meta-1.html b/_includes/meta-1.html new file mode 100644 index 00000000..7817de5e --- /dev/null +++ b/_includes/meta-1.html @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/_includes/meta-minima.html b/_includes/meta-minima.html new file mode 100644 index 00000000..bf36f877 --- /dev/null +++ b/_includes/meta-minima.html @@ -0,0 +1,6 @@ + + + + {%- seo -%} + + {%- feed_meta -%} \ No newline at end of file diff --git a/_includes/meta-share-original.html b/_includes/meta-share-original.html new file mode 100644 index 00000000..e36b8b2e --- /dev/null +++ b/_includes/meta-share-original.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/_includes/meta-share.html b/_includes/meta-share.html new file mode 100644 index 00000000..ba6322ab --- /dev/null +++ b/_includes/meta-share.html @@ -0,0 +1,17 @@ + + + + + + + + + {% if page.twuser %}{% endif %} + + + {% if page.twuser %}{% endif %} + + + + + \ No newline at end of file diff --git a/_includes/meta-viewport.html b/_includes/meta-viewport.html new file mode 100644 index 00000000..875e7100 --- /dev/null +++ b/_includes/meta-viewport.html @@ -0,0 +1,6 @@ + + {% if page.min-width %} + + {%- else -%} + + {%- endif -%} \ No newline at end of file diff --git a/_includes/navigation-2.html b/_includes/navigation-2.html new file mode 100644 index 00000000..3337f1f8 --- /dev/null +++ b/_includes/navigation-2.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/_includes/navigation.html b/_includes/navigation.html new file mode 100644 index 00000000..917d3e5f --- /dev/null +++ b/_includes/navigation.html @@ -0,0 +1,58 @@ + +
+
+ +
+ + +
\ No newline at end of file diff --git a/_includes/sandbox-call-index-original.html b/_includes/sandbox-call-index-original.html new file mode 100644 index 00000000..1bf008d8 --- /dev/null +++ b/_includes/sandbox-call-index-original.html @@ -0,0 +1,7 @@ + +
+
+ \ No newline at end of file diff --git a/_includes/sandbox-call-index.html b/_includes/sandbox-call-index.html new file mode 100644 index 00000000..3dab520b --- /dev/null +++ b/_includes/sandbox-call-index.html @@ -0,0 +1,6 @@ + +
+
+ \ No newline at end of file diff --git a/_includes/sandbox-call-original.html b/_includes/sandbox-call-original.html new file mode 100644 index 00000000..b066c0e9 --- /dev/null +++ b/_includes/sandbox-call-original.html @@ -0,0 +1,6 @@ + +
+
+ \ No newline at end of file diff --git a/_includes/sandbox-call.html b/_includes/sandbox-call.html new file mode 100644 index 00000000..a9cb70f6 --- /dev/null +++ b/_includes/sandbox-call.html @@ -0,0 +1,6 @@ + +
+
+ \ No newline at end of file diff --git a/_includes/sandbox-demonstrate.html b/_includes/sandbox-demonstrate.html new file mode 100644 index 00000000..885bb065 --- /dev/null +++ b/_includes/sandbox-demonstrate.html @@ -0,0 +1,7 @@ + +
+
Demonstrate
+
+
+ +
\ No newline at end of file diff --git a/_includes/sandbox-embed.html b/_includes/sandbox-embed.html new file mode 100644 index 00000000..4837883c --- /dev/null +++ b/_includes/sandbox-embed.html @@ -0,0 +1,8 @@ + +
+
+
+ {% include sandbox-call.html %} +
+
+
\ No newline at end of file diff --git a/_includes/sandbox-index-original.html b/_includes/sandbox-index-original.html new file mode 100644 index 00000000..c13cab77 --- /dev/null +++ b/_includes/sandbox-index-original.html @@ -0,0 +1,50 @@ + +
+ {% include sandbox-call-index-original.html %} +
+
+
+ +

+ This is the "Sandbox Mode" for To Build A Better Ballot. + If you haven't already played it, check it out here! +

+

+ Make your own voting simulation model with this tool! Here's a few that others have made: +

+

+

+ If you'd like your own model included here, + save it, copy the saved link, and tweet it while mentioning me, @ncasenmare! + And then!... maaaaaybe I'll see it and include it in the list. + I dunno, I'm really bad at checking my Twitter, which, + honestly, is probably for the best. +

+
+
\ No newline at end of file diff --git a/_includes/sandbox-index.html b/_includes/sandbox-index.html new file mode 100644 index 00000000..65256854 --- /dev/null +++ b/_includes/sandbox-index.html @@ -0,0 +1,18 @@ + +
+
+
+ {% include sandbox-call-index.html %} +
+
+ {% include sandbox-demonstrate.html %} +
+

+ This is the "Sandbox Mode" for the Smart Voting Simulator, which is based on To Build A Better Ballot. + If you haven't already played the original, check it out here! + And then check out the expanded version. +

+ {% include sandbox-note.html %} +
+
+ \ No newline at end of file diff --git a/_includes/sandbox-min.html b/_includes/sandbox-min.html new file mode 100644 index 00000000..a0ee210c --- /dev/null +++ b/_includes/sandbox-min.html @@ -0,0 +1,4 @@ + +
+ {% include sandbox-call.html %} +
\ No newline at end of file diff --git a/_includes/sandbox-note.html b/_includes/sandbox-note.html new file mode 100644 index 00000000..02c405fa --- /dev/null +++ b/_includes/sandbox-note.html @@ -0,0 +1,11 @@ + +

+ From Nicky Case: One hope for Sandbox Mode is that readers can debate with me and each other + using this tool! Not just telling me I'm wrong, but + showing me I'm wrong. Granted, this tool is very limited – it doesn't handle strategic voting + or imperfect information – but I think it's a start, and may help improve our Democratic Discourse™ +

+

+ From Paretoman: if you'd like your own models included here, + save it, copy the saved link, and tweet it with the hashtag #smartvotesim. +

\ No newline at end of file diff --git a/_includes/sandbox-original.html b/_includes/sandbox-original.html new file mode 100644 index 00000000..29a3d583 --- /dev/null +++ b/_includes/sandbox-original.html @@ -0,0 +1,21 @@ + +
+
+
+

SANDBOX MODE! (link to just this)

+ {% include sandbox-call-original.html %} +
+
+
+

+ One hope for Sandbox Mode is that readers can debate with me and each other using this tool! + Not just telling me I'm wrong, but showing me I'm wrong. + For example – + + here's a model I made in Sandbox Mode, + showing an interesting argument against Approval & Score Voting. + Granted, this tool is very limited – it doesn't handle strategic voting or imperfect information – + but I think it's a start, and may help improve our Democratic Discourse™ +

+
+
\ No newline at end of file diff --git a/_includes/sandbox.html b/_includes/sandbox.html new file mode 100644 index 00000000..58272af6 --- /dev/null +++ b/_includes/sandbox.html @@ -0,0 +1,13 @@ + +
+
+
+

SANDBOX MODE! (link to just this)

+ {% include sandbox-call.html %} +
+
+ {% include sandbox-demonstrate.html %} +
+ {% include sandbox-note.html %} +
+
\ No newline at end of file diff --git a/_includes/share.html b/_includes/share.html new file mode 100644 index 00000000..759cf6cc --- /dev/null +++ b/_includes/share.html @@ -0,0 +1,33 @@ + + {% assign share_url = page.url | replace:'index.html','' | prepend: site.baseurl | prepend: site.url | uri_escape -%} + {% assign share_text = page.banner | default:page.title | default:"" | uri_escape %} +
+ Share this: + +
\ No newline at end of file diff --git a/_includes/sim-ballot.html b/_includes/sim-ballot.html new file mode 100644 index 00000000..5ce9d23f --- /dev/null +++ b/_includes/sim-ballot.html @@ -0,0 +1,17 @@ + +
+
+

{{ include.title }}

+

{{ include.caption | replace: "`", ""}}

+ {% if include.url %} + + {% endif %} + {% if include.link %} + + {% endif %} +
+
\ No newline at end of file diff --git a/_includes/sim-intro.html b/_includes/sim-intro.html new file mode 100644 index 00000000..b01d6940 --- /dev/null +++ b/_includes/sim-intro.html @@ -0,0 +1,16 @@ + +
+
+

+ {{ include.caption | replace: "`", "" }} +

+
+
+ + {% if include.gif %} + {% include gif-show.html %} + {% endif %} +
+ +
\ No newline at end of file diff --git a/_includes/sim-newer-ballot.html b/_includes/sim-newer-ballot.html new file mode 100644 index 00000000..ea18f6d6 --- /dev/null +++ b/_includes/sim-newer-ballot.html @@ -0,0 +1,12 @@ + +
+
+

{{ include.title }}

+

{{ include.caption | replace: "`", ""}}

+
+
+ +
+
\ No newline at end of file diff --git a/_includes/sim-test.html b/_includes/sim-test.html new file mode 100644 index 00000000..2f768ccf --- /dev/null +++ b/_includes/sim-test.html @@ -0,0 +1,14 @@ + +
+
+

+

{{ include.title }}

+

{{ include.caption | replace: "`", "" }}

+

+
+
+ +
+
\ No newline at end of file diff --git a/_includes/sim.html b/_includes/sim.html new file mode 100644 index 00000000..27fb97af --- /dev/null +++ b/_includes/sim.html @@ -0,0 +1,25 @@ + +
+
+

{{ include.title }}

+

{{ include.caption | replace: "`", "" }} + {% if include.hint %} + Hint + {% endif %} +

+ {% if include.url %} + + {% endif %} + {% if include.link %} + + {% endif %} + {% if include.gif %} + {% include gif-show.html %} + {% endif %} +
+ +
\ No newline at end of file diff --git a/_includes/social.html b/_includes/social.html new file mode 100644 index 00000000..1334fc0c --- /dev/null +++ b/_includes/social.html @@ -0,0 +1,21 @@ +{%- assign social = site.minima.social_links -%} + + diff --git a/_includes/str.html b/_includes/str.html new file mode 100644 index 00000000..22846aea --- /dev/null +++ b/_includes/str.html @@ -0,0 +1 @@ +{{ include.str | replace: "`", ""}} \ No newline at end of file diff --git a/_includes/video.html b/_includes/video.html new file mode 100644 index 00000000..c1ad1185 --- /dev/null +++ b/_includes/video.html @@ -0,0 +1,4 @@ + +
+ +
\ No newline at end of file diff --git a/_layouts/default-1.html b/_layouts/default-1.html new file mode 100644 index 00000000..d9034e2c --- /dev/null +++ b/_layouts/default-1.html @@ -0,0 +1,23 @@ +--- +permalink: pretty +--- + + + + + + {% include head-1.html %} + + + + + + {%- include header.html -%} + + {{ content }} + + {%- include footer.html -%} + + + + \ No newline at end of file diff --git a/_layouts/default-original.html b/_layouts/default-original.html new file mode 100644 index 00000000..e5a3b107 --- /dev/null +++ b/_layouts/default-original.html @@ -0,0 +1,20 @@ + + + + + + {% include head-original.html %} + + + + + + + {%- include header.html -%} + + {{ content }} + + {%- include footer.html -%} + + + \ No newline at end of file diff --git a/_layouts/embed.html b/_layouts/embed.html new file mode 100644 index 00000000..72caff10 --- /dev/null +++ b/_layouts/embed.html @@ -0,0 +1,28 @@ + + + + + + + + {{ page.title }} + {% include meta-1.html %} + {% include meta-share.html title=page.title description=page.description twuser=page.twuser %} + + + + {% include css-1.html %} + {% include css-index.html %} + + + {% include js-1.html %} + + + + +
+ {{ content }} +
+ + + \ No newline at end of file diff --git a/_layouts/page-2.html b/_layouts/page-2.html new file mode 100644 index 00000000..fb4e07ca --- /dev/null +++ b/_layouts/page-2.html @@ -0,0 +1,10 @@ +--- +layout: default-1 +--- + {% include banner.html %} +
+
+ {{ content }} +
+ {% include edit.html %} +
\ No newline at end of file diff --git a/_layouts/page-3.html b/_layouts/page-3.html new file mode 100644 index 00000000..666fd2ac --- /dev/null +++ b/_layouts/page-3.html @@ -0,0 +1,13 @@ +--- +layout: default-1 +--- +{% include banner.html %} +
+ +
+ {{ content }} +
+ {% include edit.html %} + {% include sandbox.html %} + +
\ No newline at end of file diff --git a/_layouts/page-4.html b/_layouts/page-4.html new file mode 100644 index 00000000..6b11eb4a --- /dev/null +++ b/_layouts/page-4.html @@ -0,0 +1,6 @@ +--- +layout: default-1 +--- +
+ {{ content }} +
\ No newline at end of file diff --git a/_layouts/page-6.html b/_layouts/page-6.html new file mode 100644 index 00000000..8b8754da --- /dev/null +++ b/_layouts/page-6.html @@ -0,0 +1,12 @@ +--- +layout: default-1 +--- + +{% include banner.html %} +
+ + {{ content }} + + {% include sandbox.html %} + +
\ No newline at end of file diff --git a/_layouts/page-7.html b/_layouts/page-7.html new file mode 100644 index 00000000..b746aa4c --- /dev/null +++ b/_layouts/page-7.html @@ -0,0 +1,9 @@ +--- +layout: default-1 +--- + {% include banner.html %} +
+
+ {{ content }} +
+
\ No newline at end of file diff --git a/_posts/2020-06-07-added-jekyll.md b/_posts/2020-06-07-added-jekyll.md new file mode 100644 index 00000000..9b3f37e6 --- /dev/null +++ b/_posts/2020-06-07-added-jekyll.md @@ -0,0 +1,22 @@ +--- +layout: page-2 +--- + +[Jekyll](https://jekyllrb.com/) is now helping to organize this site. Check the [docs](https://jekyllrb.com/docs/). Here’s [install](https://jekyllrb.com/docs/installation/windows/) instructions for windows. + +Jekyll even worked inside folders like /sandbox/ + +To add a new page, just create a new markdown file and jekyll converts the markdown to html and puts it where you have {{ content }} in your layout.  This doesn't require any kind of configuration.  It's simple. + +Okay, now comes the hard part… how do I actually get special divs like word divs and sim divs without showing them in markdown? Includes, I guess. + +Jekyll is good because it basically adds subroutines and variables to html and it translates markdown into html. + +You probably could find another tool other than jekyll that uses the liquid syntax.  + +Now I need to know Ruby. I guess the only reason I need a Gemfile is to make sure I have the correct version of jekyll and the jekyll github plugin so that my local server will exactly show what github shows. + +*Links* +[github pages on jekyll's website](https://jekyllrb.com/docs/github-pages/). The jekyll documentation is very good. + +  diff --git a/approval.md b/approval.md new file mode 100644 index 00000000..87efc5ab --- /dev/null +++ b/approval.md @@ -0,0 +1,166 @@ +--- +permalink: /approval/ +layout: page-3 +title: Approval Voting +banner: Practical Approval Voting +description: An Interactive Guide +twuser: paretoman1 +byline: By Paretoman, Feb 2018 +--- +{% include letters.html %} + +Hi, did you vote? Do you think our elections choose the best leaders? Even if they're not the best, the people chose them, right? It's important that we have the power to choose our representatives. That's why I'm writing to you about approval voting. + +Approval voting gives a better representative. When you compare approval to what we do now, approval gets better information from the voters, gets better candidates to run, and has less problems. + +## Better Information from Voters + +Approval gets better information from the voters because approval asks you to choose the better candidates. That can mean more than one. **Try it out below.** Move the voter around the arena. See what candidates he chooses. (There are 3 candidates: {{ A }}, {{ B }}, and {{ C }}.) See that he's choosing the candidate he's closest to. He's also avoiding the candidate he's furthest from. And for the in-between, he draws a line. Anybody inside is better than anybody outside. This is a relative judgement. + +{% include sim.html +title = "Approval Voting Basics" +caption = "Pick the Better Candidates" +id = "election16" +gif = "gif/election16.gif" +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3WSQW7EMAhF7-K1VQUbDJOrRDnJqD17wU-RKlWjLABjP_-P825HO6_LVxf1u19ikplmNqQyu-_epLZIeJeIqmc7j960ne1HjtabtVN6W7krm57h6P--7MTHzutjJ_l1lwhwGZQoECUYYW0ZkgJEM-Z1M8NrN0dycnHI3jMGAcxQKtsHxmLRqYIqKSONH1uo1AwgTUgT0oQ0k3Tl9Ni4OApvYlX6yJYWrvZo2Su7Op4kgdesRPc5_YtUnKrzDkjU1140BmZCQJ4hz5iX8V6GUfOtzxiXYXQdBEwupr6gLNsWStwCsVCwGLWjwDnrY5N8UqHAeTF_fhmnGc9s-uxa61gKgAEwEBOICYBh-5ZAT8ALZEXJ-rL8y4rz_QuHr_l18gIAAA)" +%} + +In an election, the voter also needs to decide one more thing: what is he risking? Things do get a little more complicated whenever you add more people to a situation. The above was a simple model, and we just set it so that the line is drawn halfway between the best and worst candidates. In a real election, the voter will be checking the polls to see where he should draw the line. He's going to see who has a shot to win, and he's going to draw the line between those in particular because the difference the voter makes can decide who wins the election. **Try it out below.** See that when {{ A }} isn't doing well, {{ C }} voters don't need to use {{ B }} as a fallback. + +{% include sim.html +title = "Consider Risk" +caption = "Decide Which Frontrunner is Better." +id = "election17_frontrunner_status" +gif = "gif/election17_frontrunner_status.gif" +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3WSXW7EIAyE78IzqjDYGHKVKCdZtWevzaeVKlWrPPiXYWbIq7Ry3bfPKupPvcUkMo2sS2b2PLVIrsj2KntnPcrVatFylR9ppRYrl9QyYyuGHqHVf19M1sfJ_jgJ_LxLkoKMbHQacBAlGGEeIhIURCPGhSPCPsMeSNHscnZ6JwDTlcrOgT5pOtWiCpQe0tMuqT1dAGl0BiANkIadtfhycbIB3kAsGHrgMkmBKVj7OwnAOxWrnnP6F1JRqs5LQFH3aRqWmRAQatAz_DJezBBqfvgZdhlCZyMgcuL6BGXakZDkJhATBhOrHQbOWccgH1QwcF7M3z-NM1xvb-qomn0krUaQg7QAXJBZAC6jCZ8F3oLWSlpfFv9ZEvv-BUf8SXr0AgAA)" +%} + +Approval asks for this judgement from every voter. Then the votes are added and the most votes wins. That's the group judgement. **Try it out below.** Move the voter group to see which candidate wins. See that when the center moves toward a candidate, that candidate wins. + +{% include sim.html +title = "Approval Election" +caption = "" +id = "election18" +gif = "gif/election18_approval_poll_frontrunner.gif" +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3WSW2rEMAxF9-JvUyJZD0-2ErKSoV17ZR8ChTLkQ5JlH19d592Odl5XRhfLu1_iUplVprIyv-_eZG0R966Sqx7tPHqzdrYfOVpv3k7pLWpXNbPC0f991ZkfO6-PneKvu0SAi1KiQIzghCCUALGKdd2o8NqrWpxaVNlSVQlg1Kh8H1AwmlSTqihagx9bqCwPZDeG0oA0II0iXeUeG4Md8AajStdq2catRJ5En6SA11iJ7XP2F2mxr7HkHZBor73oGObIcwZ15Dl-Oe_lDOq59Tl2OYPGQcCuwPWAEr5HWOICRKAgsDpRkJxNDMpBhYLkxfL5ZZLmfLzpo9taZ6QJcMomTYATMRPgdBbRM-FNZM0l68vrL1uc719_EIbQ8gIAAA)" +%} + +Compare approval to the way we vote now. Right now, you can only choose one candidate when you go to vote, and that limits you. You can only say which candidate you like best. **Try it out below.** See that your opinion is limited to one candidate. + +{% include sim.html +title = "Choose One" +caption = "(the way we vote now)" +id = "election19" +gif = "gif/election19_pick_one.gif" +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3WSUW4FIQhF9-K3aURBmNnK5K2kadde8OQlTZpmPhDB4704n220-3l8d1F_9UfUcjVzNUVyZa9Xb1ItUj3Xqny1e_Sm7W7fo_Vm7ZbedjZlzTOM_ufLSvxbuf6tyDhXiQCXSYoAUYIR9pEhKUA0Y163MlynOJOTm1NOz5wEMFPJ7ByYm00nC7KkzPQ9jlCpEUBaCFqLOqSVpCeHR-OmBG9hVXpOuGnhqkfLXtnV-V4k8Fm10HNOfyMVp-o8AxL1OpvGwEwIGDWMGvMy3sswan70GeMyjO5BwOTG5Iay7VgocRvERsFm1I4C56zPQ_JFhgLnxfz9yzjFeM-mr661j6UAGAADMYGYABh2bgn0BLxAVpSsD8u_rDhfPzoOPd_xAgAA)" +%} + +And you also need to decide one more thing: what are you risking? If your favorite doesn't win, then the worst candidate might win. In a real election, you're going to look at the two frontrunners and pick one. **Try it out below.** See that you only get two choices. + +{% include sim.html +title = "Risky Choose One" +caption = "(the Real way we vote now)" +id = "election20" +gif = "gif/election20_risky_choose_one.gif" +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3WSWWoEMQxE7-JvEyxZ8tJXaeYkQ3L2SH40BMLQH6XN5Sq536WV677nqGLzVW8xj0gjUpGI_PWqRXJE1qqyR-a9XK0WK1f5aaUWL5fUMmIoejOg1X9fdNbHzv7YkXauklQgPQtKAQligAPjCJGQIBYYF_aAfZoaTFFUOTOqADRqZH4O6KA4yRZZsGg4b0eq5hJg6kqjAzB1T701vhwctODrmJXDYUmXM5YG07DpEwThnY7Nzjn7S2k4tclDINH2KTorcwGQ58hz5Dkv5hj1SY91OUZHAzA52PpgXcOPhRQ3oBgoGKx6omBydqJgdjJebPJi8_lpJs317Kb2alnH0mqAHKYF4ULMgnA5RfQs-BayVsr68vjPELafZe9ctmfQT_D9C18ZfaYOAwAA)" +%} + +In an election, every voter faces this risk. And the only thing they can do is pick from two. **Try it out below.** See that the winner isn't always the candidate closest to center. + +{% include sim.html +title = "A Risky Choose One Election" +caption = "(Our General Election)" +id = "election21" +gif = "gif/election21_risky_choose_one.gif" +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3WSUW4FIQhF9-K3aURAnNnKy1tJ0669yMkkTZpmPi6CHi84n220-_WK1cXi3V9impFlNEUy8ve7NzlbRHeXqLW2e_Rm7W7fo_Xm7ZbeVm7KWqSM_ufLyv63cv1bkVFXyXEgehKTBBbEEEcWkhbEUvNCTbkqO5OUySlldiZmpoCZRtLrwAQzg9VmddUBHWV1niFAUkiqCCT147fndzYuSvCUZqUYVrgTyBPMJ0jg63RsVufsN9JWXWPBQ2DRrko6I3NBJkkadeblvJjTqEf5c8blNLoGQpOLqS8oy6uFY26BWDhYjDpwEJwNBhTKCgfBi8Xz0wTF_cyma7eTp6U9ECnSBrgxswFuJ4mfDW9jax9bH57_2TH29QPStld28wIAAA)" +%} + +**To recap:** + +- Approval chooses a better winner. + - Better information is collected from the voters. + - It asks for the better candidates (plural). + - It asks for a risk assessment. + - A group judgement is formed and the candidate nearest the center wins. + - Compare to the way we vote now: + - It asks for one candidate. + - It asks for a risk assessment which leads to a bad choice between two. + - A group judgement is formed but the center doesn't always win. + +Here's a sandbox where you can try changing the number of candidates, the number of voters, and the election method. + +{% include sim.html +title = "Sandbox so far" +caption = "" +id = "election22_sandbox" +gif = "gif/election22_sandbox.gif" +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3WSQW7EIAwA_8IZVRhjTPYrUX6w6q2nqvv2Go8iVapWOdhgMxocvksrj_P0WWX4VU8ZGtmIrItEZtdVi-wW0VXFc63l0WoZ5VFe0kotlusZXVH0CK3--6Ky3laOt5Xgb7ZIdmgtr8-v57NU6VW26W7ptGAlg4CTTEJIyYgYCsGQI3d7sGOzB1siBKZHANMHm5YHOpjurBarIw9oS7W-5yJZUEgKSRHSIJ0x0Sq7cdIBT7m-JGMkbidyJ_1OAnjqTkaeG3-RY6bwcP4NiuPITWOIhp6hZ0pAz5iXcVFzaozLuOhsBMY1mfrkktP4PyEyQUwMJqN2DJyzjoErKwwcA7-fkVPcj6bHY9h3dK6zgC1JygK2EFnAlrGJy4K1UFpb6cPi1cE57kEfe9C2E83k5xfvl6cSHQMAAA)" +%} + +(By the way, there are a couple different names for our election method that we use now: first-past-the-post (FPTP), single-member plurality (SMP), or just plurality voting. First-past-the-post is a term that has been used for around 100 years. It comes from horseracing and it means the first horse to cross the finish line, even if the horse had been disqualified. It was a negative term used by people that wanted to improve the way we vote because they saw that the wrong winner was being selected. Single-member means you pick one winner as opposed to multiple representatives. Plurality means the highest count of first choices wins.) + +(Also, when we say election method what we mean is what information we write on our ballots about what we want and how we combine that information into what the group wants.) + +## Map + +We're now going to take a step up the ladder of abstraction from one election to many possible elections. If you've played around with the sandbox for a while, you might start putting together a map inside your head of where the voters have to go in order for a candidate to win. Each candidate has his own territory. I added this to the election by putting the voter center at every pixel on the map and seeing who wins. Then I colored the pixel with the winner's color. **Try it out.** See that whenever you move the center voter to the {{ C }} territory, the {{ C }} candidate is the winner. + +{% include sim.html +title = "Map of Winner" +caption = "" +id = "election23" +gif = "gif/election23_map_of_winner.gif" +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3WSW2rEMAxF9-JvUyJbspRsJcxKhnbtlXUIFMqQD72s4ys573a06759dVF_9Vt0pqfpDZH07PXqTfYRmdHFK57tOnrTdrUfOVpv1i7pbeWpLHqao__7shIfK-fHSvL3XSLAZRDOulGUyDALkwJE0-Z1M81Z2ZGcTA6pxpGYkYZBhpK0ahhghhMF0VkN8yihY-9AqjAhTUgT0kzSndvrsg8uTsCbjCrF0MJtRx5nPE4C77kdrT79i9RV16jzDkjUs5LGwgx5Nkgiz9iX8V4Gxbz0WRAx6DowrGux9QVlWY2wxS12tVCwWLWjwOl1FuS8mKPAeTF_fhmnGM9u-uy684wUAEOKFAADMQEwjCR6Al4gK7asL8u_bHO-fwFyRTIr8gIAAA)" +%} + +## Better Candidates + +In this next part, I'll describe how approval voting gives an opportunity to candidates to run for office. We'll change perspective to consider what a candidate sees. + +A candidate will win if he moves to the center. A candidate thinking about running for office would think about what his campaign message is. He would consider where that would put him on the political spectrum (left to right). If everything looks good, he would start his campaign. **Try to see what a candidate sees, below.** This is a map of every place {{ B }} could put his campaign message, and who would win if he entered the race. Move him toward the center of the voters and see that he wins. This example works for every candidate, so all the candidates will want to be at the center and try to be as broadly representative of the entire population. This is good because it serves the purpose of an election that the people have the power to choose their representatives. + +{% include sim.html +title = "The Winners' Circle" +caption = "" +id = "election24" +gif = "gif/election24_winners_circle.gif" +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3WSW2rEMAxF9-JvUSxZspXZSpiVlHbtlX0IFEoZhms9cnIl57P19rrvlaK53nK7S3ipaYp1f7-l6W7Q6FL_HY_26tK8vdq39iYt2kulzeqq4irp8udXlfy3cv1bKf5-lypwNcJyMEqcKJCJlAH10qTnOlkrTiVNj1UzhEHMieI8YGBsESURlNGPUd07UDJFshJIA9Io0q2iNE46FsKoKlYlP7h90Odgz6GA99gHPz78N9LneY0v7gGLfp1ksLDAXjBoYC_YV3BfASUwFolcR2ZHWNdk6xPKjDPCNjfZ1cTBZEkLB4tnFwtagwgHixtbzyezKOazGxniO89ICTD1kBJgYiYBZpDET8JLbOW29RH1lW3O1w83jKme8AIAAA)" +%} + +Compare that to what we have now at the general election and you'll see that some nice candidates can't run. Right now, once the two front-running candidates are settled on, there isn't much opportunity for another candidate to be running against them. **Try it out below.** Move {{ B }} to the center and see that voters are still stuck on the frontrunners. I have set this example so that about a quarter of voters who like {{ B }} best will actually vote for him as "true believers". See that this causes {{ B }} to become a spoiler. When {{ B }} moves toward {{ C }}, {{ C }} loses because {{ B }} takes votes away from {{ C }}. This is the spoiler effect. If {{ B }} was nice, he would drop out to help {{ C }} win because they're on the same side. {{ B }} is a jerk, so he stays in and spoils the election for {{ C }}. + +{% include sim.html +title = "The Spoiler Effect" +caption = "" +id = "election25" +gif = "gif/election25_spoiler.gif" +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3WSW2rtMAxF5-JvUyxZtpQzlXBGUtqxV9YiUO6l5GPr4axsyflso73u21eXsHe_dVuXoSeSK2vj_e5NzhGxzN1OPttr9Gbt1b5H6221l_S281D2PKWptz7-fbIZ2fyvXp3rz46M-poIfFHS9DBTjGwhG0kPYqnBmauqmpwsqpRbTYymMIoaxVUvKJgcorIggzJHGdWzBUgT0oQ0Ic0k3dLzOQc3J5wWo0oxrHAnkCfQJ0jgPU9g5cN-I20Xy5ybwKJdVVwsbAmiFLG32NfiyhaU5eVvBXKV7IEw5GbrG8peNcIxt9nVxsFmSY4D511nQT7JcODcmD9_DbvxeHbTZ7dTZ6QAGFKkABiYCYABMPAT8AJbcWx9rPzLDufrB89U4yz0AgAA)" +%} + +Nice candidates dropping out is very common in the primaries. With the way we vote now, primaries are necessary to prevent the vote-splitting we see with {{ B }} and {{ C }}. The important choices are made in the primary, and in the general, we are left with only two. A primary will pick the candidates in a complicated kind of way involving considerations of electability. **Try it out below.** {{ E }} wins the primary vote because primary voters saw that {{ C }} would lose the general election against the more moderate {{ A }}, and {{ B }} and {{ D }} split their vote. For more explanation, see the [Page on Primaries](primaries). + +{% include sim.html +title = "Primary Vote-Splitting" +caption = "" +id = "primary_sim" +gif = "gif/primary.gif" +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3WSW24DIQxF98I3qrCxwcxWRllJ1K69hqMqUqpoPowfHF-beZZWrvuOUcXbo94yvYpFnlRXxnTHVsZEzinrVn88apF9bUgW-y7ucooz0cvVarFylR8ptXi5pJaR1ZmbaVr992UmPmbWx4y000oEuCguAsQwjhmYFCCWNtt5mnXEaXIyqIKXGE0DRo1gYnoaMDrxAm-dC70dobpXAKkjqEPqkHqS7lwW3y4eXIfZGVeq1l4t0wZ2z2vyOurrmPhbT1-zw7D3FjZOa5s8DbKN4Z0lumCUIJKdHTpv6Azv8-j1OL2c4UfDMPhg8AFl-NHaU8gAMVAw1jETBZO7k_XPjoeCySvOv99okoy3XU1GCoDBSIGYQEwADICBnoAXyIot6yt_j7U5379wAhm1IgMAAA)" +%} + +**Final Recap:** + +- Approval chooses a better winner. + - Better information is collected from the voters. + - It asks for the better candidates (plural). + - It asks for a risk assessment. + - A group judgement is formed and the candidate nearest the center wins. + - Compare to the way we vote now: + - It asks for one candidate. + - It asks for a risk assessment which leads to a bad choice between two. + - A group judgement is formed but the center doesn't always win. +- There are better candidates running. + - The incentive is for candidates to move to the center. + - Nice candidates are available that otherwise are staying out of the race because they'll cause a spoiler effect. + - There is no need for a primary. + +Putting it all together, here's a sandbox for you to try out all the different systems and to make your own scenarios: \ No newline at end of file diff --git a/assets/minima-social-icons.svg b/assets/minima-social-icons.svg new file mode 100644 index 00000000..ff02f3ef --- /dev/null +++ b/assets/minima-social-icons.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/basics.md b/basics.md new file mode 100644 index 00000000..24f8668b --- /dev/null +++ b/basics.md @@ -0,0 +1,41 @@ +--- +permalink: /basics/ +layout: page-2 +title: The Basics +description: An Explorable Guide to Group Decision Making +byline: 'By Paretoman and Contributors, May 2020' +twuser: paretoman1 +--- +{% include letters.html %} + +Take a look at this great interactive sketch that Nicky Case made. You have two candidates {{ A }} {{ B }} and you have a voter . Just move the voter and you'll see that he is just gonna vote for whoever he's closest to. + +{% include sim-intro.html +id='model1' +caption='`click & drag
the candidates and the voter:
`' +gif='gif/model1.gif' +%} + +You can add more voters and see that you kinda get a sense of how voters are gonna vote as they move around and how an election with those voters would go. Most votes wins. + +{% include sim-intro.html +id='model2' +caption='`drag the candidates & voters around.
(to move voters, drag the middle of the crowd)
watch how that changes the election:
`' +gif="gif/model2.gif" +%} + +In this kind of voting, you get a single choice for who you want to win. The ballot is pretty simple. This is the way the ballot is now. + +{% include sim.html +title='Single Choice Voting' +caption='Also called First Past the Post.' +id='ballotSingle' +link='[link](http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRQWrEMAz8i84-RLZkJzn3AT30FnLYdlMaCNllN0tZSvv2Sp4WCiUEIsmSxjPjD2qoH4aSA0sZw8DKlollkT3TcQzEPsKlDdy2Xifqm0BS_0o9B8o20YR_n82W3U672-l2O9zUS9kZeRlRghCDEStCrszYCLBYtOuSha42o-HYYeQ6EyMCYKKg0roQMw4LqhaVoUTzoalE2S0BUgJSAlICUjKkwczEYMYq8BKkcojWEofzGXF5Llfib2KAQ_JE6p78hRQolYJnAUXp6qHCMGUE0FPQ058XxBMqhGqp_BR2KYTmBgEiM1zPQMlaJTi5DIgMBhlWFzAo2C2xIj0fluW0Pd3PE_X0uNwuh2Xe7hTo-nZ6f5iuL5f5vM2n1bpft_U4vc7rdKTPbxSiHLmuAgAA)' +gif="gif/ballotSingle.gif" +%} + +That's the end of **The Basics**. + +Why do we vote this way? Why do we even vote? In the next part, I'm going to explain voting as a way to find common ground as a group. + +{% include card.html title='Finding Common Ground' description=' - Click here next.' url='commonground' img='img/venn-diagram.png' %} \ No newline at end of file diff --git a/bees.html b/bees.html new file mode 100644 index 00000000..c64d0e63 --- /dev/null +++ b/bees.html @@ -0,0 +1,41 @@ +--- +permalink: /bees/ +layout: page-6 +title: Bees +banner: To Build a Better Ballot with Bees +description: An Interactive Guide to Alternative Voting Methods +byline: by Paretoman and Contributors, March 2019 +twuser: paretoman1 +--- + +
+ +

+ What if you had bees? +

+ +

+ Bees! +

+ +

+ Bees are cool because they vote and move. +

+ +
+ +
+
+

+ Bee Candidates +

+
+
+ +
+
+
+

Putting it all together, here's a sandbox for you to try out all the different systems and to make your own scenarios:

+
diff --git a/blog.html b/blog.html new file mode 100644 index 00000000..78ef6218 --- /dev/null +++ b/blog.html @@ -0,0 +1,22 @@ +--- +permalink: /blog/ +layout: page-7 +title: Blog +--- + + diff --git a/commonground.md b/commonground.md new file mode 100644 index 00000000..49f39cc3 --- /dev/null +++ b/commonground.md @@ -0,0 +1,310 @@ +--- +permalink: /commonground/ +layout: page-3 +title: Finding Common Ground +description: An Explorable Guide to Group Decision Making +byline: 'By Paretoman and Contributors, June 2020' +twuser: paretoman1 +--- +{% include letters.html %} + +Hello, I'm going to show you how to find common ground as a group. We'll go over the following: + +* what we mean by common ground and the middle, +* how we can find the middle by using votes and simple comparisons, +* the simplest voting method that finds the middle, +* what a median is, and +* how to bring groups together. + +Let's jump right in with an example and some diagrams. + +## The Middle + +Let's start simple for this first example. Let’s say we have to decide as a group what to have for dinner. Each person is going to have their own opinion. For example, some people are vegetarian and some people eat meat. Also some people like to spend a lot of money and some would rather spend a little. Let’s make a diagram to show their opinions. In the diagram below, we show these types of opinion on different axes. + +{% include sim.html +link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSu24CMRD8lZNrF971E0qK1CREaQ4KJzrBhQsXXaBAUfLtWXtAioSQi9n1rsezY38ro-ZtyzbpWdjollzUHEiiwJqNlYBtvESBNBHXLinGzUYrKqfJWU3OlNyqudHKqbn6NUorX9MgTVKLAkbfLKmku5XZ3QqZyk0EcmKkEEAOgPspAEQAOUG5LggIN2nFwiObLDwswADQsEOL0FgB0HBElpDN6gFrqlAqFlAtWAiyFnUIssLUiovXVdoDGsBqMTBp1lY77aXBVeoS0DXgayDkLZfA1dPult6FOoKLeBiIdhjdGwCV51qrp_Gch-Ypv6_Xh8Vp2nZTs8jTQbLVscv7ZrXLb3vJXrrttu-al34YsqTLqf8Ym8XiUeLnXdc8jKepWXX5azx8rVX5BPDUwwkPT70HwFMPeT7VMTw8DQZAtTPAz4CXCf5iQQBBwGBhViHib0ScjAywALxExNeI178ZUUz_7S_7cCqBMIEwQUqClOSqzOQB0JPAlyDrNQ_DeHw-f3bi9HI4TXnoj2f18wcgESI7fgMAAA)" +title='Where to go eat?' +caption='Horizontal Meat-Veggie axis. Vertical $-$$$ axis' +comment='show a group of people preferring many options, restaurants named and sorted by money on one axis and eco-friendliness on the other.  Meat Shack. Burger Barn. Veggie Villa. The Four Seasons.  Primo BBQ.  Raj Veggies ' +id='eat_sim' +gif="gif/eat.gif" +%} + +Really, this is about politics, and you want to make a decision for everyone in the group. You'd like to make a decision that other people will accept and won't regret. In other words, you don't want to find out that most people would rather do something else. That's why you might want to pick something in the middle. No matter what you pick in the middle, if you compare the middle option to something else, then more people are going to prefer that middle option. It's really the common ground that you're looking for.  + +{% capture cap1 %}Move {{ B }} anywhere. A line forms between that and the middle option, and you will see most people are on the side of the middle option.{% endcapture %} + +{% include sim.html link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSsWoDMQz9F80eLMmSz_cVHbodN6TQIXCQUtIhlP57ZT3SDiEYLMlPenrS3TdVWreNjQub7mXjsRSRuu-FGIgkErHSWgu1vC1vjwwpDydyeyC1PJxAlqfIeIpwzXY8Fc1QEEIQQxFDEjtMCOAWNtpJmODmQhI88SicjyIwoJGGlKDRMKCRjmhBNLJAawrluRJOQCVrVYFDkAbTxkhzAGDT8Vffkmo6fHfk7uhc7nRaVrV_uubZrnV8EIhrGNGwKoMww4iGEQ0jmsFgRAOLLdnFMKJXGM5Mx74dLG4pP34WclA4FPhI06Ggo7ZjNW-n47hcX28f77TSy_H1eTrO1xv9_AKWPS3kgAIAAA)" +title="The middle is the common ground." +caption=cap1 +comment="allow adding candidates with + and use condorcet rule, maybe make a custom pairwise comparison between the middle option, which can't be moved, and any other option.  And you can't move the voters either" +id="middle_sim" +gif="gif/middle.gif" +%} + +So how do you find the middle option?  Well, you could hold a vote and ask everyone to pick one option out of all the options.  What happens is that people will pick a side, and you end up dividing people into little groups.  It's kind of random which side wins, and that's a win with maybe with less than 25% of people. That hardly seems like a win. + +{% include sim.html +link='[link](https://paretoman.github.io/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSPW_CQAz9K9HNN8T3mXTM0K0SBcSSMFyrCNKmpAowoKr97fX5laUInXS2z_bL83O-VKke2pas1RTsVrd868qwYww_RZefbNSGavaC1-Rou9WKpMtxiStzbNVDqZWT28sduMLom8O1kTOlvjmcqe5m6rsZKuVzlBnl0CAEIQIjAiUKMEyAHFv-nGfD2KSVYRx-NIxj2BgYwBiHEoaxbABjIqIKUS0NthSilCUhSVgjvdYiD0KWkVrSfycXB6SBaTEusWBW8wqUE9js0NUxV8dmobPjpNf9h3ZBCLiIFYGuw9Ae4nlGVT-dWk6XNBbL9NZ1h-Y87_q5aNJ84OipT6ditU-v7xxs-t1u6IvNMI6Jw8U8fExF0zyzv973xeN0notVn47T4dgpxfAQ00MCDzG9h4GYHux8JTN4iBlKGJLKgM0GrCR4kYh_WRUAETBZqMVETBbRG7GElzSO02l9-ex53sV4ntM4nC7q-xe0u0DUBgMAAA)' +title='Too Many Choices' +caption='If you only say, "pick only one", then the winner can win with only a small part of the votes.' +comment='crowded election, winner gets less than 25%.  Everybody is an individual.  Everybody has an option' +id='crowded_sim' +gif="gif/crowded.gif" +%} + +This problem is called vote-splitting, which you get if you have three candidates in an election. Notice that a candidate can change the outcome of the race even if they don't win. If this is a small candidate, then we call this the spoiler effect because they spoil the election for whichever side they're on. + +{% capture cap16 %}Drag {{ C }} left and right to spoil the election for either major candidate.{% endcapture %} + +{% include sim.html +link='[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRTWvDMAz9K8FnHyx_pekxh90GXVt6SXrwRmizZclI20MZ22-frEdhUErA0ouk56fnb2XUsmmISJOr9rqhMnC2yFmM2lq332tF0uOdJm8ydmpptPJyBjkjd1h993FvyRWj7z6uLB5WqocVMnIdZUUZWkAIIigiSKKIwALIc-TrHAfmJq0s8_BPyzyWg0UAjfVoCTJgQWNLoAVQJQPOiFDKlpAUnJVZ51CHIMdMDRuMxogS-BxWJbHKC11O6JbYW-KywTnxMuf_U_ool_oSzwKJHosGGBaYUf22aj1d01Cs03vbjvVlPnRzUad5ZPTcpXOxOaa3Dwa77nDou2LXD0NiuJr7z6mo6xfOt8eueJouc7Hp0mkaT61STA8DgxNXAwwMAQEGBqgLeIYAA6NBIOmMeM2IZ4hBrHG8YARFxGaxklBisxKzJYx_TcMwnbfXr473XQ2XOQ39-ap-_gBhAcnr6AIAAA)' +title='The Spoiler Effect' +caption=cap16 +id='spoiler_sim' +gif='gif/spoiler.gif' +%} + +## Simple Comparisons + +So how do you solve that problem? Instead of dividing everybody up all at once, just consider each alternative one at a time (I guess that's two at a time).  Just between the two options below, you can tell which one's better. + +{% include sim.html +link='[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRPW_CMBD9K5FnDz5_hTBm6FapBcSSMLitBZQ0qQIMqGp_ey_3RDsg5OHu_O6e3z1_KaPmTUPeavJ-oxuqnLYmbjZaERDHiJlqp-ZGK6_mlVZB8sgdVt8c7i0ZMfrmMDK7i1R3ETLyHE2KptKihCDyCJBEEYEFkOfIz1kOzE1aWebhS0tyaS0CaKxHC9M4DqCxJaoZqkoGnBGhNFlCAjgrs84BhyDHTA2hLQIAm6v-5r1QTQldE3tN3GTulHiZ8v90PspzvsSHQJzHigFWBeZTP61aDJfUFYv03rZ9fR63eSzqNPZcPeZ0Kpa79HrgYp23230u1vuuS1w-jfuPoajrZ85Xu1w8DOexWOZ0HPpjqxTTw7rgxM8A60JAgHUB6sJM1AdYFw0CSWfEP0Z8QAxii-MFIygiNouVhBKblZgtYflL6rrhtLp8Zt53kfpDflPfvwTcre3VAgAA)' +title='Count By Pairs' +caption='Consider the alternatives one at a time.  It is simpler, and you can always tell which option is closer to the middle.' +comment='two candidates with a circle.' +id='by_pairs_sim' +gif="gif/by_pairs.gif" +%} + +And you can do this for all the options.  And you can see that the middle option would beat any other option.  This is called a round robin.  Ping pong tournaments work like this. + +{% capture cap12 %}Matching up all the pairs is called a round robin. {{ A }} wins all their matches. Mouse over the results to see each match.{% endcapture %} + +{% include sim.html +link='[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRPW_CQAz9K9HNN8Tn-0gYGbpVagGxJAzXNgJKmlQBBlS1v72-e2UpQjc8O7Zfnp-_VKlmTUNcabJhoxvyrCsjgTGsKdj0iYM2VEvknXTRZqMV5SkrLbZMOatZqZVVs1orl2MvHUbfPOkNUin1zZNKdbdS361QmX9HSVFKDVIIIguAJPIAEUBWUH7nBISbtDLCIx-N8BgBAwCNsWgRGhYAjQnIKmR1HuAyC6VkCeUCmzzLjDoEsTA1pP9eavYog5OxLolhrOUEymbaFNA1MNeAk9EpsHnW_qe2PguwASeCXIulHcxzwqp-WrUYL7EvFvG9bYf5edp2UzGP0yDZYxdPxXIXXw-SrLvtdt8V633fR0mfpv3HWMznzxKvdl3xMJ6nYtnF4zgcW6WEHmY6zg47mOkcAGY6qHNV3sHBTF8CKHd6XNbjJN5li1gW9KDw2MzXGQI2C5gNOMJL7PvxtLp8drLvIg6H7k19_wJC4-xxAwMAAA)' +title='Count By Pairs for Everyone' +caption=cap12 +comment='two candidates with a circle. Maybe I could use names for ping pong.' id='by_pairs2_sim' +gif="gif/by_pairs2.gif"%} + +So what would that look like in an actual election? That would basically be counting the votes for every possible pair of options.  Say there's options {{ A }} {{ B }} {{ C }} {{ D }} {{ E }}, and you like them in that order.  That means you like {{ A }} over {{ B }}, and {{ A }} over {{ C }}, and {{ B }} over {{ C }}, and so on. You have a choice for every pair.  And you could write that on a ballot. But really, you have a single ranking, which is easier to write.  + +You would get a ballot with the names {{ A }} {{ B }} {{ C }} {{ D }} {{ E }} written on it. And because you like them in that order, you'd write 1 2 3 4 5 on the ballot. + +Everybody writes their rankings on the ballot, and then we count all the ballots one pair at a time. If we find that one candidate was able to win all their matches, then we've found the middle. + +{% include sim.html +link='[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSQU4DMQz8i88-xImdbPbMCxC31R4KXURFta3aIoQQvB0nQy-gag8Te-zJ2NlPCjROU0kssc48iRjLkNrJgueGdqqeK42NsXrO5plJWltS9rIWJhoDk9JYmYxGYcpeEPjf57XlJjPcZOpNRkK_WpqhFkaEMCQKMEDuzsQNiDr6deZQOxldx5NRek2MAMhEReQyySEjWRANiFwl-h5CNyptJVBKUEpQSlBKrjQJ_36tOKMdmgnjCkdOrE5rk2112sZsY2u8HtKV0t6rf6UVU2vBE8Gu1p40LM8EAKsGq4bdGZ7TMLRBxYZ-l2HoHAAYOOMFMlSy9VH8j6IMiQwHGWsvcFDQW-DgcbPfHy4PH8eFRrrfrK_LlpjOL4f3u-X8dNodL7vD6tT327pdnner018_VpCSNcoCAAA)' +title='Rankings' +caption='You are saying something about each pair of options. Mouse over each pair in the tally to see how each pair is represented in your ranking.' +comment='ranked ballot with pairs' +id='pair_ballot_sim' +gif="gif/pair_ballot.gif" +%} + +You can also use a tier list, because ties are okay, too. You can have multiple top-tier candidates if you really can't decide. Voters who aren't sure can use tiers and still say something about top-tier versus bottom-tier, and every tier in between. + +

Tier List for Candy

+
+ tier list for candy +
+ +Pairwise rankings really give you a lot of power. They are used in a family of voting systems called Condorcet methods, and we'll talk about those later on their own page. + +Next, let's talk about an even simpler way to find the middle. + +## The Simplest Ballot + +The simplest way to find the middle is to just allow people to vote for more than just one candidate.  This will be really easy to explain with a Venn diagram. + +{% capture cap17 %}The voters vote for everyone in their circle.  Both like {{ C }}.{% endcapture %} + +{% include sim.html +link='[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSu24CMRD8lZNrF16_jqMLRbpICSAaoHCiE5BcOHRAJBQl356xp0AKQlfMrmc93p29b2XUeLmMUYt1a70UMVrqmCNf46xEowZnHpF1gjOzXmsl-Zo1qAkCpgERXCacGhutvBpDSYWSRNRaffOhuAZj9M0HZnSXae4yYspzknvLqWXKjsQT2JJEAhoQD8RzAQBt0cpCB4cWOhZgCZSxniWQcQDK2JrZiFlTLjhTGpXsiRTC2XLXOfJsyEFpCff45eJImpqO4woMcxo7UN6wPodCK3Jor6G7Fvii4f8_4WNpxNfcFdv2HD7QxABt9btS0_6Sumqa3ler_eQ8bNqhmqRhj-ypTadqtk1vH0gW7Waza6vFrusS0udh99lXk8kL4vm2rR7781DN2nTs98eVUpCnqcEVpwNNDYFAUwO7C6MyQ6Cp0RCkVEZuOHI1MRQP8BurSInIyWJToOZkNe_WXMZr6rr-NL8cWsz7cDgM_Vfq1M8fSXNUexgDAAA)' +title='Venn Diagram' +caption=cap17 +comment='too simple?' +id='venn_approval_sim' +gif="gif/venn_approval.gif" +%} + +On the ballot you have {{ A }} {{ B }} {{ C }} {{ D }} {{ E }} and you have a checkbox next to each one. If you like {{ A }} and {{ B }}, check both boxes.  If you like {{ D }} and {{ E }}, check those.  If you like every candidate but {{ A }}, then there you go.  This is approval voting, where you vote for those you approve of.  + +{% include sim.html +link='[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRTU_DMAz9LznnEDuO0_WGxE_gVvUwWBGTprXaBggh-O3YeeUCqnJ4_nx-dj5DCv0wVI0kdYwDURepU7eKWcJudXWNMZuV6zjGQN5Gyfwk7ufQpxgk9GRQDGJQK0nx37PiupnpNjO7zQylNptckrsMF4pIAAWgTRmZABJDG1cMdi3JxmNBplbDDAANCzyjyQaKYIXXwTMWtkOkJpT8JmDKYMpgymDKxjRQXJ8XK9rBmbEuRY45iqXFab1OfE1fW_jXMOIhuyGtV_5SC7aWij-CXNm1YMHxCgG4iSi5XaJAasF3FixdKkq6NqtgaU0ALKz4AcXCWladCgLFfMXRK-ZXdFbMf9yfTvPt4WOZQh_uluUyv-1PIYbry_x-P12fLsfldpzPlvx-PR-m5-N5OoSvH3JCY-bMAgAA)' +title='Approval Voting' +caption='Vote for as many as you like.' +comment='simple,lots of candidates, one voter' +id='approval_sim' gif="gif/approval.gif" +%} + +Say there's two populations of voters and they each like candidates in their little circle. If you allow them to vote for everybody in their circle, then the candidates in the middle get more votes than the candidates on either side.   + +{% include sim.html +altlink='[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRu27CQBD8FevqK27vieniIl2kBBCNTXGJLEPiYGQgEoqSb896pyASQi5mx7M7t49vZdS8rlOpyfmNrskHjixH1jiOaLPRiqaUmdEUShE4JaRJcGputPJqTgxBSORcq28-Tk6sGH3zsTK7q5R3FTLyHE29TdSCoiPyALREEcANkGfk5xwDe5NWln34p2Ufy2ABsLEeKUEKLGxsApuBlVLgjDRK005IBGel1jnoaMixU02akBghwc9hVJJVedhNc3q6hvYaOhhNoZd6_9_aR3ncJ9wHrXoMHLC4wL7qt1GL4ZL7YpHfm2ZfnceuHYsqj3tmT20-Fcttfvtgsm67btcW613fZ6bP4-5zKKrqhePVti0eh_NYLNt8HPbHRim2xyKDk-0GLDIEABYZ0F3AOQIWGQ2AJDPiqhHniEHmdzxghEXEZLEUSJgsoTbhAK-574fT6nJoed6Hw2EcvnKvfv4AJutP2_gCAAA)' +link='[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSPW_CMBT8K5FnD3n-CmFrhm6VWkAsgcGtIqBNCQpQCVXtb-_ZNyAVIQ_3Ps_vnf2tSjVt2xC0GLvWrUippQrJchVi2ZrUiDlYxgpi5XqtlaS2Gq7PidTmJSWsmpZaOTVFSPnsBNQafXNQXCFT6puDzORupr6bkTJfJ2m25Bq6nEgcgSNJIGAAcUBc5wHgFq0MeBA04DEAQyCNcSwBjQWQxlT0JvTq3GDLPKgkTSQnrMm91jLPgSyYWujIk4oD0-S0XFcgmNWQWjnSpn2dXE1zNS0Jk-kyh_t_hQt5EFfxrTi24_KeInpwq9-Vmg2X2Bez-L5a7ZvzuOnGoonjHt5TF0_FfBvfPuAsu81m1xXLXd9HuM_j7nMomuYF9mLbFY_DeSzmXTwO--NKKdBTVG-z0p6iek-gqJ7T-UnewVPUUBIkVwa-cODTBJ81wDdWgRSBm4U6Q8XNKvZWfIzX2PfDaXE5dNj34XAYh6_Yq58_R9yyZRgDAAA)' +title='Approval Election' +caption='Voters on both sides vote for candidates in the middle.' +comment='lots of voters' +id='election_approval_sim' +gif="gif/election_approval.gif" +%} + +Here's another example for a small group.  {{ B }} on the left gets half the vote.  {{ D }} on the right gets half the vote.  {{ C }} in the middle gets all the vote.  {{ C }} is the common ground. + +{% include sim.html +link='[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSu04DMRD8lZNrF14_79KRgg4JSESTpDDolASOXHQ8JITg2xl7EEJByMV4X-PdWb8ro2arlY9arGz0quu0tAYX8fLtktZpSR43a4vPbDZaSamykrS4mm1a5HS4RQOXK64U4Io_VG0pc2pmtPJqJoBQjQgmq_8cJCdEjP5zEGn_jXT_RsTU56R0XkxLkx2JJ7AliQQ0IB6I5wIA3KKVBQ-cFjwWYAmksZ4poHEA0thEq6XV1QJnaqNSNJEacLbWOsc4G3JgWkE9npIcGSan47gCwZzGhpQ3J_lefgtSHPbU4U5LfOX2p0_7WBv0iTvkOJ6iBIob8Jr6XKvr8S0PzXW-X68P85dp20_NPE8HWBd9fm4Wu3z3AOOm3273fXOzH4YM83LaP47NfH6F-3LXN-fjy9Qs-vw0Hp7WSoGeYgdXNxAodggEih3YXWjrDIFiR0OQmhm5-ciVxVD1wI9VkRSRk8WuQuJkibWJS7rNwzA-L9-OPeY9Ox6n8TUP6uMLKwtR8k0DAAA)' +alt2link='[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSTU_CQBD9K82e97Czn5SbPXgzUSBcWg6raQCtlBQwIUZ_u9N5MUoI6WH27bx5M_umn8qoaV2nUlOIK12TN5q8G09l4ruwWmlFI8UScYbGDDEnlXyKfHBCToGv4kh2amq08mrKLBUERK63-upjcuKM0VcfZyY3M-XNDBlpR-O8I7SAmIg8AkaiiMADkOfI7RwH1iatLOvwpWUdy8EiQMZ6UIIUWMjYBDQBKqXAGRmURk9IEs5KrXPIYyDHSjVbC2JECnoOTyWxyptfnjA9_dkwQnsJ3SXZi57_38pHGcYn7AujexgQYGTgHuq7UbP-nLtill-bZledhnU7FFUedowe2nws5pv88sZg2a7X27ZYbrsuM3wctu99UVVPfF5s2uK-Pw3FvM2HfndolGJ5GBucuB1gbAgIMDZguoD1BBgbDQIJM2LLEeuJQXzgf1JFSES8LJYSEl6WUJuwkOfcdf1xcd63_N67_X7oP3Knvn4ApcfzpRwDAAA)' +altlink='[link](https://paretoman.github.io/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSTU_DMAz9K1XOOcTOV7sbO3BDgoG4bDsEVI1BWacykBCC347jBwJpoB6enec82y99M87MlsvcWYppbZcU3HfUZYniem0N1RLmYClXhjlZYq1x0RJ1Gkl1GySKQrb5K2CuXFXnoFErpK-a3sycNcHMSCBqkqQN26NPirMwzh59wrT_Mt2_DMnKJFDXqilrd8JEFAAYiRJABqAgKO28QKcCLDpyyKLDAgyADAeURL3AkOGMrEXW6QXvdFCqnpASnvWu9-AxkBelJVlCYQIFPY9VSa0K7rvu56vHdGxHPea_j_3fIkH7hd-jhKTDhoz3xGoBBkWnWZTe5mNlFuNrGZpFuV-tdvPnadNPzbxMO8nO-nJoLu_K7YMk1_1ms-2b6-0wFEnPp-3j2MznFxJf3fXN6fg8NZd9eRp3TytjRB7GR9gVYXyMABgfMV3E80UYnxyAtDLhL0h4vhTVFy8LJkgkbJY6hYzNMu5mPNhNGYbxcPW672Xfk_1-Gl_KYN4_AZvC0nJjAwAA)' +title='Approval for a Small Group' +caption='You can move individual voters.' +comment='less voters' id='small_group_approval_sim' +gif="gif/small_group_approval.gif" +%} + +Approval voting is simple to use. It's also a huge improvement in finding the middle. + +## The Median + +Why does approval voting work? It uses a median. + +A median finds the middle of a list of numbers by putting half above it and half below it. + +*More* *importantly*, this minimizes the sum of the distances to all those numbers. + +In a way, all numbers are treated equally. It doesn't matter how far a number is from the median. Each number is pulling the median to one side, just like all the others. + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA4VTu04EMQz8l9QRih9xsvsVFHSnKw6JAmklEDoKhLhvx-tJruHQaYuxHWc8mU2-U0nr4ZgTORyoUuZejvmwzICWOiLmMmvUZqTXmsmMmo6o2dwqfUQ6S7bMNQOFa5C0ck6a1nQpKaea1pKTQV9zKPnP5yvdV9KF9h0X5nSjY4kO5hotUm71-P59GtGwg5EKQAFQRAZwSaSOHalPoZzYebzIFGdxPQGgYUXmNOIAGm7IwMJLtMj4K0JRFagRGCRQI3X0GKrgkQVV3Rko__PtDXTb0asjyvca5N4IDUk6hKqFQ9pCvuLACtsq7K84b4VtFbZVRUsFwLba0NJjRF1wV0oUDSwG1wwsVuM44kIMFAYFhr0NChrFlMYAAcDxhv_f5pVsWOxB7A9mr-E4HWQdZB1COoR03IJeAdDSwdU7YJf00PzmlsifT9v2dn76en_xt_G4fX6cttfzV_r5BWjH9i2-AwAA)" +title = "Median in 1D" +caption = "Try moving some voters. Add a candidate. The total area of the bars below is the sum of all the distances. Notice that moving the median left or right would not decrease this sum because the same number of people are on each side (same number of bars are growing/shrinking)." +comment = "Maybe it would be good to try to move the median itself?" +id = "median_1d_sim" +gif = "gif/median.gif" +%} + +A median can also be found in two dimensions by using the same idea of minimizing distance. + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA4VTwUpEMQz8l56DNGmS5u1XePC27GEFD8KCInoQcb_dpLOC4Iq8wzTJdDpN8z5ab7v9gRon7FmERMeB9hHEvOWCw4i9V0ppFk4mtuIMTQ4Xp3fizWuluS9qZZIpq5QbudaiNnLUSiatmrCTcGmJZFH5kE5G23Vq2nbt3Bs1W6HD5Uzo9OvLSmSlnbl2nEXaFca2GCK2KKNf4-T-Oo350hRBCEusADhiB6Ql1sRAmKcwNUmdTErqSIIAICOKKGVGAmRkIoKKbIsyLm8zeGUH3IyBItwMu3AcWeiMDVkthezsX19R-HpPf3RF5X_K-P8gXdb0Ylh9dUonnhsXV7TP8AyGexvaZ2P11BQUA6B9NkGJdYRtmJm-kg4VR_ccj-C2rpSj1xwSDgeOvRMOJq9TpgAGAJ2fmIP5PZoTxVjCTFI5XCcgFhALGAkYCUxDGABeAloRgLJ0M3OC-4rvj6fT0-vd-_ND_iO3p7eX4-nx9b19fgGnVnGdzAMAAA)" +title = "Median in 2D" +caption = "Move some voters. Add a candidate (+). Just like for the 1D case, the total area or length of the lines below is the sum of all the distances between the voters and the candidate or median. The median we chose here minimizes this sum. A winning candidate should also minimize this sum." +comment = "" +id = "median_2d_sim" +gif = "gif/median_2d.gif" +%} + +How does approval voting use a median? + +In a way, approval voting is asking you whether each candidate is close to you or far from you, so you're measuring distance and writing it on your ballot. + + + +Score voting can give a more precise measurement of this distance. In score voting, you give every candidate a score from 0 to 5. Add up the scores, and the winner has the highest score, just like usual. You could even say that approval voting is score voting with only two levels of support. + +Distance is measured on your ballot. When you find the highest score, you're also finding the smallest distance. So you're finding the median. + +That's the motivation for score voting. + +{% include sim.html +link='[link](https://paretoman.github.io/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRQU4DMQz8i885xI6dbPfMD-ht1QO0i6hUdau2CCEEb8fOlAuoymHs2J7MOJ-UaZymVhNr26SJeUg81IjMI5WIhna7E_GotM0mEccYZ8-zRl5ozImURuZE5pCoektO_443t7uV4W5ldbfCub_NISlSQQpFrAAD1K6MXQCroz9nDqteFOfxS-HeIwIAjSgypykOFZcN2YDMWcQXkbtQjp2AqYCpgKmAqTjTxOl2orliHJwFdjlJKkm9rEEbfRo2w7bKb-DEU4lA-6z-pVa41oY_glxd9UvD8owBkGqQatid4TsNpq11nTb0twymawbAcMUPVLBU61ZCYAVFhYKKtTcoaJht0pmenw6H5br-OM000uN2Oc-U6PK6vD_Ml-15f7rul6NXvt-Ou_llf5x39PUDc7xiOMsCAAA)' +title='Score Voting' +caption='Give as many as you like a score from 0 to 5.' +comment='simple,lots of candidates, one voter' +id='score_sim' +gif="gif/score.gif" +%} + +In practice, voters can adjust their self-reported distances, which is called using a strategy. Jameson Quinn has a good discussion of strategies in a page linked at the bottom, and I have a page that discusses how strategies play out in approval voting. Basically, using strategies means score voting changes and takes on qualities of approval voting and pairwise voting. Also, in the approval voting page, I show that when you combine approval voting with polls, you get almost the same winners as pairwise voting, which is nice. + +These are the strategies in the example below: + +- The J = Judge strategy measures distance well and would best find the median. +- The N = Normalize strategy is basically stretching your vote to the max score. Any voter would rather do N than J. +- The F = Frontrunner strategy considers polling data and stretches your vote to the max score only for the frontrunners. Other candidates can get pushed to max or 0 or in between. + +{% include sim.html +link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSy04DMQz8l5wjFCdO7O6ZP4Dbag_QLqJS1a3aIoQQfDt2pr0UVTmMX5mMHX-HFIZxlBaJZYojkUbS5lY1i7NbKpdYzmYVmaYYyK9RMj-x-yUMKQYOQ_glCjHUMFAMzaosKQYp_juW0buZ1d0Mpf4WuYRmwkuk7OGMMJQQAyqgdTlkQogN7dlqsOrJbHwWzNRrcgaAJjM8oykGDUGBp_CMJdsAfJAmJbqWAq6SkQJXAVcxrpHi5XhxQxVYCxo3nlgiW5qd2OvYG_YBcL4axd90g_tdvqVm9M2C34FgXvVgxRgrASC1FgCkVvxiRdtVkNP-VkXbLQHQcMMfNDTcam_FBTZQNChoGLxAgeCuQIEUePg_wf_JdZEESb2ZkaAlBaFSZ1KIUYhRECoIFXoUfApZ6rIebDFeX3a75fz8dZhtn5_Wy3G2jT69L5-P82l93B7O22Xvm_6x38xv2_28CT9_6sQykEkDAAA)" +title='Practical Score Voting' +caption='Try the different strategies.' +comment='score can look like approval voting' +id='score_strategy_sim' +gif="gif/score_strategy.gif" +%} + +STAR voting was created to counteract scoring strategies. It uses a final runoff where stretching your scores doesn't matter. + +STAR combines the two ways of finding the middle, scoring and counting by pairs. Its name is an acronym, STAR, Score Then Automatic Runoff.  First we score. Then we find the top two and send them to a runoff. The runoff uses the same scores but counts them by pairs for whoever each voter prefers. + +{% include sim.html +link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3WSsU4DMQyG38VzhGLHdnLdeAXa7XQDoG6VihALQuXZcfxThqLqht-xk_8-J_6iSrt1NS9c21ZW5lq4LzOyFjmJSP5yohq5um2FeB7LPdrmutGuFlLa0TcLFbJce-yKYg-p5d8XlXG3stytcE1vngjuSAlSoGCFgIEdEhCsofFLCwl_LiThFUkJLwkRCGxEsSVsWghspGM1sFryQKsJy_MeOAsNTq1BANTCaeXy-83NjjI8G1rmIqUVjbKm7Qz4Gsg1CONVZqB5Vm-t1RNeO94FuIqmDRdoQDWgGlANTZtB0LR11Eb-y9C0VwjnTscLOK7OLVuJiSKHhYPAl5QOgo6zXSANgsvqIOhzhGh_eHyKodq_nt-PNAeqozhubqyjwVEheNUBtAG0oUgaBHQDfgOQY0I-xJAswHx5Pp3OH4fPt2OMNxguP5L3f-I5AwAA)" +title='STAR Voting' +caption="Same ballot as above for score. Score from 0-5, and add up the scores. BUT THEN take the top two and count by pairs. Use the voter dude . He's in the bottom left corner. Drag him around to see what each voter was thinking." +comment='same as above but star' +id='star_sim' +gif="gif/star.gif" +%} + +## Bringing Groups Together + +Part of finding common ground is finding allies. Any voting system that avoids vote-splitting will also allow allies to come together as a team. The scoring and pairwise systems mentioned above do this, and so do the methods in this section. + +To avoid vote-splitting, you could eliminate candidates that aren't doing well. That way they don't interfere with the main candidates.  The main candidates won't see the smaller candidates as spoilers and can ally with them. This method is called RCV and is getting more popular in the US. + +{% include sim.html +link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSu04EMQz8l9QWil9Jdj8D0a22OMRVnICCBiH4dhwPS3HolGLs2JnMOPkstazb5o246k4bcyXuy4zcaJmB_G2JWbTVfafC8xS7EpvOXMtaqVhZy7cVKp5pi6ao9YBK_1ZUxs3KcrPCNbmZQc6CFALYALifGyAEsAXGdR4Q3ExFgic2JXgkQACgEUNL0GgAaKQjG8iWPKA1hfIcAWdBIUgVdQjSYNqYftdsbiiDU2GXSUjJomxJOwM-AjmCIN5kBpZn7ZraWoq3jieBXINpx_AcUh2mXXMSDtPuAJj2jpaRdzlMtwrg7Gww3DC65mkl_lJpoGhQ0JaEDgUdZ7sAFIBhdbxeP75PR3FczajD0gDhAOGAmAExw1LocAD0DPANyBpT1l18iwU8j6fL5fX94ePtHH_5_vTyfH4qXz-HLDOpJgMAAA)" +title='Single-Winner Ranked Choice Voting' +caption='Your vote counts for your top choice.  Then, do a process of elimination of the candidate in last place and repeat.' +comment='same example' +id='irv_sim' +gif='gif/irv.gif' +%} + +Also, you can have a multi-winner system where you have maybe two candidates getting elected.  For example, you're picking two pizzas to order, so you can have meat lover's pizza and you can also have a vegetarian pizza. You don't have to all eat the same pizza.  + +The idea here is to choose a set of candidates that can stand as a representation of the voters. This works with ranked ballots or scored ballots. + +{% include sim.html +link="[link](http://127.0.0.1:5500/sandbox/?v=2.5&m=H4sIAAAAAAAAA61Wa2_bNhT9K4yALTYsKKLesuMEa5ttBda0iLP2g20EskXbwmTRE-msWZf99l3yUvIjSfNlSIJL6h6ee-5DjL5ZrtUfj9PETunUHnuBa3tuDCtKQ5smepXENqXKS0Nq09CbTm2LqlM09G0a-GrvW33XtgKrb_1LI8u2Qr2PAAXOGIxrP_kBT_KiJ33RQ13NTZUEvfdwjxJogAYFgBZtQAENwEK8EAyQU9vygAgeekDkgfHQII0XIARofDBI48W4S3CX6gO-q5VSVQSqHT4K8n30oyAfmMZQPvxR4AjdyOljvtT2bN8OwB24Jr2gyTPwmgUQjz21CDRFcEwdRFp8EGNTUG6ASYcuGqpaNbF-zdYkq3LyqahYttmUbDKp3q4YE4y8qVmWw_YDyyT5jd-zWsDuM1suC7bbIxgWE0u1HYsYYuohFjEM0WARwxh9ic47xCJGLhqqkREWMMJWRKEujQ-JRUgRYUZRqk2M8xDj2dhD46PB4seoIFbzaI1uP4PSD7yqOSMj9icZWWpGY4QkR32IMUiCZUswSIICExSYYBuSEA1qTJAvQamJYnFg9FIUmzZNTVVTQ7XwmwWypSg5RbY03m-xvd_sNMFOLrbVXBa86uSFkHUxlzZZ85yVNuEb9Vx0v6kuqV9pnpAhuWMl08fesUW2LaXoNGhE3mc12fCylOyrVPAZWxbVlTmzC4WRzFF7MplYYs5rphaGqGSSzDMdsznlCJktmRjrw7iZOoDJizyTTLRqz87IbVaWD0SuGIEhrfl9VgryI1kC519FVbH6ZKd2BlAuVRzkveeS1SMmHUC_Qd87I6DV321jKQapg0Gyf7Oa68gdpdwksuB1ZxeH8EUT0dRX7mHaZEhRGZijC7MPhl8dcdyip6Q3PITv-QbtwUdcPTbiG_3r7Ks-BSmEBw6slW77nG8r-UXvOzp6d7DDQUAYBQXDVcccxDbvTcacl7wGHMIcvW0jFgvSDJMjipzNsrpLdjOIjX2baUT7aCcTaE1YeFEGBwAzjGq61N_OqR099JyLTVaReZkJMTwVa8jx9OIQrQSqJz9tJVeWDJuZyeDJJ5j6LmkYm3fghVizC6x4JsgPaiSgBXBEiGJWsj45P5tdnM_qZ8JjuByn7O0qq2VTInkYRbYAbJetJtK8dk2_bTOITsmqpVx1n-NBtcdSHgkr4cY_iNzMcDF0B6Q4V_EMMWx7vcMBbuYBuqKA42LqFPngCNFowKSLOVwg825PKTkVRGfQ1_p6dxtWz1klf-b1OpN7F5p5T6bkrB1yJHia0u79OFgVi86Jma9_mvkyaV0MvcOkYDyvP5Iv76-vr24uT8jHG3L7_ury5PLkucLeyYKpNbbUNtTdwRHh3ujC0ACfmg2i5JMmB9Ijh5P2XHe-38_ni42asGBkBaOq7tNVsVwxIbH-NhHccZzvEuqIZ-rlegXzKsmMCPlQsuGpvjf6uvV6iTOhT_dQf5Wt2e8bGIuDJHTtoD-jJ3GOK_2_RXqtVfvXm7kVjZA9YDuOzTXZXgN843WJvuJWTG3UHSx4LfG_EF7UjiiLOevAh3r3JYYmcktiVoivmdzWlQEpSY_624265sPaxc9YvE1uHzYMPjBusuoPlgNK_zv9pebbDTzK-XrEWC7g48RPpo__AZ-f7R9NDAAA)" +title='Pick a Pizza' +caption="We can order two. Decide using STV (Single Transferable Vote), also called Multi-Winner Ranked Choice Voting, and also try using a variant of Monroe's method for fully proportional representation using scores." +comment='meat lovers and veggie lovers win' +id='stv_sim' +gif='gif/stv.gif'%} + +And of course, there's primaries, which is the way we do things now. Basically, a whole party gets behind one candidate. This can work in the best-case scenario. But primaries can malfunction when there's a crowded field, or when one party has many more voters than the other party, or when there's more than two parties. + +{% include sim.html link='[link](https://paretoman.github.io/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRMWoEMQz8i2sXlizJ3n1FinTLFhdIEVhICJfiCPl7JM2FIxyHixntaMcj-bu0sm6bjkrKe92IyZkFm8GmM7alkrV9r4WiecqfwOyMQuhlbbVIWakWTW7eyvXueO9wpdW748p8qCwPFWp5HUW0KBklApEAEIkM4AFIHP26gCW_csuKKWdgt2EH2DAGY7fpDrDhgWqiWvKH3jIoxUoohY5AvUNHoO5Om-83TrQaRDh2DEu-ru6iwDJmFbpRvtEOs6CSDvLfXizjy8AjIbAseCysTxFWMbb23IVe31MBGFsHWmbepBjbGgDLM4xsWJ5pJo1RDBaGBIbFDyQY-HdwwsvpON7Pz5eP17KWp-Pr83S8nS_l5xcgCQz0sAIAAA)' title='Primaries' + +caption="Here's a best-case scenario. In reality, they don't always work so nicely." + +comment='an best-case example of primaries ABCD two groups' id='primaries_sim' + +gif='gif/primaries.gif' %} + +## Afterword + +To wrap things up, there's a lot of ways you could deal with voting (casting a ballot), and there's actually even more ways you can count ballots (tallying).  As far as casting a ballot goes, picking only one person does not find common ground in a group.  But picking multiple people… that allows you to find common ground.  And that's the point of this explanation. You have to allow people to vote for more than one person in order to find common ground. + +Still want more? Try the sandbox below, where I've added a ton more voting methods and configuration options.   + +Want more of a narrative?  Then choose your path.  I go into depth on all the voting methods above. This page was just an overview. + +Either read about primaries and polls, more details about approval voting and strategy based on polling, or an essay by Jameson Quinn on types of strategy with score voting and some resistance to it with STAR voting.  Find out more about IRV (the single-winner RCV) and STV (the multi-winner RCV). I also have a draft of a page about proportional representation using more methods than just STV. It's a work in progress. Just be sure you get to read more about Condorcet methods. I like them best. + +- [Primaries](primaries) - and electability polls +- [Approval Voting](approval) - and strategy based on polling +- [STAR Voting](star) - by Paretoman and Jameson Quinn - on STAR voting and its motivation from strategies of score voting +- [Condorcet Methods](condorcet) - I like them best because they count by pairs. +- [Instant Runoff Voting](irv) - AKA Single-Winner Ranked Choice Voting +- [The Single Transferable Vote](stv) - AKA Multi-Winner Ranked Choice Voting +- [Proportional Representation](proportional) - draft + +**External Links** + +* [Videos by CGP Grey](https://www.youtube.com/watch?v=s7tWHJfhiyo&index=1&list=PLkLBH5Kzphe0Qu8mCW1Leef2xSxPK1FIe) - I have to mention this set of videos by CGP Grey, since I do in every discussion I have ever had about voting methods with new people. He made them at the time that the UK was considering using IRV, which the UK called the Alternative Vote. +* [Link List](links) - I have a big list of links to communities, organizations, simulators, polling sites, courses, videos, essays, books, references, and bibliographies. + diff --git a/condorcet.md b/condorcet.md new file mode 100644 index 00000000..5e4f2088 --- /dev/null +++ b/condorcet.md @@ -0,0 +1,329 @@ +--- +permalink: /condorcet/ +layout: page-3 +title: Condorcet Methods +banner: Condorcet Methods Explained +description: An Interactive Guide to Alternative Voting Methods +byline: By Paretoman and Contributors, June 2019 +twuser: paretoman1 +--- +{% include letters.html %} + +I'd like to describe on this page how Condorcet voting methods can help us find a common ground candidate and elect them. + +I'll go into detail to explain the hard-to-understand parts of Condorcet methods. For that mental effort, you'll get the benefit of not needing to think about the polls to decide who to vote for but simply be able to vote for the candidates in the order you like. And you'll have better candidates to vote for. + +## Intro to Condorcet + +[If you haven't yet, please read this page about how counting votes for each pair of candidates allows us to find common ground](commonground). + +To summarize, the key mechanism that allows this method to work so well is that your support goes 100% to the candidate you like better out of every pair of candidates. This is a lot better than just choosing one candidate: there's no spoiler effect and there's no vote splitting. This is a tremendous burden off of the voters and the candidates. + +{% include sim.html +link = "[link](https://paretoman.github.io/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSu04DMRD8F9cuvN5dP67mCxDd6YpADnEiukQkCCEE387awzVBkYvxPjyeWfvLBTeMY2ZPsU5-JFJPhdtOg-VK21XL5VaNsVpOp8k7asdYvLW1kN0QvBM3uJ_qvFM3kHfJeqyWDYL_t6xSblbqzQqFfhURyCkihAASgAJSl0EmgMTQrlOD2ovReCwZqffECABNFERGwwYJyYyoIDKWaL5DF0ptBGBiCGIwMZjYmEbyf6s1JxwHJ8Mu-ejZi5Wl0bY-aTabbYnbhreS9LNyTS1wLRlPArlSe1IxPCUATCukKmaneDuFaQWLln6XwnQKABhOMJzAkrRbsR_kEigSFCSMPUNBxtkMBZkBUJDxenn7PhnFcjWjDEsFhAWWCsQUiCkgLCAs0PO4OxyOl4fP02y_9X63vs57-7Hnl-PH3Xx-eltOl-W4to_8vu7n52W18vcv6dXEticDAAA)" +title = "Pairwise Ranked Ballot" +caption = "" +comment = "" +id = "1" +gif="gif/pair_ballot.gif" +%} + +We're going to use that background map for ballot visualization in the two elections below. We'll overlay the ballots for all the voters to try to make it more clear how everybody's ballots get added together. + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3WSMWsDMQyF_4tmUyxbsnVZC50zZDtuSNsMhaMpIR1CaX97Zb0GAiVkeFYkv3xP8Rdl2swzF0usuqSZTfxUxilbKpWXJRHHjGbv5FFX2uREQhv6yZRIo2w-5L3uktO_j3fsbme62-Ec3jwIoi6oQcACAQA3iBOwuPrvVRc350TFjfzL4kbFpUBgUwQjGhcKbEpHZaimuFBzkPLYAUejAqhW9AFU3WnmxBhsaMGvIisnXzFJ_osl13xSrgc3nGNG4p7cWkoLYOn4H4AoCKrYmAJPEVSBpwiqCkFQ7ehhXYqgLUM4JhtCNqyraUSoDtJg0UDQppAOgo67vUAqBAvqIOjjzdDTdrf1V_R4fH89nl4OZxqvqGPAbrbVEdAyBPYGNAOaSWCbQkBn8DKDDMiHnuh5v67H8-7ycfB3vF0_T_v17Xyh718YJyuqEwMAAA)" +title = "FPTP Spoilers, Condorcet None" +caption = "Switch between FPTP and Condorcet" +comment = "Example of no spoiler effect - basic spoiler intro stuff." +id = "condorcet_2" +gif = "gif/condorcet_2.gif" +%} + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSsU5DMQz8F88Wih07SbsiMXfo9tShQAekiiJUhgrBt-P46AKq3nB2bF_u_PJJhdbLItVYXHe8yBCWUiNSrSxjRmLOahKRexyV3Y5JcsxLjGVeaV2YjNb0XYjJM23RFLUeUPjfF5Vxs7K6WZGS3DIVZK7IoUAMAAHSAKFALDDu84AgFyYNojjUINIABYBGDS1BUwNAox3ZQLbKgVpSqcwdSBYqBNWKOgTVYFpitfhmc0MZnBV-hZUrW5St_Nqzq0_TaxDEi87Actb-UltL8dbxTyDXYNqxPYdUh2mHVIdpdwBMe0dt5F0O060AJDsbDDesrnlaiYdDDRQNCtoqoUNBx2xXQAVgWR0K-nw_9LDZbuJF3Z9en0_vT4czzRfV0TCuW5tnMDgKAPQD0gakDUvZwwFQN8A1BmCKvOtMj_vj8XTeXt4O8aY3x4_3_fHlfKGvHw30UswyAwAA)" +title = "Condorcet: No Vote Splitting" +caption = "Switch between FPTP and Condorcet" +comment = "Example of no vote splitting. - even with that crowded example the IRV had a hard time with." +id = "condorcet_3" +gif = "gif/condorcet_3.gif" +%} + +How does this work? We have this information about each pair of candidates, so the obvious thing to do is check if there is a winner that won all of their matches, which is called the Condorcet winner. If you ever hear somebody talking about counting by pairs or Condorcet methods, well, they're pretty much the same thing. + +The name Condorcet comes from the guy that thought it up, the Marquis de Condorcet, whose actual name is Nicholas Caritat and who is actually just from the town of Condorcet, so really it gets its name from a town in France. And actually, it maybe should be called Llull because Ramon Llull thought it up 500 years before Condorcet. + +

Statues of Condorcet and Llull

+ +
+ condorcet llull +
+Also, when there is a Condorcet winner, I would say this is the perfect voting system because we get around Arrow’s Impossibility Theorem. Arrow's theorem requires a set of good principles for a voting system, and when there is a Condorcet winner, all those principles are met. The only problem is that there could be a Condorcet cycle, which we'll talk about next. + + + +## Visualizing Cycles + +There's only one hard to understand part about Condorcet methods, and that is what to do when there is a kind of a tie called a Condorcet cycle, which means there is no Condorcet winner. You already know this. It’s a rock-paper-scissors cycle. It’s like a tie because each candidate got one win, so there’s a tie in the number of wins. + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VTy24CMQz8F59ziO28lq-o2t5YDtAuEgJBBe2hquDb62SULVKFQJqJHc_a490f8rRYLot3zLxyS5HsWIoxDsFJHFYrR1yv5OhEa0I0OS6hXlF12dcbSgvvKNCCbgM5iu2YrMxy2cC7fz_LlIeZ4WGGfdPm2hPdRvIj2QONcCfSiXYSOomdpE5yJ6WTYRb8k561eRbnWZ1neZ71uT6gdivoVqszI52223aDA8LwiRPAjOJgaLaowdCiYvNaUGxeNjA9MYDfEhCMrUAgIxmngtPQCrRuudqn3MKKvhQ6inbUdJamdzt-HQ7k2HEtSBCAqprqD11279NmfabF5_lruloU6xKnVhB8U0F14L7IepD7g95fC009xPtYasOFjFcL44ShBaMHMGx9Pr3tx_Fo_5e33eVyOl-MPq0_pnNzO8K0qACYFiMApkU8JcL6CNOSB8D6BMsSLEuxzVIHTpBI6DBhbRmvaUZtFoACYHfG9nP_TDKS-Ci4uulCjWPkAsGC_RU0U9BMgWCBYEE_m_XhcPp8_f6YzKTn9XE_vdP1FyrG79fvAwAA)" +title = "Rock Paper Scissors." +caption = "Rock beats scissors, and scissors beats paper, but paper beats rock, so it’s a tie, or a cycle." +comment = "" +id = "condorcet_4_rock_paper_scissors" +gif = "gif/condorcet_4_rock_paper_scissors.gif" +%} + +This visualization **above** is a little crowded. The colors here are the same is in the case of the single ballot. The three voter's ballot diagrams are overlapping in this combined diagram. + +Let's clean up the visual by showing a map of where a candidate can beat an opponent. + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3WTy24CMQxF_8XrLGI7r-ErqrY7hgW0g4RAUEG7qCr49jq5yhSpQjOSH4lP7JuZH_K0WC6Ld8y8ckuR7FiKeRyCkzisVo64bsnRidYF0eS4hLpF1WVfdygtvKNAC7oN5Ci2MFmZrWUz3v17bKU8XBkerrBvbK490W0kP5IdaA53R7qj3Qndid1J3cndKd0ZZuAfembzDOeZzjOeZz7XA2q3gm61KjPSabttOzggDZ04wZhQHMyaLGpmaFmxeS0pNi-bMZ6Ygd4SkIytQICRjKggGlqB1luu8im3tKIvBUfRjhpnabzb8etwIMeOa0ECAFQ16g9ddu_TZn2mxef5a7paFtclTq0g-EZBdeB-kTWQ-0Dvt4VGD_E-l9pwIePTwjhhaMnoYRiyPp_e9uN4tPflbXe5nM4Xc5_WH9O5qR0hWtR2SIRoMcJAtJgRQfoI6ZNvdQnSJ0iWIFmKbZY6cAIiocOE2ozPNKM2C4zCQO6M28_9N0ELGT8FVzVdqHmMXAAsuL-CZgqaKQAWAAv62awPh9Pn6_fHZCI9r4_76Z2uv_hO5zDvAwAA)" +title = "Beat Map of Rock Paper Scissors" +caption = "This map shows that scissors is in rock's territory, but paper is outside, so paper beats rock. Wiggle rock to see its territory move." +comment = "" +id = "condorcet_beat_map_41" +gif = "gif/condorcet_beat_map_41.gif" %} + +This cycle can happen when there is division between three deeply divided groups. It goes away if we add voters to the middle. (Also, let's use odd numbers so we don't have to deal with ties.) + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VTy04DMQz8F59zWNtxHv0MxK3toYgiISpAPA4I0W_HzpAFgdAePJmsJ-PJ7jsttNluq6Vu-7QVkcTSHHGWJCXv94k43hBtifsSO_6OdQe1Jsk5GNXEOfrZqiOJLqXNkijThs6dEtlYFpfyveplSX8e32n_7vR_d3gZ2hw-6byjZUd-oAOeQCbQCfIENkGZoE7QJuir4Lf0qs2rOK_qvMrzqs9xQLgVuNVIZkcPNzfjDc6gkRMXFA-Ks1ePRb30wYrP66T4vOzF9cQL8pYM0kaDQEYqVg2rPho0Lj7iUx60wpdCR2FHXWfreuf719OJEieOhgIBqKqrvtPz7fXx6vBEm5en1-OHs7guSeoNeRkqX08Q_PMyg5DfhP5uyeO0bJMProxhc8WnhvFyH6ThozAMZwjJdIgYQjJDQUhWsULUhqjLMvoKoi6IqCCiYsNzDFggUeCgoLfCQUVvFRRFQbwVt13nbwELFT8BR3opB4-RGgQbRmow02CmQbBBsMHP1eF0eni5fHs8-rd2cbi_O17Txyf46F9d8gMAAA)" +title = "Middle Voters Break the Cycle" +caption = "The middle voters' choice determines who wins because the rest of the voters can’t agree." +comment = "" +id = "condorcet_5_middle_voters" +gif = "gif/condorcet_5_middle_voters.gif" +%} + +Or if we add one candidate to the middle, then they win. + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VTy24CMQz8l5xziO04Dz6j6m3hAGKRqqJStfRQIfj2OhllS1WhPXjs2JPJJHtxwa2mKauvuvETM3viYogie06xIbWals3GO2q9LMVTDW3FurUayNlzjK1D3Cp4F93K3arzTnuabMzWsoXg_322Uh6u1IcrFDo3NU3utnZh7WxDAzQADyADxAF0gDRAHqAMUBfCX-qFmxZyWthpoaeFn9oGTS1DrTRn1u50OPQOiijDJ0oIZhRFi2ZLC7VXOfSM7bxkwfjYAvzmiKLRiAXQcEZWkNU-IO26m31CvSzQJeARyBHjmcj3r7UmjIJPjO_iPl_282774Vbnj6_5alVcFHvpIzEMhpbQuMSW8H0i922x88e_m8fUjxYzHhYOE2svKp6A4igKS1Q6jcISVQRYohlZQQuMTaHPJRibYEiCIUm7WjEhCRQJChJmMxRkzGZGEASYmXG3efwEkJDx5Kk7FlsdRyogLDhSgZgCMQWEBYQFenbb4_F0fv5-n-1lPW3fXue9u_4A0be67dYDAAA)" +title = "Common ground Candidate Breaks Cycle" +caption = "A common ground candidate enters the race and wins all their matches." +comment = "" +id = "condorcet_6_candidate_common_ground" +gif = "gif/condorcet_6_candidate_common_ground.gif" +%} + +There is tremendous competitive pressure to go towards the middle as you can see in this new diagram, a win map. The solid yellow region is where {{ B }} needs to be in order to win. + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VTQW4CMQz8i885xHGcZHlG1RtwALGHqqitaHuoELy9jodskSq0B48dezKZZM8UabVetxIab8M65RhYsiHOEpKKoxq46nYbiL1XQ0rZm4st1N4iOWjpHUKrGCjTiq4TBVJPi43ZWrUQw7_PVtrDlenhCkfn5q6JrhuKG7INDfAAaQAZIA-gA5QB6gBtgGkh_KNeuHkh54WdF3pe-Llv0NUmqDV_xEJGBnu4IJg_nC2aGz1MXk3Rs2THZAtGkyzA5pRRVCdNoEkVWUM2-YD0W-6uCXtZIEfAI5Aj6mbzzWApGK2-iRjfmT5fDvN-d6LV1-l7vlgV95OCBHs-lPs2bAye8Li7nqT7RO7bsuvNOmqoFt81V7wnHCZPXlTcvOIoCksUziosUUUAi4JFGzphbImeFRhbYEiBIQVO2OunAlcLFBTMViiomK0JQRBgZsXd1vH24WJtN4u7Y7nXcaQGwoYjNYhpENNA2EDYoGe_Ox7fv55_Pmb71Z52b6_zgS6_tHc9bs0DAAA)" +title = "Win Map" +caption = "This is a map of where B would have to go to win. Move B around." +comment = "" +id = "condorcet_7_win_map" +gif = "gif/condorcet_7_win_map.gif" + %} + +This map is much fuller when there are voters in the center. (Also, the striped region in the below diagram is just a regular tie, not a cycle, like the above diagram with thinner striping.) + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VTy24CMQz8l5xziO04j_2MqjfgAGIPVVFb0fZQIfj2OhmyVFRoDx479mQyyZ5ccNNqVYInoo1fMWdPXAxRjJ61dqSeMm023lFrzupZWgtL8lRiaxHxOfRest4-r9UWQ5sSNwXvopvcpTrvtKfJqGwtWwj-32cr5eFKfbhCoXNT0-kuaxfWzjY0QAPwADJAHEAHSAPkAcoAdSG8US_ctJDTwk4LPS381DZoahlqzR-xEJHBHkoI5g9Fi-ZGC7VXOfSM7ZhkwWjYAmzmiKJ2UgYNZ2QFWe0D0q6-uSbUywI5Ah6BHNFuNl0NloTR3DcR4zu5z5f9vNse3fR1_J7PVsX9sBdvT8rFto29CnytQH_vrxX4viD3I7Hrj3qrt2rqKmLG-8LhYu1FxUtQHE1hkcJphUWqCGBRsGhBJ4xOoWcJRicYlGBQgjNiQhJcTlCQMJuhIGM2M4IgwNyMu87jX4CruVwtbw7GVseRCggLjlQgpkBMAWEBYYGe3fZweP96_vmY7dd72r69znt3_gW2C1UD8gMAAA)" +title = "Win Map with Middle" +caption = "Middle voters make it easy for common ground candidates to win. Move B around." +comment = "" +id = "win_map_middle_sim" +gif = "gif/win_map_middle.gif" + %} + +Let's go back to the beat maps. They show where any candidate would need to go to beat {{ B }} in a one-on-one. It looks like B has most locations locked down except for a white angular area in the center. + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VTy24CMQz8l5xziGPnxWdUvS0cQIBUFZWqpYcKwbfX8ShLS4X24IljT8aT7NkFt5imKr60lZ8iF09UFBFXHyV0lMiT1NXKO-q1hXy0kihaXKw4Ni_NAGutoTp3sVsE78Qt3LU575Its1LpXtEQ_L9Pd-rDnfZwh4JxU9fprksXlk4PVEADxAF4ABkgDZAHKAPUAdpMeKOeuWkmp5mdZnqa-akf0NVGqOXuzNId93urIEEaPlFGUKNINKotPTTLxmCrqPOSBuWLGuB3FCSVhjWAJhasKlbNGrg_gW4fk6UZuhg8DDmsPJNeaf96aUYr-Fj5zu7zZbvbrD_c4vTxtbtoFhcVPVuLhBsDEvT7Insi3if4vkXsPPkrRrKNKgUPDcNJs2TCk0gYLcGixAiwKCUEWJQKVtVOSjA6B2vIMDrDoAyDcjLVrEIyKDIUZPQWKCjoLRGBEWBuwV2X8VNAQsEvQOag9DxGqiCsGKlCTIWYCsIKwgo9m_XhcDw9f7_v9KU9rd9ed1t3-QHvGxz6-gMAAA)" +title = "Beat Maps" +caption = "B beats any candidate in the yellow region. Move everybody to see how their beat maps change." +comment = "" +id = "beat_map_middle_sim" +gif = "gif/beat_map_middle_sim.gif" +%} + +Also, just to make a pretty picture, you can see that if the two middle voters are exactly in the middle, then the regions where the cycle happens become very thin. + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VTy24CMQz8F59ziOM8-Yyqt4UDCJCqolK19FAh-PY6HmVpqdAePHHsyXg2OZOnxTTV6Kqs3BSkOOaiiKW6EH1HiR3Hulo5YqsVF6wkSHOcextzc6l1EKvV9q6kqPUuoYV3FGlB10aOki2zUule0eDdv0936sOd9nCHvXFz10nXJfkl6YEKeIAwgAwQB0gD5AHKAHWANhPeqGdunsl5ZueZnmd-7gd0tQFqpTuzpON-bxUckYZPnBHUKI4a1ZYemmWDt1XQeVmD8gUN8DtEJJVGNIAmFKwqVs0apF-Bbp-wpQW6BDwCOaI8k16E_vXSjFbwifKd6fNlu9usP2hx-vjaXTSLHxWcWEv0NwYk-PeP7Ilwn5D7lmjnxb9iYrZRY8FFw3CxWTLhSiSMlmBREqNJsCglBFiUClYVJTA6e-vLMDrDoAyDcjLV-g4ogyJDQUZvgYKC3hIQBAHmFvzrMh4FJBQ8ATYHY89jpArCipEqxFSIqSCsIKzQs1kfDsfT8_f7Tm_a0_rtdbelyw-7tzRV-gMAAA)" +title = "Thin Beat Maps" +caption = "" +comment = "" +id = "thin_beat_map_middle_sim" +gif = "gif/thin_beat_map_middle_sim.gif" +%} + +In an election, there are many more voters (the dots below), and {{ B }}'s beat map is a circle (close to it). + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VTQW4CMQz8i885xHYSZ3lG1RtwAMGhKmorSg8VgrfXyZBFqEJ78NhOJuNJ9kyRFstljYGZ12EpYoGlOuKUguSpIctBYlmvA3FbzEmC17xTahC1tk2Ll_piR8XaWqVFDJRoQdeJAuWeFifwnnmI4d_nnfq0Mz3tcOzc3NTRdUVxRX6gAx5ABtAB0gB5gDKADVAHmGbCO_XMzTM5z-w80_PMz-2Aplag1v1RDwkZ7OGC4P5w8uhutDD1qsSeiY_JHpxGPMBmSSjmTiqgEUNWkU19g7YLb64p97JCjoJHIUdzN5tvBmvBVuuHqPOd6fttt99ujrQ4HX_2F6_ifiRo8IdEKd6vq6X8mMpj6ocvOfSvpanrTvmxWvrpyfCuMFSaejHjBWSMlGFNVvRgTc4IsCZjlFyxEgaX2LMCgwuMKYoiHFEXUkBRoKBgr0GBYa8JAhQYTDXcsY1_ABKs3qxuzqVWx0gVhBUjVYipuKUKwgrCCj3bzeHweXr9_dr7L_ey-Xjf7-jyBzeDGAjgAwAA)" +title = "B's Election Beat Map" +caption = "Only candidates more near the middle can win. Move A, C, and D to make them win." +comment = "" +id = "election_beat_map_middle_sim" +gif = "gif/election_beat_map_middle.gif" +%} + +Let's look at all the candidates' beat maps to show that there is a cycle between {{ A }}, {{ C }}, and {{ D }}, but {{ B }} beats them all. Also, the white spot in the middle is where a new candidate could win. + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VTQW4DIQz8C2cO2AYDeUbV2yaHRNlD1ait0vRQRcnbaxix0aqKVlqPjT0Mw-7VBbeZphI8Ee38xJw9cTFkb8-iDdXkqdTdzjtqzRTZU062oq0ltzFRK9XWbEhz6xW3Cd5Ft3H36rxLPVUjsLVsIfh_j62Upyv16QqFzk1NnbtvXdg629AADcADyABxgDSADpAHKAPUhfBBvXDTQk4LOy30tPBT26CpZag1f8RCRAZ7SBHMH4oWzY0Waq9y6BnbMcmC0bAF2MwRxdRJGTSckRVktQ9Iu_DmmlAvC-QIeARyxHgm8v1prYrR3DcR47u677fjfNif3eZy_plvVsX9sJc-EsPjulpK65TXqay2i7HrjmsRUfvuMeO7wqFi7cWELyDhSAnWJEGANSkhwJqEo6SCFhisoWcKgxXGKFg0dcViQhQUCgWK2QwFGbOZEQQBpmbccR7_ACRkfPHUnYutjiMVEBYcqUBMwS0VEBYQFug57E-nz8vr79dsv9zL_uN9PrrbH9CaVlbgAwAA)" +title = "All Beat Maps in Election" +caption = "The spot in the middle is where a new candidate can win." +comment = "" +id = "all_election_beat_map_middle_sim" +gif = "gif/all_election_beat_map_middle.gif" +%} + +These examples are made to show particular concepts, and a real election would have a wider spread of voters and candidates. + +### Median Math - How to Make a Beat Map + +To get a more fundamental understanding of Condorcet cycles, let's talk about how this beat map is related to the median. In this section, I'll show that the reason we have Condorcet cycles is because there's no one single position in two dimensions that would beat all the other positions head-to-head. + +The median works in one dimension just like the Condorcet winner. If you compare the median to any other position, then, in a vote, it would beat the other position. + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA21SMU4EMQz8i-sUsWPHyb6Cgm51xSFRIK0EQkdxQvB2HI8OCk5bzNqOxzNOPqnStu-ss_R5KjtPL15Pp0J8y7PZihttUkhpo-9KhYy2WqjHoah5QC3_vqiMqNA3a6e75ZllaXa_zDWnMGMMC8IGUACUcAeEFNbAkXI5BnAhCZ5ICmdSBAAaURwJmhYAGnFEA9HMhlbTJq9lcBYaBDXspkFQmNl2xrGOAtja_O3XpFo_y1pm5JYJsr2tH80u_aOLLS6p6jlHIU5h0bAqgzCDRWtJYbBoBoBFA4sNHJm4z5p9HSwd9joW1S3lL2kdFB0KOnodChy9LjnMGwCrcdyV356NoziwlyIrBzsDZIMRQciAkKEpchgAWga4ns7H8Xp5vL49x0N9OD7ez8fL5UpfPzIyeM7oAgAA)" +title = "Median Beats All in 1D" +caption = "Candidate A takes the position at the median of the position of voters and wins. Move B to show there's no other position that beats A." +comment = "maybe continue using candidate B for the middle" +id = "median_beats_1d_sim" +gif = "gif/median_beats_1d.gif" +%} + +In two dimensions, the median doesn’t work this way. Finding the median in two dimensions actually has many definitions. + +One definition is called the geometric median. It minimizes the distance between itself and all the points. More precisely, it minimizes the sum of these distances. A more familiar concept is the mean, which minimizes the sum of the squares of these distances. + +The geometric median works in this simple symmetric case: + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA22SsW7DMAxE_0WzBpESJdlf0aGbkSEFOhQw0KJIh6Bovr00H9KlgWGc6BNPd5S_U0nrtomV7O8pb1KXrMVOp5wkmLY4o0dd01pyamlNt5Jysii7b3JuOJT873FmOpNu0np6SC9Ba7XHtJQ4RYRjRCmxIg3AiXTArUhz9IPVwQ-QnNR1_KNKfFQFkNHGFpepDsjooJpUSzTUEjHlGIYEUTFUKzyGPMy6SfivHQK1uvz1t5A6FnJf6H3hYpseixZdDbno6mG1Da4Cc42IxqgMY0ZEqyFhRDQDiGiDarJl4T5L9HVUOvE6g-oW9qsb6Uh0HHR6Bw4GvUNDflSA0Qzuatx_GyyMyVzykXoQZyI2hQojEyOzhclpAF4mWi_nfX-_PF8_Xv1Hfdq_Ps_72-Wafn4BRE0OAuoCAAA)" +title = "Median Beats All in 2D" +caption = "In this circular distribution of voters, the candidate that positions themselves at the geometric median beats all other candidates." +comment = "" +id = "median_beats_2d_sim" +gif = "gif/median_beats_2d.gif" + %} + +The geometric median doesn't work in the multi-modal distributions we've been looking at that have Condorcet cycles. + +You can see that the zone of positions that could beat this median is pretty small compared to other ideas for where the median would be. Maybe that's a good figure of merit, but it doesn't quite describe the idea of a single median. + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VTu27DMAz8F80aTFKiZH9G0c3JkCAeigZtkaZDESTfXooHOQiKwANf0vF4lC9hCNM8Uxojad7GmUqOPOh2GwOhwtFyVtEaWYo5LGqpsR02T0s7K2EaYkhhCrcxxJA9VAOwWjEzxH-fVerTyvi0QoNjU2MXbpswbII1NIe6w92R7qTu5O5od0p3anfGFfAOvWLTCk4rOq3wtOJTa9DYMtiaPmImIYI8pDCmDyWzpgabGT3LNqYl2cYkM-w1hsyckMwOyoDhgqgiGv2CtO021YQ8LaAjwBHQEcOZycUVxbXiDcSwLuH77bDsd6cwnU8_y9Wy2A3Z8TTc19RCegz5MRRv418Lk_NNaO4Z9a6p4C1hkDR6MmPrGWNkyJGhaoYcOcNAjowRMkTNEFUHjxQoCjFUkMzOVoyIAkLBQHG3gEHB3cIOXwQGQhbstfR3DwoFr5wiR4mp5TFSBWAFYAWZis1UAFYAVvCpwNvvjsfP8-vv12J_28vu4305hOsfg2mgwsgDAAA)" +title = "Median Doesn't Quite Beat All in a Multi-Modal Distribution" +caption = "In this triangular distribution of voters, there is a small white region near the center where a candidate can take a position to beat the candidate positioned at the median." +comment = "" +id = "median_2d_multimodal_sim" +gif = "gif/median_2d_multimodal.gif" +%} + +There's another definition of median you could think of. You could project to one dimension and find the median as usual. + +I used these projected medians to make the beat maps. The median isn't a single position. There’s a projection for every angle, so you have a median associated with each angle. Just choose an origin, and you can make a plot. So I used each candidate as an origin and found where the median voter was. Then I doubled that distance to see a position that would tie with the candidate. That made the beat map. + +By the way, there’s also something called a windmill problem that 3Blue1Brown made a video about, and this is actually a really good visual for a two-dimensional median. Another interesting way to find a median is Tukey's half-space depth. + +To restate the main point of this section, the reason we have Condorcet cycles is because there's no one single position in two dimensions that would beat all the other positions head-to-head. + +In the voting systems to follow, I added the beat maps to try to make voting more visually understandable. + +I’d also like to point out that, in a case where you have to resolve a cycle, really any of the candidates would be an okay choice. The differences between Condorcet methods are just in how they go about resolving the cycle. + +## Specific Condorcet Methods + +A variety of voting methods will pick the Condorcet winner if it exists. I'm just going to go through a couple different methods: Minimax, Schulze, an alternative way to count Schulze, and Ranked Pairs. I think Minimax is the simplest, so let’s start there. + +### Minimax + +Minimax is a Condorcet method. That means if there's a guy that beats everyone head to head, then that guy wins. That's easy. It's a clear winner. Sometimes there isn't a clear winner. Then you go through some rules. If you're just finding one winner, then the rules are pretty simple. You want to find who lost by the least, basically. So just start crossing off all the smallest losses until there is that guy that has no losses. **Try it out below.** + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSW44DIQy7C9_5IO8wVxn1JNXu2TdgVaq6qubDOCHGmHmOOa77XkKxHnQLT8q5F-bEJb1iWyS-a65d4seDBu8Zntx8nS1Fi_diOcmUvUXHNWnYuMZvDBo-LqYRPde9bJj07-tOfe2srx2e5yhmiLOAwgAbwAEBaANsjX2cN7S20JDW6aLwsSoCgIwYWMtoA2QkwQpsHdB5jPKOAEoKQwolhZK20t354dub45hQaCquyySkZN22-br_JvxO5J3oS3YTO1r2eZTFsWCJJ4J9W6foCNMZIMeUKwDWHW_pCMETvTpnOUKIeYqBAAIBBAIIP361jQQkAg4CswkHidmEg1QwvGbiNfP1OyWa9ZFZ4koFwcKVCmYKZgqCBcHafn7-AEVp81gTAwAA)" +title = "Minimax" +caption = "" +comment = "" +id = "minimax_sim" +gif = "gif/minimax.gif" +%} + +### Schulze + +Schulze breaks ties in a way that aligns with the intent of a Condorcet winner; Schulze takes all the pairs of candidates and strings the pairs together into chains. At the beginning of the chain is the winner and at the end the loser. The strength of a chain is only as strong as its weakest link. There are many chains between any pair of candidates. The strongest chain is found for each pair. Magically, there is only one candidate who goes undefeated. This is a really cool way to break ties. **Try it out below.** + +There's an excellent website that Rob LeGrand created that counts ballots using many more Condorcet methods than I have programmed so far on this site, so I have included his code directly into this site. You can see the explanations are a little bit different in formatting. + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3WSwU4DMQxE_yVnH2I7dpz9DMRt1UMRPVEBBy4IwbfXyahSVVTtYTJxMnlx9qfUsu37EPJxoF24Uq9z0Iw4JEfcBonNOdOc4sOBCs89XDn9WEuCBs_BMJIqc4mWrVJpZSt_WqhY2ZiK576s9ZRK_76sxMPKeFjhuo5iRjgLLAC4QQzikATgxOE8zlIyW6hI5rQUXqgiEMRIg8sYTUGMdLiAG0u0LlCeLUCSAkiRpEjSTNqzf_jmYl8QikzFdZmElFqWW73efxq-NXJr9Bo7TVtZ7f6o5guhdTwR8NtYk4ZmGkNkQZlCgG54S0MTrKMW6yxDE7yuSUcDHA1wNMBt8WqCOCIcBI69HQQdezsIusLhNTtes19_p45i3PWs40qBwMCVAjABmEBgIDDA83I8nz--nr8_T_n3Ph3f306v5fcC57Zt6SkDAAA)" +title = "Schulze" +caption = "" +comment = "" +id = "schulze_sim" +gif = "gif/schulze.gif" +%} + +### Schulze Alternative + +Here's an alternative way to count Schulze that is a lot like Minimax, but with an extra rule. Also, I may have the implementation incorrect, but it seems to work. Again, this Schulze Alternative breaks ties in a way that aligns with the intent of a Condorcet winner; there's a tie, so it sees who is tied, and just picks from that group. The candidates with the least losses are automatically in that group, and also we add in anybody that beat them (and anybody that beats anybody we add). Then we have a group that beats everyone, and we pick from that group. After that, it's just minimax. Resolve the tie by making the smallest change you can; remove the smallest tie. And keep making that Condorcet group each step (this is called the Schwartz set if you want to tell your friends). **Try it out below.** + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3WSwU4DMQxE_yVnH2I7jpP9DMRt1UMRPVEBBy4IwbfX8ahSVVTtYTJxMnlx9qfUsu37FOrzQLtwJa9r0Ix4SIy4TRJbc6YxxYcDFV57uHL4mUsGTV6DaSRV1hItW6XSylb-vFCxsjGVHvui5iGV_n1RGQ8r82GFax7FjHAWWABwgxikQwKAA4fjOAuJbKEikdNCOFFFIIiRBhcxGoIYcbgBN1O0JiivFiBJAaRIUiRpJO3RP3xrcU8IRabiukxCSi3KrV7vvwzfGrk1eo1dpmVWuz-q9URojicCfps5aWimMUQSyhQCdMNbGppgjtrIswxN6DUnOxrQ0YCOBnRLXg2QjogOgo69DgLHXgeBKxxe0_Gafv2dHMVx1zPHlQYCB640ADMAMxA4EDjA83I8nz--nr8_T_H3Ph3f306v5fcCdowcpSkDAAA)" +title = "Schulze Alternative" +caption = "" +comment = "" +id = "schulze_alt_sim" +gif = "gif/schulze_alt.gif" +%} + +### Ranked Pairs + +Ranked pairs is a lot like Minimax. If there's a clear winner that beats everyone head-to-head, then they are the winner. If there isn't a clear winner, then we follow some rules. If you're just interested in the #1 winner and not #2, then the rules are pretty simple. You want to find who won by the most, basically. So start crossing off candidates that lost by a lot. Unless they are the last one left and the guy they lost to already had a bigger loss. Also, you can just stop once you figure out who the winner is. You don't have to go through the whole list. **Try it out below.** + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3WSwU4DMQxE_yVnH2I7jp39DMRt1UMRPVEBBy4IwbfXyahSVVTtYTJxMnlx9qfUsu37EOrjQLtwJa9z0Iw4JEfcBonNOdOc4sOBCs89XDn9WEuCBs_BMJIqc4mWrVJpZSt_UahY2ZhKz31Z85RK_76sxMPKeFjhuo5iRjgLLAC4QQzSIQnAicN5nKVktlCRzGkpvFBFIIiRBpcxmoIYcbiAG0u0LlCeLUCSAkiRpEjSTNqzf_jm4r4gFJmK6zIJKbUst3q9_zR8a-TW6DV2mray2v1RrS-E5ngi4LexJg3NNIbIgjKFAN3wloYmmKMW6yxDE3pdkx0N6GhARwO6LV5NkI6IDoKOvQ4Cx14HgSscXtPxmn79nRzFuOuZ40qBwMCVAjABmEBgIDDA83I8nz--nr8_T_n3Ph3f306v5fcCmdLP3ykDAAA)" +title = "Ranked Pairs" +caption = "" +comment = "" +id = "ranked_pairs_sim" +gif = "gif/ranked_pairs.gif" +%} + +### Even More Condorcet Methods + +Rob LeGrand's site has a lot more voting methods, so try them out in the simulation below. The explanations in the sidebar are very thorough and explain the way in which the winner was selected. One detail is that he uses a tie-breaking ballot whenever he runs into a regular tie (not a cycle) + +{% include sim.html +link = "[link](https://paretoman.github.io/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3WSsU4EMQxE_yW1i9iOE2c_A9GtrjjEVZyAggYh-HYcDyedDp22mEycTF6c_Sq1bPs-hfo80C5cadQ1aEbsEiNuk8TWnGlM8eFAhdcerhx-5hKnyWswjaTKWqJlq1Ra2cqPFipWNqbSY1_URkilf19U_G5l3q1wzaN4QfHyAg8CbhCDdEgQcPBwnGchES5UJIJaCCerCAQx0uAiRkMQIwPO4WaK1iRdJIokBZAiSZGktnDp71uLe0IoMhX3ZRJSalFu9dKAZfjayLXRS-wyLbPa7VGtJ0IbeCPgt5mThm4aQyShTCFANzymoQk2UPM8y9CEXnOyowEdDehoQLfk1QDpiOgg6Ng7QDCwd4BgKBxec-A1x-V_Gij6Tc8GruQIdFzJAeOAcQQ6Ah08T8fz-e3j8fP9FL_vw_H15fRcvn8Bl3tygyoDAAA)" +title = "More Methods" +caption = "Rob LeGrand did a lot of work to include all these Condorcet methods, and some that aren't." +comment = "" +id = "robla_sim" +gif = "gif/robla.gif" +%} + +## Strategy + +In each of these Condorcet methods, voters would have a hard time trying to use a strategy. What I mean by strategy is that you might say you like someone less than you actually do. You might put a viable competitor low on your ballot below people you actually don’t like as much in the hope that the tiebreaker would work in your favor. However, these strategies are hard to think about because they can backfire. + +**Backfiring** - Jameson Quinn's study of how often strategies backfire versus work. The Condorcet methods Schulze and Ranked Pairs (Rp) have a high likelihood of backfiring and a small likelihood of working. + +backfire + +Pair counts are never affected by strategy. It’s only in a cycle-breaker that voters would even think about using strategy, because cycle-breakers don’t use just plain pair counts. In a Condorcet cycle, candidates are virtually in a tie, so candidates will always feel a pressure to move toward the middle, and there will always be a spot in the middle for a new common ground candidate to enter the race. + +## Afterword + +You may have noticed the above examples seem to all be ties, so what does it matter which condorcet method we choose? They all seem to be about the same. As long as we pick one, we’ll be okay. + +The benefit of Condorcet methods is that voters don’t split the vote and candidates don’t need to fight each other to avoid splitting the vote. By using a Condorcet method, we end up finding common ground. + +### Links + +[Jameson Quinn's VSE Study on Strategies](http://electionscience.github.io/vse-sim/VSE/). + +[Rob LeGrand's Calculator](http://www.cs.angelo.edu/~rlegrand/rbvote/calc.html) + +### Sandbox Notes + +In the sandbox mode below, you can find the visualizations under the "viz" menu and Rob LeGrand's methods under the "RBVote" option for voting system. \ No newline at end of file diff --git a/css/index.css b/css/index.css index 733d8021..71dec236 100644 --- a/css/index.css +++ b/css/index.css @@ -1,23 +1,86 @@ + +html { font-size: 16px; } + +/** Reset some basic elements */ +body, h1, h2, h3, h4, h5, h6, p, blockquote, pre, hr, dl, dd, ol, ul, figure { margin: 0; padding: 0; } + + body{ margin:0; font-family: Helvetica, Arial, sans-serif; - font-size: 22px; color:#333; font-weight: 300; + min-width: 380px; } b, strong{ font-weight: bold; } +/* colors */ + a{ color: hsl(240,80%,70%); } a:hover{ color: hsl(240,80%,80%); } -#content{} + +.letter{ + -webkit-text-stroke: 0.5px black; +} +.letterBig{ + -webkit-text-stroke: 1px black; +} + +#sandbox-section, +#sandbox-index-section, +#sandbox-embed-section, +.banner, +.sim-ballot, +.sim-test { + /* background: #EFE9D5; */ + /* background-color: #e6e6e6; */ + /* background-color: #bed7dbd2; */ + background-color: #dfebec; +} + +#content{ + font-size: 20px; +} .words{ + max-width: 900px; + margin: 0 auto; + padding: 0px 20px; +} +.words-original{ width: 670px; margin: 0 auto; } + +.video-container { + overflow: hidden; + position: relative; + width:90%; + border: 10px solid black; + border-top-width: 0px; + border-bottom-width: 4px; + border-left-width: 7px; + border-right-width: 12px; + margin: auto; +} + +.video-container::after { + padding-top: 56.25%; + display: block; + content: ''; +} + +.video-container iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + #content p{ - text-align: justify; - line-height: 1.7em; + text-align: left; + line-height: 1.5em; color: #333; } #content p.caption{ @@ -34,6 +97,12 @@ a:hover{ color: hsl(240,80%,80%); } display: block; height: auto; } + +.words > img { + display: block; + margin: 0 auto; +} + #content p .underline{ text-decoration: underline; } @@ -48,39 +117,53 @@ hr{ margin: 50px 0; } li{ - margin-bottom: 0.4em; + margin-top: 0.4em; } #content .ballot_title{ - font-size: 1.75em; + text-align: left; + max-width: 540px; + font-size: 1.35em; margin-bottom: 0.4em; position: relative; - left:5px; + left:0px; + padding: 0px 20px; + padding-top: 20px; } #content p.ballot_caption{ margin-top: 0; font-size: 0.8em; - text-align: center; - width: 260px; + text-align: left; + max-width: 540px; line-height: 1.2em; + padding: 0px 20px; + padding-bottom: 20px; } #content p.caption-test{ font-weight: bold; font-size: 1.2em; line-height: 1.4em; } -.sim-intro, .sim-ballot, .sim-test, .sim-sandbox{ - margin: 0 auto; +.sim-container{ + margin:auto; } -.sim-intro{ - width: 600px; +.sim-intro, .sim-ballot, .sim-test, .sim-sandbox{ + display: flex; + text-align:center; + padding: 0px 20px; } .sim-ballot{ - width: 670px; - margin: 35px auto 50px auto; + margin: 35px -20px 50px -20px; } .sim-test{ - width: 800px; - margin: 50px auto 40px auto; + margin: 50px -20px 40px -20px; +} +.contain-model{ + position: relative; + margin: 0px auto 25px auto; + text-align: center; +} +#content { + /* padding: 0px 20px; */ } #content p.quote{ border-radius: 5px; @@ -94,46 +177,71 @@ li{ color: #000; font-weight: bolder; } + .banner{ - margin: 50px 0; - background: #EFE9D5; - width: 100%; - height: 200px; + font-size: 22px; + color: #222; border-bottom: 5px solid rgba(0,0,0,0.2); + text-align: center; + padding: 10px; + margin-bottom: 30px; } -.banner img{ - margin: 0 auto; - display: block; - width: 850px; - height: 200px; -} + +.banner h1, +.banner p{ + margin: .3em 0em; +} +.banner p{ + font-size: .7em; +} +/* .banner p{ + font-size: .7em; + margin: .3em 0em; +} +.banner h1{ + margin: .3em 0em; + font-weight: bold; + text-shadow: 0px 0px 3px #555; + text-transform: uppercase; +} +@supports (-webkit-text-stroke: 2px #555) { + .banner h1 { + -webkit-text-stroke: 2px #555; + -webkit-text-fill-color: white; + } + } */ + + #splash{ position: relative; overflow: hidden; margin-top: 0; - height: 400px; + height: 200px; } #splash img{ position: absolute; width: 960px; - height: 200px; + /* height: 200px; */ margin: auto; top:0; left:0; right:0; bottom:0; pointer-events: none; } -#sandbox{ - background: #EFE9D5; - overflow:hidden; +#sandbox-section{ margin-top: 50px; +} +#sandbox-section, +#sandbox-index-section{ + overflow:hidden; border-bottom: 5px solid rgba(0,0,0,0.2); - padding: 30px 0; } -.sim-sandbox{ - width: 800px; +#sandbox-section, +#sandbox-index-section, +#sandbox-embed-section{ + padding: 30px 0; } -.sim-sandbox iframe{ - background:#fff; - border:20px solid #fff; +.sim-sandbox .div-sandbox{ + /* background:#fff; */ + /* border:20px solid #fff; */ } #end{ font-size: 18px; @@ -250,4 +358,104 @@ ul.share-buttons .sr-only { height: 1px; width: 1px; overflow: hidden; +} + + +/* for "newer.html" An Even Better Ballot */ +.banner .notation { + /* font-size: 30px; */ + color: rgb(255, 0, 0); + font-family:'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif; +} +.banner .strike { + text-decoration:line-through; +} +/* end */ + +/* card stuff */ + +/** CONTENT **/ +#content div.card { + display: inline-block; + vertical-align: top; +} + +#content div.card > a > div{ + + display: inline-block; + overflow: hidden; + width: 290px; + margin-bottom:20px; + /*float: left;*/ + + -webkit-transition: opacity 0.2s ease-in-out; + -moz-transition: opacity 0.2s ease-in-out; + -ms-transition: opacity 0.2s ease-in-out; + transition: opacity 0.2s ease-in-out; + +} +#content div.card > a:first-child > div{ + margin-top:20px; +} +#content div.card > a{ + text-decoration: none; + display: inline-block; +} +#content div.card > a:hover > div{ + opacity: 0.5; +} +#content div.card > a > div > img{ + width: 290px; + margin-bottom: 5px; +} +#content div.card > a > div > img.drawBorder{ + width: 286px; + border: 2px solid #ccc; +} +#content div.card > a > div > div{ + + color:#000; + font-size: 18px; + line-height: 21px; + text-align: left; + + width:100%; + +} + +#content div.card > a > div > div > span{ + color:#aaa; +} + + +/* end card stuff */ + +#content div.picture-container { + text-align: center; +} + +#content img.picture { + display: inline-block; + width: 290px; + height: unset; + vertical-align: top; + margin-bottom: 20px; +} +#content img.picture100 { + display: inline-block; + width: 100%; + height: unset; + vertical-align: top; +} + +p#editnote { + font-size: 0.8em; +} +p#editnote { + text-align: center; +} + +img.gif_show { + max-width: calc(100vw - 60px); + margin-bottom: 20px; } \ No newline at end of file diff --git a/css/index_original.css b/css/index_original.css new file mode 100644 index 00000000..6da49bb6 --- /dev/null +++ b/css/index_original.css @@ -0,0 +1,259 @@ +body{ + margin:0; + font-family: Helvetica, Arial, sans-serif; + color:#333; + font-weight: 300; +} +b, strong{ + font-weight: bold; +} +a{ color: hsl(240,80%,70%); } +a:hover{ color: hsl(240,80%,80%); } +#content{ + font-size: 22px; +} +.words{ + width: 670px; + margin: 0 auto; +} +#content p{ + text-align: justify; + line-height: 1.7em; + color: #333; +} +#content p.caption{ + line-height: 1.5em; + text-align: center; +} +#content p img{ + height: 1em; + position: relative; + top:3px; +} +#content p img.real{ + margin: 0 auto; + display: block; + height: auto; +} +#content p .underline{ + text-decoration: underline; +} +iframe{ + border:none; + display: block; + margin: 0px auto 25px auto; +} +hr{ + border: none; + border-bottom: 5px dashed #ccc; + margin: 50px 0; +} +li{ + margin-bottom: 0.4em; +} +#content .ballot_title{ + font-size: 1.75em; + margin-bottom: 0.4em; + position: relative; + left:5px; +} +#content p.ballot_caption{ + margin-top: 0; + font-size: 0.8em; + text-align: center; + width: 260px; + line-height: 1.2em; +} +#content p.caption-test{ + font-weight: bold; + font-size: 1.2em; + line-height: 1.4em; +} +.sim-intro, .sim-ballot, .sim-test, .sim-sandbox{ + margin: 0 auto; +} +.sim-intro{ + width: 600px; +} +.sim-ballot{ + width: 670px; + margin: 35px auto 50px auto; +} +.sim-test{ + width: 800px; + margin: 50px auto 40px auto; +} +#content p.quote{ + border-radius: 5px; + background: #eee; + padding: 20px; + color: #888; + font-size: 1.2em; + text-align: center; +} +#content p.quote em{ + color: #000; + font-weight: bolder; +} +.banner{ + margin: 50px 0; + background: #EFE9D5; + width: 100%; + height: 200px; + border-bottom: 5px solid rgba(0,0,0,0.2); +} +.banner img{ + margin: 0 auto; + display: block; + width: 850px; + height: 200px; +} +#splash{ + position: relative; + overflow: hidden; + margin-top: 0; + height: 400px; +} +#splash img{ + position: absolute; + width: 960px; + height: 200px; + margin: auto; + top:0; left:0; right:0; bottom:0; + pointer-events: none; +} +#sandbox{ + background: #EFE9D5; + overflow:hidden; + margin-top: 50px; + border-bottom: 5px solid rgba(0,0,0,0.2); + padding: 30px 0; +} +.sim-sandbox{ + width: 800px; +} +.sim-sandbox iframe{ + background:#fff; + border:20px solid #fff; +} +#end{ + font-size: 18px; + line-height: 1.5em; + background: #222; + padding: 30px 0; + overflow: hidden; +} + +/** THE END **/ +#end p{ + color: #999; +} +#end h1, #end h2, #end p b, #end p strong{ + color: #fff; +} +#end h1, #end h2{ + text-align: center; +} +#end a{ + font-weight: bold; + color: #cc2727; +} +#end a:hover { + color: #dd3838; +} +#credits{ + width: 960px; + margin: 25px auto 50px auto; + text-align: justify; +} +#uncopyright{ + width:100%; + height:180px; + margin-bottom: 40px; +} +#appendix_container{ + width: 100%; + overflow: hidden; + padding-top: 2px; +} +.appendix_title{ + color:#fff; + font-size: 24px; + font-weight: bold; +} +#further_reading{ + width:600px; + float:left; + color: #888; +} +.further_book{ + font-size:18px; + overflow:hidden; +} +.further_book img{ + width:100px; + float:left; + margin-right:20px; +} +#supporters{ + line-height: 1.2em; + width:320px; + float:right; + font-size:16px; + color:#888; +} +#supporter_drawings{ + overflow: hidden; + margin-top: 20px; + text-align: center; +} +#supporter_drawings > div{ + display: inline-block; + width:100px; + height:140px; + margin-bottom: 15px; + text-align: center; + position: relative; +} +#supporter_drawings > div > img{ + position: absolute; + width:100px; + height:100px; + left:0px; + top:0px; +} +#supporter_drawings > div > div{ + position: absolute; + width: 80px; + height: 37px; + left:10px; + top:103px; +} + +ul.share-buttons{ + list-style: none; + padding: 0; + text-align: center; + margin-top: 5px; + margin-bottom: 50px; +} + +ul.share-buttons li{ + display: inline; +} + +ul.share-buttons .sr-only { + position: absolute; + clip: rect(1px 1px 1px 1px); + clip: rect(1px, 1px, 1px, 1px); + padding: 0; + border: 0; + height: 1px; + width: 1px; + overflow: hidden; +} + +.sim-container{ + margin:auto; + text-align: center; +} \ No newline at end of file diff --git a/css/some-minima.css b/css/some-minima.css new file mode 100644 index 00000000..b5663cdb --- /dev/null +++ b/css/some-minima.css @@ -0,0 +1,138 @@ + +/* from minima style */ + +/** Set `margin-bottom` to maintain vertical rhythm */ +.words > h1, .words > h2, .words > h3, .words > h4, .words > h5, .words > h6, .words > p, .words > blockquote, .words > pre, .words > ul, .words > ol, .words > dl, .words > figure, .words > .highlight { margin-bottom: 15px; } + +hr { margin-top: 30px; margin-bottom: 30px; } + +/** Images */ +.words > img { max-width: 100%; vertical-align: middle; } + +/** Figures */ +.words > figure > img { display: block; } + +.words > figcaption { font-size: 14px; } + +/** Lists */ +ul, ol { margin-left: 30px; } + +li > ul, li > ol { margin-bottom: 0; } + +/** Links */ +a { color: #2a7ae2; text-decoration: none; } +a:visited { color: #1756a9; } +a:hover { text-decoration: underline; } +.social-media-list a:hover, .pagination a:hover { text-decoration: none; } +.social-media-list a:hover .username, .pagination a:hover .username { text-decoration: underline; } + + + +/** Code formatting */ +.highlight > pre, code { font-family: "Menlo", "Inconsolata", "Consolas", "Roboto Mono", "Ubuntu Mono", "Liberation Mono", "Courier New", monospace; font-size: 0.9375em; border: 1px solid #e8e8e8; border-radius: 3px; background-color: #eeeeff; } + +.highlight > code { padding: 1px 5px; } + +.highlight > pre { padding: 8px 12px; overflow-x: auto; } +.highlight > pre > code { border: 0; padding-right: 0; padding-left: 0; } + +.highlight { border-radius: 3px; background: #eeeeff; } +.highlighter-rouge .highlight { background: #eeeeff; } + + +/** Wrapper */ +.wrapper { padding-right: 15px; padding-left: 15px; } +@media screen and (min-width: 800px) { .wrapper { padding-right: 30px; padding-left: 30px; } } + +/** Clearfix */ +.wrapper:after { content: ""; display: table; clear: both; } + +/** Icons */ +.orange { color: #f66a0a; } + +.grey { color: #828282; } + +.svg-icon { width: 16px; height: 16px; display: inline-block; fill: currentColor; padding: 5px 3px 2px 5px; vertical-align: text-bottom; } + + +/** Tables */ +.words > table { margin-bottom: 30px; width: 100%; text-align: left; color: #3f3f3f; border-collapse: collapse; + text-align: center; + word-break:break-word; + /* border: 1px solid #e8e8e8; */ +} +.words > table tr:nth-child(even) { background-color: #f7f7f7; } +.words > table th, .words > table td { padding: 10px 15px; } +.words > table th { background-color: #f0f0f0; border: 1px solid #e0e0e0; } +.words > table td { border: 1px solid #e8e8e8; } +/* @media screen and (max-width: 800px) { */ + .words > table { display: block; overflow-x: auto; -webkit-overflow-scrolling: touch; -ms-overflow-style: -ms-autohiding-scrollbar; } +/* } */ + + +/** Site header */ +.site-header, .site-footer { --text-1: white; --text-2: grey; --back:#222;} +.site-header { background-color: var(--back); color: var(--text-1);} + +.site-header { border-top: 5px solid var(--back); border-bottom: 1px solid #e8e8e8; min-height: 55.95px; line-height: 1.2; position: relative; } + +.site-title { font-size: 1.625rem; font-weight: 300; letter-spacing: -1px; margin-bottom: 0; float: left; } +@media screen and (max-width: 600px) { .site-title { padding-right: 45px; } } +.site-title, .site-title:visited { color: var(--text-1); } + +.site-nav {z-index: 1;} + +.site-nav { position: absolute; top: 9px; right: 15px; background-color: var(--back); border: 1px solid #e8e8e8; border-radius: 5px; text-align: right; } +.site-nav .nav-trigger { display: none; } +.site-nav .menu-icon { float: right; width: 36px; height: 26px; line-height: 0; padding-top: 10px; text-align: center; } +.site-nav .menu-icon > svg path { fill: var(--text-1); } +.site-nav label[for="nav-trigger"] { display: block; float: right; width: 36px; height: 36px; z-index: 2; cursor: pointer; } +.site-nav input ~ .trigger { clear: both; display: none; } +.site-nav input:checked ~ .trigger { display: block; padding-bottom: 5px; } +.site-nav .page-link { color: var(--text-2); line-height: 1.2; display: block; padding: 5px 10px; margin-left: 20px; } +.site-nav .page-link { white-space: nowrap; } +.site-nav .page-link { margin-right: 0; } +.site-nav .current {color: white; } +@media screen and (min-width: 600px) { .site-nav { position: static; float: right; border: none; background-color: inherit; } + .site-nav label[for="nav-trigger"] { display: none; } + .site-nav .menu-icon { display: none; } + .site-nav input ~ .trigger { display: block; } + .site-nav .page-link { display: inline; padding: 0; margin-left: auto; } + .site-nav .page-link { margin-left: 20px; } } + + +/** Site footer */ +.site-footer { background-color: var(--back); color: var(--text-1);} +.site-footer { border-top: 1px solid #e8e8e8; padding: 30px 0; } + +.footer-heading { font-size: 1.125rem; margin-bottom: 15px; } + +.feed-subscribe .svg-icon { padding: 5px 5px 2px 0; } + +.contact-list, .social-media-list, .pagination { list-style: none; margin-left: 0; } + +.footer-col-wrapper, .social-links { font-size: 0.9375rem; color: #828282; } + +.footer-col { margin-bottom: 15px; } + +.footer-col-1, .footer-col-2 { width: calc(50% - (30px / 2)); } + +.footer-col-3 { width: calc(100% - (30px / 2)); } + +@media screen and (min-width: 800px) { .footer-col-1 { width: calc(35% - (30px / 2)); } + .footer-col-2 { width: calc(20% - (30px / 2)); } + .footer-col-3 { width: calc(45% - (30px / 2)); } } +@media screen and (min-width: 600px) { .footer-col-wrapper { display: flex; } + .footer-col { width: calc(100% - (30px / 2)); padding: 0 15px; } + .footer-col:first-child { padding-right: 15px; padding-left: 0; } + .footer-col:last-child { padding-right: 0; padding-left: 15px; } } + + +.post-list { margin-left: 0; list-style: none; } +.post-list > li { margin-bottom: 30px; } + +.post-meta { font-size: 14px; color: #828282; } + +.post-link { display: block; font-size: 1.5rem; } + +/* end from minima style */ diff --git a/draft-321.md b/draft-321.md new file mode 100644 index 00000000..0b80db4b --- /dev/null +++ b/draft-321.md @@ -0,0 +1,37 @@ +--- +permalink: /draft-321/ +layout: page-2 +title: 3-2-1 +description: draft +byline: by Jameson Quinn and Paretoman +twuser: bettercount_us +--- +{% include letters.html %} + +## 3-2-1 voting + +In 3-2-1 voting, voters rate each candidate "Good", "OK", or "Bad". To find the winner, you first narrow it down to three semifinalists, the candidates with the most "good" ratings. Then, narrow it further to two finalists, the candidates with the fewest "bad" ratings. Finally, the winner is the one preferred on more ballots. + +3-2-1 voting is another method that avoids the chicken dilemma by addressing scoring strategies. + +3-2-1 voting is doing something similar to STAR by adding a second additional round of counting votes. It's kind of like there are two rounds of counting approval ballots and one round of counting preference ballots. The first round of approval just counts "good". The second round counts "good" and "okay". + +Important todo: I haven't put good strategies into 3-2-1 yet. Right now, these strategies are okay for this example, but other examples might not be correct. + +{% include sim.html +link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSy2oDMQz8F59NsWTZ1u65n5Dbsoe22ZJASEIelFLab6_kIS2lhD2M9djxjKyPkMI4TXWIpGWOE5VqJ7YTF_45SWSheY6BvJmSRsrJ4xzGFIOEMXxRDjGUMFIM1bqs2AxS_PdZRe9WhrsVSv0ucgnVRGbPMXKQQQIogNq1kKkgMbQ7HYZe5NQjpt7DDAANCyKjyQYVyYZIERkLm3ufHZs-8lGAKzNK4MrgysY1Ueyft1b0gDPDM0WO7kmc1LvEnbpz4dvBSKfeI_1P-UsrcCwNjwKpMvRkwfQKASCyZABEFjxegeHSUNN-U4HhmgCwWjH9Cqu1dBsur4KiQkHFyBsUNPzboKBlRHi5hpdrt_1pKOrvfKJ4HpY0AagzKcQoxCgIFYQKPQo-hSx1WQ_FVg7Cnp92u8Nl9X5cbJtXm9Oy2D6fN4e3x-X8ctoeL9vD3vf8ul8vr9v9sg6f3_tmOp49AwAA)" +title = "3-2-1 Strategy" +caption = "These strategies try to give frontrunners their own score with three levels of support." +id = "ballot10" +comment = "one-voter 3-2-1" +%} + +Here again, the game of chicken is not being played anymore. The Frontrunner strategy doesn't change the result from Normalize strategy. Candidates wouldn't have to go negative against their nearby rivals in order to ensure that their voters would at least be moderately strategic and wouldn't just normalize. + +{% include sim.html +link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3WSQWoDMQxF7-K1KJZkyXbOkV2SRQsJXQRSSjelNGevrE9gSimz-CNL_n6S_VVq2R0OLEzc5okOzIO41_U3G7HN04kKZw07RV3WxN9oWWNRY6tGy65SaWVX7qyFimXssTOSPSSMRWgK1e0XyRHJSn--yMx_M1zTnhcZM7mTz7UsWNaFcSy3y-VYgoUDi0PAxA4JKG6hcb6GxGFCRcI4FoVzhwgEzQlsxHKDwEY6ooEILlqTnEnWZOCl8FLNeoWX2mogCnkVOjbDUefGo9XHKFbA20A2Dk23QUuftj2ieR7bOm4L0G3momGmxhBJGFMIcM0gaN06cuP3tA0z8ApB946bcUzSLTvQIHJ4OVB8pnSgdOztQOmKqCGHy-yPF9aRHI-hkVJb6-htVAgMB2AGYAbmNCxPGeAZ8BvAGgvryeJFAuzl-Xq9few_387R-v71_Xwu3z-WC-vnSwMAAA)" +title = "Not Playing Chicken with 3-2-1 Voting" +caption = "" +id = "election14" +comment = "chicken with 3-2-1" +%} \ No newline at end of file diff --git a/draft-common-ground.md b/draft-common-ground.md new file mode 100644 index 00000000..611e1406 --- /dev/null +++ b/draft-common-ground.md @@ -0,0 +1,53 @@ +--- +permalink: /draft-common-ground/ +twuser: paretoman1 +byline: 'By Paretoman and Contributors, May 2020' +description: An Explorable Guide to Group Decision Making +banner: Smart Voting Simulator +layout: page-3 +title: Home +--- +{% include letters.html %} + +Let's start off by explaining what we want from a group decision. I explain in this video: + +{% include video.html url='https://www.youtube.com/embed/hUYLTh_aSTE' %} + +To find common ground, you have to allow people to vote with more than one candidate. + +Now that we talked about what we want, let's talk about how we can build something and get there. Take a look at this great interactive sketch that Nicky Case made. You have two candidates {{ A }} {{ B }} and you have a voter . Just move the voter and you'll see that he is just gonna vote for whoever he's closest to. + +{% include sim-intro.html id='model1' caption='`click & drag
the candidates and the voter:
`' %} + +You can add more voters and see that you kinda get a sense of how voters are gonna vote as they move around and how an election with those voters would go. Most votes wins. + +{% include sim-intro.html id='model2' caption='`drag the candidates & voters around.
(to move voters, drag the middle of the crowd)
watch how that changes the election:
`' %} + +In this kind of voting, you get a single choice for who you want to win. The ballot is pretty simple. This is the way the ballot is now. + +{% include sim-ballot.html title='SINGLE CHOICE VOTING' caption='Also called First Past the Post.' id='ballotSingle' link='[link](http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRQWrEMAz8i84-RLZkJzn3AT30FnLYdlMaCNllN0tZSvv2Sp4WCiUEIsmSxjPjD2qoH4aSA0sZw8DKlollkT3TcQzEPsKlDdy2Xifqm0BS_0o9B8o20YR_n82W3U672-l2O9zUS9kZeRlRghCDEStCrszYCLBYtOuSha42o-HYYeQ6EyMCYKKg0roQMw4LqhaVoUTzoalE2S0BUgJSAlICUjKkwczEYMYq8BKkcojWEofzGXF5Llfib2KAQ_JE6p78hRQolYJnAUXp6qHCMGUE0FPQ058XxBMqhGqp_BR2KYTmBgEiM1zPQMlaJTi5DIgMBhlWFzAo2C2xIj0fluW0Pd3PE_X0uNwuh2Xe7hTo-nZ6f5iuL5f5vM2n1bpft_U4vc7rdKTPbxSiHLmuAgAA)' %} + +So, what would the suggestions that I made look like? With ranking, this is what the ballot would look like. And it would be counted a little differently. You count for each pair. For each pair, most votes wins. And if somebody goes undefeated, they win the election. + +{% include sim-ballot.html title='PAIR-WISE RANKED VOTING' caption='`again, click & drag`' id='ballotPair' link='[link](http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRQU7EMAz8i885xE7stD3zAsSt6mFhi6hYtSt2EUII3o6T0UpIqMrBsceezDhfFGkYR-uCsE5hZNXAHftNNHqtTFMgri2cUuAkNU80xECZhj6Q0sCBzDti-He8t-wi3S7S7yIc29NcFdVUkEIQZwRFsKaMXQBnj_5c8tA3UJzHi8KtRwQBNJKRaRsQQ7Eg65A5i_geYhPKdSVgSmBKYEpgSs40cmA0GkbBl2CVgziUK13tydVetZvldkk3KLe5_Jcyw2ku-BZIzH0rKhamjAB5CnmKfSm-UGFUwaJYl8KoRQSYNGzdwGLaLCQXYqAwKDCsukBBwWyBgsfD6bRdHz7PMw10f1hf5yMFurxsH3fz5eltOV-XbXXo5309zs_L6vD3L0JAR1KrAgAA)' %} + +And here's approval voting. You can add more candidates and more voters and see how they vote. Approval lets you pick as many candidates as you like. Still, most votes wins. + +{% include sim-ballot.html title='APPROVAL VOTING' caption='`yup, stiiiiill click & drag`' id='ballotApproval' link='[link](http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRwUrFQAz8lz3vockm2bY3wU_wVnp4-ioWSlveeyoi-u1mdxAEKT1MssnOzkw_QxP6YcgWSfIYB1LySrxiKpWOYwxUVii3kdq29Cn0TQwSenJQhxjMV5r47_PlfDhpDyfd4YSa-jYVSaVltFBEAlCAVWXkAkgc_bnk0NUhO48fMtUdZgBoWNBpvcCGw4yuRecs7EE0VSiVTMCUwJTAlMCUnGnwNLFouAq-BKsU2UdS6MqOFHvFrvBv4YRDKoXUe_KXUuBUMv4LJEpXDxWBKQEgTyFPkZfiFyqMaq76FHEpjFoDgElD6gYW02qhiDNQGBQYos5QkHE3c2V6PC3Ldnv42KfQh7t9v2xvpyXEcH3Z3u-n69Nl3m_ztvrw-3U9T8_zOp3D1w9Qth21rgIAAA)' %} + +And if you want to see, here's score voting. It's just kind of a more specific form of approval voting. + +{% include sim-ballot.html title='SCORE VOTING' caption='you guessed it' id='ballotScore' link='[link](http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRQWrDQAz8y551sLSS1va5P2huxoc2cWkgxCFJKaW0b692h0KhGB9mtZJmZ8afqUvjNJVC3NtME-tA7B4n4Uw88DxT4jrCotRLLXMaO0qaRmZKFkDJY6Kjf18Ml81Ov9kZNjvctbe5KqqloIQiVoABvCnjEMAaGM_lgKE1JXjiUrjNiABAI4rK2oI4LguqHlWwSATRNaFcMwFTBlMGUwZTDqaJiTHoWAVfhlUmiZZWujqj1V61q_J7CMIp14O2Pf1LqXCqBf8FEnVol4bAjAGQZ5BnyMvwCw1GrTR9hrgMRr0DwKQjdQeLW7NQxTkoHAocURcoKNgt0pien06n9b77uCxpTI_79bokSrfX9f1hue2vx8v9uJ6j8_12Piwvx_NySF8_76woUaoCAAA)' %} + +And those are the basic vote-tallying methods. You just need to add the rule that the person with the highest tally wins. And now you've got a basic voting method that solves a lot of problems. + +The spoiler effect is one of those problems. Even though {{ B }} should win, the election system is spoiling this. But if you use approval or condorcet or score, then it'll work out. + +{% capture cap2 %}drag {{ C }} to just under {{ B }} to create a spoiler effect.
then compare these four different voting methods:{% endcapture %} + +{% include sim-test.html title='' caption=cap2 id='election31' %} + +There's a lot of details, but that's the gist of it. + +Putting it all together, here's a sandbox for you to try out all the different systems and to make your own scenarios: + diff --git a/draft-digression-median-systems.md b/draft-digression-median-systems.md new file mode 100644 index 00000000..84dc6a8a --- /dev/null +++ b/draft-digression-median-systems.md @@ -0,0 +1,27 @@ +--- +permalink: /draft-digression-median-systems/ +layout: page-2 +title: 3-2-1 +description: draft +byline: by Jameson Quinn and Paretoman +twuser: bettercount_us +--- +## Digression: median systems + +[skip this digression](newer#lightly-strategic-voting-normalization) + +Ever watched the Olympics? In events like figure skating, there are a number of Olympic judges from different countries. In order to prevent any one judge from having too much influence, they use a "trimmed mean": they throw away the highest and lowest scores before they take the average. + +Olympic judges are supposedly supposed to be unbiased. But for voters, there's nothing wrong with having political opinions. (I mean, obviously your opinions, dear reader, are the best, and everybody else should just listen to you. But much as I'd like to, I can't force them to.) So let's imagine voting used the olympic system. + +Throwing away just one highest and lowest score, with thousands or millions of voters, obviously wouldn't make an appreciable difference. So we'd have to throw away some more scores. And, why not? Let's throw away a few more while we're at it. + +When should we stop? When there's just 1 or 2 scores left to take the average. But if you do that, most people wouldn't called that a "trimmed mean" anymore; they'd just call it the median, the middle number. + +There are several voting methods that use the median to find the winner. (It turns out that can lead to a lot of ties, so you need a tiebreaker system to avoid that.) In the 1910s, over a dozen US cities, starting with Grand Junction, CO, used "Bucklin voting", a median-based system using a hybrid ranked/rated ballot. More recently, voting theorists Balinski and Laraki have proposed Majority Judgment, a median-based method using a rated ballot that's now been used in a number of competitions. + +Using the median reduces the incentives for a single voter to strategize. After all, unless you happen to be the exact median rating for a candidate, the only thing that matters about the rating you gave them is whether it's above or below the median. + +Still, large groups of voters can still get a strategic advantage. The larger the voting bloc, the greater the chance is that the median rating for a given candidate happens to be a voter in that bloc. + +I used to think that median voting methods were the best. But then I did the simulations I'll talk about below, and they were merely OK; not much better outcomes than approval voting, but without the simplicity. I still think they're hugely better than FPTP and a bit better than IRV, but I'm not sold enough on them for it to be worth my time programming them in to Nicky's simulator. \ No newline at end of file diff --git a/draft-gerrymander.md b/draft-gerrymander.md new file mode 100644 index 00000000..bd0967f4 --- /dev/null +++ b/draft-gerrymander.md @@ -0,0 +1,41 @@ +--- +permalink: /draft-gerrymander/ +layout: page-3 +title: Condorcet Methods +banner: Condorcet Methods Explained +description: An Interactive Guide to Alternative Voting Methods +byline: By Paretoman and Contributors, June 2019 +twuser: paretoman1 +--- +{% include letters.html %} + +I'd like to talk about gerrymandering. Districts are not very good. Drawing districts is bad. + +## Intro to Condorcet + +This is how you amplify your majority. + +{% include sim.html +link = "[link](https://paretoman.github.io/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSS47FMAi7S9Yswj_pVap3ktHM2YdgdfVUdWETEmOgP2OO6755K7Hoh4ptEvFmTDq52SL1aBZkNoupS73Iw2yRqBwWTLK5mdQLazbJ1D8fGnwqScWy48Q6rknDxjX-5qDhHUZdqlwWTPr6KrNeM_s1w9UkF3DfUMpzJl2P4YINABMcgHLBVlg1-cS7j6XU6lS4RaV0pEARGaB0tAA6kogWot0PdLYZPoPgTigcqSIPR1pKN9PXd54FLkJd0X5toBo0cgpKWrTronWpQ_gh8pAqdush1ir2Xs6qnBck1oZmbHe_PjtyNOIYiWO0jkYco3WMxBNXVld1jCQmAIMNjCOgEo7dlZGARMBBYCkJB4m3KQAsJeEg4SCfXyyRXM_czhnaWRBbaGfByIKRZW1yOeDs4PcfK4WSAkYDAAA)" +title = "Amplify Majority" +caption = "" +comment = "" +id = "1" %} + +Ideally, we would draw districts to exactly match the voters. + +{% include sim.html +link = "[link](https://paretoman.github.io/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSu2pDMQz9F88aLMmy7HxFh24hQwodChdaSjqE0n57ZZ0GQkK4w9HDko6O7nepZbff8zSaeqAwnKS2tCYpz7CkOjVZWWEmEw9LTWmONBoJw-qk0tOKAvZ_y0QOByq85lgdNNZjNzJeb2VyVNl6GwOlriR7jfq-qrTsKpVWduW3FiqWbo9WkfOASndfZMbDzHyY4RCCAzhfKPmKSc5jsOAGAAnugGDBLTBm8vJnhiW6RVQ4m0r0kQCF1wDRRwPQRxzegDezQGuS4SUEZ0LBSBV5MNLotGe6-1ZZx0N0V6zPJLFgI6NOToPiyKXVa2VWgG8DchvQ67Er0HJKe0ynBR0LcJwVy7aZelhNz7CoQTKD9AbJDNIbJDPHk5FTDZL1CoDwHXJ1dOmG2waRjhYdDDqO5mDgqHUB4GgOqR0M_PILOpLjouuKYZ2BZgPrDBAZIDJakhwGwI1ejtv2fno-f7zG3_60fX0et7fTufz8Ab6TmCGjAwAA)" +title = "Votes Match Seats" +caption = "" +comment = "" +id = "2" %} + +Candidates could match voters a little better. + +{% include sim.html +link = "[link](https://paretoman.github.io/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSu2pDMQz9F88aLMmy7HxFh24hQwodChdaSjqE0n57ZZ0GQkK4w9HDko6O7nepZbff8zSaeqAwnKS2tCYpz7CkOjVZWWEmEw9LTWmONBoJw-qk0tOKAvZ_y0QOByq85lgdNNZjNzJeb2VyVNl6GwOlriR7jfq-qrTsKpVWduW3FiqWbo9WkfOASndfZMbDzHyY4RCCAzhfKPmKSc5jsOAGAAnugGDBLTBm8vJnhiW6RVQ4m0r0kQCF1wDRRwPQRxzegDezQGuS4SUEZ0LBSBV5MNLotGe6-1ZZx0N0V6zPJLFgI6NOToPiyKXVa2VWgG8DchvQ67Er0HJKe0ynBR0LcJwVy7aZelhNz7CoQTKD9AbJDNIbJDPHk5FTDZL1CoDwHXJ1dOmG2waRjhYdDDqO5mDgqHUB4GgOqR0M_PILOpLjouuKYZ2BZgPrDBAZIDJakhwGwI1ejtv2fno-f7zG3_60fX0et7fTufz8Ab6TmCGjAwAA)" +title = "Votes Match Seats" +caption = "" +comment = "" +id = "3" %} \ No newline at end of file diff --git a/draft-intro.html b/draft-intro.html new file mode 100644 index 00000000..60df5dbd --- /dev/null +++ b/draft-intro.html @@ -0,0 +1,64 @@ + + + + + + + To Build an Even Better Ballot? + + + + + + + + + +
+ + + +
+ +

My name is Jameson Quinn. If spending 10,000 hours on something makes you an expert, then I'm easily an expert on voting methods. And right here in your browser, I want to give you a tool that can help you think like an expert on this in a whole lot less than 10,000 hours. If you read all the way through this page, you'll be well on your way. You'll know a lot about voting methods, and you'll understand in a very intuitive way how much this knowledge could be used to make politics healthier.

+ +

I think this tool is a lot of fun and I'm really excited to share it with you. But before I do, I should acknowledge the people who have made this possible. Nicky Case made the first version; Pareto Man did most of the work on the current one; and some of the key ideas come from Ka-Ping Yee and Warren Smith. There's more acknowledgements at the bottom. If you want to be part of this chain of awesomeness, this is all open source, so you're free to make it even better. You can also support Electology.org, aka the Center for Election Science, a nonprofit that works on these issues.

+ +

So, let's get started.

+ +

If you live in the US (or Canada or the UK, for that matter), you've probably read plenty of depressing or scary articles about politics recently. There are a lot of problems in politics, and it can seem hopeless. How can we possibly fix it all?

+ +

I want to show you how democracy itself can be fixed. Doing that wouldn't immediately fix all the other problems. But it would make fixing everything else substantially easier.

+ +

In order to see how to fix democracy, you have to understand how it's broken, right at its root: we're doing voting wrong. Choose-one plurality voting, the voting method we have in most big English-speaking countries, is just about the worst voting method ever used. It's easy to see why other methods are better. It doesn't take complicated rules to improve on choose-one plurality; approval voting is arguably even simpler, and better in every way.

+ +

Once I've shown you that, I'll move on to part II, where I'll get into some of the more complex elements of voting theory (aka social choice theory; a subfield of game theory). I'll discuss strategic voting, and show you several other voting methods, such as score voting, IRV, Condorcet, Star, and 3-2-1. I'll discuss some of the strengths and weaknesses of these methods, and let you play with the examples that have convinced me which methods are better for which situations — approval for simplicity, 3-2-1 or Star for a balance betwen expressiveness and robustness against strategy, and score for situations where the voters share common end goals and are merely voting on the best means to those goals — and which are usually worse — plurality worst of all, but IRV not too much better.

+ +

Imagine your town is planning to buy one fire truck of a fancy new kind, and they're having a vote to decide which firehouse to keep it at. Every voter wants it kept as close to their home as possible, so it can arrive quickly in case of a fire.

+ +

How would you vote? It's a trick question. How you'd vote depends on what voting method is being used. Let's start with choose-one plurality voting, where you're only allowed to, um, choose one. Here's a simple example, so you can play around with it; drag the shapes/candidates/firehouses and the voter/home representing you and see what happens to the ballot.

+ + + +

Say that everyone votes like that; a simple, honest assesment of which candidate is closest, without strategizing about other voters. Here's how it might come out:

+ + + +

This is an easy voting situation to visualize, because all that matters is where the candidates (firehouses) and voters (homes) are physically. In real life, of course, it's not so much your physical location that matters, but your "location" on ideology and/or issues; and that's hard to even define or know, much less draw on a neat 2-dimensional map. But these 2D let us show pretty much all the interesting kinds of things that can happen. So now I'll stop saying "firehouse" and "home" and just say "candidate" and "voter".

+ +

So what's wrong with this voting method? Well, anyone who doesn't vote for one of the two frontrunners has no say in which of them wins; their vote is essentially wasted. So if two candidates are similar, they "split the vote" and "spoil the election" so that neither one of them wins. For instance, in the election above, reset and then try moving the hexagon close to the square. Hexagon steals votes from Square and so Triangle wins!

+ +

+ +
+
+ + + + + \ No newline at end of file diff --git a/essay/essay-dec-5.html b/essay/essay-dec-5.html index 227d67ee..66366ea1 100644 --- a/essay/essay-dec-5.html +++ b/essay/essay-dec-5.html @@ -3,8 +3,8 @@ - -essay + + essay @@ -12,236 +12,236 @@ -

No, this is not about the 2016 U.S. election. Not just that, anyway.

+

No, this is not about the 2016 U.S. election. Not just that, anyway.

-

First, I need to explain a weird glitch in our voting system. -Let's say there's two candidates, Steven Square () and Tracy Triangle (), on a couple political axes. (for example, “left vs. right” and “globalist vs. nationalist”) Let's also say there's a voter () simply votes for whoever's political position is closest. What would that look like?

+

First, I need to explain a weird glitch in our voting system. + Let's say there's two candidates, Steven Square () and Tracy Triangle (), on a couple political axes. (for example, “left vs. right” and “globalist vs. nationalist”) Let's also say there's a voter () simply votes for whoever's political position is closest. What would that look like?

-

drag the candidates & voter around, and see how that changes their vote:

+

drag the candidates & voter around, and see how that changes their vote:

-

It's a tough choice. Triangle's got some sharp points, but Square sees things from more sides. But in the end, you can only vote for one.

+

It's a tough choice. Triangle's got some sharp points, but Square sees things from more sides. But in the end, you can only vote for one.

-

Of course, there's more than just one voter in an election. (one would hope so, anyway!) Let's simulate what an election would look like with 100+ voters.

+

Of course, there's more than just one voter in an election. (one would hope so, anyway!) Let's simulate what an election would look like with 100+ voters.

-

drag the candidates & voters around, and see how that changes the election:

+

drag the candidates & voters around, and see how that changes the election:

-

Now let's consider a different election. Say Tracy Triangle is already beating Steven Square in the polls, and a third candidate, Henry Hexagon (), sees this. (Hexagon's supporters like how he tackles problems from more angles) Inspired by her success, Hexagon swoops in and takes a political position close to Triangle's.

+

Now let's consider a different election. Say Tracy Triangle is already beating Steven Square in the polls, and a third candidate, Henry Hexagon (), sees this. (Hexagon's supporters like how he tackles problems from more angles) Inspired by her success, Hexagon swoops in and takes a political position close to Triangle's.

-

Now, you'd think giving the voters more of what they want should result in a better choice, or at least, not result in a worse choice, right? Well...

+

Now, you'd think giving the voters more of what they want should result in a better choice, or at least, not result in a worse choice, right? Well...

-

at first, beats . -drag to just under , -and see what happens:

+

at first, beats . + drag to just under , + and see what happens:

-

That's right. Steven Square, our least popular candidate, now wins! This is because when you have two good candidates, they "steal" votes from each other, letting a bad third candidate win.

+

That's right. Steven Square, our least popular candidate, now wins! This is because when you have two good candidates, they "steal" votes from each other, letting a bad third candidate win.

-

This is called the spoiler effect. The most famous real-world example of this was in 2000, when Ralph Nader "stole" votes from Al Gore, letting George Bush win. And though the spoiler effect didn't play a big role in 2016, its impact could still be felt.

+

This is called the spoiler effect. The most famous real-world example of this was in 2000, when Ralph Nader "stole" votes from Al Gore, letting George Bush win. And though the spoiler effect didn't play a big role in 2016, its impact could still be felt.

-

In the Republican primary, one anti-establishment nominee – Trump – ran against sixteen GOP establishment nominees, who all "stole" votes from each other, letting Trump grab the nomination, easily. As for the Democratic primary, fear of splitting the vote prevented Sanders from running as independent. And to cap it all off, there was always the worry that other candidates like Johnson, Stein, and McMullin could spoil the election.

+

In the Republican primary, one anti-establishment nominee – Trump – ran against sixteen GOP establishment nominees, who all "stole" votes from each other, letting Trump grab the nomination, easily. As for the Democratic primary, fear of splitting the vote prevented Sanders from running as independent. And to cap it all off, there was always the worry that other candidates like Johnson, Stein, and McMullin could spoil the election.

-

But again, this is not about the 2016 U.S. election.

+

But again, this is not about the 2016 U.S. election.

-

This is about designing a democracy that people can trust.

+

This is about designing a democracy that people can trust.

-

Despite so much hoopla around the 2016 election, a full half of Americans did not vote. Even of those who voted for Clinton/Trump, over 20% of them said their candidates were untrustworthy, and voted for them anyway. And around the world, people's trust in their governments – or the trustworthiness of their governments – has never been lower. It's more than America at stake. It's every democracy in the world.

+

Despite so much hoopla around the 2016 election, a full half of Americans did not vote. Even of those who voted for Clinton/Trump, over 20% of them said their candidates were untrustworthy, and voted for them anyway. And around the world, people's trust in their governments – or the trustworthiness of their governments – has never been lower. It's more than America at stake. It's every democracy in the world.

-

...so yeah, no pressure.

+

...so yeah, no pressure.

-

Rebuilding trust is a complex problem with no easy solutions – but I think there is an easy first step. It's a step that could get rid of our “lesser of two evils” problem, and give us citizens more choices, better choices. And yet, it won't be as daunting as fixing campaign finance or gerrymandering or political gridlock, no, it'd just require changing a piece of paper, and how we count those pieces of paper.

+

Rebuilding trust is a complex problem with no easy solutions – but I think there is an easy first step. It's a step that could get rid of our “lesser of two evils” problem, and give us citizens more choices, better choices. And yet, it won't be as daunting as fixing campaign finance or gerrymandering or political gridlock, no, it'd just require changing a piece of paper, and how we count those pieces of paper.

-

This idea is not the most important issue. It won't solve everything. But as a first step? It'd give us the biggest bang-for-buck.

+

This idea is not the most important issue. It won't solve everything. But as a first step? It'd give us the biggest bang-for-buck.

-

Let's talk about how to build a better ballot.

+

Let's talk about how to build a better ballot.

-
+
-

Lemme address a couple concerns first.

+

Lemme address a couple concerns first.

-

First objection. Why would the people in power wanna change the voting system that got them in power? Well – the spoiler effect has historically hurt both major parties, not just the minor parties. Getting rid of that glitch would be a win-win-win! Also, voting reform is already picking up steam worldwide. Just last month, Maine adopted Instant Runoff Voting, and Justin Trudeau, Cutie-In-Chief of Canada, will be moving his nation towards a better voting system in 2017.

+

First objection. Why would the people in power wanna change the voting system that got them in power? Well – the spoiler effect has historically hurt both major parties, not just the minor parties. Getting rid of that glitch would be a win-win-win! Also, voting reform is already picking up steam worldwide. Just last month, Maine adopted Instant Runoff Voting, and Justin Trudeau, Cutie-In-Chief of Canada, will be moving his nation towards a better voting system in 2017.

-

Second objection. Didn't some guy prove that all voting systems will be unfair? Not quite. You're thinking of the infamous Impossibility Theorem by Kenneth Arrow, the mathematician in the 1950's who founded the whole study of voting systems!

+

Second objection. Didn't some guy prove that all voting systems will be unfair? Not quite. You're thinking of the infamous Impossibility Theorem by Kenneth Arrow, the mathematician in the 1950's who founded the whole study of voting systems!

-

But 1) some voting systems can still be more fair than others, even if none are perfect. And 2) that's actually a misconception – Kenneth Arrow's proof only applies to voting systems where you rank candidates. His proof doesn't cover voting systems where you don't rank candidates. We'll see a couple of those, later – along with a few of the most popular alternatives to our current, glitchy voting system.

+

But 1) some voting systems can still be more fair than others, even if none are perfect. And 2) that's actually a misconception – Kenneth Arrow's proof only applies to voting systems where you rank candidates. His proof doesn't cover voting systems where you don't rank candidates. We'll see a couple of those, later – along with a few of the most popular alternatives to our current, glitchy voting system.

-

But first, let's take a closer look at the voting system we do have:

+

But first, let's take a closer look at the voting system we do have:

-

FIRST PAST THE POST (FPTP)

+

FIRST PAST THE POST (FPTP)

-

// ballot

+

// ballot

-

How To Count: Simply add up the votes. Whoever gets the most votes, wins.

+

How To Count: Simply add up the votes. Whoever gets the most votes, wins.

-

Sounds logical enough. But as you saw earlier, it can lead to a weird glitch, where having two good candidates can make the election go to a third bad candidate. This is why some people vote "strategically", voting not for their actual honest favorite, but voting for the lesser of two evils. And strategic voting is fine – but! – ask yourself this: how can we expect our elected officials to be honest, when our voting system itself doesn't let us be honest?

+

Sounds logical enough. But as you saw earlier, it can lead to a weird glitch, where having two good candidates can make the election go to a third bad candidate. This is why some people vote "strategically", voting not for their actual honest favorite, but voting for the lesser of two evils. And strategic voting is fine – but! – ask yourself this: how can we expect our elected officials to be honest, when our voting system itself doesn't let us be honest?

-

So, to fix the spoiler effect, other voting systems have been suggested. Such as...

+

So, to fix the spoiler effect, other voting systems have been suggested. Such as...

-

RANKED VOTING

+

RANKED VOTING

-

// ballot

+

// ballot

-

How To Count: There's actually several different suggestions for how to count these kinds of ballots. Here, I'll just show you the three most popular:

+

How To Count: There's actually several different suggestions for how to count these kinds of ballots. Here, I'll just show you the three most popular:

-

Instant Runoff Voting (IRV): This one is the most popular alternative to First Past The Post. Australia and Ireland use them in national elections. Maine adopted it just last month. And Justin Trudeau, Prime Man-ister of Canada, is seriously considering it.

+

Instant Runoff Voting (IRV): This one is the most popular alternative to First Past The Post. Australia and Ireland use them in national elections. Maine adopted it just last month. And Justin Trudeau, Prime Man-ister of Canada, is seriously considering it.

-

Instant Runoff is a bit more complicated than First Past The Post, but here's how it works:

+

Instant Runoff is a bit more complicated than First Past The Post, but here's how it works:

-
    -
  1. Count up the #1 choices.
  2. -
  3. If someone has more than 50%, they win! END.
  4. -
  5. If not, eliminate the last-place loser.
  6. -
  7. Run a new "round" of the election, minus that loser.
  8. -
  9. Repeat until someone has 50% or more.
  10. -
+
    +
  1. Count up the #1 choices.
  2. +
  3. If someone has more than 50%, they win! END.
  4. +
  5. If not, eliminate the last-place loser.
  6. +
  7. Run a new "round" of the election, minus that loser.
  8. +
  9. Repeat until someone has 50% or more.
  10. +
-

If that seems like too much, there is a simpler method for counting ranked ballots...

+

If that seems like too much, there is a simpler method for counting ranked ballots...

-

Borda Count: Simply add up the rank numbers. Like in golf, whoever has the lowest score, wins. Borda count is used in Slovenia, and a bunch of tiny islands in Micronesia.

+

Borda Count: Simply add up the rank numbers. Like in golf, whoever has the lowest score, wins. Borda count is used in Slovenia, and a bunch of tiny islands in Micronesia.

-

But if you want an even nerdier way of voting, you could try...

+

But if you want an even nerdier way of voting, you could try...

-

Condorcet Method: Run a simulated "election" between every pair of candidates, using the info on voters' ballots. IF there's a candidate who beats all other candidates in one-on-one "elections", that candidate wins the real election. However, that's a very big "IF". (as we'll see later...) The upside is, when this method does pick a winner, it's always the “theoretically best” candidate! Currently, this method is not being used by any governments, and is only being used by neeerrrds.

+

Condorcet Method: Run a simulated "election" between every pair of candidates, using the info on voters' ballots. IF there's a candidate who beats all other candidates in one-on-one "elections", that candidate wins the real election. However, that's a very big "IF". (as we'll see later...) The upside is, when this method does pick a winner, it's always the “theoretically best” candidate! Currently, this method is not being used by any governments, and is only being used by neeerrrds.

-

So, those are the voting systems where you rank candidates. (The ones that magic math man Kenneth Arrow proved would always be unfair in some big way!) But what about voting systems where you don't rank candidates? They're less well-known, but at least now you'll know 'em:

+

So, those are the voting systems where you rank candidates. (The ones that magic math man Kenneth Arrow proved would always be unfair in some big way!) But what about voting systems where you don't rank candidates? They're less well-known, but at least now you'll know 'em:

-

APPROVAL VOTING

+

APPROVAL VOTING

-

// ballot

+

// ballot

-

How To Count: Simply add up the approvals. Whoever gets the most approvals, wins.

+

How To Count: Simply add up the approvals. Whoever gets the most approvals, wins.

-

Wait, picking more than one candidate? Doesn't that violate the one-vote-per-person rule? I hear you ask. Well, your vote was never a single check mark, your vote was always the whole ballot. And on this ballot, you get to honestly express all the candidates you approve of, not just your favorite or strategic second-favorite.

+

Wait, picking more than one candidate? Doesn't that violate the one-vote-per-person rule? I hear you ask. Well, your vote was never a single check mark, your vote was always the whole ballot. And on this ballot, you get to honestly express all the candidates you approve of, not just your favorite or strategic second-favorite.

-

But if you want a more expressive voting system, why not try...

+

But if you want a more expressive voting system, why not try...

-

SCORE VOTING

+

SCORE VOTING

-

// ballot

+

// ballot

-

How To Count: Simply add up the ratings. Whoever has the highest average score, wins. Kind of like Amazon reviews, but with democracy. (Note: this is not ranking, because two candidates can have the same score.)

+

How To Count: Simply add up the ratings. Whoever has the highest average score, wins. Kind of like Amazon reviews, but with democracy. (Note: this is not ranking, because two candidates can have the same score.)

-

So them's our top 6 voting systems: the one we use, and five popular alternatives. But how can we tell if these alternatives are actually better? What glitches might they have? And which voting system – if any – can we say is "the best"?

+

So them's our top 6 voting systems: the one we use, and five popular alternatives. But how can we tell if these alternatives are actually better? What glitches might they have? And which voting system – if any – can we say is "the best"?

-

Like before, let's simulate 'em.

+

Like before, let's simulate 'em.

-
+
-

Remember that simulation of the spoiler effect from earlier? Well, here it is again, but now you can switch between the six different voting systems! Here's the "spoiler effect" simulation again. Move candidates & voters around, and see how different voting systems deal with spoilers:

+

Remember that simulation of the spoiler effect from earlier? Well, here it is again, but now you can switch between the six different voting systems! Here's the "spoiler effect" simulation again. Move candidates & voters around, and see how different voting systems deal with spoilers:

-

drag blah blah blah

+

drag blah blah blah

-

// election 1 - spoiler redux

+

// election 1 - spoiler redux

-

As you could see, every voting system except First Past The Post is immune to the spoiler effect. So, that's it, right? Ding dong, the glitch is dead? Just pick any other alternative voting system and be done with it?

+

As you could see, every voting system except First Past The Post is immune to the spoiler effect. So, that's it, right? Ding dong, the glitch is dead? Just pick any other alternative voting system and be done with it?

-

But, alas. In getting rid of one glitch, some of these alternative voting systems create other glitches – for some, the cure is even worse than the disease.

+

But, alas. In getting rid of one glitch, some of these alternative voting systems create other glitches – for some, the cure is even worse than the disease.

-

For example, here's a sim of Instant Runoff Voting. In the beginning, Tracy Triangle () is already winning, and you're going to move the voters even closer to her. Obviously, if a candidate is already winning an election and becomes even more popular, they should still win afterwards, right?

+

For example, here's a sim of Instant Runoff Voting. In the beginning, Tracy Triangle () is already winning, and you're going to move the voters even closer to her. Obviously, if a candidate is already winning an election and becomes even more popular, they should still win afterwards, right?

-

You can probably guess where this is going...

+

You can probably guess where this is going...

-

// election 2 - irv

+

// election 2 - irv

-

What happened? At first, (Hexagon) is eliminated in the first round, so (Triangle) goes against a weaker (Square), and wins. But when you move the voters closer to (Triangle), the loser changes. Now, (Square) is eliminated in the first round, so (Triangle) goes against a stronger (Hexagon), and loses.

+

What happened? At first, (Hexagon) is eliminated in the first round, so (Triangle) goes against a weaker (Square), and wins. But when you move the voters closer to (Triangle), the loser changes. Now, (Square) is eliminated in the first round, so (Triangle) goes against a stronger (Hexagon), and loses.

-

Under Instant Runoff, it's possible for a winning candidate to lose, by becoming more popular. What a glitch.

+

Under Instant Runoff, it's possible for a winning candidate to lose, by becoming more popular. What a glitch.

-

How often does this happen in real life? There are a couple examples, and mathematicians estimate this glitch would happen about 14.5% of the time. But sadly, we can't know for sure, because governments usually don't even release enough info about the ballots to reconstruct an IRV election, and double-check the results.

+

How often does this happen in real life? There are a couple examples, and mathematicians estimate this glitch would happen about 14.5% of the time. But sadly, we can't know for sure, because governments usually don't even release enough info about the ballots to reconstruct an IRV election, and double-check the results.

-

So, not only is Instant Runoff's glitch as undemocratic as First Past The Post's glitch, it's possibly worse – because while FPTP's counting method is simple and transparent, Instant Runoff is anything but. And a lack of transparency is even deadlier nowadays, when our trust in government is already so low.

+

So, not only is Instant Runoff's glitch as undemocratic as First Past The Post's glitch, it's possibly worse – because while FPTP's counting method is simple and transparent, Instant Runoff is anything but. And a lack of transparency is even deadlier nowadays, when our trust in government is already so low.

-

So much for Instant Runoff. What about the second-most-popular alternative, Borda Count? In this next simulation, you move a losing candidate closer to another losing candidate. Under First Past The Post, the spoiler effect would split their votes, making both of them lose even more. But watch what happens under Borda Count instead...

+

So much for Instant Runoff. What about the second-most-popular alternative, Borda Count? In this next simulation, you move a losing candidate closer to another losing candidate. Under First Past The Post, the spoiler effect would split their votes, making both of them lose even more. But watch what happens under Borda Count instead...

-

// election 3 - anti-spoiler effect

+

// election 3 - anti-spoiler effect

-

Yup. Borda Count has a reverse spoiler effect. Instead of one good candidate hurting another good candidate by moving closer, with Borda Count, one bad candidate can help another bad candidate by moving closer.

+

Yup. Borda Count has a reverse spoiler effect. Instead of one good candidate hurting another good candidate by moving closer, with Borda Count, one bad candidate can help another bad candidate by moving closer.

-

Here's what happened: at first, some voters felt (Square) > (Triangle) > (Hexagon), but when you moved (Hexagon) closer to (Square), those voters then swung to (Square) > (Hexagon) > (Triangle), hurting (Triangle)'s total count enough to make her lose to (Square).

+

Here's what happened: at first, some voters felt (Square) > (Triangle) > (Hexagon), but when you moved (Hexagon) closer to (Square), those voters then swung to (Square) > (Hexagon) > (Triangle), hurting (Triangle)'s total count enough to make her lose to (Square).

-

Still, Borda's not the worst, and at least it's simpler and more transparent than Instant Runoff. But how does Condorcet Method compare? When Condorcet picks a winner, it's always the “theoretically best” winner – but that's when it picks a winner.

+

Still, Borda's not the worst, and at least it's simpler and more transparent than Instant Runoff. But how does Condorcet Method compare? When Condorcet picks a winner, it's always the “theoretically best” winner – but that's when it picks a winner.

-

So far, I've just been simulating voters as a single group, with a center and some spread. But seeing how polarized politics is nowadays, one could imagine several groups of voters, with totally different centers. Now, Condorcet tries to pick the candidate who beats all other candidates in one-on-one races. But with polarized voters, you could end up with a Rock-Paper-Scissors-like loop, where a majority of voters prefer A to B, B to C, and C to A.

+

So far, I've just been simulating voters as a single group, with a center and some spread. But seeing how polarized politics is nowadays, one could imagine several groups of voters, with totally different centers. Now, Condorcet tries to pick the candidate who beats all other candidates in one-on-one races. But with polarized voters, you could end up with a Rock-Paper-Scissors-like loop, where a majority of voters prefer A to B, B to C, and C to A.

-

In certain situations, the other voting systems just had glitches. In Condorcet, the voting system crashes. Try it out for yourself:

+

In certain situations, the other voting systems just had glitches. In Condorcet, the voting system crashes. Try it out for yourself:

-

// election 4 - create your own cycle

+

// election 4 - create your own cycle

-

Now, in actual practice – not that any government actually uses this voting system – when Condorcet fails to find a winner, it falls back to another method like Borda Count. But if you do that, it'll get the glitches of its backup method. So it goes.

+

Now, in actual practice – not that any government actually uses this voting system – when Condorcet fails to find a winner, it falls back to another method like Borda Count. But if you do that, it'll get the glitches of its backup method. So it goes.

-

First Past The Post. Instant Runoff. Borda Count. Condorcet Method. Those were all the voting systems that use ranking – the ones that our math boy, Kenneth Arrow, proved would always be unfair or glitchy in some big way. What about the voting systems that don't use ranking, like Approval & Score voting? Well...

+

First Past The Post. Instant Runoff. Borda Count. Condorcet Method. Those were all the voting systems that use ranking – the ones that our math boy, Kenneth Arrow, proved would always be unfair or glitchy in some big way. What about the voting systems that don't use ranking, like Approval & Score voting? Well...

-

...I couldn't come up with a simulation to show their flaws. Because, in theory, they don't have many big flaws.

+

...I couldn't come up with a simulation to show their flaws. Because, in theory, they don't have many big flaws.

-

But that's a really, really, really big “in theory!” It may be that, in practice, strategic voters use approval & score voting exactly like First Past The Post – only approving or giving 5 stars to their top candidate, and disapproving or giving 1 star to all others, even if they actually like the others. Thus, strategic voters may not express an honest second choice – but then again, other voting systems like FPTP and IRV punish you for expressing an honest first choice. So, in the end approval & score voting may still be better!...

+

But that's a really, really, really big “in theory!” It may be that, in practice, strategic voters use approval & score voting exactly like First Past The Post – only approving or giving 5 stars to their top candidate, and disapproving or giving 1 star to all others, even if they actually like the others. Thus, strategic voters may not express an honest second choice – but then again, other voting systems like FPTP and IRV punish you for expressing an honest first choice. So, in the end approval & score voting may still be better!...

-

...mmmaybe. We'll need more data before we can be certain. So, here's a graph from an academic paper. I can tell it was made by real academics, because it's makin' my eyes bleed:

+

...mmmaybe. We'll need more data before we can be certain. So, here's a graph from an academic paper. I can tell it was made by real academics, because it's makin' my eyes bleed:

-

+

-

oh god, is that a drop shadow

+

oh god, is that a drop shadow

-

Anyway, this graph shows the results of 2.2 million simulations – simulations similar to the ones you played above! The first thing to note is that strategic voting makes voters less happy than honest voting – in all voting systems! I was surprised when I first learnt that. But it makes sense if you think about, say, a room full of people trying to talk. Any person can be "strategic" by raising their voice above others, but if everybody is "strategic", nobody can hear anybody anymore, and all you're left with is sore throats and sad peeps.

+

Anyway, this graph shows the results of 2.2 million simulations – simulations similar to the ones you played above! The first thing to note is that strategic voting makes voters less happy than honest voting – in all voting systems! I was surprised when I first learnt that. But it makes sense if you think about, say, a room full of people trying to talk. Any person can be "strategic" by raising their voice above others, but if everybody is "strategic", nobody can hear anybody anymore, and all you're left with is sore throats and sad peeps.

-

But the other thing to note, is which voting systems would make the most people the happiest! If you have mostly honest voters, Score Voting is best. (with Borda Count a close second) And if you have mostly strategic voters, then both Approval & Score Voting are best. (also, with strategic voters, Instant Runoff does just as bad as First Past The Post)

+

But the other thing to note, is which voting systems would make the most people the happiest! If you have mostly honest voters, Score Voting is best. (with Borda Count a close second) And if you have mostly strategic voters, then both Approval & Score Voting are best. (also, with strategic voters, Instant Runoff does just as bad as First Past The Post)

-

However, those are still computer simulations. How would these different voting systems play out in real life? Well, we can't just get the DeLorean up to 88, go back in time before the 2016 election, change the voting system, and see what would happen instead – or can we?!

+

However, those are still computer simulations. How would these different voting systems play out in real life? Well, we can't just get the DeLorean up to 88, go back in time before the 2016 election, change the voting system, and see what would happen instead – or can we?!

-

No, no we can't. But last month, researchers did something close enough. A polling study asked 1,000+ U.S. registered voters to rank & rate the six U.S. presidential candidates, to simulate who would've won the vote (the popular vote, anyway) under different voting systems! The results: under Instant Runoff, Condorcet, and Approval Voting, the winner would've been Hillary Clinton. But under Score Voting, the winner would've been Donald Trump. And under Borda Count, the winner would've been... uh... Gary Johnson?

+

No, no we can't. But last month, researchers did something close enough. A polling study asked 1,000+ U.S. registered voters to rank & rate the six U.S. presidential candidates, to simulate who would've won the vote (the popular vote, anyway) under different voting systems! The results: under Instant Runoff, Condorcet, and Approval Voting, the winner would've been Hillary Clinton. But under Score Voting, the winner would've been Donald Trump. And under Borda Count, the winner would've been... uh... Gary Johnson?

-

?????

+

?????

-

// an election possibility how this could've happened

+

// an election possibility how this could've happened

-

Anyway.

+

Anyway.

-

Before we conclude all this – remember Kenneth Arrow? The infamous mathematician who founded the study of voting systems, and in the 1950's, proved that all ranking-based voting systems would be unfair? Well, in an interview 60 years later, Kenneth Arrow had this to say about what voting method he'd push for, now:

+

Before we conclude all this – remember Kenneth Arrow? The infamous mathematician who founded the study of voting systems, and in the 1950's, proved that all ranking-based voting systems would be unfair? Well, in an interview 60 years later, Kenneth Arrow had this to say about what voting method he'd push for, now:

-

// blockquote style plz

+

// blockquote style plz

-

“Well, I’m a little inclined to think that score systems [like Approval & Score Voting] where you categorize in maybe three or four classes [so, giving a score out of 3 or 4, not 10 or 100] probably – in spite of what I said about manipulation [strategic voting] – is probably the best.”

+

“Well, I’m a little inclined to think that score systems [like Approval & Score Voting] where you categorize in maybe three or four classes [so, giving a score out of 3 or 4, not 10 or 100] probably – in spite of what I said about manipulation [strategic voting] – is probably the best.”

-

That's as strong an endorsement as you'll ever squeeze out of a math-head.

+

That's as strong an endorsement as you'll ever squeeze out of a math-head.

-
+
-

An open letter.

+

An open letter.

-

ahem

+

ahem

-

DEAR JUSTIN “TOTES ADORBZ” TRUDEAU
-(and everyone else around the world pushing for voting reform)

+

DEAR JUSTIN “TOTES ADORBZ” TRUDEAU
+ (and everyone else around the world pushing for voting reform)

-

Thank you for taking this small but powerful first step! We've known for way too long that our current voting system – First Past The Post – forces voters to be dishonest, creates a polarizing "lesser of two evils" scenario, and screws over both major and minor candidates.

+

Thank you for taking this small but powerful first step! We've known for way too long that our current voting system – First Past The Post – forces voters to be dishonest, creates a polarizing "lesser of two evils" scenario, and screws over both major and minor candidates.

-

However, you've probably thinking of going for Instant Runoff Voting (or some other variant, like Single Transferable Vote). But IRV still has a glitch as undemocratic as FPTP's – and worse, in our age of distrust, Instant Runoff's lack of transparency will be deadly for democracy. Yes, sure, IRV was the best voting system mathematicians could come up with... fifty years ago. But now, thanks to simulations and real-life studies, we know of voting systems which are much better, much more transparent, and much more democratic.

+

However, you've probably thinking of going for Instant Runoff Voting (or some other variant, like Single Transferable Vote). But IRV still has a glitch as undemocratic as FPTP's – and worse, in our age of distrust, Instant Runoff's lack of transparency will be deadly for democracy. Yes, sure, IRV was the best voting system mathematicians could come up with... fifty years ago. But now, thanks to simulations and real-life studies, we know of voting systems which are much better, much more transparent, and much more democratic.

-

Personally – I'm leaning towards Score Voting. It's simple, expressive, honest, transparent, and already familiar to anyone who's seen Amazon's or Yelp's “five star” review system. But that's just my humble opinion. You could also make a solid case for Approval Voting, which is even simpler, and would work better on existing voting machines. Or you could go for Borda Count, because it'd be an epic prank.

+

Personally – I'm leaning towards Score Voting. It's simple, expressive, honest, transparent, and already familiar to anyone who's seen Amazon's or Yelp's “five star” review system. But that's just my humble opinion. You could also make a solid case for Approval Voting, which is even simpler, and would work better on existing voting machines. Or you could go for Borda Count, because it'd be an epic prank.

-

I won't claim to know which is The Best™ voting system. I shall keep open this discussion, as long as we have this discussion. For two reasons:

+

I won't claim to know which is The Best™ voting system. I shall keep open this discussion, as long as we have this discussion. For two reasons:

-

One – If I claim one voting system is the best, end of story, all the public choice theory nerds will be on my butt, yelling, BUT NICKY WHAT ABOUT QUADRATIC VOTE BUYING

+

One – If I claim one voting system is the best, end of story, all the public choice theory nerds will be on my butt, yelling, BUT NICKY WHAT ABOUT QUADRATIC VOTE BUYING

-

Two – Keeping the discussion going is what democracy is.

+

Two – Keeping the discussion going is what democracy is.

-

A recent study (NYT article, PDF of original study) found that in many Western countries – from Sweden to Australia to the United States – support for democracy has plummeted over the last several generations. Today, one in six Americans say it'd be "good" or "very good" to be under literal army rule. And in 2011, almost a full quarter of young Americans said democracy was a "bad" or "very bad" way to run a country.

+

A recent study (NYT article, PDF of original study) found that in many Western countries – from Sweden to Australia to the United States – support for democracy has plummeted over the last several generations. Today, one in six Americans say it'd be "good" or "very good" to be under literal army rule. And in 2011, almost a full quarter of young Americans said democracy was a "bad" or "very bad" way to run a country.

-

Our age of distrust goes a lot deeper than the technical details of a voting system. This isn't gonna be One Weird Trick To Fix Democracy. But as a first step, a low-hanging fruit, a way to show that, yes, you really do want to make the system respond to the needs and wants and pains and hopes and dreams of your people – fixing our voting system's a good way to do that.

+

Our age of distrust goes a lot deeper than the technical details of a voting system. This isn't gonna be One Weird Trick To Fix Democracy. But as a first step, a low-hanging fruit, a way to show that, yes, you really do want to make the system respond to the needs and wants and pains and hopes and dreams of your people – fixing our voting system's a good way to do that.

-

This isn't just about trying to build a better ballot.

+

This isn't just about trying to build a better ballot.

-

This is about trying to build a better democracy.

+

This is about trying to build a better democracy.

-

<3,
-~ Nicky Case

+

<3,
+ ~ Nicky Case

-

P.S: Since you've read & played all the way to here, let me give you a bonus! A “sandbox mode” of the election simulator, with up to five candidates. You can also save & share a custom election scenario with others. Happy simulating!

+

P.S: Since you've read & played all the way to here, let me give you a bonus! A “sandbox mode” of the election simulator, with up to five candidates. You can also save & share a custom election scenario with others. Happy simulating!

-
+
-

What can you do? Educators, translators, blah blah blah

+

What can you do? Educators, translators, blah blah blah

- + \ No newline at end of file diff --git a/essay/essay.md b/essay/essay.md index f4fdd8c1..af69c0da 100644 --- a/essay/essay.md +++ b/essay/essay.md @@ -1,3 +1,7 @@ +--- +layout: none +--- + No, this is not about the 2016 U.S. election. Not *just* that, anyway. First, I need to explain a weird glitch in our voting system. diff --git a/favicon-original.ico b/favicon-original.ico new file mode 100644 index 00000000..baccb22a Binary files /dev/null and b/favicon-original.ico differ diff --git a/favicon-original.png b/favicon-original.png new file mode 100644 index 00000000..baccb22a Binary files /dev/null and b/favicon-original.png differ diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 00000000..39abfb4c Binary files /dev/null and b/favicon.ico differ diff --git a/favicon.png b/favicon.png index baccb22a..39abfb4c 100644 Binary files a/favicon.png and b/favicon.png differ diff --git a/gif/ab_general.gif b/gif/ab_general.gif new file mode 100644 index 00000000..87bbfca6 Binary files /dev/null and b/gif/ab_general.gif differ diff --git a/gif/ab_primary.gif b/gif/ab_primary.gif new file mode 100644 index 00000000..bfa9f7b9 Binary files /dev/null and b/gif/ab_primary.gif differ diff --git a/gif/action_1.gif b/gif/action_1.gif new file mode 100644 index 00000000..19e0ed03 Binary files /dev/null and b/gif/action_1.gif differ diff --git a/gif/action_2_tie.gif b/gif/action_2_tie.gif new file mode 100644 index 00000000..85eebedb Binary files /dev/null and b/gif/action_2_tie.gif differ diff --git a/gif/again_proportional_two_to_one.gif b/gif/again_proportional_two_to_one.gif new file mode 100644 index 00000000..fbba5a7a Binary files /dev/null and b/gif/again_proportional_two_to_one.gif differ diff --git a/gif/all_election_beat_map_middle.gif b/gif/all_election_beat_map_middle.gif new file mode 100644 index 00000000..f143b352 Binary files /dev/null and b/gif/all_election_beat_map_middle.gif differ diff --git a/gif/amplify.gif b/gif/amplify.gif new file mode 100644 index 00000000..81dd7ea9 Binary files /dev/null and b/gif/amplify.gif differ diff --git a/gif/approval.gif b/gif/approval.gif new file mode 100644 index 00000000..a25056d3 Binary files /dev/null and b/gif/approval.gif differ diff --git a/gif/approval_quota.gif b/gif/approval_quota.gif new file mode 100644 index 00000000..33aaf009 Binary files /dev/null and b/gif/approval_quota.gif differ diff --git a/gif/asymmetric_info.gif b/gif/asymmetric_info.gif new file mode 100644 index 00000000..4fc06543 Binary files /dev/null and b/gif/asymmetric_info.gif differ diff --git a/gif/ballot11_best_frontrunner.gif b/gif/ballot11_best_frontrunner.gif new file mode 100644 index 00000000..81b943ef Binary files /dev/null and b/gif/ballot11_best_frontrunner.gif differ diff --git a/gif/ballot12_not_worst_frontrunner.gif b/gif/ballot12_not_worst_frontrunner.gif new file mode 100644 index 00000000..43969fa0 Binary files /dev/null and b/gif/ballot12_not_worst_frontrunner.gif differ diff --git a/gif/ballot4_judge.gif b/gif/ballot4_judge.gif new file mode 100644 index 00000000..950bbf89 Binary files /dev/null and b/gif/ballot4_judge.gif differ diff --git a/gif/ballot5_normalize.gif b/gif/ballot5_normalize.gif new file mode 100644 index 00000000..cac74093 Binary files /dev/null and b/gif/ballot5_normalize.gif differ diff --git a/gif/ballot8_frontrunners.gif b/gif/ballot8_frontrunners.gif new file mode 100644 index 00000000..1e2ea070 Binary files /dev/null and b/gif/ballot8_frontrunners.gif differ diff --git a/gif/ballotSingle.gif b/gif/ballotSingle.gif new file mode 100644 index 00000000..750a3093 Binary files /dev/null and b/gif/ballotSingle.gif differ diff --git a/gif/beat_map_middle_sim.gif b/gif/beat_map_middle_sim.gif new file mode 100644 index 00000000..3d207ee1 Binary files /dev/null and b/gif/beat_map_middle_sim.gif differ diff --git a/gif/by_pairs.gif b/gif/by_pairs.gif new file mode 100644 index 00000000..a5bd841d Binary files /dev/null and b/gif/by_pairs.gif differ diff --git a/gif/by_pairs2.gif b/gif/by_pairs2.gif new file mode 100644 index 00000000..2412e671 Binary files /dev/null and b/gif/by_pairs2.gif differ diff --git a/gif/center_squeeze_primary.gif b/gif/center_squeeze_primary.gif new file mode 100644 index 00000000..d611ccf5 Binary files /dev/null and b/gif/center_squeeze_primary.gif differ diff --git a/gif/condorcet_2.gif b/gif/condorcet_2.gif new file mode 100644 index 00000000..a433f6db Binary files /dev/null and b/gif/condorcet_2.gif differ diff --git a/gif/condorcet_3.gif b/gif/condorcet_3.gif new file mode 100644 index 00000000..5f4446e4 Binary files /dev/null and b/gif/condorcet_3.gif differ diff --git a/gif/condorcet_4_rock_paper_scissors.gif b/gif/condorcet_4_rock_paper_scissors.gif new file mode 100644 index 00000000..22e0acbe Binary files /dev/null and b/gif/condorcet_4_rock_paper_scissors.gif differ diff --git a/gif/condorcet_5_middle_voters.gif b/gif/condorcet_5_middle_voters.gif new file mode 100644 index 00000000..229be93c Binary files /dev/null and b/gif/condorcet_5_middle_voters.gif differ diff --git a/gif/condorcet_6_candidate_common_ground.gif b/gif/condorcet_6_candidate_common_ground.gif new file mode 100644 index 00000000..9a70ee10 Binary files /dev/null and b/gif/condorcet_6_candidate_common_ground.gif differ diff --git a/gif/condorcet_7_win_map.gif b/gif/condorcet_7_win_map.gif new file mode 100644 index 00000000..84213327 Binary files /dev/null and b/gif/condorcet_7_win_map.gif differ diff --git a/gif/condorcet_beat_map_41.gif b/gif/condorcet_beat_map_41.gif new file mode 100644 index 00000000..5822b250 Binary files /dev/null and b/gif/condorcet_beat_map_41.gif differ diff --git a/gif/create_one_wide.gif b/gif/create_one_wide.gif new file mode 100644 index 00000000..d20f2aa0 Binary files /dev/null and b/gif/create_one_wide.gif differ diff --git a/gif/crowded.gif b/gif/crowded.gif new file mode 100644 index 00000000..3bc6e43e Binary files /dev/null and b/gif/crowded.gif differ diff --git a/gif/distribution_matching.gif b/gif/distribution_matching.gif new file mode 100644 index 00000000..8d3c9008 Binary files /dev/null and b/gif/distribution_matching.gif differ diff --git a/gif/district.gif b/gif/district.gif new file mode 100644 index 00000000..bfce2d04 Binary files /dev/null and b/gif/district.gif differ diff --git a/gif/eat.gif b/gif/eat.gif new file mode 100644 index 00000000..be1609c5 Binary files /dev/null and b/gif/eat.gif differ diff --git a/gif/election11_chicken_score.gif b/gif/election11_chicken_score.gif new file mode 100644 index 00000000..e337cf88 Binary files /dev/null and b/gif/election11_chicken_score.gif differ diff --git a/gif/election13_no_chicken_star.gif b/gif/election13_no_chicken_star.gif new file mode 100644 index 00000000..130d313f Binary files /dev/null and b/gif/election13_no_chicken_star.gif differ diff --git a/gif/election16.gif b/gif/election16.gif new file mode 100644 index 00000000..3072133d Binary files /dev/null and b/gif/election16.gif differ diff --git a/gif/election17_frontrunner_status.gif b/gif/election17_frontrunner_status.gif new file mode 100644 index 00000000..17c89608 Binary files /dev/null and b/gif/election17_frontrunner_status.gif differ diff --git a/gif/election18_approval_poll_frontrunner.gif b/gif/election18_approval_poll_frontrunner.gif new file mode 100644 index 00000000..437e563d Binary files /dev/null and b/gif/election18_approval_poll_frontrunner.gif differ diff --git a/gif/election19_pick_one.gif b/gif/election19_pick_one.gif new file mode 100644 index 00000000..130f7ee8 Binary files /dev/null and b/gif/election19_pick_one.gif differ diff --git a/gif/election20_risky_choose_one.gif b/gif/election20_risky_choose_one.gif new file mode 100644 index 00000000..d57e698d Binary files /dev/null and b/gif/election20_risky_choose_one.gif differ diff --git a/gif/election21_risky_choose_one.gif b/gif/election21_risky_choose_one.gif new file mode 100644 index 00000000..8ac1c712 Binary files /dev/null and b/gif/election21_risky_choose_one.gif differ diff --git a/gif/election22_sandbox.gif b/gif/election22_sandbox.gif new file mode 100644 index 00000000..b652875d Binary files /dev/null and b/gif/election22_sandbox.gif differ diff --git a/gif/election23_map_of_winner.gif b/gif/election23_map_of_winner.gif new file mode 100644 index 00000000..60ed7b21 Binary files /dev/null and b/gif/election23_map_of_winner.gif differ diff --git a/gif/election24_winners_circle.gif b/gif/election24_winners_circle.gif new file mode 100644 index 00000000..2f0adae5 Binary files /dev/null and b/gif/election24_winners_circle.gif differ diff --git a/gif/election25_spoiler.gif b/gif/election25_spoiler.gif new file mode 100644 index 00000000..30893f4d Binary files /dev/null and b/gif/election25_spoiler.gif differ diff --git a/gif/election31_spoilers.gif b/gif/election31_spoilers.gif new file mode 100644 index 00000000..ff43f9e8 Binary files /dev/null and b/gif/election31_spoilers.gif differ diff --git a/gif/election_approval.gif b/gif/election_approval.gif new file mode 100644 index 00000000..1e4d1639 Binary files /dev/null and b/gif/election_approval.gif differ diff --git a/gif/election_beat_map_middle.gif b/gif/election_beat_map_middle.gif new file mode 100644 index 00000000..4256cd6e Binary files /dev/null and b/gif/election_beat_map_middle.gif differ diff --git a/gif/even_2.gif b/gif/even_2.gif new file mode 100644 index 00000000..418991fa Binary files /dev/null and b/gif/even_2.gif differ diff --git a/gif/even_distribution.gif b/gif/even_distribution.gif new file mode 100644 index 00000000..9df48827 Binary files /dev/null and b/gif/even_distribution.gif differ diff --git a/gif/facility.gif b/gif/facility.gif new file mode 100644 index 00000000..79e0fcbe Binary files /dev/null and b/gif/facility.gif differ diff --git a/gif/fptp_bad_polls_sim.gif b/gif/fptp_bad_polls_sim.gif new file mode 100644 index 00000000..695d5595 Binary files /dev/null and b/gif/fptp_bad_polls_sim.gif differ diff --git a/gif/game_setup.gif b/gif/game_setup.gif new file mode 100644 index 00000000..34736186 Binary files /dev/null and b/gif/game_setup.gif differ diff --git a/gif/honest_primary.gif b/gif/honest_primary.gif new file mode 100644 index 00000000..49d0dada Binary files /dev/null and b/gif/honest_primary.gif differ diff --git a/gif/irv.gif b/gif/irv.gif new file mode 100644 index 00000000..972bd96d Binary files /dev/null and b/gif/irv.gif differ diff --git a/gif/irv_ballot.gif b/gif/irv_ballot.gif new file mode 100644 index 00000000..066484f1 Binary files /dev/null and b/gif/irv_ballot.gif differ diff --git a/gif/irv_not_wasted.gif b/gif/irv_not_wasted.gif new file mode 100644 index 00000000..c3469af0 Binary files /dev/null and b/gif/irv_not_wasted.gif differ diff --git a/gif/irv_second_counts.gif b/gif/irv_second_counts.gif new file mode 100644 index 00000000..74431c70 Binary files /dev/null and b/gif/irv_second_counts.gif differ diff --git a/gif/median.gif b/gif/median.gif new file mode 100644 index 00000000..49d9f26c Binary files /dev/null and b/gif/median.gif differ diff --git a/gif/median_2d.gif b/gif/median_2d.gif new file mode 100644 index 00000000..7d6ccb68 Binary files /dev/null and b/gif/median_2d.gif differ diff --git a/gif/median_2d_multimodal.gif b/gif/median_2d_multimodal.gif new file mode 100644 index 00000000..c0ffb22f Binary files /dev/null and b/gif/median_2d_multimodal.gif differ diff --git a/gif/median_beats_1d.gif b/gif/median_beats_1d.gif new file mode 100644 index 00000000..01c03b36 Binary files /dev/null and b/gif/median_beats_1d.gif differ diff --git a/gif/median_beats_2d.gif b/gif/median_beats_2d.gif new file mode 100644 index 00000000..93013bb1 Binary files /dev/null and b/gif/median_beats_2d.gif differ diff --git a/gif/middle.gif b/gif/middle.gif new file mode 100644 index 00000000..887e3a52 Binary files /dev/null and b/gif/middle.gif differ diff --git a/gif/minimax.gif b/gif/minimax.gif new file mode 100644 index 00000000..b85a1369 Binary files /dev/null and b/gif/minimax.gif differ diff --git a/gif/model1.gif b/gif/model1.gif new file mode 100644 index 00000000..c40b3fc4 Binary files /dev/null and b/gif/model1.gif differ diff --git a/gif/model2.gif b/gif/model2.gif new file mode 100644 index 00000000..81b95f94 Binary files /dev/null and b/gif/model2.gif differ diff --git a/gif/new_can_win_circle.gif b/gif/new_can_win_circle.gif new file mode 100644 index 00000000..6629ee19 Binary files /dev/null and b/gif/new_can_win_circle.gif differ diff --git a/gif/no_spoilers.gif b/gif/no_spoilers.gif new file mode 100644 index 00000000..7d7b2403 Binary files /dev/null and b/gif/no_spoilers.gif differ diff --git a/gif/pair_ballot.gif b/gif/pair_ballot.gif new file mode 100644 index 00000000..9cb5be37 Binary files /dev/null and b/gif/pair_ballot.gif differ diff --git a/gif/pair_quota.gif b/gif/pair_quota.gif new file mode 100644 index 00000000..e1597bb1 Binary files /dev/null and b/gif/pair_quota.gif differ diff --git a/gif/pairwise_election.gif b/gif/pairwise_election.gif new file mode 100644 index 00000000..fc6e956e Binary files /dev/null and b/gif/pairwise_election.gif differ diff --git a/gif/pairwise_intro.gif b/gif/pairwise_intro.gif new file mode 100644 index 00000000..fd9b04fd Binary files /dev/null and b/gif/pairwise_intro.gif differ diff --git a/gif/party_dominance.gif b/gif/party_dominance.gif new file mode 100644 index 00000000..0bd1c8b7 Binary files /dev/null and b/gif/party_dominance.gif differ diff --git a/gif/power.gif b/gif/power.gif new file mode 100644 index 00000000..b7512671 Binary files /dev/null and b/gif/power.gif differ diff --git a/gif/primaries.gif b/gif/primaries.gif new file mode 100644 index 00000000..007a883f Binary files /dev/null and b/gif/primaries.gif differ diff --git a/gif/primary.gif b/gif/primary.gif new file mode 100644 index 00000000..ddf28cf5 Binary files /dev/null and b/gif/primary.gif differ diff --git a/gif/prop.gif b/gif/prop.gif new file mode 100644 index 00000000..6fb32561 Binary files /dev/null and b/gif/prop.gif differ diff --git a/gif/proportional_two_to_one.gif b/gif/proportional_two_to_one.gif new file mode 100644 index 00000000..5fd8201a Binary files /dev/null and b/gif/proportional_two_to_one.gif differ diff --git a/gif/ranked_pairs.gif b/gif/ranked_pairs.gif new file mode 100644 index 00000000..5a0c2a2a Binary files /dev/null and b/gif/ranked_pairs.gif differ diff --git a/gif/robla.gif b/gif/robla.gif new file mode 100644 index 00000000..fad8f833 Binary files /dev/null and b/gif/robla.gif differ diff --git a/gif/sandbox.gif b/gif/sandbox.gif new file mode 100644 index 00000000..1c16c7db Binary files /dev/null and b/gif/sandbox.gif differ diff --git a/gif/schulze.gif b/gif/schulze.gif new file mode 100644 index 00000000..2c634031 Binary files /dev/null and b/gif/schulze.gif differ diff --git a/gif/schulze_alt.gif b/gif/schulze_alt.gif new file mode 100644 index 00000000..53edf61e Binary files /dev/null and b/gif/schulze_alt.gif differ diff --git a/gif/score.gif b/gif/score.gif new file mode 100644 index 00000000..dfaf65b7 Binary files /dev/null and b/gif/score.gif differ diff --git a/gif/score_quota.gif b/gif/score_quota.gif new file mode 100644 index 00000000..a2dc19f0 Binary files /dev/null and b/gif/score_quota.gif differ diff --git a/gif/score_strategy.gif b/gif/score_strategy.gif new file mode 100644 index 00000000..cdd641c7 Binary files /dev/null and b/gif/score_strategy.gif differ diff --git a/gif/semi_proportional.gif b/gif/semi_proportional.gif new file mode 100644 index 00000000..ae8fdf88 Binary files /dev/null and b/gif/semi_proportional.gif differ diff --git a/gif/sequential_monroe_score.gif b/gif/sequential_monroe_score.gif new file mode 100644 index 00000000..17b3f9da Binary files /dev/null and b/gif/sequential_monroe_score.gif differ diff --git a/gif/small_group_approval.gif b/gif/small_group_approval.gif new file mode 100644 index 00000000..63497008 Binary files /dev/null and b/gif/small_group_approval.gif differ diff --git a/gif/spoiler.gif b/gif/spoiler.gif new file mode 100644 index 00000000..2db5b75d Binary files /dev/null and b/gif/spoiler.gif differ diff --git a/gif/standard_spoiler.gif b/gif/standard_spoiler.gif new file mode 100644 index 00000000..d8327a4b Binary files /dev/null and b/gif/standard_spoiler.gif differ diff --git a/gif/star.gif b/gif/star.gif new file mode 100644 index 00000000..8066913f Binary files /dev/null and b/gif/star.gif differ diff --git a/gif/star_ballot.gif b/gif/star_ballot.gif new file mode 100644 index 00000000..10cb7586 Binary files /dev/null and b/gif/star_ballot.gif differ diff --git a/gif/star_election.gif b/gif/star_election.gif new file mode 100644 index 00000000..082179e7 Binary files /dev/null and b/gif/star_election.gif differ diff --git a/gif/star_election_tougher.gif b/gif/star_election_tougher.gif new file mode 100644 index 00000000..a1602d91 Binary files /dev/null and b/gif/star_election_tougher.gif differ diff --git a/gif/star_scores.gif b/gif/star_scores.gif new file mode 100644 index 00000000..b6052ddf Binary files /dev/null and b/gif/star_scores.gif differ diff --git a/gif/stv.gif b/gif/stv.gif new file mode 100644 index 00000000..fd89b7a4 Binary files /dev/null and b/gif/stv.gif differ diff --git a/gif/stv_minimax.gif b/gif/stv_minimax.gif new file mode 100644 index 00000000..4f1bc3fb Binary files /dev/null and b/gif/stv_minimax.gif differ diff --git a/gif/thin_beat_map_middle_sim.gif b/gif/thin_beat_map_middle_sim.gif new file mode 100644 index 00000000..e1daf6ce Binary files /dev/null and b/gif/thin_beat_map_middle_sim.gif differ diff --git a/gif/three.gif b/gif/three.gif new file mode 100644 index 00000000..eca1ba10 Binary files /dev/null and b/gif/three.gif differ diff --git a/gif/tough_irv.gif b/gif/tough_irv.gif new file mode 100644 index 00000000..fe1a16fd Binary files /dev/null and b/gif/tough_irv.gif differ diff --git a/gif/venn_approval.gif b/gif/venn_approval.gif new file mode 100644 index 00000000..97eb6d8e Binary files /dev/null and b/gif/venn_approval.gif differ diff --git a/gif/voter_chart.gif b/gif/voter_chart.gif new file mode 100644 index 00000000..bfa10098 Binary files /dev/null and b/gif/voter_chart.gif differ diff --git a/gif/wasted_vote.gif b/gif/wasted_vote.gif new file mode 100644 index 00000000..e0fa06d5 Binary files /dev/null and b/gif/wasted_vote.gif differ diff --git a/gif/win_map_middle.gif b/gif/win_map_middle.gif new file mode 100644 index 00000000..434f8273 Binary files /dev/null and b/gif/win_map_middle.gif differ diff --git a/gif/yesno.gif b/gif/yesno.gif new file mode 100644 index 00000000..1824aa3f Binary files /dev/null and b/gif/yesno.gif differ diff --git a/gif/yesno_many.gif b/gif/yesno_many.gif new file mode 100644 index 00000000..62e4f492 Binary files /dev/null and b/gif/yesno_many.gif differ diff --git a/img/backfire.png b/img/backfire.png new file mode 100644 index 00000000..f063c260 Binary files /dev/null and b/img/backfire.png differ diff --git a/img/basics.png b/img/basics.png new file mode 100644 index 00000000..da7cd04e Binary files /dev/null and b/img/basics.png differ diff --git a/img/candy-tier-list.png b/img/candy-tier-list.png new file mode 100644 index 00000000..b300a1fe Binary files /dev/null and b/img/candy-tier-list.png differ diff --git a/img/chicken.png b/img/chicken.png new file mode 100644 index 00000000..d96c6440 Binary files /dev/null and b/img/chicken.png differ diff --git a/img/condorcet.jpg b/img/condorcet.jpg new file mode 100644 index 00000000..a7ebe0ff Binary files /dev/null and b/img/condorcet.jpg differ diff --git a/img/condorcet.png b/img/condorcet.png new file mode 100644 index 00000000..ca9f1190 Binary files /dev/null and b/img/condorcet.png differ diff --git a/img/facility location.png b/img/facility location.png new file mode 100644 index 00000000..3f7321ba Binary files /dev/null and b/img/facility location.png differ diff --git a/img/irv.png b/img/irv.png new file mode 100644 index 00000000..6c4247a6 Binary files /dev/null and b/img/irv.png differ diff --git a/img/llull.jpg b/img/llull.jpg new file mode 100644 index 00000000..4a15052d Binary files /dev/null and b/img/llull.jpg differ diff --git a/img/logo.png b/img/logo.png index 4038edbc..22136660 100644 Binary files a/img/logo.png and b/img/logo.png differ diff --git a/img/logo_approval_superman.png b/img/logo_approval_superman.png new file mode 100644 index 00000000..d9cb04f2 Binary files /dev/null and b/img/logo_approval_superman.png differ diff --git a/img/logo_bees.png b/img/logo_bees.png new file mode 100644 index 00000000..de5e7440 Binary files /dev/null and b/img/logo_bees.png differ diff --git a/img/logo_better.png b/img/logo_better.png new file mode 100644 index 00000000..023b8693 Binary files /dev/null and b/img/logo_better.png differ diff --git a/img/logo_newer.png b/img/logo_newer.png new file mode 100644 index 00000000..4038edbc Binary files /dev/null and b/img/logo_newer.png differ diff --git a/img/logo_quotaApproval.png b/img/logo_quotaApproval.png new file mode 100644 index 00000000..45c929e8 Binary files /dev/null and b/img/logo_quotaApproval.png differ diff --git a/img/logo_quotaApproval_old.png b/img/logo_quotaApproval_old.png new file mode 100644 index 00000000..677d4c09 Binary files /dev/null and b/img/logo_quotaApproval_old.png differ diff --git a/img/primary-strategy.png b/img/primary-strategy.png new file mode 100644 index 00000000..fefb5aa1 Binary files /dev/null and b/img/primary-strategy.png differ diff --git a/img/proportional.png b/img/proportional.png new file mode 100644 index 00000000..bb01d464 Binary files /dev/null and b/img/proportional.png differ diff --git a/img/proportional2.png b/img/proportional2.png new file mode 100644 index 00000000..aa711401 Binary files /dev/null and b/img/proportional2.png differ diff --git a/img/sandbox.png b/img/sandbox.png new file mode 100644 index 00000000..97c71a9f Binary files /dev/null and b/img/sandbox.png differ diff --git a/img/split-vote.png b/img/split-vote.png new file mode 100644 index 00000000..23c85740 Binary files /dev/null and b/img/split-vote.png differ diff --git a/img/spoiler.png b/img/spoiler.png new file mode 100644 index 00000000..5c39d516 Binary files /dev/null and b/img/spoiler.png differ diff --git a/img/stv.png b/img/stv.png new file mode 100644 index 00000000..51dc9b40 Binary files /dev/null and b/img/stv.png differ diff --git a/img/superman.jpg b/img/superman.jpg new file mode 100644 index 00000000..91c4c0dd Binary files /dev/null and b/img/superman.jpg differ diff --git a/img/venn-diagram.png b/img/venn-diagram.png new file mode 100644 index 00000000..3bb62d2b Binary files /dev/null and b/img/venn-diagram.png differ diff --git a/img/winner-circle.png b/img/winner-circle.png new file mode 100644 index 00000000..ed1338e3 Binary files /dev/null and b/img/winner-circle.png differ diff --git a/index.html b/index.html deleted file mode 100644 index 9b22fa03..00000000 --- a/index.html +++ /dev/null @@ -1,893 +0,0 @@ - - - - - - To Build a ...Better Ballot - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- -

No, this is not about the 2016 U.S. election. Not just that, anyway.

- -

First, I need to explain a weird glitch in our voting system. - Let's say there's two candidates, Steven Square and Tracy Triangle , on a couple political axes. (for example, “left vs. right” and “globalist vs. nationalist”) Let's also say there's a voter who simply votes for whoever's political position is closest. What would that look like?

- -
-
- -

- click & drag
the candidates and the voter:
-

- - -
-
- -

It's a tough choice. Triangle's got some sharp points, but Square understands more sides! Alas, in the end, you can only vote for one.

- -

Of course, there's more than just one voter in an election. Let's simulate what an election would look like with 100+ voters.

- -
-
- -

- drag the candidates & voters around.
(to move voters, drag the middle of the crowd)
watch how that changes the election:
-

- - -
-
- -

Now let's consider a different election. Say Tracy Triangle is already beating Steven Square in the polls, and a third candidate, Henry Hexagon , sees this. (Hexagon's supporters like how he tackles problems from more angles) Inspired by her success, Hexagon swoops in and takes a political position close to Triangle's.

- -

Now, you'd think giving the voters more of what they want should result in a better choice, or at least, not result in a worse choice, right? Well...

- -
-
- -

- - at first, beats .
- drag to just under ,
- and see what happens:
-

- - -
-
- -

That's right. Steven Square, our least popular candidate, now wins! This is because when you have two good candidates, they "steal" votes from each other, letting a bad third candidate win.

- -

This is called the spoiler effect. The most famous real-world example of this was in 2000, when Ralph Nader "stole" votes from Al Gore, letting George Bush win. And though the spoiler effect didn't play a big role in 2016, its impact could still be felt.

- -

In the Republican primary, one anti-establishment nominee, Trump, ran against sixteen GOP establishment nominees, who all "stole" votes from each other, letting Trump grab the nomination, easily. As for the Democratic primary, fear of splitting the vote prevented Sanders from running as independent. And to cap it all off, there was always the worry that other candidates like Johnson, Stein, and McMullin could spoil the election.

- -

But again, this is not about the 2016 U.S. election.

- -

This is about designing a democracy that people can trust.

- -

Despite so much hoopla around the 2016 election, a full half of Americans did not vote. Even of those who voted for Clinton/Trump, 20% of them said their candidates were untrustworthy, and voted for them anyway. And around the world, people's trust in their governments – or the trustworthiness of their governments – has never been lower. It's more than America at stake. It's every democracy in the world.

- -

...so yeah, no pressure.

- -

Rebuilding trust is a complex problem with no easy solutions. But I think there is an easy first step. It's a step that could get rid of our “lesser of two evils” problem, and give us citizens more choices, better choices. And yet, it won't be as daunting as fixing campaign finance or gerrymandering or lack of proportional representation, no, it'd just require changing a piece of paper, and how we count those pieces of paper.

- -

This idea is not the most important issue. It won't solve everything. But as a first step? It'd give us the biggest bang-for-buck.

- -

Let's talk about how to build a better ballot.

- -
- - - - -
- -

Now, some of you may have a couple objections!

- -

First objection. Why would the people in power change the voting method that got them in power? Well, the spoiler effect has cost both Dems & Reps a major election before. Getting rid of that glitch would be a win-win for major and minor parties! Also, voting reform is already picking up steam. Just last month, Maine adopted Instant Runoff, and Justin Trudeau, Canada's Cutie-In-ChiefCynic-in-Chief, will be moving his nation towards a better voting system in 2017. (UPDATE: actually, he didn't do that.)

- -

Second objection. Didn't some guy once prove that all voting methods will be unfair? Not quite. You're thinking of the infamous Impossibility Theorem by Kenneth Arrow, the mathematician in the 1950's who founded the whole study of voting methods.

- -

Two answers to that: 1) some voting methods can still be more fair than others, even if none are perfect. And 2) Kenneth Arrow's proof doesn't apply to all voting methods! That's a misconception. It only applies to voting methods where you rank candidates. Later, we'll see some voting methods where you don't rank candidates – along with other alternatives to our current, glitchy voting method.

- -

But first, let's take a closer look at the voting method we do have:

- -
-
- -

FIRST PAST THE POST (FPTP)

-

same as before. click & drag
the candidates and voter

- - -
-
- -

How To Count: Simply add up the votes. Whoever gets the most votes, wins.

- -

Sounds logical enough. But as you saw earlier, it can lead to a weird glitch, where having two good candidates can make the election go to a third bad candidate. This is why some people vote "strategically", voting not for their actual honest favorite, but voting for the lesser of two evils. And strategic voting is fine – but! – ask yourself this: how can we expect our elected officials to be honest, when our voting method itself doesn't let us be honest?

- -

So, to fix the spoiler effect, other voting methods have been suggested. Such as...

-
-
- -

RANKED VOTING

-

again, click & drag

- - - -
-
- -

How To Count: There's actually several different ways to count these kinds of ballots. Here, I'll just show you the top three:

- -

- Instant Runoff Voting (IRV): - This one is the most popular alternative to First Past The Post (FPTP). - Australia and Ireland use it in national elections. - San Francisco, Minneapolis, and Portland, Maine use it in local elections. - And Justin Trudeau, Prime Man-ister of Canada, - is leaning towards Instant Runoff, too. -

- -

- (Note: Instant Runoff Voting is also called “Ranked Choice Voting”, - even though there's other ways to count ranked ballots. - IRV is also often just called “Alternative Vote”, - even though there's a flippin' dozen other voting methods. - Such selfish naming! Sheesh!) -

- -

IRV is a bit more complicated than FPTP, but here's how it works:

- -
    -
  1. Count up the #1 choices.
  2. -
  3. If someone has more than 50%, they win! END.
  4. -
  5. If not, eliminate the last-place loser.
  6. -
  7. Run a new "round" of the election, minus that loser.
  8. -
  9. Repeat until someone has 50% or more.
  10. -
- -

If that seems like too much, there is a much simpler method of counting ranked ballots...

- -

Borda Count: Simply add up the rank numbers. Like in golf, whoever has the lowest score, wins. Borda count is used in Slovenia and a bunch of tiny islands in Micronesia.

- -

But if you want an even nerdier way of voting, you could try...

- -

Condorcet Method: Run a simulated "election" between every pair of candidates, using the info on voters' ballots. IF there's a candidate who beats all other candidates in one-on-one "elections", that candidate wins the real election. However, that's a very big "IF". (as we'll see later...) The upside is, when this method does pick a winner, it's always the “theoretically best” candidate! Currently, this method is not being used by any governments, and is only being used by neeerrrrrds.

- -

So, those are the voting methods where you rank candidates – the ones that Kenneth Arrow proved would always be unfair in some big way! But what of voting methods where you don't rank candidates? They're less well-known, but now, at least you'll know 'em:

- -
-
- -

APPROVAL VOTING

-

yup, stiiiiill click & drag

- - - -
-
- -

How To Count: Simply add up the approvals. Whoever gets the most approvals, wins.

- -

Wait, picking more than one candidate? Doesn't that violate the one-vote-per-person rule? I hear you ask. Well, your vote was never a single check mark, your vote was always the whole ballot. And on this ballot, you get to honestly express all the candidates you approve of, not just your favorite or strategic second-favorite.

- -

But if you want a more expressive voting method, why not try...

- -
-
- -

SCORE VOTING

-

you guessed it

- - - -
-
- -

How To Count: Simply add up the ratings. Whoever has the highest average score, wins. Kind of like Amazon reviews, but with democracy. (Note: this is not a ranking method, because two candidates can have the same score.)

- -

So there's our top 6 voting methods: the one we use, and five popular alternatives. But how can we tell if these alternatives are actually better? What glitches might they have? And which voting method – if any – can we say is "the best"?

- -

Like before, let's simulate 'em.

- -
- - - - -
-

Remember that simulation of the spoiler effect from earlier? Well, here it is again, but now you can switch between the six different voting methods! Here's the "spoiler effect" simulation again. See how different voting methods deal with potential spoilers:

-
-
-

- drag to just under to create a spoiler effect.
- then compare the 6 different voting methods: -
- - (note: in the rare cases there's a tie, i just randomly pick a winner) - -

- - -
-
- -

As you could see, every voting method except First Past The Post is immune to the spoiler effect. So, that's it, right? Ding dong, the glitch is dead? Just pick any other alternative voting method and be done with it?

- -

But, alas. In getting rid of one glitch, some of these alternative voting methods create other glitches – for some, the cure is even worse than the disease.

- -

For example, here's a sim of Instant Runoff Voting. In the beginning, Tracy Triangle is already winning, and you're going to move the voters even closer to her. Obviously, if a candidate is already winning an election, and becomes even more popular, they should still win afterwards, right?

- -

You can probably guess where this is going...

-
-
- -

- drag the voters slowly up towards : -

- - - -
-
- -

What happened? - Originally, is eliminated in the first round, so - - goes against a weaker , and wins. - But when you move the voters closer to , - the loser changes! - So now, is eliminated in the first round, - which means goes against a stronger , - and loses.

- -

Under Instant Runoff, it's possible for a winning candidate to lose, by becoming more popular. What a glitch!

- -

How often does this actually happen in real life? There's a couple confirmed examples, and mathematicians estimate this glitch would happen about 14.5% of the time. But sadly, we can't know for sure, because governments usually don't release enough info about the ballots to reconstruct an IRV election & double-check the results.

- -

So, not only is Instant Runoff's glitch as undemocratic as First Past The Post's glitch, it's possibly worse – because while FPTP's counting method is simple and transparent, Instant Runoff is anything but. And a lack of transparency is an even deadlier sin nowadays, when our trust in government is already so low.

- -

(But wait! We'll be talking about the risk of strategic voting later. - Can IRV can make a comeback? Stay tuned...)

- -

So much for the most popular alternative. What about the second-most popular, Borda Count? In this next simulation, you move a losing candidate closer to another losing candidate. Under FPTP, the spoiler effect would split their votes, making both of them lose even more. But watch what happens under Borda Count instead...

- -
-
- -

- drag to just slightly left of : -

- - - -
-
- -

Yup. Borda Count has a reverse spoiler effect. Instead of one good candidate hurting another good candidate by moving closer, with Borda Count, one bad candidate can help another bad candidate by moving closer.

- -

Here's what happened: at first, some voters ranked - >>, - but when you moved closer to , - those voters then swung to ranking - >>, - hurting enough - to make her lose to .

- -

Still, Borda's not the worst, and at least it's simpler and more transparent than Instant Runoff. But how does Condorcet Method compare? When Condorcet picks a winner, it's always the “theoretically best” winner – but that's when it picks a winner.

- -

So far, I've just been simulating voters as a single group, with a center and some spread. But seeing how polarized politics is nowadays, one could imagine several groups of voters, with totally different centers. Now, Condorcet tries to pick the candidate who beats all other candidates in one-on-one races. But with polarized voters, you could end up with a Rock-Paper-Scissors-like loop, where a majority of voters prefer A to B, B to C, and C to A.

- -

In certain situations, the other voting methods just had glitches. In Condorcet, the voting method crashes. Try it out for yourself:

-
-
- -

- create your own “condorcet cycle”!
- move the voters in such a way that NOBODY wins: -

- - - -
-
- -

Now, in actual practice – not that any government actually uses this voting method – when Condorcet fails to find a winner, the election falls back to another method like Borda Count. But if you do that, it'll get the glitches of its backup method. So it goes.

- -

First Past The Post. Instant Runoff. Borda Count. Condorcet Method. Those were all the voting methods that use ranking – the ones that our math boy, Kenneth Arrow, proved would always be unfair or glitchy in some big way. What about the voting methods that don't use ranking, like Approval & Score voting? Well...

- -

...I couldn't come up with a simulation to show their flaws. Because, in theory, they don't have many big flaws.

- -

- But that's a really, really, really big “in theory!” - It may be that, in practice, strategic voters use Approval & Score Voting exactly like First Past The Post – - only approving or giving 5 stars to their top candidate, and disapproving or giving 1 star to all others, - even if they actually like the others. - (See FairVote's critique of Approval Voting, and defense of Instant Runoff) -

- -

- Then again, even if Approval & Score Voting disincentivize you from expressing an honest second choice, - FPTP and IRV punish you for expressing an honest first choice. - Besides, if Approval can be "gamed", then that goes double for IRV. - (See this mathematician's critique of FairVote's critique, and defense of Approval) - So, in the end... [confused shrugging sounds]

- -

- We're gonna need a hecka lot more simulations. -

- -

- So, below is a chart - (source), - showing the results of 2.2 million simulations. - A huge variety of scenarios were tested. All-honest voters. - All-strategic voters. Half-honest, half-strategic. - Voters who know each others' preferences. - Voters who don't know each others' preferences. - Voters who only sorta-know each others' preferences. - And so on. - You can tell that a real mathematician made this chart, - because it's makin' my eyes bleed: -

- -

- -

- Each voting method's results is shown as an ugly-blue bar. - The further to the right a voting method is, the more it "maximizes happiness" for the voters. - The higher up a voting method is, the simpler it is. - And a bar's width shows the range of a voting method's performance, - given different ratios of honest-to-strategic voters. -

- -

- The first thing to note is that strategic voting makes voters less happy than honest voting - – in all voting methods! I was very surprised when I first learnt that. - (But it makes sense, if you think about, say, a crowded room full of people trying to talk. Any one person can be "strategic" by shouting over others, but if everybody is "strategic", nobody can hear anybody, and all you're left with is sore throats and sad peeps.)

- -

The other thing to note is which voting methods make people the happiest. If you have mostly honest voters, Score Voting is best. (with Borda Count a close second) And if you have mostly strategic voters, then both Approval & Score Voting are best. (and with strategic voters, IRV does just as bad as FPTP)

- -

However, those are still computer simulations. How would these different voting methods play out in real life? Well, we can't just get the DeLorean up to 88, go back in time before the 2016 election, change the voting method, and see what would happen...

- -

...or can we?!

- -

No, no we can't. But last month, researchers did something close enough. - A polling study asked 1,000+ U.S. registered voters to rank & rate the six presidential candidates, - to simulate who would've won the (popular) vote under different voting methods! - (But keep in mind that if we had a different voting method in the primaries, we'd have different candidates entirely. - So take this study with a pillar of salt.) - The results: under Instant Runoff, Condorcet, and Approval Voting, the winner would've been Hillary Clinton. But under Score Voting, the winner would've been Donald Trump. And under Borda Count, the winner would've been... uh... Gary Johnson? -

- -

?????

- -
-
-

- a guesstimated model of the 2016 US election?...
- - how Clinton wins IRV, - Trump wins Score, - and Johnson wins Borda?? - -

- -
-
- -

Anyway.

- -

Before we wrap all this up – remember Kenneth Arrow? The infamous mathematician who founded the study of voting methods in the 1950's? Well, in an interview 60 years later, Kenneth Arrow had this to say, about which voting method he likes most now:

- -

- “Well, I’m a little inclined to think that score methods [like Approval & Score Voting] where you categorize in maybe three or four classes [so, giving a score out of 3 or 4, not 10 or 100] probably – in spite of what I said about manipulation [strategic voting] – is probably the best.”

- -

That's as strong an endorsement as you'll ever squeeze out of a math-head.

-
- - - - -
- -

ahem

- -

- DEAR JUSTIN “TOTES ADORBZ” TRUDEAU
- (and everyone else around the world pushing for voting reform) -

- -

Thank you for taking this small but powerful first step! We've known for way too long that our current voting method – First Past The Post – forces voters to be dishonest, creates a polarizing "lesser of two evils" scenario, and screws over both major and minor candidates.

- -

- However, you're probably only considering Instant Runoff Voting. - Which, to be fair, is better than than First Past The Post, - and if it's a choice between just those two, definitely go for Instant Runoff. - But IRV still has a glitch as undemocratic as FPTP's – - and worse, in our age of distrust, Instant Runoff's lack of transparency may be deadly for democracy. - Yes, sure, IRV was the best voting method we could come up with... - in 1870. - And since then, IRV has dominated the conversation, - unwittingly framing the whole voting reform debate as “simple vs expressive”. -

- -

- But that is a false choice. Thanks to computer simulations, real-life studies, and a bunch of math nerds, - we now know of voting methods that are both simple and expressive. -

- -

- Personally, I'm leaning towards Score Voting. - It's simple, very expressive, and already familiar to anyone who's seen Amazon's or Yelp's “five star” review method. - But that's just my humble opinion. - You could also make the case that Approval Voting is more practical, - because it's even simpler, and would already work with existing voting machines! - All you'd need to do is change the instructions from - “vote for the candidate you like” to “vote for the candidates you like”. -

- -

- Or maybe I'm completely wrong about Instant Runoff Voting, and it's actually pretty okay. - Heck, you could even go for Borda Count, as a hilarious prank. -

- -

I won't claim to know which voting method is The Best™. I shall keep open this discussion, just as long as we have this discussion. For three reasons:

- -

1) If I claim one voting method is the best, end of story, all the social-choice-theory nerds will be on my butt, yelling, BUT NICKY WHAT ABOUT QUADRATIC VOTE BUYING

- -

2) We still need to test these alternative voting methods with actual experience, - not just annoying internet flame wars between IRV advocates and Score Voting advocates theory. - All the more reason for small towns, local states, and nations like Canada to be pioneers, to bravely experiment!

- -

3) Keeping the discussion going is what democracy is.

- -

A recent study found that in many Western countries – from Sweden to Australia to the United States – support for democracy has plummeted over the last several generations. - In 2011, almost a full quarter of young Americans said democracy was a "bad" or "very bad" way to run a country. - And today, one in six Americans say it'd be "good" or "very good" to be under actual military rule. -

- -

Our age of distrust goes a lot deeper than the technical details of a voting method. There isn't gonna be One Weird Trick to fix democracy. But as a first step, a low-hanging fruit, a way to show that, yes, you will make the method respond to the needs and wants and pains and hopes and dreams of your people – well, fixing our voting method's a good start as any.

- -

Because, this isn't just about trying to build a better ballot.

- -

This is about trying to build a better democracy.

- -

<3,
- ~ Nicky Case

- -
- -

P.S: Since you've read & played this all the way, here, have a bonus! - A “Sandbox Mode” of the election simulator, with up to five candidates. - You can also save & share your very own custom election scenario with others. Happy simulating!

-
- - - - -
-
-

SANDBOX MODE! (link to just this)

- -
-
-

- One hope for Sandbox Mode is that readers can debate with me and each other using this tool! - Not just telling me I'm wrong, but showing me I'm wrong. - For example – - - here's a model I made in Sandbox Mode, - showing an interesting argument against Approval & Score Voting. - Granted, this tool is very limited – it doesn't handle strategic voting or imperfect information – - but I think it's a start, and may help improve our Democratic Discourse™ -

-
-
- - - -
-
- - -
- -
-
PUBLIC DOMAIN
- Zero rights reserved. - I'm giving away - all my art/code/words, - so that you - teachers, mathematicians, hobbyists, activists, and policy wonks - can use them however you like! - This is for you. - Get my source code on GitHub! -
-
- -
- - -
- -
“BUT WHAT CAN I DO?”
- -

- For citizens: Remember, think global, but act local. - Change from the bottom-up lasts longer. - If you're in the US, - find your representative - and badger 'em. - If you're in Canada, - find your Member of Parliament - and badger 'em. - Also if you're Canadian, - fill out the MyDemocracy.ca survey before the end of 2016! - This survey has a few questions specifically about voting reform! - (sadly, the question is still framed as "simple vs expressive". - that is why i've been so gung-ho about Approval & Score, - and maybe a bit too mean towards IRV) -

- -

- For learners: - Watch CGP Grey's Politics in the Animal Kingdom series! - It's charming, and covers more ground than I did here – it explains - gerrymandering, proportional representation, and more. - Also, read Gaming The Vote by - William Poundstone. - It's a thrilling read, - with dramatic human stories of crooks & conmen trying to game our glitchy voting systems – - and sometimes, succeeding. -

- -

- For teachers: - This entire "explorable explanation" is public domain, copyright-free, - meaning you already have permission to use this freely in your classes! - You can even use the Sandbox Mode to create your own material, - or as a tool for students to make something on their own. -

- -

- For coders: - This is all open source! - So you can get my code on GitHub, and remix it to your heart's content. - (sorry in advance for my messy code) -

- -

- Check out these organizations: - Though they may differ on what voting method they like best, - they all have a common goal: to reform the one we have. - Electology likes Approval Voting most, - FairVote likes Instant Runoff most, - and RangeVoting.org likes Score Voting most. -

- -
ON THE SHOULDERS OF GIANTS
-

- This "explorable explanation" was directly inspired by these two projects: -

- -

- Voting Sim Visualization by Ka-Ping Yee (2005) - was a real eye-opener. - (hat tip to Bret Victor for sharing it with me!) - I've heard lots of written debate over FPTP vs IRV vs Condorcet vs Approval vs blah blah blah, - but I'd never seen their difference visualized so clearly! - It gave me instant insight. - And it actually changed my mind – I used to think IRV was pretty good, - but after seeing IRV's messiness (as shown above), I realized it's actually kinda stinky cheese. -

-

- However, even this brilliant visualization was still too abstract. - And since it wasn't interactive, I couldn't test the many questions & scenarios that came to mind. - So that's why my second inspiration was... -

- -

- Up and Down the Ladder of Abstraction - by Bret Victor (2011). - It's one of the web's earliest "explorable explanations" (also a term Bret coined) - and it is gorgeous. - Obviously, I borrowed the format of mixing words & "games" to explain things, - but I also followed the formula of starting concrete – one voter – - then moving up to the more abstract – a whole election. -

-

- - You can learn more about Explorable Explanations here. -

-

- And last but not least, thank you to all the math & policy nerds - who spent way too much time thinking about all this. -

- -
- STAY IN TOUCH, MAYBE?
-

- Every once in a while, I'll fall into an endless rabbithole – - like this one on voting methods – and slowly crawl my way out, bloodied and bruised, - with a new interactive thing for you! - If you wanna find out - when I finally get around to making new shtuff, you can... -

- -

- And if you wanna see more of my past projects, - check out my wobsite! -

-

- See you again soon! Have a Happy New Year 2017, or try to, anyway. -

- -
- - -
-
A BIG THANKS TO ALL MY
-
SUPPORTERS
- - - -
-
aimee jarboe
-
frank leon rose
-
jared cosulich
-
louis-jean teitelbaum
-
matt hughes
-
micah cowan
-
michael alan huff
-
natalie sun
-
noel lehmann
-
phil dougherty
-
tom cascio
-
tom knowles
-
-
-
-
- Adam M. Smith
- Alex Dytrych
- Andrew
- Andy
- Artemiy Solopov
- Aschelon
- ben fei
- Benjamin Riggs
- Bob Wise
- Brandon
- Brent Werness
- Brian Wu
- Bruno Guerrero
- Buster Benson
- Casey Ross
- Charlie McIlwain
- Christopher
- Colin
- Colin
- Cort Stratton
- Craig Steele
- Daniel Horowitz
- Daniel Shiffman
- Dave Tu
- David Smit
- Dylan Meconis
- Fahrstuhl
- Feiya Wang
- Forrest Oliphant
- Frank Leon Rose
- Henry Reich
- Iñaki
- J. Hu
- Jacob Christian Munch-Andersen
- Jacques Frechet
- James Hogan
- Janusz Leidgens
- John_Ca
- Johnny Owens
- Joseph Perry
- Joshua Horowitz
- Julia Karmo
- Karen Cooper
- Kat Suricata
- Kate Fractal
- Kathryn Long
- Kevin
- Kevin Wang
-
-
- Klemen Slavic
- kuerqing1024
- Linda Booth Sweeney
- Maic Lopez Saenz
- Matt "Kupo" Roszak
- Matt Warren
- May-Li Khoe
- Mekki MacAulay
- Micah Cowan
- Michael Duke
- Michelle Brown
- Michelle Kelly
- Milan Pingel
- Monika Denes
- Mustafa Alic
- Nick Schrag
- Nikita
- Noah Swartz
- Pablo Lopez Soriano
- Pat Mächler
- Peter McEvoy
- Philip Tibitoski
- Piotr Migdal
- Rachel Nabors
- Raphael D'Amico
- Richard Hackathorn
- Rob Napier
- Roland Tanglao
- Ryan Barker
- Sam Anderson
- Sam Maynard
- Samira Nedungadi
- Sarah Barbour
- sarah mathys
- SB Sigma
- Seanny123
- Serguei Filimonov
- Sigpipe
- Sylvain Francis
- Syria Carys Sirlay
- T_Caramel
- TisGood
- Tony Onodi
- Traci Lawson
- Yona
- Yu-Han Kuo
- Zach Smith
- Zoe Bogner -
-
-
-
AND SPECIAL THANKS TO
-
-
- Alex Dytrych
- Alex Jaffe
- Brian Bucklew
- Chris Walker
- Christine Zhang
- Dan Zajdband
- Daniel Cook
- Droqen
- Jason Grinblat
-
-
- Jessie Salz
- Lisa Charlotte Rost
- Martin Shelton
- Patrick Dubroy
- Pietro Passarelli
- Sandhya Kambhampati
- Tanya Short
-
-
-
-
- -
-
- sharing is caring! - -
-
- -
- - - - diff --git a/index.md b/index.md new file mode 100644 index 00000000..23eeaded --- /dev/null +++ b/index.md @@ -0,0 +1,49 @@ +--- +layout: page-2 +title: Home +banner: Smart Voting Simulator +description: An Explorable Guide to Group Decision Making +byline: 'By Paretoman and Contributors, May 2020' +twuser: paretoman1 +--- +{% include letters.html %} + +This is the home of the Smart Voting Simulator, a great resource to learn how voting methods work, visually and interactively. + +You’ll really enjoy this site if you feel like there's got to be a smarter way to vote than what we’re doing right now. + +## Intro + +{% include card.html title='The Basics' description=" - #1 - Start here. See the 2D model we use for this site." url='basics' img='img/basics.png' class='drawBorder'%} + +{% include card.html title='Finding Common Ground' description=' - #2 - Why do we vote? To make smarter group decisions.' url='commonground' img='img/venn-diagram.png' %} + +## In Depth + +{% include card.html title="Condorcet Methods" description=' - the best way to find common ground' url='condorcet' img='img/condorcet.png' %} + +{% include card.html title='Approval Voting' description=' - a practical improvement - And it works with strategy.' url='approval' img='img/winner-circle.png' %} + +{% include card.html title="STAR Voting" description=' - and a deeper dive into using strategy and the game of chicken.' url='star' img='img/chicken.png' %} + +{% include card.html title="Instant Runoff Voting" description=" - a kinder politics with a kind of Ranked Choice Voting (RCV) for single-winner elections" url='irv' img='img/irv.png' class='drawBorder' %} + +{% include card.html title="The Single Transferable Vote" description=" - an introduction to proportional representation with STV, the multi-winner form of Ranked Choice Voting RCV" url='stv' img='img/stv.png' class='drawBorder' %} + +{% include card.html title="Proportional Representation" description=" - Bring the candidates closer to the voters. Continuing after STV, here are more kinds of proportional representation without parties, using scoring and paired methods." url='proportional' img='img/proportional2.png' %} + +{% include card.html title='Primaries' description=" - They don't always work." url='primaries' img='img/primary-strategy.png' class='drawBorder' %} + +## In Deep + +Where to find more information: + +* [Links](links) - I have a big list of links to communities, organizations, simulators, polling sites, courses, videos, essays, books, references, and bibliographies. + +Now that you have learned so much, you can try and share your ideas in the sandbox. Also, try the original simulation that this sim was based on. + +{% include card.html title="Sandbox" description=' - Try everything. Share your creations.' url='sandbox/' img='img/sandbox.png' %} + +{% include card.html title="To Build a Better Ballot" description=" - Nicky Case's original sim" url='original' img='img/split-vote.png' class='drawBorder' %} + +Finally, for coders, [here's technical instructions on how to modify this sim.](modify) \ No newline at end of file diff --git a/irv.md b/irv.md new file mode 100644 index 00000000..08a46e9a --- /dev/null +++ b/irv.md @@ -0,0 +1,160 @@ +--- +permalink: /irv/ +layout: page-3 +title: Instant Runoff Voting +description: An Interactive Guide to IRV (the Single-Winner form of Ranked Choice Voting RCV) +byline: 'by Paretoman, June 2020' +--- +{% include letters.html %} + +I'd like to describe an ideal of cooperative politics and how it is better achieved with **instant runoff voting (the single-winner form of Ranked Choice Voting)**, where (in most situations) + +* candidates that are on the same side can support each other, and +* voters can vote for their favorite candidate. + +This is in contrast to what we have now with **single choice voting**, where + +* candidates are forced into opposition with each other just by the way we vote and end up using terms like spoiler to intimidate each other, and +* voters face a dilemma when they have to decide whether to betray their fellow supporters and support a candidate who is more viable but not their favorite. + +## Spirit of Cooperation + +In an ideal world, like-minded people would gather together to work as a group to accomplish goals that are larger than themselves. The higher the goal, the bigger and broader the group needs to be. + +We can get closer to this ideal if we use Ranked Choice Voting because this voting method allows candidates to ally and work together as a team. This happened in an actual election for mayor in 2018 where two candidates came together to support each other against a third. The campaign not only got the strength of both candidates, but because the ballots were Ranked Choice ballots, it got the strength of all their combined voters. This happens in any system which avoids vote-splitting. In Ranked Choice Voting and also in approval voting, in STAR voting, in score voting, and in pairwise ranked Condorcet voting, there is a spirit of cooperation because voters can support more than one candidate. Candidates see each other as assets to help reach out to new voters rather than liabilities that take their voters away. + +So.. what is Ranked Choice Voting and how does it work? + + + + + +## Introducing RCV + +Ranked choice voting asks voters to rank the candidates in order from best to worst. Here's an example ballot where you can just write a number next to each candidate: + +{% include sim.html +id="irv_ballot_sim" +gif = "gif/irv_ballot.gif" +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSy04DMQz8lSrnCMWJnTj9CA6IC9rtoYieqFokuCAE347t0UpIqNrDxK_JjLNfqaT9sujMxPOQF5plO1mutn445ETeMmtm9ailfcmJ0z79cMpJIuzWYrVhUPK_zyp6szJvVqgENxHIqSKEAGIA7icTQAYmgNjQrmsGM4rVeCxZCZHRVAPQVI7BKjFQO5IDkSKaMdBKCCVfAZgaBLWGOgQ1Y1ooExo7SuBrsEq5WomdznvY7bldrtvBCJfmB445_kvJcMoDzwCJPCMpWJiQP86ank7v63rZ3V93a_KXgnGBXIFxEQCMC1gF6xMY7wVA0dlhumN9XcKSi-2g6FDUsfoBRQMLGzUoRgNgYQMvOLZfaKCo265yy-x5WFQQKoUmhRiFGGUkBQA9Cj6FLHVZd8P-OvA8H8_n68fj59vJVvZwvLyeXtL3Lx2Xrl0VAwAA)" title = "Ranked Choice Voting Ballot" +caption = "Your vote counts for your top choice. It's like a stack of cards. The top card is who you like best. Under that is your second choice, and so on." +comment = "single ballot" +%} + +During counting, your vote counts for your top candidate. One-by-one, the candidate with the least number of votes is eliminated and taken out of the running. Still, your vote counts for your top candidate of those remaining. The process of elimination can continue until there's only one candidate left, or until the winner is decided: that means someone has gotten more than 50% of the votes. Once that happens, nobody else can beat them. + +This is pretty similar to what we do now, eliminating candidates in the run up to the election. Except now that voters put all the information on their ballot, the process of elimination is transparent. + +Here's a sketch that shows everyone connected to their first pick. Colored flow lines show some voters moving to their next choice after their top pick is eliminated. The flow diagram shown on the right is called a Sankey diagram, after it's inventor. + +{% capture cap5 %}Your vote counted for {{ A }}, your favorite, in the first round. Not enough people chose {{ A }} as their favorite, so {{ A }} was eliminated. Your vote counted for {{ B }} over {{ C }} in the final round, so you didn't spoil the election for {{ B }} by voting for {{ A }}.{% endcapture %} + +{% include sim.html +id="irv_second_counts_sim" +gif="gif/irv_second_counts.gif" +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRu04EMQz8l9Qu4md29zMQ3WqLQ3fVnYCCBiH-HcdTHOKEUkycscdj56v1tu37ErTKQTtHJ5Y-bx7E63Ic1HhmcAixRTKLkvJ817Z1atY2o-Z1j8wUejiZO5LJVtT_nuSW5B7ei1n_ZbhXQ57eZigIYYkNAFMcgLTA6ZSznSakNlOTXvYldSRBAJARQ4pXgUBGBqIF0VoF2ssoz6VwEQpDquChpKm0MzESAxT0FKNyLcvucsZYwbzK_aq16Eqwqrff0hbVzgY-CFYNAzsW57DpGNhh02HTHYCBfYDD2hwDRwdwZQaGDawtvHxqGglIBBzEWjDgYKB2CEAB-LmX0-329vH8-X5pW3s6vV4v5_b9A5Xa76erAgAA)" title = "Election with Instant Runoff Voting" +caption = cap5 +comment = "do a transfer, you still win" +%} + +**Instant Runoff Voting (IRV)** is the formal name for this counting procedure. It refers to Ranked Choice Voting when there is only one candidate being elected. + +**The Single Transferable Vote (STV)** is the formal name for a similar procedure with an extra step. It refers to Ranked Choice Voting when there's more than one winner. You could call it multi-winner RCV. The "Single" in the name comes from the fact that your vote counts for a single candidate, your top choice. The "Transferable" part refers to when if, in an elimination round, your top choice gets eliminated, then your vote transfers to your new top choice for the next round of counting. There is much more to say about the procedure of STV, but that will be all we mention it on this page. + +Around the world, IRV has been called by various names: the Alternative Vote (UK referendum 2011) and preferential voting (Canada 2015). STV is also known as the Hare-Clark system in Australia. IRV and STV are used nationally in Ireland, Australia, and India, and for cities and states in the US. + + + +## Less Strategy, More Honesty + +By being able to express more on a Ranked Choice ballot, voters no longer face a common dilemma. The dilemma happens when you have to decide, "Am I going to vote for my favorite?" or "Am I going to show support where it would count in determining the winner of the election?" Honestly, the voter would like to support their favorite candidate and send a message of strength with all the other supporters that "this candidate represents us the best and deserves recognition". + +{% capture cap6 %}Same example as above. You don't have to vote strategically to support {{ B }}. You can support {{ A }} over {{ B }} and {{ B }} over {{ C }}. There is no benefit to switching from H=honesty to F=frontrunner strategy.{% endcapture %} + +{% include sim.html +id="irv_not_wasted_sim" +gif = "gif/irv_not_wasted.gif" +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSu04DMRD8F9dbeJ_25TMQ3SlFEKmIgIIGIfh21juKgkDoirF31uOZ9X203g77PoM2OdLO0Ymlr5UH8TaPR2q8OjiE2CKZqaS86toOnZq1Q_uyRs1rG9mc3EhIaeq_v-Rmcn_qxWz_MtxLnZeXSTFWSVCCDTYAXHAA0gZbYl6pCanP1CS1siipJQkCgIwYWrwOCGRkYDex2-qA9jLLaxBchMKQKngoaSrtTIzGAAU9RVymnHyzm5wxxrCWclum7C5osDpvP6Ut6jobeBRYNQR2DM9h0xHYYdNh0x2AwD7AYWyOwNEBXJ2BsIGxhZdPTSMBiYCD2AoGHAycHQJQAF5u4OXG9QcaIOd1RqRkq45IswMQacLMhJlpZXQ6ADN_OF0uL2_376_n_F_vTs9P58f2-Q3G8My5_gIAAA)" title = "RCV: Less Spoiler Dilemma" +caption = cap6 +comment = "slider showing strategy of voters. show a single voter, too. Show choice between systems FPTP and IRV." +%} + +This dilemma is familiar to anyone who has voted in a single-choice voting election. (More formally, this kind of choose-only-one voting is called plurality or First Past the Post, FPTP). + +{% capture cap9 %}Do I waste my vote on my favorite {{ A }} or do I support the more viable {{ B }}? Change from H=honesty, to F=frontrunner strategy to get a better result.{% endcapture %} + +{% include sim.html +id="wasted_vote_sim" +gif = "gif/wasted_vote.gif" +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSy0oEMRD8l5z7kH4mM1_hwdswhxU8CAOKrIdF9Nu3k2JZUWQOlXR1KlWd-Sy1rNvWgxbZaeOoxFLHyoN46ftOhUcHhxBbJNOVlEddy1qpWFnLdy1UfG4jm5NrCSlN9feXXE_uT30yy78M16nOw0unaKMkKMEGGwAuOABpgy0xr9SE1GcqklpZlNSSBAFARgwtPg8IZKRh17Fb5gGt0yyPQfAkFIZUwUNJU2ljYjQGKOgp4jLl5Ivd5YwxhrGU-zJlN0GDzfP2U9piXmcNjwKrhsCO4TlsOgI7bDpsugMQ2Bs4jM0ROCqAZ2cgbGBs4dOnppGARMBBLBMaHDScbQJQAF6u4eXa7QdqIPttRqRko45IvQIQqcNMh5lu02h3AGb-dDqO1_Pj5e05_9eH4-P9dLycL-XrCt84MXMBAwAA)" +title = "The Single-Choice Dilemma" +caption = cap9 +comment = "slider showing strategy of voters. Maybe show a single voter, too. Show choice between systems FPTP and IRV." +%} + +## Electability Polling + +Still, there will be tough situations where if there are a large number of candidates that are all doing well. Then it may be up to the voter to vote strategically and vote for a more electable candidate. The voter should rely on head-to-head polling information to see who would win the final round. This is the same kind of problem that is faced in the primaries. The nice part is that this is handled in a single stage of voting. There are other ways to handle this electability problem. Some voting systems can even handle it without voters having to rely on polls and you can read about that on the page about [finding common ground](http://./commonground.html). + +{% include sim.html +id="tough_sim" +gif = "gif/tough_irv.gif" +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSu04EMQz8l9QWihM7du4zEN1qi0NcxQkoaBCCb8fxaIUAoS3Gr0xmnH0vtZy2bTpxmzttrHZErjRtBTNK1vedCq9Zlk7c28p7OVUqUk7lUwoVzXTEUPQsIMip_v6i59H7U8_O_LfDNdl5aXAatkoNJchgAUAFD0DIYAmMKxcEP1NpNbMWXC2gAUDTBCNB0wNA0wyZI5t5oNcUy2sRnI0Opt4BENSDaWPKb40ONMHYYZipUY-mJOUK-AjaEQTp1lYgeVJ-0spI2WJ4EggV2FWsTiFSIVIhUmFXFQC7auh53qSwOyqAc3Jg9wNLG5o2loUBigEFYyYYFBjOWgN0ANZkeDc7fh9D07_3Q7LqsOQVgBd0WHJYcgEoAPt28Dlk-ZJ1Y1Tuz9fr8-vd28slfuLb89Pj5aF8fAFAn7oMFQMAAA)" +title = "A Tough Situation Solved by Strategy" +caption = "Strategic voting can help turn a tough situation into a successful election when there are useful head-to-head polls." +comment = "Silver lining. So, maybe show an option button to turn on or off strategic voting." %} + +This is an improvement over single-choice voting. The polls are much more useful since rankings are given and any pair of candidates can be compared head-to-head, so voters are able to strategize better. Attempting the same strategy with FPTP and FPTP polls can turn a small lead in the polls to a big lead at the ballot box. + +{% capture cap18 %}{{ B }} loses even though he's in the middle because he never got good poll numbers in FPTP polls.{% endcapture %} + +{% include sim.html +id="fptp_bad_polls_sim" +gif="gif/fptp_bad_polls_sim.gif" +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSu04EMQz8l9QuYjuOk_sKCrrVFYdEgbQSCB3FCcG343i0QoDQFuNXJjPOvpdaTts2B7HMM21sfkTDaPoKZpRcz2cqvGa5KbHKyrWcKpVWTuWzFiqWaY-h6HlAkFP9_UVvRO9PPTvz3w7XZOelYVD3VRKUIIMbACq4A0IGt8C4ckHwMxWpmUlwSYAAQCMNI0GjAaARRzaQzTygNcXyWgRnQ8GkCoAgDaaNKb812tEEo8Iwk5BGsyXlCvgI5AiCdJMVtDzZftK2nrKb40kgtMGuYXUGkQaRBpEGu2YA2DVHb-RNBru9AjgnO3bfsbRuaWNZ6KDoUNBngkOB46wLQAFYk-Pd_Ph9HM3xvR9qqw5LowLwggOWBiyNBjAA9v1w2ffn6_3t5TH-1rv97fWyP11v5eMLMekMPQEDAAA)" +title = "FPTP: Tough to Strategize" +caption = cap18 +comment = "C loses in the middle" +%} + +It is very important that head-to-head polling is reported because the final round is a head-to-head match. This information is available in a ranked poll, but without it, the polls are not that much better than FPTP polls. + +## Sp***er + +The worst part of single-choice voting is the term spoiler. It sounds bad. and there's a reason it sounds bad, because that's how people intend to use it. The hope is to get another candidate's votes by intimidating their voters. This insult would no longer work in a Ranked Choice election. It would only be accurate when the spoiler is getting more first-choice votes than the accuser. And then it would not be effective because voters behind that large a candidate would not be intimidated. + +Ranked choice voting would lower the barrier to entry for new candidates who don't have many supporters yet. Hopefully, this would create competitive pressure to bring out the best candidates. + +The technical definition of a spoiler is a candidate with a small enough level of support that they cannot win themselves, but they can change who wins merely by being in the election. You would think that the only way to change who wins is to be the winner. For instant runoff voting, it's true if the candidate is small enough to be called a spoiler. + +{% capture cap7 %}{{ B }} accuses {{ C }} of spoiling the election, hoping he’ll get {{ C }}’s supporters{% endcapture %} + +{% include sim.html +id="standard_spoiler_sim" +gif = "gif/standard_spoiler.gif" +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSvUoGMRB8l9Qpsr_J3VNY2B1XfIKFcKDIZ_Eh-uxudjwQRa6YZGczN7PJe2ll3bZlVOJlrxsN-V4xeWVZ9r0Wmi2kwQjPvZS11aJlLZ-t1GK59WgKrgeEZm2_v-BGcH_qySz_MtRSnaYH77PAKMAEKQAeyAFhgjQwfigBoU61cChFkUOJAxgAGVa0WB5gyHDHbmC35AFpaZXmGCgJgSER8DAkobRRJTQ6KOgJwlLloDTl5oLOBZ-LENyyR_Oc_pRUT8PacRWwqAhqGJnBniGowZ4hqBkAQa2Dw7gMQb0BKDsdIR3jcssIEkYcEg4HviR0OOg42xkgAAyo48b6-Ww6yHHOpkrVWUek0QCINGBmwMzQNDoMgFk_XI7j-Xp_e3mMV3p3vL1ejqfrrXx8AQl2JQHwAgAA)" +title = "Accusation of Spoiler" +caption = cap7 +comment = "standard spoiler example. Maybe needs a switch between FPTP and IRV." +%} + +{% capture cap8 %}{{ B }} is okay with {{ C }} running because {{ C }}’s supporters will support {{ B }} against {{ A }}.{% endcapture %} + +{% include sim.html +id="no_spoilers_sim" +gif = "gif/no_spoilers.gif" +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSu07EQAz8l60tFL92nfsMRBelOMRVnICCBiH4drweIiEQSjH2jncy4-S9Le20bWsQy7rTxqHflXAn0XXfqfEcYUtGZfbaTgs1a6f2aY2aV9tzKLmRkDUtv5_kIrk_58Ws_zK8lDpPDzF7QQ8PbABY4A5ID2yJ-T5NSHGmJimUh5JCkiAAyIhhxOuCQEYGukC31gVdyinPLXARCkOq4GFIU2ljYgx2UNBTZGWSpKzkZsFHIUeRglvNWN2zn5LWy7ANfAlYNAR1bMxhzxHUYc8R1B2AoD7AYV2OoH0BcE12hOxYV_eKoGmkQ6LDQV8LBhwM3B0CUAAWNPDFxvHXDJBx7IaUbJ4jUkAwEClgJmAmrIyGA-AnoBewFdPWzaB2f75en1_v3l4u-e_enp8eLw_t4wsWUHUZAwMAAA)" +title = "No More Accusations" +caption = cap8 +comment = "no spoilers" +%} + +Resistance to spoilers cannot be overstated. If you decide to become engaged in your community and run for office, the other candidates can't call you a spoiler anymore. They might even see you as an ally who can help bring in supporters. Running for office is a great avenue for people to become involved in politics, and it would be nice if they are welcomed. + +## Afterword + +In summary we have illustrated the problem that the fundamental way in which we cast our votes pits every candidate against each other. We can address the problem by allowing voters to support more than one choice. That means candidates can join each other as teams and work together on campaigns, at least until they become large enough to become each other's best competition. We also showed that voters are able to support candidates that they feel best represent themselves, and even in more complex situations where they need to strategize, voters will be able to rely on more informative head-to-head polls. + + + + \ No newline at end of file diff --git a/js/index.js b/js/index.js index 8ba04fa9..65c6d3bd 100644 --- a/js/index.js +++ b/js/index.js @@ -1,7 +1,9 @@ var _onscroll = function(){ var scrollY = window.pageYOffset; var innerHeight = window.innerHeight; - document.getElementById("splash_iframe").contentWindow.postMessage({ + var a = document.getElementById("splash_iframe") + if (a == null) return + a.contentWindow.postMessage({ isOnScreen: (scrollY<400) },"*"); }; diff --git a/js/viewport-min-width.js b/js/viewport-min-width.js new file mode 100644 index 00000000..b738ca9f --- /dev/null +++ b/js/viewport-min-width.js @@ -0,0 +1,39 @@ +/** + * This JavaScript adds a "min-width" attribute to the viewport meta tag. If + * the device's width is less than the min-width, we will scale the viewport + * to the min-width; otherwise we will leave the meta tag alone. + * + * This script must be added *after* the viewport meta tag, since it relies on + * that tag already being available in the DOM. + * + * Implementation note: The reason we remove the old tag and insert a new one + * is that Firefox doesn't pick up changes to the viewport + * meta tag. + * + * Author: Brendan Long + * License: Public Domain - http://unlicense.org/ + * See: https://github.com/brendanlong/viewport-min-width-polyfill + */ +(function() { + var viewport = document.querySelector("meta[name=viewport]"); + if (viewport) { + var content = viewport.getAttribute("content"); + var parts = content.split(","); + for (var i = 0; i < parts.length; ++i) { + var part = parts[i].trim(); + var pair = part.split("="); + if (pair[0] === "min-width") { + var minWidth = parseInt(pair[1]); + if (screen.width < minWidth) { + document.head.removeChild(viewport); + + var newViewport = document.createElement("meta"); + newViewport.setAttribute("name", "viewport"); + newViewport.setAttribute("content", "width=" + minWidth); + document.head.appendChild(newViewport); + break; + } + } + } + } +})(); diff --git a/links.md b/links.md new file mode 100644 index 00000000..dac39053 --- /dev/null +++ b/links.md @@ -0,0 +1,96 @@ +--- +permalink: /links/ +layout: page-2 +title: Links +--- + +Here's some great resources for expanding your voting knowledge. Some were referenced in the pages on this site. + +#### Community + +- [EndFPTP Subreddit](https://www.reddit.com/r/EndFPTP/) - for newcomers +- [Election Methods Mailing List](https://electorama.com/em/) - for discussion of the nitty-gritty details +- [Searchable Archive of EM Mailing List](http://election-methods.5485.n7.nabble.com/) +- [ElectoWiki](https://electowiki.org/) - a wiki for election methods to accompany the EM mailing list +- [ElectoRama](https://wiki.electorama.com/wiki/Main_Page) - the predecessor of ElectoWiki + +#### Organizations + +- [Colorado Approves](https://coloradoapproves.org/) - The inspiration for the page on finding common ground came from a discussion with Blake. +- [Unsplit the Vote](https://unsplitthevote.org/) - End vote-splitting. (Thanks for the feedback.) +- [Center for Election Science ](https://www.electionscience.org/) - likes approval voting + +- [STAR Voting](https://www.starvoting.us/) - likes STAR voting. Related: [Equal Vote](http://www.equal.vote/) +- [FairVote](https://www.fairvote.org/) - likes RCV, both single- and multi-winner versions +- [FairVote Canada](https://www.fairvote.ca/) - likes proportional representation +- [Electoral Reform Society](https://www.electoral-reform.org.uk/) - UK - likes proportional representation with STV + +#### Simulators + +- [District](http://polytrope.com/district/) - a game about representation and redistricting +- [To Build a Better Ballot](https://ncase.me/ballot) - by Nicky Case - the original sim this sim is based on +- [Voteline](http://zesty.ca/voting/voteline/) - by Ka-Ping Yee, who is the originator of win maps AKA Yee diagrams +- [Arrow's Theorem: Visualizing 3-Candidate, 3-Voter Elections with Hexagons](https://hexagon.bettervoting.org/) - Andy's excellent explorable explanation of Arrow's Theorem, accompanied by a [YouTube video](https://youtu.be/Uvax1Hj8t_E), and a [software repository](https://github.com/abjennings/socialchoice-hexagons). I really like this because it's the only constructive proof I have seen that starts with Unanimity, IIA, and determinability (no ties) and ends with dictatorship. +- [Political Sim](https://www.accuratedemocracy.com/s_sim.htm) - by [Accurate Democracy](https://www.accuratedemocracy.com/) - the earliest interactive voting simulator - It requires some technical know-how to get going. +- [Median Voter](https://pianop.ly/voting/median.html) - a simple conceptual experiment with discussion of strategy and median and mean +- [RBVote Calculator](http://www.cs.angelo.edu/~rlegrand/rbvote/calc.html) - I copied all of his methods into this simulator under "RBVote". +- [More Software on ElectoWiki](https://electowiki.org/wiki/Voting_links#Software) + +#### Polling Sites + +- [Just raise your hands](https://www.youtube.com/watch?v=orybDrUj4vA) or use your new knowledge of voting systems in your next group decision with these helpful polling sites: +- [ElectoWiki's List of Polling Sites](https://electowiki.org/wiki/Voting_links#Polling_sites) - Many are on the list, but the ones below stand out to me. +- [★.vote](https://star.vote) - STAR - used by STARvoting.us +- [Condorcet Internet Voting Service](https://civs.cs.cornell.edu/civs_create.html) - Condorcet - many kinds +- [Doodle](https://doodle.com/en/) - Approval - low cost +- [OpaVote](https://www.opavote.com/) - Many kinds of voting - but you pay for it or host it yourself. +- [Rankit.vote](https://rankit.vote/) - Ranked Choice Voting - used by Fairvote.org + +#### Videos + +- [CGP Grey](https://www.youtube.com/watch?v=s7tWHJfhiyo&index=1&list=PLkLBH5Kzphe0Qu8mCW1Leef2xSxPK1FIe) - This set of videos are excellent intro to voting methods, ready to share. He made them at the time that the UK was considering using IRV, which the UK called the Alternative Vote for historical reasons. +- [Tactical Voting](https://www.youtube.com/watch?v=tE-ktIxN-6Q) - a good overview of what strategy means in voting +- [Simulating Alternate Voting Systems](https://www.youtube.com/watch?v=yhO6jfHPFQU) - from Primer - a 2D simulation of the voter's dilemma and how IRV/RCV and Approval address it. + +#### Courses + +- [Making Better Group Decisions: Voting, Judgement Aggregation, and Fair Division](https://www.youtube.com/watch?v=EYJ3ta080bI&list=PLBmjSBnW7zFm9rutlKI0nm9dQDZfnSo_g&index=3) - by Eric Pacuit - formal logic and math perspective - on my Youtube playlist + - [Syllabus](http://ai.stanford.edu/~epacuit/classes/voting-fall2012.html) from a previous year + - [Split Cycle](https://arxiv.org/pdf/2004.02350.pdf) - a new method he invented with Wes Holliday +- [Game Theory](http://www.game-theory-class.org/) - on Coursera and Youtube - from Stanford and UBC - by Matthew O. Jackson, Kevin Leyton-Brown, and Yoav Shoham + +#### Essays + +- Check out more of Jameson Quinn's excellent explanations of voting theory: +- [A Voting Theory Primer for Rationalists](https://www.lesswrong.com/posts/D6trAzh6DApKPhbv4/a-voting-theory-primer-for-rationalists) +- [Five General Voting Pathologies: Lesser Names of Moloch](https://www.lesswrong.com/posts/4vEFX6EPpdQZfqnnS/5-general-voting-pathologies-lesser-names-of-moloch) +- [VSE Study on Strategies](http://electionscience.github.io/vse-sim/VSE/) + +#### Books + +- [Gaming the Vote](https://books.google.com/books/about/Gaming_the_Vote.html?id=_24bJHyBV6sC) - by William Poundstone +- [Mathematics and Democracy](https://books.google.com/books?id=SA3NqUzlZZIC) - by Steven Brams +- [Accurate Democracy](https://accuratedemocracy.com/) - by Rob Loring - see the eBook.pdf link + +#### Political Science General References + +- [Stanford Encyclopedia of Philosophy](https://plato.stanford.edu/entries/voting-methods/) on Voting Methods +- [Oxford Handbook of Electoral Systems](https://www.oxfordhandbooks.com/view/10.1093/oxfordhb/9780190258658.001.0001/oxfordhb-9780190258658) - academic subscription required +- [ACE Project](http://www.aceproject.org/ace-en/topics/es) - guide to electoral systems +- [Electoral System Database](https://www.idea.int/publications/catalogue/electoral-system-design-database-codebook?lang=en) from [IDEA ](https://www.idea.int/) +- [PARLINE](http://archive.ipu.org/parline-e/parlinesearch.asp) - database of national parliaments + +#### Bibliographies + +- Find some good reading material +- [Stanford Encyclopedia of Philosophy on Voting Methods](https://plato.stanford.edu/entries/voting-methods/#Bib) - everything +- [Proportional Representation Society of Australia](https://www.prsa.org.au/bibliogr.htm) - Single Transferable Vote +- [Fairvote](https://www.fairvote.org/proportional_representation_library#bibliography) - Ranked Choice Voting +- [Jack Santucci](https://www.voteguy.com/rcv-academic/) - Ranked Choice Voting +- [Warren Smith](https://www.rangevoting.org/RVPapers.html) - Score Voting +- [Stephen Martin](https://sites.google.com/site/stephendownesmartin/puppet-mastery/bookshelf) - game theory and voting + +#### Tangents +- [Windmill Problem](https://www.youtube.com/watch?v=M64HUIJFTZM) - by 3Blue1Brown +- [Facility Location Problem](https://en.wikipedia.org/wiki/Facility_location_problem) - on Wikipedia +- [Facility Location Problem](https://www.gurobi.com/resource/facility-location-demo/) - on Gurobi's demo site (free demo but requires setting up an account) \ No newline at end of file diff --git a/modify.md b/modify.md new file mode 100644 index 00000000..33ef6467 --- /dev/null +++ b/modify.md @@ -0,0 +1,73 @@ +--- +permalink: /modify/ +layout: page-2 +title: Modify the Simulator +--- + +These are the build instructions to help you run this site on your computer. + +There are two paths: modify a build, or modify the source. + +## Sandbox Editor + +Also, there is some ability to write code in the sandbox. Just hit "dev" under "Voting systems by Type" and "Create One" under "what voting system?". Then you can load and edit the code for any system. This is limited to only the parts of the code that count the ballots and display the output. To change how voters fill in their ballot, you'll have to modify the code locally on your computer. Instructions are below. + +{% include gif-show.html +gif = "gif/create_one_wide.gif" +%} + +## Modify a Build + +If you want to mess around with the javascript code for calculating elections and votes, then it's quicker and easier to work from the already-compiled repository, the [build](https://github.com/paretoman/ballot_site/). + +To get started from this build, do the following: + +1. Download the repository from github. + +2. Start an http server. For example, start a terminal in the downloaded folder and type: + + ``` + python -m http.server + ``` + +Here's more detailed instructions for those that are new to programming web pages: + +1. Download the repository from github. +2. Install VS Code. +3. Open VS Code, and open the repository folder you downloaded. +4. Install the Live Server extension by Ritwick Dey. +5. Hit "Go Live" in bottom right corner, small text. +6. Hit F5 in VS Code. This starts a debug session. +7. The first time you hit F5, you have to edit the config file. Change the port from 8080 to 5500 because that's what Live Server uses. Then save. Then hit F5. + +Now you are in a debug session where you can search through the code for interesting parts and set breakpoints and check out variable contents. You can basically watch the code work. + +You can make changes. When you hit save on your changes, the current page you're on in the browser will reload with your changes. + +## Modify the Source + +If you want to edit the html or add more js files, then you'll want to run the jekyll build tool at some point. + +Jekyll is a build tool. The reason I'm using Jekyll is I want to modularize my html, and jekyll seems to be popular and minimal, and github does it. + +Here are instructions for building this site on your local machine. + +1. Install Ruby. You might want to follow [these instructions from jekyll​](https://jekyllrb.com/docs/installation/windows/). + +2. Do these to install jekyll and bundler and to start a server for the code. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +gem install jekyll bundler +gem install jekyll -v 3.8.7 +bundle exec jekyll serve +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Other tips for Developing the site: + +- I edit markdown with [Typora](https://typora.io/) after copying it in from a better word processor. + +- Here's a good [tutorial](https://learn.cloudcannon.com/) for jekyll from CloudCannon. The jekyll site has great tutorials, too. + +## License + +All of this is available without copyright under the [CC0 public domain license](https://creativecommons.org/share-your-work/public-domain/cc0/). The only exception is other people's stuff. I used some software packages that other people have made. You can tell where they are by reading the source code. \ No newline at end of file diff --git a/newer.html b/newer.html deleted file mode 100644 index d286c840..00000000 --- a/newer.html +++ /dev/null @@ -1,1281 +0,0 @@ - - - - - - To Build an Even Better Ballot? - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - -
- - - -
- -

You read/played Nicky Case's great explainer on voting methods. And you still want more!

- -

Never fear. I have two more voting methods to teach you about, and I hope I can convince you that they're the best yet.

- -

Who's "me"? I'm Jameson Quinn, a statistics student at Harvard and a board member of the Center for Election Science. I'm not as good a designer or interactive programmer - as Nicky, as you can see from the title above. But I have spent a lot of time thinking about voting methods.

- -

I'm also not as cool as Nicky, but I'm going to pretend to be. If my pale imitation of their chatty tone puts you off, then... well, I guess you can wait for my peer-reviewed paper.

- -

So, the story so far: FPTP voting is horrible. There's all kinds of other voting methods that would be better. We're looking for the one that's best. Some activists think that's Instant Runoff Voting (IRV), but Nicky and I both disagree; it can prematurely eliminate centrists, and requires centralized counting. Nicky says they lean towards score voting, and I can see why: it's easy to understand, and in all the simulations Nicky built, it does a great job of satisfying all the little dot-on-screen voters.

- -

By the end of this interactive, you're going to understand the three methods I consider the best. One of them Nicky already showed you: approval voting, which is great because it's so simple and because it has absolutely no downsides versus FPTP. The other two are newer, but they use similar basic ideas to what you've already seen. I hope you like all three methods.

- -

But before I show you the new methods, I have to show you why score voting isn't already the greatest method possible. So I have to explain strategic voting.

- -

Unstrategic voting

- -

Remember how Nicky's voters worked in Score Voting? They just gave each candidate a number based on the absolute distance. Like this:

- - -
-
- -

UNSTRATEGIC BALLOT

-

Judge, don't choose.

- - - -
-
-

Description: this should just be an unstrategic score voter, as in ncase's thing. Allow ballot picture to show 0 scores.

- -

But notice something about that ballot? For many positions of the voter and candidate, the ballot doesn't include a score of 5 or a score of 0. If you actually voted like that, you'd be giving up on some of your voting power. Say Triangle is your favorite, and you give them a score of 3, which also happens to be their average score. If Square wins with a score of 3.2, you're going to feel very silly for not giving a triangle a 5, in order to pull their average up as high as you can.

- -

Digression: median systems

- - skip this digression - -

Ever watched the Olympics? In events like figure skating, there are a number of Olympic judges from different countries. In order to prevent any one judge from having too much influence, they use a "trimmed mean": they throw away the highest and lowest scores before they take the average.

- -

Olympic judges are supposedly supposed to be unbiased. But for voters, there's nothing wrong with having political opinions. (I mean, obviously your opinions, dear reader, are the best, and everybody else should just listen to you. But much as I'd like to, I can't force them to.) So let's imagine voting used the olympic system.

- -

Throwing away just one highest and lowest score, with thousands or millions of voters, obviously wouldn't make an appreciable difference. So we'd have to throw away some more scores. And, why not? Let's throw away a few more while we're at it.

- -

When should we stop? When there's just 1 or 2 scores left to take the average. But if you do that, most people wouldn't called that a "trimmed mean" anymore; they'd just call it the median, the middle number.

- -

There are several voting methods that use the median to find the winner. (It turns out that can lead to a lot of ties, so you need a tiebreaker system to avoid that.) In the 1910s, over a dozen US cities, starting with Grand Junction, CO, used "Bucklin voting", a median-based system using a hybrid ranked/rated ballot. More recently, voting theorists Balinski and Laraki have proposed Majority Judgment, a median-based method using a rated ballot that's now been used in a number of competitions.

- -

Using the median reduces the incentives for a single voter to strategize. After all, unless you happen to be the exact median rating for a candidate, the only thing that matters about the rating you gave them is whether it's above or below the median.

- -

Still, large groups of voters can still get a strategic advantage. The larger the voting bloc, the greater the chance is that the median rating for a given candidate happens to be a voter in that bloc.

- -

I used to think that median voting methods were the best. But then I did the simulations I'll talk about below, and they were merely OK; not much better outcomes than approval voting, but without the simplicity. I still think they're hugely better than FPTP and a bit better than IRV, but I'm not sold enough on them for it to be worth my time programming them in to Nicky's simulator.

- -

Lightly-strategic voting: "normalization"

- -

As I suggested above, in the real world, in score voting and similar methods, most voters will want to make sure that their ballot contains at least one candidate each at the top rating and at the bottom rating. The simplest way to do that (even though the name sounds complicated and math-y) is "normalization". That just means that you set the top and bottom ratings to be however good your favorite and least-favorite candidates are, and then spread the rest of the ratings evenly between that.

- -

Here's an example of a "normalizing" voter for you to play with:

- -
-
- -

Normalizing Voter

-

Best is 5. Worst is 1.

- - - -
-
-

Description: this should be like ncase's "drag the voter" examples, with a normalizing score voter. This voter should have a series of circles, where the closest one intersects the closest candidate and the farthest one the farthest candidate. Thus, as you moved the voter or candidates the circles would change size.

- -

As soon as voters are using strategy in score voting — even a very slight amount of strategy, such as normalization — it is no longer fully exempt from impossibility theorems like Arrow's theorem. In particular, it no longer obeys independence of irrelevant alternatives (IIA); adding or removing a losing candidate can change how voters normalize, and thus change who wins. For instance, in the simulation below, try moving the losing yellow traingle candidate, and see what happens.

- -
-
- -

- non-optimal
-

- - -
-
-

This should be a score voting scenario with 100% normalizing voters. the voter median around (0,0), and candidates square (3,0), triangle (-2,0), pentagon (-4,0). Pentagon voters are not enthusiastic enough about triangle so square wins, even though triangle is higher utility. Removing penta fixes the problem.

- -

But a bigger problem than IIA violation, which in the real world is relatively hard to engineer or take advantage of, is the issue of differential voting power. In the following election, one of the groups of voters normalizes, and the other one doesn't. The two groups are the same size, but the normalizing group gets a candidate they like a lot better.

- -
-
- -

- Strategists win
-

- - -
-
-

groups of voters at (-4,0)non-normalizing and (4,0)normalizing. Cands at (-2.5,.5), (-2,0), (0,0), (2,0), (2.5,.5). (2,0) wins.

- -

Is that kind of thing realistic? Perhaps not in the form above; few voters would be so unstrategic as not to normalize. But actually, normalizing as above is a pretty weak strategy. To strategize even more strongly, voters could somehow assess which two candidates were the frontrunners, and use them as the endpoints for normalization. Mostly, this means casting an approval-like ballot that gives every candidate either a 5 or a 0. For instance, in the election below, the voter believes that the red hexagon and the blue square are the frontrunners. Compare the strongly strategic voter below with the normalized voter in the second example above.

- - -
-
- -

Strong Strategic Voter

-

Almost like strategic approval.

- - - -
-
- -

Single-voter example with 3 candidates and a voter who normalizes based on only the 2 of them.

- -

Just as normalized voters can have more voting power than naive voters, strongly strategic voters can have more than normalized voters (as long as they are not too far off in guessing the frontrunners). This is a potential weakness of score voting.

- -

Even stronger strategic voters could only choose the best of the frontrunners.

- -
-
- -

Best Frontrunner

-

And everybody you like better.

- - - -
-
- -

A more risk-averse, safe strategic voter could avoid the worst frontrunner by voting for everyone better.

- -
-
- -

Not the Worst Frontrunner

-

Just everybody you like better.

- - - -
-
-

These two strategies are really just different extremes of the strong strategic voter.

- -

Is approval voting immune to this kind of voting strategy? No; in fact, in the early seventies, mathematicians Gibbard and Satterthwaite both independently proved the theorem which bears their names, showing that no non-dictatorial voting method with more than 2 options is entirely immune to strategy. Unlike Arrow's theorem, which Nicky discussed, this one goes for any kind of voting method — ranked, rated, or whatever. So yes, approval voting is more resistant to strategy than score; but not immune.

- -

(Note: advocates for IRV sometimes garble this point by saying that in approval voting the best strategy is to "bullet vote" for only your favorite candidate, and that this would lead it to devolve back to FPTP. That's just wrong; if voters are strategically voting for somebody other than their favorite in FPTP, then there's no way it would make sense for them to bullet vote in approval. Approval voting isn't perfect, but it simply does not break down to FPTP.)

- -

One scenario where approval becomes a factor is called the "chicken dilemma". Imagine 3 groups of voters, with 25%, 30%, and 45% of the vote respectively. Say that the first two groups both prefer each other over the third, so that either of them could beat that third opponent by 55% to 45%. Whichever of the first two groups strategically gives fewer approvals to its rival will win... unless neither of them gives enough, in which case the opponent will win. This is called a "chicken dilemma" because it's like a game of chicken between the voters for the two similar rivals: they can "swerve" and let their second-favorite win, or they can "drive straight" and either win (if the other side swerves) or crash (if the other side doesn't).

- -

Here's that scenario in sandbox form. The slider on the right controls the percent of the smallest group that is strongly strategic between triangle and square; all the rest of the voters use normalized strategy (that is, approve any candidate better than the average of their favorite and least-favorite).

- - -
-
- -

- Playing Chicken
-

- - -
-
- -

Clumps of voters at (x,y,size): (-2,1,6); (-2,-1,4)slider; (3,0,10).

- - -

The "chicken dilemma" is a genuinely tough situation for almost any voting method. The motivations for the two rival factions to vote strategically are hard to minimize safely. Voting methods that go too far out of their way to punish strategic voters in this scenario tend to get the wrong answer in other, more-common scenarios like center squeeze. Look at how these various methods deal with both of these situations:

- - - -
-
- -

- Playing Chicken with Different Methods
-

- - -
-
- -

- Center Squeeze
-

- - -
-
- -

two different sandboxes. One repeats chicken dilemma case from above but also lets you switch voting methods (plurality, IRV, approval, score). The other one has center squeeze.

- -

So, now I've explained why strategic voting makes things tricky, I can explain my two favorite voting methods: star voting and 3-2-1 voting.

- -

Star voting

- -

"Star", or more precisely, "s+ar", stands for "score plus automatic runoff." In this method, voters use the same ballot as score voting. Between the two candidates with the highest scores, the winner is the one that comes higher on more ballots. Here's a one-voter star election. You can choose between three kinds of strategy: normalized, moderately strategic, and strongly strategic. The first and last kinds you've seen before. The third is almost like a strongly strategic voter, except that the ratings may be changed by one to avoid giving a frontrunner the same score as any other candidate. Here's a one-voter election:

- -
-
- -

Star Strong

-

Keeps a space for the best.

- - - -
-
- -

one-voter star election

- -

And here's the chicken dilemma you saw above:

- -
-
- -

- Chicken Star
-

- - -
-
- -

chicken dilemma with star

- -

3-2-1 voting

- -

In 3-2-1 voting, voters rate each candidate "Good", "OK", or "Bad". To find the winner, you first narrow it down to three semifinalists, the candidates with the most "good" ratings. Then, narrow it further to two finalists, the candidates with the fewest "bad" ratings. Finally, the winner is the one preferred on more ballots. Here's a ballot to play with:

- -
-
- -

321 Strategic

-

Approval with an extra level.

- - - -
-
- -

one-voter 3-2-1

- -

And here's the chicken dilemma. Note "moderately strategic" doesn't change the result from "normalized". So unlike in star voting, candidates wouldn't have to go negative against their nearby rivals in order to ensure that their voters would at least be moderately strategic and wouldn't just normalize.

- -
-
- -

- 321 Chicken
-

- - -
-
- -

chicken dilemma with 3-2-1

- -

Putting it all together, here's a sandbox for you to try out all the different systems and to make your own scenarios:

- -
- - - -
-
-

SANDBOX MODE! (link to just this)

- -
-
-

- One hope for Sandbox Mode is that readers can debate with me and each other using this tool! - Not just telling me I'm wrong, but showing me I'm wrong. - For example – - - here's a model I made in Sandbox Mode, - showing an interesting argument against Approval & Score Voting. - Granted, this tool is very limited – it doesn't handle strategic voting or imperfect information – - but I think it's a start, and may help improve our Democratic Discourse™ -

-
-
-
- - - - - -

- -

- -

- -

- -

- -

- -

- -

- -

- -

- -

- -

- -

- -

- -

- -

- - - - -
-
- -

- click & drag
the candidates and the voter:
-

- - -
-
- -

It's a tough choice. Triangle's got some sharp points, but Square understands more sides! Alas, in the end, you can only vote for one.

- -

Of course, there's more than just one voter in an election. Let's simulate what an election would look like with 100+ voters.

- -
-
- -

- drag the candidates & voters around.
(to move voters, drag the middle of the crowd)
watch how that changes the election:
-

- - -
-
- -

Now let's consider a different election. Say Tracy Triangle is already beating Steven Square in the polls, and a third candidate, Henry Hexagon , sees this. (Hexagon's supporters like how he tackles problems from more angles) Inspired by her success, Hexagon swoops in and takes a political position close to Triangle's.

- -

Now, you'd think giving the voters more of what they want should result in a better choice, or at least, not result in a worse choice, right? Well...

- -
-
- -

- - at first, beats .
- drag to just under ,
- and see what happens:
-

- - -
-
- -

That's right. Steven Square, our least popular candidate, now wins! This is because when you have two good candidates, they "steal" votes from each other, letting a bad third candidate win.

- -

This is called the spoiler effect. The most famous real-world example of this was in 2000, when Ralph Nader "stole" votes from Al Gore, letting George Bush win. And though the spoiler effect didn't play a big role in 2016, its impact could still be felt.

- -

In the Republican primary, one anti-establishment nominee, Trump, ran against sixteen GOP establishment nominees, who all "stole" votes from each other, letting Trump grab the nomination, easily. As for the Democratic primary, fear of splitting the vote prevented Sanders from running as independent. And to cap it all off, there was always the worry that other candidates like Johnson, Stein, and McMullin could spoil the election.

- -

But again, this is not about the 2016 U.S. election.

- -

This is about designing a democracy that people can trust.

- -

Despite so much hoopla around the 2016 election, a full half of Americans did not vote. Even of those who voted for Clinton/Trump, 20% of them said their candidates were untrustworthy, and voted for them anyway. And around the world, people's trust in their governments – or the trustworthiness of their governments – has never been lower. It's more than America at stake. It's every democracy in the world.

- -

...so yeah, no pressure.

- -

Rebuilding trust is a complex problem with no easy solutions. But I think there is an easy first step. It's a step that could get rid of our “lesser of two evils” problem, and give us citizens more choices, better choices. And yet, it won't be as daunting as fixing campaign finance or gerrymandering or lack of proportional representation, no, it'd just require changing a piece of paper, and how we count those pieces of paper.

- -

This idea is not the most important issue. It won't solve everything. But as a first step? It'd give us the biggest bang-for-buck.

- -

Let's talk about how to build a better ballot.

- -
- - - - -
- -

Now, some of you may have a couple objections!

- -

First objection. Why would the people in power change the voting method that got them in power? Well, the spoiler effect has cost both Dems & Reps a major election before. Getting rid of that glitch would be a win-win for major and minor parties! Also, voting reform is already picking up steam. Just last month, Maine adopted Instant Runoff, and Justin Trudeau, Canada's Cutie-In-ChiefCynic-in-Chief, will be moving his nation towards a better voting system in 2017. (UPDATE: actually, he didn't do that.)

- -

Second objection. Didn't some guy once prove that all voting methods will be unfair? Not quite. You're thinking of the infamous Impossibility Theorem by Kenneth Arrow, the mathematician in the 1950's who founded the whole study of voting methods.

- -

Two answers to that: 1) some voting methods can still be more fair than others, even if none are perfect. And 2) Kenneth Arrow's proof doesn't apply to all voting methods! That's a misconception. It only applies to voting methods where you rank candidates. Later, we'll see some voting methods where you don't rank candidates – along with other alternatives to our current, glitchy voting method.

- -

But first, let's take a closer look at the voting method we do have:

- -
-
- -

FIRST PAST THE POST (FPTP)

-

same as before. click & drag
the candidates and voter

- - -
-
- -

How To Count: Simply add up the votes. Whoever gets the most votes, wins.

- -

Sounds logical enough. But as you saw earlier, it can lead to a weird glitch, where having two good candidates can make the election go to a third bad candidate. This is why some people vote "strategically", voting not for their actual honest favorite, but voting for the lesser of two evils. And strategic voting is fine – but! – ask yourself this: how can we expect our elected officials to be honest, when our voting method itself doesn't let us be honest?

- -

So, to fix the spoiler effect, other voting methods have been suggested. Such as...

-
-
- -

RANKED VOTING

-

again, click & drag

- - - -
-
- -

How To Count: There's actually several different ways to count these kinds of ballots. Here, I'll just show you the top three:

- -

- Instant Runoff Voting (IRV): - This one is the most popular alternative to First Past The Post (FPTP). - Australia and Ireland use it in national elections. - San Francisco, Minneapolis, and Portland, Maine use it in local elections. - And Justin Trudeau, Prime Man-ister of Canada, - is leaning towards Instant Runoff, too. -

- -

- (Note: Instant Runoff Voting is also called “Ranked Choice Voting”, - even though there's other ways to count ranked ballots. - IRV is also often just called “Alternative Vote”, - even though there's a flippin' dozen other voting methods. - Such selfish naming! Sheesh!) -

- -

IRV is a bit more complicated than FPTP, but here's how it works:

- -
    -
  1. Count up the #1 choices.
  2. -
  3. If someone has more than 50%, they win! END.
  4. -
  5. If not, eliminate the last-place loser.
  6. -
  7. Run a new "round" of the election, minus that loser.
  8. -
  9. Repeat until someone has 50% or more.
  10. -
- -

If that seems like too much, there is a much simpler method of counting ranked ballots...

- -

Borda Count: Simply add up the rank numbers. Like in golf, whoever has the lowest score, wins. Borda count is used in Slovenia and a bunch of tiny islands in Micronesia.

- -

But if you want an even nerdier way of voting, you could try...

- -

Condorcet Method: Run a simulated "election" between every pair of candidates, using the info on voters' ballots. IF there's a candidate who beats all other candidates in one-on-one "elections", that candidate wins the real election. However, that's a very big "IF". (as we'll see later...) The upside is, when this method does pick a winner, it's always the “theoretically best” candidate! Currently, this method is not being used by any governments, and is only being used by neeerrrrrds.

- -

So, those are the voting methods where you rank candidates – the ones that Kenneth Arrow proved would always be unfair in some big way! But what of voting methods where you don't rank candidates? They're less well-known, but now, at least you'll know 'em:

- -
-
- -

APPROVAL VOTING

-

yup, stiiiiill click & drag

- - - -
-
- -

How To Count: Simply add up the approvals. Whoever gets the most approvals, wins.

- -

Wait, picking more than one candidate? Doesn't that violate the one-vote-per-person rule? I hear you ask. Well, your vote was never a single check mark, your vote was always the whole ballot. And on this ballot, you get to honestly express all the candidates you approve of, not just your favorite or strategic second-favorite.

- -

But if you want a more expressive voting method, why not try...

- -
-
- -

SCORE VOTING

-

you guessed it

- - - -
-
- -

How To Count: Simply add up the ratings. Whoever has the highest average score, wins. Kind of like Amazon reviews, but with democracy. (Note: this is not a ranking method, because two candidates can have the same score.)

- -

So there's our top 6 voting methods: the one we use, and five popular alternatives. But how can we tell if these alternatives are actually better? What glitches might they have? And which voting method – if any – can we say is "the best"?

- -

Like before, let's simulate 'em.

- -
- - - - -
-

Remember that simulation of the spoiler effect from earlier? Well, here it is again, but now you can switch between the six different voting methods! Here's the "spoiler effect" simulation again. See how different voting methods deal with potential spoilers:

-
-
-

- drag to just under to create a spoiler effect.
- then compare the 6 different voting methods: -
- - (note: in the rare cases there's a tie, i just randomly pick a winner) - -

- - -
-
- -

As you could see, every voting method except First Past The Post is immune to the spoiler effect. So, that's it, right? Ding dong, the glitch is dead? Just pick any other alternative voting method and be done with it?

- -

But, alas. In getting rid of one glitch, some of these alternative voting methods create other glitches – for some, the cure is even worse than the disease.

- -

For example, here's a sim of Instant Runoff Voting. In the beginning, Tracy Triangle is already winning, and you're going to move the voters even closer to her. Obviously, if a candidate is already winning an election, and becomes even more popular, they should still win afterwards, right?

- -

You can probably guess where this is going...

-
-
- -

- drag the voters slowly up towards : -

- - - -
-
- -

What happened? - Originally, is eliminated in the first round, so - - goes against a weaker , and wins. - But when you move the voters closer to , - the loser changes! - So now, is eliminated in the first round, - which means goes against a stronger , - and loses.

- -

Under Instant Runoff, it's possible for a winning candidate to lose, by becoming more popular. What a glitch!

- -

How often does this actually happen in real life? There's a couple confirmed examples, and mathematicians estimate this glitch would happen about 14.5% of the time. But sadly, we can't know for sure, because governments usually don't release enough info about the ballots to reconstruct an IRV election & double-check the results.

- -

So, not only is Instant Runoff's glitch as undemocratic as First Past The Post's glitch, it's possibly worse – because while FPTP's counting method is simple and transparent, Instant Runoff is anything but. And a lack of transparency is an even deadlier sin nowadays, when our trust in government is already so low.

- -

(But wait! We'll be talking about the risk of strategic voting later. - Can IRV can make a comeback? Stay tuned...)

- -

So much for the most popular alternative. What about the second-most popular, Borda Count? In this next simulation, you move a losing candidate closer to another losing candidate. Under FPTP, the spoiler effect would split their votes, making both of them lose even more. But watch what happens under Borda Count instead...

- -
-
- -

- drag to just slightly left of : -

- - - -
-
- -

Yup. Borda Count has a reverse spoiler effect. Instead of one good candidate hurting another good candidate by moving closer, with Borda Count, one bad candidate can help another bad candidate by moving closer.

- -

Here's what happened: at first, some voters ranked - >>, - but when you moved closer to , - those voters then swung to ranking - >>, - hurting enough - to make her lose to .

- -

Still, Borda's not the worst, and at least it's simpler and more transparent than Instant Runoff. But how does Condorcet Method compare? When Condorcet picks a winner, it's always the “theoretically best” winner – but that's when it picks a winner.

- -

So far, I've just been simulating voters as a single group, with a center and some spread. But seeing how polarized politics is nowadays, one could imagine several groups of voters, with totally different centers. Now, Condorcet tries to pick the candidate who beats all other candidates in one-on-one races. But with polarized voters, you could end up with a Rock-Paper-Scissors-like loop, where a majority of voters prefer A to B, B to C, and C to A.

- -

In certain situations, the other voting methods just had glitches. In Condorcet, the voting method crashes. Try it out for yourself:

-
-
- -

- create your own “condorcet cycle”!
- move the voters in such a way that NOBODY wins: -

- - - -
-
- -

Now, in actual practice – not that any government actually uses this voting method – when Condorcet fails to find a winner, the election falls back to another method like Borda Count. But if you do that, it'll get the glitches of its backup method. So it goes.

- -

First Past The Post. Instant Runoff. Borda Count. Condorcet Method. Those were all the voting methods that use ranking – the ones that our math boy, Kenneth Arrow, proved would always be unfair or glitchy in some big way. What about the voting methods that don't use ranking, like Approval & Score voting? Well...

- -

...I couldn't come up with a simulation to show their flaws. Because, in theory, they don't have many big flaws.

- -

- But that's a really, really, really big “in theory!” - It may be that, in practice, strategic voters use Approval & Score Voting exactly like First Past The Post – - only approving or giving 5 stars to their top candidate, and disapproving or giving 1 star to all others, - even if they actually like the others. - (See FairVote's critique of Approval Voting, and defense of Instant Runoff) -

- -

- Then again, even if Approval & Score Voting disincentivize you from expressing an honest second choice, - FPTP and IRV punish you for expressing an honest first choice. - Besides, if Approval can be "gamed", then that goes double for IRV. - (See this mathematician's critique of FairVote's critique, and defense of Approval) - So, in the end... [confused shrugging sounds]

- -

- We're gonna need a hecka lot more simulations. -

- -

- So, below is a chart - (source), - showing the results of 2.2 million simulations. - A huge variety of scenarios were tested. All-honest voters. - All-strategic voters. Half-honest, half-strategic. - Voters who know each others' preferences. - Voters who don't know each others' preferences. - Voters who only sorta-know each others' preferences. - And so on. - You can tell that a real mathematician made this chart, - because it's makin' my eyes bleed: -

- -

- -

- Each voting method's results is shown as an ugly-blue bar. - The further to the right a voting method is, the more it "maximizes happiness" for the voters. - The higher up a voting method is, the simpler it is. - And a bar's width shows the range of a voting method's performance, - given different ratios of honest-to-strategic voters. -

- -

- The first thing to note is that strategic voting makes voters less happy than honest voting - – in all voting methods! I was very surprised when I first learnt that. - (But it makes sense, if you think about, say, a crowded room full of people trying to talk. Any one person can be "strategic" by shouting over others, but if everybody is "strategic", nobody can hear anybody, and all you're left with is sore throats and sad peeps.)

- -

The other thing to note is which voting methods make people the happiest. If you have mostly honest voters, Score Voting is best. (with Borda Count a close second) And if you have mostly strategic voters, then both Approval & Score Voting are best. (and with strategic voters, IRV does just as bad as FPTP)

- -

However, those are still computer simulations. How would these different voting methods play out in real life? Well, we can't just get the DeLorean up to 88, go back in time before the 2016 election, change the voting method, and see what would happen...

- -

...or can we?!

- -

No, no we can't. But last month, researchers did something close enough. - A polling study asked 1,000+ U.S. registered voters to rank & rate the six presidential candidates, - to simulate who would've won the (popular) vote under different voting methods! - (But keep in mind that if we had a different voting method in the primaries, we'd have different candidates entirely. - So take this study with a pillar of salt.) - The results: under Instant Runoff, Condorcet, and Approval Voting, the winner would've been Hillary Clinton. But under Score Voting, the winner would've been Donald Trump. And under Borda Count, the winner would've been... uh... Gary Johnson? -

- -

?????

- -
-
-

- a guesstimated model of the 2016 US election?...
- - how Clinton wins IRV, - Trump wins Score, - and Johnson wins Borda?? - -

- -
-
- -

Anyway.

- -

Before we wrap all this up – remember Kenneth Arrow? The infamous mathematician who founded the study of voting methods in the 1950's? Well, in an interview 60 years later, Kenneth Arrow had this to say, about which voting method he likes most now:

- -

- “Well, I’m a little inclined to think that score methods [like Approval & Score Voting] where you categorize in maybe three or four classes [so, giving a score out of 3 or 4, not 10 or 100] probably – in spite of what I said about manipulation [strategic voting] – is probably the best.”

- -

That's as strong an endorsement as you'll ever squeeze out of a math-head.

-
- - - - -
- -

ahem

- -

- DEAR JUSTIN “TOTES ADORBZ” TRUDEAU
- (and everyone else around the world pushing for voting reform) -

- -

Thank you for taking this small but powerful first step! We've known for way too long that our current voting method – First Past The Post – forces voters to be dishonest, creates a polarizing "lesser of two evils" scenario, and screws over both major and minor candidates.

- -

- However, you're probably only considering Instant Runoff Voting. - Which, to be fair, is better than than First Past The Post, - and if it's a choice between just those two, definitely go for Instant Runoff. - But IRV still has a glitch as undemocratic as FPTP's – - and worse, in our age of distrust, Instant Runoff's lack of transparency may be deadly for democracy. - Yes, sure, IRV was the best voting method we could come up with... - in 1870. - And since then, IRV has dominated the conversation, - unwittingly framing the whole voting reform debate as “simple vs expressive”. -

- -

- But that is a false choice. Thanks to computer simulations, real-life studies, and a bunch of math nerds, - we now know of voting methods that are both simple and expressive. -

- -

- Personally, I'm leaning towards Score Voting. - It's simple, very expressive, and already familiar to anyone who's seen Amazon's or Yelp's “five star” review method. - But that's just my humble opinion. - You could also make the case that Approval Voting is more practical, - because it's even simpler, and would already work with existing voting machines! - All you'd need to do is change the instructions from - “vote for the candidate you like” to “vote for the candidates you like”. -

- -

- Or maybe I'm completely wrong about Instant Runoff Voting, and it's actually pretty okay. - Heck, you could even go for Borda Count, as a hilarious prank. -

- -

I won't claim to know which voting method is The Best™. I shall keep open this discussion, just as long as we have this discussion. For three reasons:

- -

1) If I claim one voting method is the best, end of story, all the social-choice-theory nerds will be on my butt, yelling, BUT NICKY WHAT ABOUT QUADRATIC VOTE BUYING

- -

2) We still need to test these alternative voting methods with actual experience, - not just annoying internet flame wars between IRV advocates and Score Voting advocates theory. - All the more reason for small towns, local states, and nations like Canada to be pioneers, to bravely experiment!

- -

3) Keeping the discussion going is what democracy is.

- -

A recent study found that in many Western countries – from Sweden to Australia to the United States – support for democracy has plummeted over the last several generations. - In 2011, almost a full quarter of young Americans said democracy was a "bad" or "very bad" way to run a country. - And today, one in six Americans say it'd be "good" or "very good" to be under actual military rule. -

- -

Our age of distrust goes a lot deeper than the technical details of a voting method. There isn't gonna be One Weird Trick to fix democracy. But as a first step, a low-hanging fruit, a way to show that, yes, you will make the method respond to the needs and wants and pains and hopes and dreams of your people – well, fixing our voting method's a good start as any.

- -

Because, this isn't just about trying to build a better ballot.

- -

This is about trying to build a better democracy.

- -

<3,
- ~ Nicky Case

- -
- -

P.S: Since you've read & played this all the way, here, have a bonus! - A “Sandbox Mode” of the election simulator, with up to five candidates. - You can also save & share your very own custom election scenario with others. Happy simulating!

-
- - - - -
-
-

SANDBOX MODE! (link to just this)

- -
-
-

- One hope for Sandbox Mode is that readers can debate with me and each other using this tool! - Not just telling me I'm wrong, but showing me I'm wrong. - For example – - - here's a model I made in Sandbox Mode, - showing an interesting argument against Approval & Score Voting. - Granted, this tool is very limited – it doesn't handle strategic voting or imperfect information – - but I think it's a start, and may help improve our Democratic Discourse™ -

-
-
- - - -
-
- - -
- -
-
PUBLIC DOMAIN
- Zero rights reserved. - I'm giving away - all my art/code/words, - so that you - teachers, mathematicians, hobbyists, activists, and policy wonks - can use them however you like! - This is for you. - Get my source code on GitHub! -
-
- -
- - -
- -
“BUT WHAT CAN I DO?”
- -

- For citizens: Remember, think global, but act local. - Change from the bottom-up lasts longer. - If you're in the US, - find your representative - and badger 'em. - If you're in Canada, - find your Member of Parliament - and badger 'em. - Also if you're Canadian, - fill out the MyDemocracy.ca survey before the end of 2016! - This survey has a few questions specifically about voting reform! - (sadly, the question is still framed as "simple vs expressive". - that is why i've been so gung-ho about Approval & Score, - and maybe a bit too mean towards IRV) -

- -

- For learners: - Watch CGP Grey's Politics in the Animal Kingdom series! - It's charming, and covers more ground than I did here – it explains - gerrymandering, proportional representation, and more. - Also, read Gaming The Vote by - William Poundstone. - It's a thrilling read, - with dramatic human stories of crooks & conmen trying to game our glitchy voting systems – - and sometimes, succeeding. -

- -

- For teachers: - This entire "explorable explanation" is public domain, copyright-free, - meaning you already have permission to use this freely in your classes! - You can even use the Sandbox Mode to create your own material, - or as a tool for students to make something on their own. -

- -

- For coders: - This is all open source! - So you can get my code on GitHub, and remix it to your heart's content. - (sorry in advance for my messy code) -

- -

- Check out these organizations: - Though they may differ on what voting method they like best, - they all have a common goal: to reform the one we have. - Electology likes Approval Voting most, - FairVote likes Instant Runoff most, - and RangeVoting.org likes Score Voting most. -

- -
ON THE SHOULDERS OF GIANTS
-

- This "explorable explanation" was directly inspired by these two projects: -

- -

- Voting Sim Visualization by Ka-Ping Yee (2005) - was a real eye-opener. - (hat tip to Bret Victor for sharing it with me!) - I've heard lots of written debate over FPTP vs IRV vs Condorcet vs Approval vs blah blah blah, - but I'd never seen their difference visualized so clearly! - It gave me instant insight. - And it actually changed my mind – I used to think IRV was pretty good, - but after seeing IRV's messiness (as shown above), I realized it's actually kinda stinky cheese. -

-

- However, even this brilliant visualization was still too abstract. - And since it wasn't interactive, I couldn't test the many questions & scenarios that came to mind. - So that's why my second inspiration was... -

- -

- Up and Down the Ladder of Abstraction - by Bret Victor (2011). - It's one of the web's earliest "explorable explanations" (also a term Bret coined) - and it is gorgeous. - Obviously, I borrowed the format of mixing words & "games" to explain things, - but I also followed the formula of starting concrete – one voter – - then moving up to the more abstract – a whole election. -

-

- - You can learn more about Explorable Explanations here. -

-

- And last but not least, thank you to all the math & policy nerds - who spent way too much time thinking about all this. -

- -
- STAY IN TOUCH, MAYBE?
-

- Every once in a while, I'll fall into an endless rabbithole – - like this one on voting methods – and slowly crawl my way out, bloodied and bruised, - with a new interactive thing for you! - If you wanna find out - when I finally get around to making new shtuff, you can... -

- -

- And if you wanna see more of my past projects, - check out my wobsite! -

-

- See you again soon! Have a Happy New Year 2017, or try to, anyway. -

- -
- - -
-
A BIG THANKS TO ALL MY
-
SUPPORTERS
- - - -
-
aimee jarboe
-
frank leon rose
-
jared cosulich
-
louis-jean teitelbaum
-
matt hughes
-
micah cowan
-
michael alan huff
-
natalie sun
-
noel lehmann
-
phil dougherty
-
tom cascio
-
tom knowles
-
-
-
-
- Adam M. Smith
- Alex Dytrych
- Andrew
- Andy
- Artemiy Solopov
- Aschelon
- ben fei
- Benjamin Riggs
- Bob Wise
- Brandon
- Brent Werness
- Brian Wu
- Bruno Guerrero
- Buster Benson
- Casey Ross
- Charlie McIlwain
- Christopher
- Colin
- Colin
- Cort Stratton
- Craig Steele
- Daniel Horowitz
- Daniel Shiffman
- Dave Tu
- David Smit
- Dylan Meconis
- Fahrstuhl
- Feiya Wang
- Forrest Oliphant
- Frank Leon Rose
- Henry Reich
- Iñaki
- J. Hu
- Jacob Christian Munch-Andersen
- Jacques Frechet
- James Hogan
- Janusz Leidgens
- John_Ca
- Johnny Owens
- Joseph Perry
- Joshua Horowitz
- Julia Karmo
- Karen Cooper
- Kat Suricata
- Kate Fractal
- Kathryn Long
- Kevin
- Kevin Wang
-
-
- Klemen Slavic
- kuerqing1024
- Linda Booth Sweeney
- Maic Lopez Saenz
- Matt "Kupo" Roszak
- Matt Warren
- May-Li Khoe
- Mekki MacAulay
- Micah Cowan
- Michael Duke
- Michelle Brown
- Michelle Kelly
- Milan Pingel
- Monika Denes
- Mustafa Alic
- Nick Schrag
- Nikita
- Noah Swartz
- Pablo Lopez Soriano
- Pat Mächler
- Peter McEvoy
- Philip Tibitoski
- Piotr Migdal
- Rachel Nabors
- Raphael D'Amico
- Richard Hackathorn
- Rob Napier
- Roland Tanglao
- Ryan Barker
- Sam Anderson
- Sam Maynard
- Samira Nedungadi
- Sarah Barbour
- sarah mathys
- SB Sigma
- Seanny123
- Serguei Filimonov
- Sigpipe
- Sylvain Francis
- Syria Carys Sirlay
- T_Caramel
- TisGood
- Tony Onodi
- Traci Lawson
- Yona
- Yu-Han Kuo
- Zach Smith
- Zoe Bogner -
-
-
-
AND SPECIAL THANKS TO
-
-
- Alex Dytrych
- Alex Jaffe
- Brian Bucklew
- Chris Walker
- Christine Zhang
- Dan Zajdband
- Daniel Cook
- Droqen
- Jason Grinblat
-
-
- Jessie Salz
- Lisa Charlotte Rost
- Martin Shelton
- Patrick Dubroy
- Pietro Passarelli
- Sandhya Kambhampati
- Tanya Short
-
-
-
-
- -
-
- sharing is caring! - -
-
- -
- - - - diff --git a/newer.md b/newer.md new file mode 100644 index 00000000..b751a1fb --- /dev/null +++ b/newer.md @@ -0,0 +1,193 @@ +--- +permalink: /newer/ +layout: page-3 +title: An Even Better Ballot +banner: To Build an Even Better Ballot +description: An addendum to the Interactive Guide to Approval Voting +description-banner: An addendum to the Interactive Guide to Approval Voting +byline: by Jameson Quinn & nicky case, dec 2016 sep 2017 +twuser: bettercount_us +--- + +{% include letters.html %} + +You read/played Nicky Case's great [explainer on voting methods](original). And you still want more! + +Never fear. I have two more voting methods to teach you about, and I hope I can convince you that they're the best yet. + +Who's "me"? I'm Jameson Quinn, a statistics student at Harvard and a board member of the Center for Election Science. I'm not as good a designer or interactive programmer as Nicky, as you can see from the title above. But I have spent a lot of time thinking about voting methods. + +I'm also not as cool as Nicky, but I'm going to pretend to be. If my pale imitation of their chatty tone puts you off, then... well, I guess you can wait for my peer-reviewed paper. + +So, the story so far: FPTP voting is horrible. There's all kinds of other voting methods that would be better. We're looking for the one that's best. Some activists think that's Instant Runoff Voting (IRV), but Nicky and I both disagree; it can prematurely eliminate centrists and requires centralized counting. Nicky leans towards score voting, and I can see why: it's easy to understand, and in all the simulations Nicky built, it does a great job of satisfying all the little dot-on-screen voters. + +By the end of this interactive, you're going to understand the three methods I consider the best. One of them Nicky already showed you: approval voting, which is great because it's so simple and because it has absolutely no downsides versus FPTP. The other two are newer, but they use similar basic ideas to what you've already seen. I hope you like all three methods. + +But before I show you the new methods, I have to show you why score voting isn't already the greatest method possible. So I have to explain strategic voting. + +## Unstrategic Voting + + +Remember how Nicky's voters worked in Score Voting? They just gave each candidate a number based on the absolute distance. Like this: + +{% include sim.html +title = "Unstrategic Ballot" +caption = "Judge, don't choose." +id = "ballot4" +comment = "Description: this should just be an unstrategic score voter, as in ncase's thing. Allow ballot picture to show 0 scores." +link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSQU4DMQz8S84RihPHzu6ZH8Bt1QO0i6hUdau2CCEEb8fOUA5FVQ4T28lkxvFnSGGcJqVIQqs4UZXfXeb0l2sxF_WckO3qahUD-bWhRCL2sIQxxcBhDN9EIYYaRopB7JAV1SDFf8sq7WZluFmh1N8iVyCmtnguIwcZxIAKkK6FTAWxob1ZDYZezEZmyUz9TM4A0GRGZDTFQJBURA2RsWRz703Mpo-8FeAqkFTAVcBVjGuyXmP5YQEBWAtcW5NjiWxldmI_x-7W3XO-bIx4cu_M_S5fUzN8s-JrIJiHnqzoYSVA7iJqAUBqxRdW2K6KWutvVdiWBIBhgWGBYandigsUUAgUCBqvUKC4q1CgBRH-T_F_epkiRbFd9UhhqSUALDWIaRDTQNhA2KCnga9BVnNZdzYYA4Q9P-12y_nx4zDbTD-sl-NsU316Xd7v59P6uD2ct8vep_1tv5lftvt5E75-ABCA83FMAwAA)" + +%} + +But notice something about that ballot? For many positions of the voter and candidate, the ballot doesn't include a score of 5 or a score of 0. If you actually voted like that, you'd be giving up on some of your voting power. Say {{ B }} is your favorite, and you give them a score of 3, which also happens to be their average score. If {{ A }} wins with a score of 3.2, you're going to feel very silly for not giving {{ B }} a 5, in order to pull their average up as high as you can. + +## Lightly-strategic voting: "normalization" + +As I suggested above, in the real world, in score voting and similar methods, most voters will want to make sure that their ballot contains at least one candidate each at the top rating and at the bottom rating. The simplest way to do that (even though the name sounds complicated and math-y) is "normalization". That just means that you set the top and bottom ratings to be however good your favorite and least-favorite candidates are, and then spread the rest of the ratings evenly between that. + +Here's an example of a "normalizing" voter for you to play with: + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSQU4DMQz8S84RihPHzu6ZH8Bt1QO0i6hUdau2CCEEb8fOUA5FVQ4T28lkxs5nSGGcJqVIQqs4UZXfXeb0l2sxF_WckO3qahUD-bWhRCL2sIQxxcBhDN9EIYYaRopB7JAV1SDFf8sq7WZluFmh1N8iVyCmtnguIwcZxIAKkK6FTAWxob1ZDYZezEZmyUz9TM4A0GRGZDTFQJBURA2RsWRz703Mpo-8FeAq4CrgKuAqxjVZr7H8sIAArAWurcmxRLYyO7GfY3fr7jlfNkY8uXfmfpevqRm-WTEaCOahJyt6WAmQu4haAJBaMcIK21VRa_2tCtuSADAsmIHAsNRuxQUKKAQKBI1XKFDcVSjQggjzU8xPL79IUWxXPVJYagkAwgYxDWIaCBsIG_Q08DXIai7rzj7GAGHPT7vdcn78OMz2px_Wy3G2X316Xd7v59P6uD2ct8vef_vbfjO_bPfzJnz9AEHATI9MAwAA)" +title = "Normalizing Voter" +caption = "Best is 5. Worst is 0." +id = "ballot5" +comment = "Description: this should be like ncase's 'drag the voter' examples, with a normalizing score voter. This voter should have a series of circles, where the closest one intersects the closest candidate and the farthest one the farthest candidate. Thus, as you moved the voter or candidates the circles would change size." +%} + +As soon as voters are using strategy in score voting — even a very slight amount of strategy, such as normalization — it is no longer fully exempt from impossibility theorems like Arrow's theorem. In particular, it no longer obeys independence of irrelevant alternatives (IIA); adding or removing a losing candidate can change how voters normalize, and thus change who wins. For instance, in the simulation below, try moving the losing {{ B }}, and see what happens. + +{% include sim.html +link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3WSXW7EIAyE78IzqrDBxslVoj3Jqj37Gj5ltVJb5WH8g4cZk2dp5bwu0VbF2qNeGu-oET1qkX3G3nkvZ6tllLP8iJRabOeep7I5E45Wf33Zimz93Tn-7Ujb5LI0iFT36scqK2WkyAAQIg6kEhmJeW1POHZVky-LmnySoAA0OshsDyg0OsmCLFk0N9C2YFnLgKkjqHcymLot1VU46IzC17EsVbM1Nt0K5A70DpLw6isYe258Ug7f14zJgyBxHLtoLM4E0H25dQB5ZgBGbdJjXYZRbwAmHZPOuty2hSXOoXAUOKueKJjMThRMFjR5scmLzfvfmTTj3k3tdaw6lqIBWArEBGICSwFhoCfgC2TFkvVl-bctYd8vXQbN5fwCAAA)" +title = "Non-optimal" +caption = "" +id = "election7" +comment = "This should be a score voting scenario with 100% normalizing voters. the voter median around (0,0), and candidates A (3,0), B (-2,0), D (-4,0). Pentagon voters are not enthusiastic enough about B so A wins, even though B is higher utility. Removing D fixes the problem. " +%} + +But a bigger problem than IIA violation, which in the real world is relatively hard to engineer or take advantage of, is the issue of differential voting power. In the following election, one of the groups of voters normalizes, and the other one doesn't. The two groups are the same size, but the normalizing group gets a candidate they like a lot better. + +{% include sim.html +link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3WSUW4DIQxE7-Jvq8JgDJurbPKb3x6gas9ew1Oatmq10g62YTxjeJMil_O0UtR6uelZv1b515qrm4r9vScrTS5FxeUiH2ai0nccuT-LI0FGybQfouXnl_WZ9fI7vyvHvxUru4MtSWYaoXGsdCXdlpKrvN7vV8m-5qSRZQGkLvPE7F8Tspmp1CTOZDWiCuCvOlHStARo6iCaRMcma2Urr2s0MDWUtUYEU-tLvua3NgZH4Wt4t83h0C3jbs9lfS6T9qzs9X3ev1N77HY-uCakOoY7k-wG1C2iNwCZvQMY7oPa3H06hqMAmA3MBmOLvnW2FBJQBAri2DBQMDg7UDAY1ODmBjc3Hi9qUJyPGWlTX3kszQJgaSJmImZiaUI40TPhm8iaS9ZLz-e3hL1_AuiA1g8cAwAA)" +title = "Strategists win" +caption = "" +id = "election8" +comment = "groups of voters at (-4,0)non-normalizing and (4,0)normalizing. Candidates at (-2.5,.5), (-2,0), (0,0), (2,0), (2.5,.5). (2,0) wins." +%} + +Is that kind of thing realistic? Perhaps not in the form above; few voters would be so unstrategic as not to normalize. But actually, normalizing as above is a pretty weak strategy. To strategize even more strongly, voters could somehow assess which two candidates were the frontrunners, and use them as the endpoints for normalization. Mostly, this means casting an approval-like ballot that gives every candidate either a 5 or a 0. For instance, in the election below, the voter believes {{ A }}, {{ B }}, and {{ C }} are the frontrunners. Compare the strongly strategic voter below with the normalized voter in the second example above. + +{% include sim.html +link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSQU4DMQz8S84RihPHzu6ZH8Bt1QO0i6hUdau2CCEEb8fOUA5FVQ6T2M5kxvFnSGGcJqVIQqs4UZXfXeb0F2sxF_WYkO3qahUD-bWhRCL2YwljioHDGL6JQgw1jBSDWJEl1SDFf8sy7WZmuJmh1N8iVyCmtngsIwYZxIAKkK6FTAWxob1ZDYaezEZmwUy9JmcAaDLjZDTFQBBUnBpOxpLNvTcxmz7yVoCrZKTAVcBVjGuyXmN5saAKrAWurcmxRLY0O7HXsbt195wvGyOe3Dtzv8vX1AzfrPgaCOahByt6WAkAqbUAILXiCytsV0Wu9bcqbEsCwLDgDwSGpXYrLlBAIVAgaLxCgeKuQoEWnPB_iv_TyxQpku2qRwpLLQGoMzWIaRDTQNhA2KCnga9BVnNZdzYYA4Q9P-12y_nx4zDbTD-sl-NsU316Xd7v59P6uD2ct8vep_1tv5lftvt5E75-APMG_FdMAwAA)" +title = "Strong Strategic Voter" +caption = "Almost like strategic approval." +id = "ballot8" +comment = "Single-voter example with 3 candidates and a voter who normalizes based on only the 2 of them." +%} + +Just as normalized voters can have more voting power than naive voters, strongly strategic voters can have more than normalized voters (as long as they are not too far off in guessing the frontrunners). This is a potential weakness of score voting. + +Even stronger strategic voters could only choose the best of the frontrunners. + +{% include sim.html +link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSQU4DMQz8S84RihPHzu6ZH8Bt1QO0i6hUdau2CCEEb8fOUA5FVQ6T2M5kxvFnSGGcJqVIQqs4UZXfXeb0F2sxF_WYkO3qahUD-bWhRCL2YwljioHDGL6JQgw1jBSDWJEl1SDFf8sy7WZmuJmh1N8iVyCmtngsIwYZxIAKkK6FTAWxob1ZDYaezEZmwUy9JmcAaDLjZDTFQBBUnBpOxpLNvTcxmz7yVoCr5F5YwFXAVYxrsl5jebGAAKwFrq3JsUS2NDux17G7dfecLxsjntw7c7_L19QM36z4GgjmoQcrelgJkLuIWgCQWvGFFbarItf6WxW2JQFgWPAHAsNSuxUXKKAQKBA0XqFAcVehQAtO-D_F_-llihTJdtUjhaWWANQVNohpENNA2EDYoKeBr0FWc1l3NhgDhD0_7XbL-fHjMNtMP6yX42xTfXpd3u_n0_q4PZy3y96n_W2_mV-2-3kTvn4AokZDqUwDAAA)" +title = "Best Frontrunner" +caption = "And everybody you like better." +id = "ballot11" +%} + +A more risk-averse, safe strategic voter could avoid the worst frontrunner by voting for everyone better. + +{% include sim.html +link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSQU4DMQz8S84RihPHzu6ZH8Bt1QO0i6hUdau2CCEEb8fOUA5FVQ4T28lkxs5nSGGcJqVIQqs4UZXfXeb0l2sxF_WckO3qahUD-bWhRCL2sIQxxcBhDN9EIYYaRopB7JAV1SDFf8sq7WZluFmh1N8iVyCmtnguIwcZxIAKkK6FTAWxob1ZDYZezEZmyUz9TM4A0GRGZDTFQJBURA2RsWRz703Mpo-8FeAquTMXcBVwFeOarNdYflhAANYC19bkWCJbmZ3Yz7G7dfecLxsjntw7c7_L19QM36wYDQTz0JMVPawEyF1ELQBIrRhhhe2qqLX-VoVtSQAYFsxAYFhqt-ICBRQCBYLGKxQo7ioUaEGE-Snmp5dfpCi2qx4pLLUEoN72BjENYhoIGwgb9DTwNchqLuvOPsYAYc9Pu91yfvw4zPanH9bLcbZffXpd3u_n0_q4PZy3y95_-9t-M79s9_MmfP0A1o3sPUwDAAA)" +title = "Not the Worst Frontrunner" +caption = "Just everybody you like better." +id = "ballot12" +%} + +These two strategies are really just different extremes of the strong strategic voter. + +Is approval voting immune to this kind of voting strategy? No; in fact, in the early seventies, mathematicians Gibbard and Satterthwaite both independently proved the theorem which bears their names, showing that no non-dictatorial voting method with more than 2 options is entirely immune to strategy. Unlike Arrow's theorem, which Nicky discussed, this one goes for any kind of voting method — ranked, rated, or whatever. So yes, approval voting is more resistant to strategy than score; but not immune. + +Note: in FPTP, you would have to strategically vote for somebody other than their favorite, and approval voting allows you to support both your favorite and your strategic pick. + +One scenario where approval becomes a factor is called the "chicken dilemma". Imagine 3 groups of voters, with 25%, 30%, and 45% of the vote respectively. Say that the first two groups both prefer each other over the third, so that either of them could beat that third opponent by 55% to 45%. Whichever of the first two groups strategically gives fewer approvals to its rival will win... unless neither of them gives enough, in which case the opponent will win. This is called a "chicken dilemma" because it's like a game of chicken between the voters for the two similar rivals: they can "swerve" and let their second-favorite win, or they can "drive straight" and either win (if the other side swerves) or crash (if the other side doesn't). + +Here's that scenario in sandbox form. The slider on the right controls the percent of the smallest group that is strongly strategic between {{ A }} and {{ B }}; all the rest of the voters use normalized strategy (that is, approve any candidate better than the average of their favorite and least-favorite). + +{% include sim.html +link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3WSS24EMQhE7-I1iszXeK4yM9tsc4AoOXswpZE6iqJeVGNw8aD7c8xxu99ZmNj2k-7MSbzmedtG7Pv5pMFdw0FV1zX1ltY1XjV-anTcJg0bt_HNc9DwjqNuVnKVlLEIbaF5fSqZlZz056nM_jdTLY49HzJmiqDY51hwrAfjMT7e3x-jWLiwuARMHJCCYiut_lpSzYSGlHEdCvcNEQiGE9iI9wWBjSxEiQguOpucSc5m4KXwUu16hZf6GaAK-RQGLsNR98XD5msVJ-BrIBcH02tg7WPXFhbd1ha-FqBt96Fjp84QaRhXCHDdIRjdF3L5e9uOHcSEYPrAlwlsMrwn0CIKeAVQYrcsoCzcXUBZisiQw8dcrz9sIZmvpZGSnXPMlhMCwwRMAiaxp_TukuBJ-CWw8mC9ef2RB-zrB8ZdeYg2AwAA)" +title = "Playing Chicken" +caption = "" +id = "election10" +comment = "Clumps of voters at (x,y,size): (-2,1,6); (-2,-1,4)slider; (3,0,10)." +%} + +The "chicken dilemma" is a genuinely tough situation for almost any voting method. The motivations for the two rival factions to vote strategically are hard to minimize safely. Voting methods that go too far out of their way to punish strategic voters in this scenario tend to get the wrong answer in other, more-common scenarios like center squeeze. Look at how these various methods deal with both of these situations: + +{% include sim.html +link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSTWsbMRT8K0Xnl6KnjyfJt6QQ6C24Sy6OD27inExljEkppf3tHWlo2VLKwo6eNJo383a_O-82u50GFU1tLzvVKlr8WLUkmtt-L04nR03AmxysapqcDE4enOg2XlxyG_dTvROXZ224icMCcMGwHbKTFsSvHxAqCF7-eXDS_nuCNqOFDneq2DQTa2JFrErxUnSQAklxGHty_fX1ycGGwqgC6FKNAJuagHATAWgd4BhtsBl03giBwLiBMgg1LgTKhMKqsqJK9DOHShizolakVoyTH6kV84gD4nAfjZepGNtKI_nfgxmFrouwUkhxXaSpk9Ytks22qfD70XRqczNzwlkJYZrJkUC7ORMYPRee1b-nnTkD8wSmN34Z4yQtzwQRjoxaRivWJhRaKbxbaKVEVolntFLGP-fuH5YHdP64fcT7rl9eDsDb8_nS3w4nLD8998sR-KF_eemX5-N17C23W0C8CTcKXPr53fK1Y7W9e-zXoxs_cmGPuvoShcOqnkCHlekq01UOvuZpuzJgpVZlzjpyvs_44Zn08-F06tfl2_mIWf4x_uMXJYo73bEDAAA)" +title = "Playing Chicken with Different Methods" +caption = "" +id = "election11" +%} + +{% include sim.html +link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSwWpCMRD8lZJzLNkku4netFDoTax4eXqQqqfSyEPaQ2m_vbsZBKHIg7ez2c1kZpNvF9xsGCizJ-adH6hmRclQEEVlt_OOeg8HzYPlyc2Cd9nN3G923nFPRZu0VjQE_-_TSr1bmd6tUOjcRL1DxEvxUq0QUUgmYuva6bR1KoUyliGJBEE1UdaoCpKGaV-NSq2LUalJQ0SAtZiRcd8QQRMLsopMWaKOInRl5KPNBVwJ2lJCB7iScg2kjWSNghIY0_SGI3dCA3QF8QqUcEgGct-Xbymz9GNywd1AZJ72RcYUmRBiP5whjyGPGQFWuaCGgTGsSkCASYFJwcCEuwUTJ6AQKBAMu0BBwd4CBSUhw50VKCj2jtzzcr3U61y388P6qylaLTbtclTwstrof9HGw17jU_s4tPHteFE8P5_H9rl_V_j61kZrTpM4IcvX85Wzx1lwRr0O3CefbR1zqgEBc6pwWOGwQmXlLr3CZAVfhddqXh9Z37O5_fkD-BG5DlwDAAA)" +title = "Center Squeeze" +caption = "" +id = "election12" +comment = "two different sandboxes. One repeats chicken dilemma case from above but also lets you switch voting methods (plurality, IRV, approval, score). The other one has center squeeze." +%} + +So, now I've explained why strategic voting makes things tricky, I can explain my two favorite voting methods: star voting and 3-2-1 voting. + +## Star voting + +"Star" stands for "score then automatic runoff." In this method, voters use the same ballot as score voting. Between the two candidates with the highest scores, the winner is the one that comes higher on more ballots. + +Here's a one-voter star election. Try the N = Normalized and F = Frontrunner strategies. These are almost the same as in score voting, except that the ratings may be changed a little to avoid giving the same score to two frontrunners. That is an important change that can matter in the runoff. Here's a one-voter election: + +{% include sim.html +link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSy04DMQz8l5wjFDtO4t0zf1Buqx6gXUSlqlu1RQgh-HbsjApCqNrDxI-dzDj-CCmM01SHSFrWcaJS7cR24sI_J4kstF7HQN5MSSPl5HEOY4pBwhi-iEMMJYwUQ7UuKzaDFP99VtGbleFmhVK_i1xCNZHZc4wcZJAACqB2LWQqSAztToehFzn1iKn3MANAw4LIaLJBRbIhUkTGwubeZ8emj3wU4MqMErgyuLJxTRT7560VPeDM8EyRo3sSJ_UucafuXPh6MNKp90j_U_7SChxLw6NAqgw9WTC9QgCILBkAkQWPV2C4NNS031RguCYArFZMv8JqLd2Gy6ugqFBQMfIGBQ3_NihoGRFeruHl2nV_Gor6O58onoclTQDqTAoxCjEKQgWhQo-CTyFLXdZdsZWDsKfH_X65PLwfZ9vm1WY5zbbP55fl7X4-b06742W3HHzPXw_b-Xl3mLfh8xuKlzI8PQMAAA)" +title = "Star Strong" +caption = "Keeps a space for the best." +id = "ballot9" +comment = "one-voter star election" +%} + +In STAR voting, the runoff resolves the chicken dilemma example we've been using. + +{% include sim.html +link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3WSQU4DMQxF75K1hWI7dpJeA3ZtF4DaVaUixAahcnYcf1UahNAs_jh2fp6dfJVadvs9CxO3eaQ98yDudf3NRmzzeKTCWcNOUZc18Tda1ljU2KrRsqtUWtmVb5ZCxTL22BnJHhLGIjSF6vaL5IhkpT9fZOa_Ga5pz4uMmdzJ51oWLOvCOJTr-XwowcKBxSFgYocEFLfQOF9D4jChImEci8K5QwSC5gQ2YrlBYCMd0UAEF61JziRrMvBSeKlmvcJLbTUQhbwKHZvhqHPj0ep9FCvgbSAbh6bboKVP2x7RPI9tHbcF6DZz0TBTY4gkjCkEuGYQtG4dufF72oYZeIWge8fNOCbplh1oEDm8HCg-UzpQOvZ2oHRF1JDDZfb7C-tIjvvQSKmtdfQ2KgSGAzADMANzGpanDPAM-A1gjYX1YPEiAfbyfLlcP54-307R-uPr9f1Ubj-GvhvISwMAAA)" +title = "Chicken Star" +caption = "" +id = "election13" +comment = "chicken dilemma with star" +%} + +## 3-2-1 voting + +In 3-2-1 voting, voters rate each candidate "Good", "OK", or "Bad". To find the winner, you first narrow it down to three semifinalists, the candidates with the most "good" ratings. Then, narrow it further to two finalists, the candidates with the fewest "bad" ratings. Finally, the winner is the one preferred on more ballots. Here's a ballot to play with: + +{% include sim.html +link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSy2oDMQz8F59NsWTZ1u65n5Dbsoe22ZJASEIelFLab6_kIS2lhD2M9djxjKyPkMI4TXWIpGWOE5VqJ7YTF_45SWSheY6BvJmSRsrJ4xzGFIOEMXxRDjGUMFIM1bqs2AxS_PdZRe9WhrsVSv0ucgnVRGbPMXKQQQIogNq1kKkgMbQ7HYZe5NQjpt7DDAANCyKjyQYVyYZIERkLm3ufHZs-8lGAKzNK4MrgysY1Ueyft1b0gDPDM0WO7kmc1LvEnbpz4dvBSKfeI_1P-UsrcCwNjwKpMvRkwfQKASCyZABEFjxegeHSUNN-U4HhmgCwWjH9Cqu1dBsur4KiQkHFyBsUNPzboKBlRHi5hpdrt_1pKOrvfKJ4HpY0AagzKcQoxCgIFYQKPQo-hSx1WQ_FVg7Cnp92u8Nl9X5cbJtXm9Oy2D6fN4e3x-X8ctoeL9vD3vf8ul8vr9v9sg6f3_tmOp49AwAA)" +title = "321 Strategic" +caption = "Extra rounds of approval." +id = "ballot10" +comment = "one-voter 3-2-1" +%} + +And here's the chicken dilemma. Note that Frontrunner strategy doesn't change the result from Normalize strategy. Candidates wouldn't have to go negative against their nearby rivals in order to ensure that their voters would at least be moderately strategic and wouldn't just normalize. + +{% include sim.html +link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3WSQWoDMQxF7-K1KJZkyXbOkV2SRQsJXQRSSjelNGevrE9gSimz-CNL_n6S_VVq2R0OLEzc5okOzIO41_U3G7HN04kKZw07RV3WxN9oWWNRY6tGy65SaWVX7qyFimXssTOSPSSMRWgK1e0XyRHJSn--yMx_M1zTnhcZM7mTz7UsWNaFcSy3y-VYgoUDi0PAxA4JKG6hcb6GxGFCRcI4FoVzhwgEzQlsxHKDwEY6ooEILlqTnEnWZOCl8FLNeoWX2mogCnkVOjbDUefGo9XHKFbA20A2Dk23QUuftj2ieR7bOm4L0G3momGmxhBJGFMIcM0gaN06cuP3tA0z8ApB946bcUzSLTvQIHJ4OVB8pnSgdOztQOmKqCGHy-yPF9aRHI-hkVJb6-htVAgMB2AGYAbmNCxPGeAZ8BvAGgvryeJFAuzl-Xq9few_387R-v71_Xwu3z-WC-vnSwMAAA)" +title = "321 Chicken" +caption = "" +id = "election14" +comment = "chicken dilemma with 3-2-1" +%} + +## Afterword + +**From Paretoman**: To finish where Jameson left off, STAR voting and 3-2-1 voting encourage voters to put better information on their ballot. Both voting methods use additional rounds to help more accurately determine the best candidate. The additional rounds also give them some resistance to the chicken dilemma. \ No newline at end of file diff --git a/oncemore.html b/oncemore.html new file mode 100644 index 00000000..b2d4ab20 --- /dev/null +++ b/oncemore.html @@ -0,0 +1,745 @@ +--- +permalink: /oncemore/ +title: Draft for An Even Better Ballot +banner: To Build an Even Better Ballot +description: an addendum to @ncasenmare's interactive guide to alternative voting methods +twuser: bettercount_us +--- + + + + + + + {{ page.title }} + {% include meta-1.html %} + {% include meta-share.html title=page.title description=page.description twuser=page.twuser %} + + + {% include css-index.html %} + + + + + + + + + + + + + + + + + + +
+
+ +

I wanted to start this voting exploration off with a topical joke about how it relates to the latest election travesty. But if I do that, it will seem dated as soon as the next election travesty comes along. So instead I'll use something that totally won't seem dated 20 years from now: a Twitter feed.

+ + + + + +

Never fear. I have two more voting methods to teach you about, and I hope I can convince you that they're the best yet.

+ +

Who's "me"? I'm Jameson Quinn, a statistics student at Harvard and a board member of the Center for Election Science. I'm not as good a designer or interactive programmer + as Nicky, as you can see from the title above. But I have spent a lot of time thinking about voting methods.

+ +

I'm also not as cool as Nicky, but I'm going to pretend to be. If my pale imitation of their chatty tone puts you off, then... well, I guess you can wait for my peer-reviewed paper.

+ +

So, the story so far: FPTP voting is horrible. There's all kinds of other voting methods that would be better. We're looking for the one that's best. Some activists think that's Instant Runoff Voting (IRV), but Nicky and I both disagree; it can prematurely eliminate centrists, and requires centralized counting. Nicky says they lean towards score voting, and I can see why: it's easy to understand, and in all the simulations Nicky built, it does a great job of satisfying all the little dot-on-screen voters.

+ +

By the end of this interactive, you're going to understand the three methods I consider the best. One of them Nicky already showed you: approval voting, which is great because it's so simple and because it has absolutely no downsides versus FPTP. The other two are newer, but they use similar basic ideas to what you've already seen. I hope you like all three methods.

+ +

But before I show you the new methods, I have to show you why score voting isn't already the greatest method possible. So I have to explain strategic voting.

+ +

Unstrategic voting

+ +

Remember how Nicky's voters worked in Score Voting? They just gave each candidate a number based on the absolute distance. Like this:

+ + +
+ +
+ +
+
+ +

UNSTRATEGIC BALLOT

+

Judge, don't choose.

+ + +
+
+

Description: this should just be an unstrategic score voter, as in ncase's thing. Allow ballot picture to show 0 scores.

+ +

But notice something about that ballot? For many positions of the voter and candidate, the ballot doesn't include a score of 5 or a score of 0. If you actually voted like that, you'd be giving up on some of your voting power. Say Triangle is your favorite, and you give them a score of 3, which also happens to be their average score. If Square wins with a score of 3.2, you're going to feel very silly for not giving a triangle a 5, in order to pull their average up as high as you can.

+ +

Digression: median systems

+ + skip this digression + +

Ever watched the Olympics? In events like figure skating, there are a number of Olympic judges from different countries. In order to prevent any one judge from having too much influence, they use a "trimmed mean": they throw away the highest and lowest scores before they take the average.

+ +

Olympic judges are supposedly supposed to be unbiased. But for voters, there's nothing wrong with having political opinions. (I mean, obviously your opinions, dear reader, are the best, and everybody else should just listen to you. But much as I'd like to, I can't force them to.) So let's imagine voting used the olympic system.

+ +

Throwing away just one highest and lowest score, with thousands or millions of voters, obviously wouldn't make an appreciable difference. So we'd have to throw away some more scores. And, why not? Let's throw away a few more while we're at it.

+ +

When should we stop? When there's just 1 or 2 scores left to take the average. But if you do that, most people wouldn't called that a "trimmed mean" anymore; they'd just call it the median, the middle number.

+ +

There are several voting methods that use the median to find the winner. (It turns out that can lead to a lot of ties, so you need a tiebreaker system to avoid that.) In the 1910s, over a dozen US cities, starting with Grand Junction, CO, used "Bucklin voting", a median-based system using a hybrid ranked/rated ballot. More recently, voting theorists Balinski and Laraki have proposed Majority Judgment, a median-based method using a rated ballot that's now been used in a number of competitions.

+ +

Using the median reduces the incentives for a single voter to strategize. After all, unless you happen to be the exact median rating for a candidate, the only thing that matters about the rating you gave them is whether it's above or below the median.

+ +

Still, large groups of voters can still get a strategic advantage. The larger the voting bloc, the greater the chance is that the median rating for a given candidate happens to be a voter in that bloc.

+ +

I used to think that median voting methods were the best. But then I did the simulations I'll talk about below, and they were merely OK; not much better outcomes than approval voting, but without the simplicity. I still think they're hugely better than FPTP and a bit better than IRV, but I'm not sold enough on them for it to be worth my time programming them in to Nicky's simulator.

+ + +

Lightly-strategic voting: "normalization"

+ +

As I suggested above, in the real world, in score voting and similar methods, most voters will want to make sure that their ballot contains at least one candidate each at the top rating and at the bottom rating. The simplest way to do that (even though the name sounds complicated and math-y) is "normalization". That just means that you set the top and bottom ratings to be however good your favorite and least-favorite candidates are, and then spread the rest of the ratings evenly between that.

+ +

Here's an example of a "normalizing" voter for you to play with:

+ +
+ +
+ +
+
+ +

Normalizing Voter

+

Best is 5. Worst is 1.

+ + +
+ +
+

Description: this should be like ncase's "drag the voter" examples, with a normalizing score voter. This voter should have a series of circles, where the closest one intersects the closest candidate and the farthest one the farthest candidate. Thus, as you moved the voter or candidates the circles would change size.

+ +

As soon as voters are using strategy in score voting — even a very slight amount of strategy, such as normalization — it is no longer fully exempt from impossibility theorems like Arrow's theorem. In particular, it no longer obeys independence of irrelevant alternatives (IIA); adding or removing a losing candidate can change how voters normalize, and thus change who wins. For instance, in the simulation below, try moving the losing yellow traingle candidate, and see what happens.

+ +
+
+ +
+
+ +

+ non-optimal
+

+ +
+
+

This should be a score voting scenario with 100% normalizing voters. the voter median around (0,0), and candidates square (3,0), triangle (-2,0), pentagon (-4,0). Pentagon voters are not enthusiastic enough about triangle so square wins, even though triangle is higher utility. Removing penta fixes the problem.

+ + +
+
+ +
+ +
+

But a bigger problem than IIA violation, which in the real world is relatively hard to engineer or take advantage of, is the issue of differential voting power. In the following election, one of the groups of voters normalizes, and the other one doesn't. The two groups are the same size, but the normalizing group gets a candidate they like a lot better.

+ +
+
+ +

+ Strategists win
+

+ +
+
+

groups of voters at (-4,0)non-normalizing and (4,0)normalizing. Cands at (-2.5,.5), (-2,0), (0,0), (2,0), (2.5,.5). (2,0) wins.

+ +

Is that kind of thing realistic? Perhaps not in the form above; few voters would be so unstrategic as not to normalize. But actually, normalizing as above is a pretty weak strategy. To strategize even more strongly, voters could somehow assess which two candidates were the frontrunners, and use them as the endpoints for normalization. Mostly, this means casting an approval-like ballot that gives every candidate either a 5 or a 0. For instance, in the election below, the voter believes that the red hexagon and the blue square are the frontrunners. Compare the strongly strategic voter below with the normalized voter in the second example above.

+ + +
+
+ +
+
+ +

Strong Strategic Voter

+

Almost like strategic approval.

+ + +
+
+ +

Single-voter example with 3 candidates and a voter who normalizes based on only the 2 of them.

+ +

Just as normalized voters can have more voting power than naive voters, strongly strategic voters can have more than normalized voters (as long as they are not too far off in guessing the frontrunners). This is a potential weakness of score voting.

+ +

Even stronger strategic voters could only choose the best of the frontrunners.

+ +
+
+ +
+
+ +

Best Frontrunner

+

And everybody you like better.

+ + +
+
+ +

A more risk-averse, safe strategic voter could avoid the worst frontrunner by voting for everyone better.

+ +
+ +
+ +
+
+ +

Not the Worst Frontrunner

+

Just everybody you like better.

+ + +
+
+

These two strategies are really just different extremes of the strong strategic voter.

+ +

Is approval voting immune to this kind of voting strategy? No; in fact, in the early seventies, mathematicians Gibbard and Satterthwaite both independently proved the theorem which bears their names, showing that no non-dictatorial voting method with more than 2 options is entirely immune to strategy. Unlike Arrow's theorem, which Nicky discussed, this one goes for any kind of voting method — ranked, rated, or whatever. So yes, approval voting is more resistant to strategy than score; but not immune.

+ +

(Note: advocates for IRV sometimes garble this point by saying that in approval voting the best strategy is to "bullet vote" for only your favorite candidate, and that this would lead it to devolve back to FPTP. That's just wrong; if voters are strategically voting for somebody other than their favorite in FPTP, then there's no way it would make sense for them to bullet vote in approval. Approval voting isn't perfect, but it simply does not break down to FPTP.)

+ +

One scenario where approval becomes a factor is called the "chicken dilemma". Imagine 3 groups of voters, with 25%, 30%, and 45% of the vote respectively. Say that the first two groups both prefer each other over the third, so that either of them could beat that third opponent by 55% to 45%. Whichever of the first two groups strategically gives fewer approvals to its rival will win... unless neither of them gives enough, in which case the opponent will win. This is called a "chicken dilemma" because it's like a game of chicken between the voters for the two similar rivals: they can "swerve" and let their second-favorite win, or they can "drive straight" and either win (if the other side swerves) or crash (if the other side doesn't).

+ +

Here's that scenario in sandbox form. The slider on the right controls the percent of the smallest group that is strongly strategic between triangle and square; all the rest of the voters use normalized strategy (that is, approve any candidate better than the average of their favorite and least-favorite).

+ + +
+
+ +
+
+ +

+ Playing Chicken
+

+ +
+
+ +

Clumps of voters at (x,y,size): (-2,1,6); (-2,-1,4)slider; (3,0,10).

+ + +

The "chicken dilemma" is a genuinely tough situation for almost any voting method. The motivations for the two rival factions to vote strategically are hard to minimize safely. Voting methods that go too far out of their way to punish strategic voters in this scenario tend to get the wrong answer in other, more-common scenarios like center squeeze. Look at how these various methods deal with both of these situations:

+ + + +
+
+ +
+
+ +

+ Playing Chicken with Different Methods
+

+ +
+
+ +
+
+ +

+ Center Squeeze
+

+ +
+
+ +

two different sandboxes. One repeats chicken dilemma case from above but also lets you switch voting methods (plurality, IRV, approval, score). The other one has center squeeze.

+ +

So, now I've explained why strategic voting makes things tricky, I can explain my two favorite voting methods: star voting and 3-2-1 voting.

+ +
+
+ +
+ +
+

Star voting

+ +

"Star", or more precisely, "s+ar", stands for "score plus automatic runoff." In this method, voters use the same ballot as score voting. Between the two candidates with the highest scores, the winner is the one that comes higher on more ballots. Here's a one-voter star election. You can choose between three kinds of strategy: normalized, moderately strategic, and strongly strategic. The first and last kinds you've seen before. The third is almost like a strongly strategic voter, except that the ratings may be changed by one to avoid giving a frontrunner the same score as any other candidate. Here's a one-voter election:

+
+
+ +

Star Strong

+

Keeps a space for the best.

+ + +
+
+ +

one-voter star election

+ +

And here's the chicken dilemma you saw above:

+ +
+
+ +
+
+ +

+ Chicken Star
+

+ +
+
+ +

chicken dilemma with star

+ +

3-2-1 voting

+ +

In 3-2-1 voting, voters rate each candidate "Good", "OK", or "Bad". To find the winner, you first narrow it down to three semifinalists, the candidates with the most "good" ratings. Then, narrow it further to two finalists, the candidates with the fewest "bad" ratings. Finally, the winner is the one preferred on more ballots. Here's a ballot to play with:

+ +
+
+ +
+
+ +

321 Strategic

+

Approval with an extra level.

+ + +
+
+ +

one-voter 3-2-1

+ +

And here's the chicken dilemma. Note "moderately strategic" doesn't change the result from "normalized". So unlike in star voting, candidates wouldn't have to go negative against their nearby rivals in order to ensure that their voters would at least be moderately strategic and wouldn't just normalize.

+ +
+
+ +
+
+ +

+ 321 Chicken
+

+ +
+
+ +

chicken dilemma with 3-2-1

+ +
+



















+
+ +
+ +
+

Putting it all together, here's a sandbox for you to try out all the different systems and to make your own scenarios:

+
+ +
+ + + + + + + + + + {% include sandbox.html %} + + + +
+
+ + +
+ +
+
PUBLIC DOMAIN
+ Zero rights reserved. + I'm giving away + all my art/code/words, + so that you + teachers, mathematicians, hobbyists, activists, and policy wonks + can use them however you like! + This is for you. + Get my source code on GitHub! +
+
+ +
+ + +
+ +
“BUT WHAT CAN I DO?”
+ +

+ For citizens: Remember, think global, but act local. + Change from the bottom-up lasts longer. + If you're in the US, + find your representative + and badger 'em. + If you're in Canada, + find your Member of Parliament + and badger 'em. + Also if you're Canadian, + fill out the MyDemocracy.ca survey before the end of 2016! + This survey has a few questions specifically about voting reform! + (sadly, the question is still framed as "simple vs expressive". + that is why i've been so gung-ho about Approval & Score, + and maybe a bit too mean towards IRV) +

+ +

+ For learners: + Watch CGP Grey's Politics in the Animal Kingdom series! + It's charming, and covers more ground than I did here – it explains + gerrymandering, proportional representation, and more. + Also, read Gaming The Vote by + William Poundstone. + It's a thrilling read, + with dramatic human stories of crooks & conmen trying to game our glitchy voting systems – + and sometimes, succeeding. +

+ +

+ For teachers: + This entire "explorable explanation" is public domain, copyright-free, + meaning you already have permission to use this freely in your classes! + You can even use the Sandbox Mode to create your own material, + or as a tool for students to make something on their own. +

+ +

+ For coders: + This is all open source! + So you can get my code on GitHub, and remix it to your heart's content. + (sorry in advance for my messy code) +

+ +

+ Check out these organizations: + Though they may differ on what voting method they like best, + they all have a common goal: to reform the one we have. + Electology likes Approval Voting most, + FairVote likes Instant Runoff most, + and RangeVoting.org likes Score Voting most. +

+ +
ON THE SHOULDERS OF GIANTS
+

+ This "explorable explanation" was directly inspired by these two projects: +

+ +

+ Voting Sim Visualization by Ka-Ping Yee (2005) + was a real eye-opener. + (hat tip to Bret Victor for sharing it with me!) + I've heard lots of written debate over FPTP vs IRV vs Condorcet vs Approval vs blah blah blah, + but I'd never seen their difference visualized so clearly! + It gave me instant insight. + And it actually changed my mind – I used to think IRV was pretty good, + but after seeing IRV's messiness (as shown above), I realized it's actually kinda stinky cheese. +

+

+ However, even this brilliant visualization was still too abstract. + And since it wasn't interactive, I couldn't test the many questions & scenarios that came to mind. + So that's why my second inspiration was... +

+ +

+ Up and Down the Ladder of Abstraction + by Bret Victor (2011). + It's one of the web's earliest "explorable explanations" (also a term Bret coined) + and it is gorgeous. + Obviously, I borrowed the format of mixing words & "games" to explain things, + but I also followed the formula of starting concrete – one voter – + then moving up to the more abstract – a whole election. +

+

+ + You can learn more about Explorable Explanations here. +

+

+ And last but not least, thank you to all the math & policy nerds + who spent way too much time thinking about all this. +

+ +
+ STAY IN TOUCH, MAYBE?
+

+ Every once in a while, I'll fall into an endless rabbithole – + like this one on voting methods – and slowly crawl my way out, bloodied and bruised, + with a new interactive thing for you! + If you wanna find out + when I finally get around to making new shtuff, you can... +

+ +

+ And if you wanna see more of my past projects, + check out my wobsite! +

+

+ See you again soon! Have a Happy New Year 2017, or try to, anyway. +

+ +
+ + +
+
A BIG THANKS TO ALL MY
+
SUPPORTERS
+ + + +
+
+
aimee jarboe
+
+
+
frank leon rose
+
+
+
jared cosulich
+
+
+
louis-jean teitelbaum
+
+
+
matt hughes
+
+
+
micah cowan
+
+
+
michael alan huff
+
+
+
natalie sun
+
+
+
noel lehmann
+
+
+
phil dougherty
+
+
+
tom cascio
+
+
+
tom knowles
+
+
+
+
+
+ Adam M. Smith
+ Alex Dytrych
+ Andrew
+ Andy
+ Artemiy Solopov
+ Aschelon
+ ben fei
+ Benjamin Riggs
+ Bob Wise
+ Brandon
+ Brent Werness
+ Brian Wu
+ Bruno Guerrero
+ Buster Benson
+ Casey Ross
+ Charlie McIlwain
+ Christopher
+ Colin
+ Colin
+ Cort Stratton
+ Craig Steele
+ Daniel Horowitz
+ Daniel Shiffman
+ Dave Tu
+ David Smit
+ Dylan Meconis
+ Fahrstuhl
+ Feiya Wang
+ Forrest Oliphant
+ Frank Leon Rose
+ Henry Reich
+ Iñaki
+ J. Hu
+ Jacob Christian Munch-Andersen
+ Jacques Frechet
+ James Hogan
+ Janusz Leidgens
+ John_Ca
+ Johnny Owens
+ Joseph Perry
+ Joshua Horowitz
+ Julia Karmo
+ Karen Cooper
+ Kat Suricata
+ Kate Fractal
+ Kathryn Long
+ Kevin
+ Kevin Wang
+
+
+ Klemen Slavic
+ kuerqing1024
+ Linda Booth Sweeney
+ Maic Lopez Saenz
+ Matt "Kupo" Roszak
+ Matt Warren
+ May-Li Khoe
+ Mekki MacAulay
+ Micah Cowan
+ Michael Duke
+ Michelle Brown
+ Michelle Kelly
+ Milan Pingel
+ Monika Denes
+ Mustafa Alic
+ Nick Schrag
+ Nikita
+ Noah Swartz
+ Pablo Lopez Soriano
+ Pat Mächler
+ Peter McEvoy
+ Philip Tibitoski
+ Piotr Migdal
+ Rachel Nabors
+ Raphael D'Amico
+ Richard Hackathorn
+ Rob Napier
+ Roland Tanglao
+ Ryan Barker
+ Sam Anderson
+ Sam Maynard
+ Samira Nedungadi
+ Sarah Barbour
+ sarah mathys
+ SB Sigma
+ Seanny123
+ Serguei Filimonov
+ Sigpipe
+ Sylvain Francis
+ Syria Carys Sirlay
+ T_Caramel
+ TisGood
+ Tony Onodi
+ Traci Lawson
+ Yona
+ Yu-Han Kuo
+ Zach Smith
+ Zoe Bogner +
+
+
+
AND SPECIAL THANKS TO
+
+
+ Alex Dytrych
+ Alex Jaffe
+ Brian Bucklew
+ Chris Walker
+ Christine Zhang
+ Dan Zajdband
+ Daniel Cook
+ Droqen
+ Jason Grinblat
+
+
+ Jessie Salz
+ Lisa Charlotte Rost
+ Martin Shelton
+ Patrick Dubroy
+ Pietro Passarelli
+ Sandhya Kambhampati
+ Tanya Short
+
+
+
+
+ +
+
+ sharing is caring! + +
+
+ + + + + + + + \ No newline at end of file diff --git a/original.html b/original.html new file mode 100644 index 00000000..2e3e5251 --- /dev/null +++ b/original.html @@ -0,0 +1,919 @@ +--- +permalink: /original/ +layout: default-original +title: Original +banner: To Build a Better Ballot +description: an interactive guide to alternative voting methods +twuser: ncasenmare +--- + + + + + + + +
+ + + +
+ +

No, this is not about the 2016 U.S. election. Not just that, anyway.

+ +

First, I need to explain a weird glitch in our voting system. + Let's say there's two candidates, Steven Square and Tracy Triangle , on a couple political axes. (for example, “left vs. right” and “globalist vs. nationalist”) Let's also say there's a voter who simply votes for whoever's political position is closest. What would that look like?

+ +
+
+
+

+ click & drag
the candidates and the voter:
+

+
+
+ +
+ +
+
+ +

It's a tough choice. Triangle's got some sharp points, but Square understands more sides! Alas, in the end, you can only vote for one.

+ +

Of course, there's more than just one voter in an election. Let's simulate what an election would look like with 100+ voters.

+ +
+
+
+

+ drag the candidates & voters around.
(to move voters, drag the middle of the crowd)
watch how that changes the election:
+

+
+
+ +
+
+
+ +

Now let's consider a different election. Say Tracy Triangle is already beating Steven Square in the polls, and a third candidate, Henry Hexagon , sees this. (Hexagon's supporters like how he tackles problems from more angles) Inspired by her success, Hexagon swoops in and takes a political position close to Triangle's.

+ +

Now, you'd think giving the voters more of what they want should result in a better choice, or at least, not result in a worse choice, right? Well...

+ +
+
+
+

+ + at first, beats .
+ drag to just under ,
+ and see what happens:
+

+
+
+ +
+
+
+ +

That's right. Steven Square, our least popular candidate, now wins! This is because when you have two good candidates, they "steal" votes from each other, letting a bad third candidate win.

+ +

This is called the spoiler effect. The most famous real-world example of this was in 2000, when Ralph Nader "stole" votes from Al Gore, letting George Bush win. And though the spoiler effect didn't play a big role in 2016, its impact could still be felt.

+ +

In the Republican primary, one anti-establishment nominee, Trump, ran against sixteen GOP establishment nominees, who all "stole" votes from each other, letting Trump grab the nomination, easily. As for the Democratic primary, fear of splitting the vote prevented Sanders from running as independent. And to cap it all off, there was always the worry that other candidates like Johnson, Stein, and McMullin could spoil the election.

+ +

But again, this is not about the 2016 U.S. election.

+ +

This is about designing a democracy that people can trust.

+ +

Despite so much hoopla around the 2016 election, a full half of Americans did not vote. Even of those who voted for Clinton/Trump, 20% of them said their candidates were untrustworthy, and voted for them anyway. And around the world, people's trust in their governments – or the trustworthiness of their governments – has never been lower. It's more than America at stake. It's every democracy in the world.

+ +

...so yeah, no pressure.

+ +

Rebuilding trust is a complex problem with no easy solutions. But I think there is an easy first step. It's a step that could get rid of our “lesser of two evils” problem, and give us citizens more choices, better choices. And yet, it won't be as daunting as fixing campaign finance or gerrymandering or lack of proportional representation, no, it'd just require changing a piece of paper, and how we count those pieces of paper.

+ +

This idea is not the most important issue. It won't solve everything. But as a first step? It'd give us the biggest bang-for-buck.

+ +

Let's talk about how to build a better ballot.

+ +
+ + + + +
+ +

Now, some of you may have a couple objections!

+ +

First objection. Why would the people in power change the voting method that got them in power? Well, the spoiler effect has cost both Dems & Reps a major election before. Getting rid of that glitch would be a win-win for major and minor parties! Also, voting reform is already picking up steam. Just last month, Maine adopted Instant Runoff, and Justin Trudeau, Canada's Cutie-In-ChiefCynic-in-Chief, will be moving his nation towards a better voting system in 2017. (UPDATE: actually, he didn't do that.)

+ +

Second objection. Didn't some guy once prove that all voting methods will be unfair? Not quite. You're thinking of the infamous Impossibility Theorem by Kenneth Arrow, the mathematician in the 1950's who founded the whole study of voting methods.

+ +

Two answers to that: 1) some voting methods can still be more fair than others, even if none are perfect. And 2) Kenneth Arrow's proof doesn't apply to all voting methods! That's a misconception. It only applies to voting methods where you rank candidates. Later, we'll see some voting methods where you don't rank candidates – along with other alternatives to our current, glitchy voting method.

+ +

But first, let's take a closer look at the voting method we do have:

+ +
+
+
+

FIRST PAST THE POST (FPTP)

+

same as before. click & drag
the candidates and voter

+
+
+ +
+
+
+ +

How To Count: Simply add up the votes. Whoever gets the most votes, wins.

+ +

Sounds logical enough. But as you saw earlier, it can lead to a weird glitch, where having two good candidates can make the election go to a third bad candidate. This is why some people vote "strategically", voting not for their actual honest favorite, but voting for the lesser of two evils. And strategic voting is fine – but! – ask yourself this: how can we expect our elected officials to be honest, when our voting method itself doesn't let us be honest?

+ +

So, to fix the spoiler effect, other voting methods have been suggested. Such as...

+
+
+
+

RANKED VOTING

+

again, click & drag

+
+
+ +
+ +
+
+ +

How To Count: There's actually several different ways to count these kinds of ballots. Here, I'll just show you the top three:

+ +

+ Instant Runoff Voting (IRV): + This one is the most popular alternative to First Past The Post (FPTP). + Australia and Ireland use it in national elections. + San Francisco, Minneapolis, and Portland, Maine use it in local elections. + And Justin Trudeau, Prime Man-ister of Canada, + is leaning towards Instant Runoff, too. +

+ +

+ (Note: Instant Runoff Voting is also called “Ranked Choice Voting”, + even though there's other ways to count ranked ballots. + IRV is also often just called “Alternative Vote”, + even though there's a flippin' dozen other voting methods. + Such selfish naming! Sheesh!) +

+ +

IRV is a bit more complicated than FPTP, but here's how it works:

+ +
    +
  1. Count up the #1 choices.
  2. +
  3. If someone has more than 50%, they win! END.
  4. +
  5. If not, eliminate the last-place loser.
  6. +
  7. Run a new "round" of the election, minus that loser.
  8. +
  9. Repeat until someone has 50% or more.
  10. +
+ +

If that seems like too much, there is a much simpler method of counting ranked ballots...

+ +

Borda Count: One way is to simply add up the rank numbers, and like in golf, whoever has the lowest score, wins. Many countries will first reverse the rank numbers and then add them up so that the highest score wins. Borda count is used in Slovenia and a bunch of tiny islands in Micronesia.

+ +

But if you want an even nerdier way of voting, you could try...

+ +

Condorcet Method: Run a simulated "election" between every pair of candidates, using the info on voters' ballots. IF there's a candidate who beats all other candidates in one-on-one "elections", that candidate wins the real election. However, that's a very big "IF". (as we'll see later...) The upside is, when this method does pick a winner, it's always the “theoretically best” candidate! Currently, this method is not being used by any governments, and is only being used by neeerrrrrds.

+ +

So, those are the voting methods where you rank candidates – the ones that Kenneth Arrow proved would always be unfair in some big way! But what of voting methods where you don't rank candidates? They're less well-known, but now, at least you'll know 'em:

+ +
+
+
+

APPROVAL VOTING

+

yup, stiiiiill click & drag

+
+
+ +
+
+
+ +

How To Count: Simply add up the approvals. Whoever gets the most approvals, wins.

+ +

Wait, picking more than one candidate? Doesn't that violate the one-vote-per-person rule? I hear you ask. Well, your vote was never a single check mark, your vote was always the whole ballot. And on this ballot, you get to honestly express all the candidates you approve of, not just your favorite or strategic second-favorite.

+ +

But if you want a more expressive voting method, why not try...

+ +
+
+
+

SCORE VOTING

+

you guessed it

+
+
+ +
+ +
+
+ +

How To Count: Simply add up the ratings. Whoever has the highest average score, wins. Kind of like Amazon reviews, but with democracy. (Note: this is not a ranking method, because two candidates can have the same score.)

+ +

So there's our top 6 voting methods: the one we use, and five popular alternatives. But how can we tell if these alternatives are actually better? What glitches might they have? And which voting method – if any – can we say is "the best"?

+ +

Like before, let's simulate 'em.

+ +
+ + + + +
+

Remember that simulation of the spoiler effect from earlier? Well, here it is again, but now you can switch between the six different voting methods! Here's the "spoiler effect" simulation again. See how different voting methods deal with potential spoilers:

+
+
+
+

+ drag to just under to create a spoiler effect.
+ then compare the 6 different voting methods: +
+ + (note: in the rare cases there's a tie, i just randomly pick a winner) + +

+
+
+ +
+
+
+ +

As you could see, every voting method except First Past The Post is immune to the spoiler effect. So, that's it, right? Ding dong, the glitch is dead? Just pick any other alternative voting method and be done with it?

+ +

But, alas. In getting rid of one glitch, some of these alternative voting methods create other glitches – for some, the cure is even worse than the disease.

+ +

For example, here's a sim of Instant Runoff Voting. In the beginning, Tracy Triangle is already winning, and you're going to move the voters even closer to her. Obviously, if a candidate is already winning an election, and becomes even more popular, they should still win afterwards, right?

+ +

You can probably guess where this is going...

+
+
+
+

+ drag the voters slowly up towards : +

+
+
+ +
+ +
+
+ +

What happened? + Originally, is eliminated in the first round, so + + goes against a weaker , and wins. + But when you move the voters closer to , + the loser changes! + So now, is eliminated in the first round, + which means goes against a stronger , + and loses.

+ +

Under Instant Runoff, it's possible for a winning candidate to lose, by becoming more popular. What a glitch!

+ +

How often does this actually happen in real life? There's a couple confirmed examples, and mathematicians estimate this glitch would happen about 14.5% of the time. But sadly, we can't know for sure, because governments usually don't release enough info about the ballots to reconstruct an IRV election & double-check the results.

+ +

So, not only is Instant Runoff's glitch as undemocratic as First Past The Post's glitch, it's possibly worse – because while FPTP's counting method is simple and transparent, Instant Runoff is anything but. And a lack of transparency is an even deadlier sin nowadays, when our trust in government is already so low.

+ +

(But wait! We'll be talking about the risk of strategic voting later. + Can IRV can make a comeback? Stay tuned...)

+ +

So much for the most popular alternative. What about the second-most popular, Borda Count? In this next simulation, you move a losing candidate closer to another losing candidate. Under FPTP, the spoiler effect would split their votes, making both of them lose even more. But watch what happens under Borda Count instead...

+ +
+
+
+

+ drag to just slightly left of : +

+
+
+ +
+ +
+
+ +

Yup. Borda Count has a reverse spoiler effect. Instead of one good candidate hurting another good candidate by moving closer, with Borda Count, one bad candidate can help another bad candidate by moving closer.

+ +

Here's what happened: at first, some voters ranked + >>, + but when you moved closer to , + those voters then swung to ranking + >>, + hurting enough + to make her lose to .

+ +

Still, Borda's not the worst, and at least it's simpler and more transparent than Instant Runoff. But how does Condorcet Method compare? When Condorcet picks a winner, it's always the “theoretically best” winner – but that's when it picks a winner.

+ +

So far, I've just been simulating voters as a single group, with a center and some spread. But seeing how polarized politics is nowadays, one could imagine several groups of voters, with totally different centers. Now, Condorcet tries to pick the candidate who beats all other candidates in one-on-one races. But with polarized voters, you could end up with a Rock-Paper-Scissors-like loop, where a majority of voters prefer A to B, B to C, and C to A.

+ +

In certain situations, the other voting methods just had glitches. In Condorcet, the voting method crashes. Try it out for yourself:

+
+
+
+

+ create your own “condorcet cycle”!
+ move the voters in such a way that NOBODY wins: +

+
+
+ +
+ +
+
+ +

Now, in actual practice – not that any government actually uses this voting method – when Condorcet fails to find a winner, the election falls back to another method like Borda Count. But if you do that, it'll get the glitches of its backup method. So it goes.

+ +

First Past The Post. Instant Runoff. Borda Count. Condorcet Method. Those were all the voting methods that use ranking – the ones that our math boy, Kenneth Arrow, proved would always be unfair or glitchy in some big way. What about the voting methods that don't use ranking, like Approval & Score voting? Well...

+ +

...I couldn't come up with a simulation to show their flaws. Because, in theory, they don't have many big flaws.

+ +

+ But that's a really, really, really big “in theory!” + It may be that, in practice, strategic voters use Approval & Score Voting exactly like First Past The Post – + only approving or giving 5 stars to their top candidate, and disapproving or giving 1 star to all others, + even if they actually like the others. + (See FairVote's critique of Approval Voting, and defense of Instant Runoff) +

+ +

+ Then again, even if Approval & Score Voting disincentivize you from expressing an honest second choice, + FPTP and IRV punish you for expressing an honest first choice. + Besides, if Approval can be "gamed", then that goes double for IRV. + (See this mathematician's critique of FairVote's critique, and defense of Approval) + So, in the end... [confused shrugging sounds]

+ +

+ We're gonna need a hecka lot more simulations. +

+ +

+ So, below is a chart + (source), + showing the results of 2.2 million simulations. + A huge variety of scenarios were tested. All-honest voters. + All-strategic voters. Half-honest, half-strategic. + Voters who know each others' preferences. + Voters who don't know each others' preferences. + Voters who only sorta-know each others' preferences. + And so on. + You can tell that a real mathematician made this chart, + because it's makin' my eyes bleed: +

+ +

+ +

+ Each voting method's results is shown as an ugly-blue bar. + The further to the right a voting method is, the more it "maximizes happiness" for the voters. + The higher up a voting method is, the simpler it is. + And a bar's width shows the range of a voting method's performance, + given different ratios of honest-to-strategic voters. +

+ +

+ The first thing to note is that strategic voting makes voters less happy than honest voting + – in all voting methods! I was very surprised when I first learnt that. + (But it makes sense, if you think about, say, a crowded room full of people trying to talk. Any one person can be "strategic" by shouting over others, but if everybody is "strategic", nobody can hear anybody, and all you're left with is sore throats and sad peeps.)

+ +

The other thing to note is which voting methods make people the happiest. If you have mostly honest voters, Score Voting is best. (with Borda Count a close second) And if you have mostly strategic voters, then both Approval & Score Voting are best. (and with strategic voters, IRV does just as bad as FPTP)

+ +

However, those are still computer simulations. How would these different voting methods play out in real life? Well, we can't just get the DeLorean up to 88, go back in time before the 2016 election, change the voting method, and see what would happen...

+ +

...or can we?!

+ +

No, no we can't. But last month, researchers did something close enough. + A polling study asked 1,000+ U.S. registered voters to rank & rate the six presidential candidates, + to simulate who would've won the (popular) vote under different voting methods! + (But keep in mind that if we had a different voting method in the primaries, we'd have different candidates entirely. + So take this study with a pillar of salt.) + The results: under Instant Runoff, Condorcet, and Approval Voting, the winner would've been Hillary Clinton. But under Score Voting, the winner would've been Donald Trump. And under Borda Count, the winner would've been... uh... Gary Johnson? +

+ +

?????

+ +
+
+
+

+ a guesstimated model of the 2016 US election?...
+ + how Clinton wins IRV, + Trump wins Score, + and Johnson wins Borda?? + +

+
+
+ +
+
+
+ +

Anyway.

+ +

Before we wrap all this up – remember Kenneth Arrow? The infamous mathematician who founded the study of voting methods in the 1950's? Well, in an interview 60 years later, Kenneth Arrow had this to say, about which voting method he likes most now:

+ +

+ “Well, I’m a little inclined to think that score methods [like Approval & Score Voting] where you categorize in maybe three or four classes [so, giving a score out of 3 or 4, not 10 or 100] probably – in spite of what I said about manipulation [strategic voting] – is probably the best.”

+ +

That's as strong an endorsement as you'll ever squeeze out of a math-head.

+
+ + + + +
+ +

ahem

+ +

+ DEAR JUSTIN “TOTES ADORBZ” TRUDEAU
+ (and everyone else around the world pushing for voting reform) +

+ +

Thank you for taking this small but powerful first step! We've known for way too long that our current voting method – First Past The Post – forces voters to be dishonest, creates a polarizing "lesser of two evils" scenario, and screws over both major and minor candidates.

+ +

+ However, you're probably only considering Instant Runoff Voting. + Which, to be fair, is better than than First Past The Post, + and if it's a choice between just those two, definitely go for Instant Runoff. + But IRV still has a glitch as undemocratic as FPTP's – + and worse, in our age of distrust, Instant Runoff's lack of transparency may be deadly for democracy. + Yes, sure, IRV was the best voting method we could come up with... + in 1870. + And since then, IRV has dominated the conversation, + unwittingly framing the whole voting reform debate as “simple vs expressive”. +

+ +

+ But that is a false choice. Thanks to computer simulations, real-life studies, and a bunch of math nerds, + we now know of voting methods that are both simple and expressive. +

+ +

+ Personally, I'm leaning towards Score Voting. + It's simple, very expressive, and already familiar to anyone who's seen Amazon's or Yelp's “five star” review method. + But that's just my humble opinion. + You could also make the case that Approval Voting is more practical, + because it's even simpler, and would already work with existing voting machines! + All you'd need to do is change the instructions from + “vote for the candidate you like” to “vote for the candidates you like”. +

+ +

+ Or maybe I'm completely wrong about Instant Runoff Voting, and it's actually pretty okay. + Heck, you could even go for Borda Count, as a hilarious prank. +

+ +

I won't claim to know which voting method is The Best™. I shall keep open this discussion, just as long as we have this discussion. For three reasons:

+ +

1) If I claim one voting method is the best, end of story, all the social-choice-theory nerds will be on my butt, yelling, BUT NICKY WHAT ABOUT QUADRATIC VOTE BUYING

+ +

2) We still need to test these alternative voting methods with actual experience, + not just annoying internet flame wars between IRV advocates and Score Voting advocates theory. + All the more reason for small towns, local states, and nations like Canada to be pioneers, to bravely experiment!

+ +

3) Keeping the discussion going is what democracy is.

+ +

A recent study found that in many Western countries – from Sweden to Australia to the United States – support for democracy has plummeted over the last several generations. + In 2011, almost a full quarter of young Americans said democracy was a "bad" or "very bad" way to run a country. + And today, one in six Americans say it'd be "good" or "very good" to be under actual military rule. +

+ +

Our age of distrust goes a lot deeper than the technical details of a voting method. There isn't gonna be One Weird Trick to fix democracy. But as a first step, a low-hanging fruit, a way to show that, yes, you will make the method respond to the needs and wants and pains and hopes and dreams of your people – well, fixing our voting method's a good start as any.

+ +

Because, this isn't just about trying to build a better ballot.

+ +

This is about trying to build a better democracy.

+ +

<3,
+ ~ Nicky Case

+ +
+ +

P.S: Since you've read & played this all the way, here, have a bonus! + A “Sandbox Mode” of the election simulator, with up to five candidates. + You can also save & share your very own custom election scenario with others. Happy simulating!

+
+ + + + + {% include sandbox-original.html %} + + + +
+
+ + +
+ +
+
PUBLIC DOMAIN
+ Zero rights reserved. + I'm giving away + all my art/code/words, + so that you + teachers, mathematicians, hobbyists, activists, and policy wonks + can use them however you like! + This is for you. + Get my source code on GitHub! +
+
+ +
+ + +
+ +
“BUT WHAT CAN I DO?”
+ +

+ For citizens: Remember, think global, but act local. + Change from the bottom-up lasts longer. + If you're in the US, + find your representative + and badger 'em. + If you're in Canada, + find your Member of Parliament + and badger 'em. + Also if you're Canadian, + fill out the MyDemocracy.ca survey before the end of 2016! + This survey has a few questions specifically about voting reform! + (sadly, the question is still framed as "simple vs expressive". + that is why i've been so gung-ho about Approval & Score, + and maybe a bit too mean towards IRV) +

+ +

+ For learners: + Watch CGP Grey's Politics in the Animal Kingdom series! + It's charming, and covers more ground than I did here – it explains + gerrymandering, proportional representation, and more. + Also, read Gaming The Vote by + William Poundstone. + It's a thrilling read, + with dramatic human stories of crooks & conmen trying to game our glitchy voting systems – + and sometimes, succeeding. +

+ +

+ For teachers: + This entire "explorable explanation" is public domain, copyright-free, + meaning you already have permission to use this freely in your classes! + You can even use the Sandbox Mode to create your own material, + or as a tool for students to make something on their own. +

+ +

+ For coders: + This is all open source! + So you can get my code on GitHub, and remix it to your heart's content. + (sorry in advance for my messy code) +

+ +

+ Check out these organizations: + Though they may differ on what voting method they like best, + they all have a common goal: to reform the one we have. + Electology likes Approval Voting most, + FairVote likes Instant Runoff most, + and RangeVoting.org likes Score Voting most. +

+ +
ON THE SHOULDERS OF GIANTS
+

+ This "explorable explanation" was directly inspired by these two projects: +

+ +

+ Voting Sim Visualization by Ka-Ping Yee (2005) + was a real eye-opener. + (hat tip to Bret Victor for sharing it with me!) + I've heard lots of written debate over FPTP vs IRV vs Condorcet vs Approval vs blah blah blah, + but I'd never seen their difference visualized so clearly! + It gave me instant insight. + And it actually changed my mind – I used to think IRV was pretty good, + but after seeing IRV's messiness (as shown above), I realized it's actually kinda stinky cheese. +

+

+ However, even this brilliant visualization was still too abstract. + And since it wasn't interactive, I couldn't test the many questions & scenarios that came to mind. + So that's why my second inspiration was... +

+ +

+ Up and Down the Ladder of Abstraction + by Bret Victor (2011). + It's one of the web's earliest "explorable explanations" (also a term Bret coined) + and it is gorgeous. + Obviously, I borrowed the format of mixing words & "games" to explain things, + but I also followed the formula of starting concrete – one voter – + then moving up to the more abstract – a whole election. +

+

+ + You can learn more about Explorable Explanations here. +

+

+ And last but not least, thank you to all the math & policy nerds + who spent way too much time thinking about all this. +

+ +
+ STAY IN TOUCH, MAYBE?
+

+ Every once in a while, I'll fall into an endless rabbithole – + like this one on voting methods – and slowly crawl my way out, bloodied and bruised, + with a new interactive thing for you! + If you wanna find out + when I finally get around to making new shtuff, you can... +

+ +

+ And if you wanna see more of my past projects, + check out my wobsite! +

+

+ See you again soon! Have a Happy New Year 2017, or try to, anyway. +

+ +
+ + +
+
A BIG THANKS TO ALL MY
+
SUPPORTERS
+ + + +
+
+
aimee jarboe
+
+
+
frank leon rose
+
+
+
jared cosulich
+
+
+
louis-jean teitelbaum
+
+
+
matt hughes
+
+
+
micah cowan
+
+
+
michael alan huff
+
+
+
natalie sun
+
+
+
noel lehmann
+
+
+
phil dougherty
+
+
+
tom cascio
+
+
+
tom knowles
+
+
+
+
+
+ Adam M. Smith
+ Alex Dytrych
+ Andrew
+ Andy
+ Artemiy Solopov
+ Aschelon
+ ben fei
+ Benjamin Riggs
+ Bob Wise
+ Brandon
+ Brent Werness
+ Brian Wu
+ Bruno Guerrero
+ Buster Benson
+ Casey Ross
+ Charlie McIlwain
+ Christopher
+ Colin
+ Colin
+ Cort Stratton
+ Craig Steele
+ Daniel Horowitz
+ Daniel Shiffman
+ Dave Tu
+ David Smit
+ Dylan Meconis
+ Fahrstuhl
+ Feiya Wang
+ Forrest Oliphant
+ Frank Leon Rose
+ Henry Reich
+ Iñaki
+ J. Hu
+ Jacob Christian Munch-Andersen
+ Jacques Frechet
+ James Hogan
+ Janusz Leidgens
+ John_Ca
+ Johnny Owens
+ Joseph Perry
+ Joshua Horowitz
+ Julia Karmo
+ Karen Cooper
+ Kat Suricata
+ Kate Fractal
+ Kathryn Long
+ Kevin
+ Kevin Wang
+
+
+ Klemen Slavic
+ kuerqing1024
+ Linda Booth Sweeney
+ Maic Lopez Saenz
+ Matt "Kupo" Roszak
+ Matt Warren
+ May-Li Khoe
+ Mekki MacAulay
+ Micah Cowan
+ Michael Duke
+ Michelle Brown
+ Michelle Kelly
+ Milan Pingel
+ Monika Denes
+ Mustafa Alic
+ Nick Schrag
+ Nikita
+ Noah Swartz
+ Pablo Lopez Soriano
+ Pat Mächler
+ Peter McEvoy
+ Philip Tibitoski
+ Piotr Migdal
+ Rachel Nabors
+ Raphael D'Amico
+ Richard Hackathorn
+ Rob Napier
+ Roland Tanglao
+ Ryan Barker
+ Sam Anderson
+ Sam Maynard
+ Samira Nedungadi
+ Sarah Barbour
+ sarah mathys
+ SB Sigma
+ Seanny123
+ Serguei Filimonov
+ Sigpipe
+ Sylvain Francis
+ Syria Carys Sirlay
+ T_Caramel
+ TisGood
+ Tony Onodi
+ Traci Lawson
+ Yona
+ Yu-Han Kuo
+ Zach Smith
+ Zoe Bogner +
+
+
+
AND SPECIAL THANKS TO
+
+
+ Alex Dytrych
+ Alex Jaffe
+ Brian Bucklew
+ Chris Walker
+ Christine Zhang
+ Dan Zajdband
+ Daniel Cook
+ Droqen
+ Jason Grinblat
+
+
+ Jessie Salz
+ Lisa Charlotte Rost
+ Martin Shelton
+ Patrick Dubroy
+ Pietro Passarelli
+ Sandhya Kambhampati
+ Tanya Short
+
+
+
+
+ +
+
+ sharing is caring! + +
+
+ +
+ + \ No newline at end of file diff --git a/play/ballot1.html b/play/ballot1.html deleted file mode 100644 index 1dfbd33e..00000000 --- a/play/ballot1.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/play/ballot10.html b/play/ballot10.html deleted file mode 100644 index b60c2d42..00000000 --- a/play/ballot10.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/play/ballot11.html b/play/ballot11.html deleted file mode 100644 index b60c2d42..00000000 --- a/play/ballot11.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/play/ballot12.html b/play/ballot12.html deleted file mode 100644 index b60c2d42..00000000 --- a/play/ballot12.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/play/ballot2.html b/play/ballot2.html deleted file mode 100644 index 1dfbd33e..00000000 --- a/play/ballot2.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/play/ballot3.html b/play/ballot3.html deleted file mode 100644 index 1dfbd33e..00000000 --- a/play/ballot3.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/play/ballot4.html b/play/ballot4.html deleted file mode 100644 index 1dfbd33e..00000000 --- a/play/ballot4.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/play/ballot5.html b/play/ballot5.html deleted file mode 100644 index 1dfbd33e..00000000 --- a/play/ballot5.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/play/ballot6.html b/play/ballot6.html deleted file mode 100644 index b60c2d42..00000000 --- a/play/ballot6.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/play/ballot7.html b/play/ballot7.html deleted file mode 100644 index b60c2d42..00000000 --- a/play/ballot7.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/play/ballot8.html b/play/ballot8.html deleted file mode 100644 index b60c2d42..00000000 --- a/play/ballot8.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/play/ballot9.html b/play/ballot9.html deleted file mode 100644 index b60c2d42..00000000 --- a/play/ballot9.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/play/css/ballot.css b/play/css/ballot.css index c0725176..d4f3329a 100644 --- a/play/css/ballot.css +++ b/play/css/ballot.css @@ -1,13 +1,24 @@ /********************* BALLOT DESIGN **********************/ +.div-ballot { + background-color: #ffffff00; + padding: 10px; + /* margin: -10px; */ + text-align:center; +} +.div-election .div-ballot { + + margin: 0px -10px; + padding: 0px; +} -#ballot{ +.div-ballot #ballot{ width: 375px; height: 250px; + margin: 0px; margin-left: 20px; - margin-bottom: 10px; overflow: hidden; float:left; @@ -16,29 +27,204 @@ BALLOT DESIGN color: #302707; background-size: 100% 100%; - border: 2px solid #fff; + border: 2px solid #ccc; position: relative; } -#ballot .ballot_sprite{ +.div-ballot #ballot .ballot_sprite{ position: absolute; background-size: 100% auto; } -.button-group{ +.div-ballot .button-group{ margin-left: 0px; + text-align: left; float:left; clear:both; } -#reset { +.div-ballot #reset { + margin-top: 10px; top: 270px; left: 275px } -.model { +.div-ballot .model { height:260px; - margin-bottom: 10px; +} + +.div-ballot > div{ + vertical-align: top; +} +.div-ballot #b-left { + display:inline-block; + float:none; + width:254px; + text-align: center; + margin: 10px; +} +.div-ballot #b-right { + margin: 10px; + margin-left: -10px; + display:inline-block; +} + +.div-ballot #caption { + text-align: center; +} +.div-ballot #caption > div > div{ + text-align: left; +} + +.div-ballot #caption{ + margin-top: 0px; + float:left; + clear: both; + text-align: left; + line-height: 1.0em; + font-size: 23px; +} + +.div-ballot #caption img{ + height: 1em; + top:2px; + position: relative; +} +.div-ballot #caption .small{ + font-size: 14px; + line-height: 1.0em; +} + +.div-ballot #paper { + text-align: center; +} +.div-ballot #paper div{ + display: block; + vertical-align:top; + text-align: left; +} +.div-ballot #paper table{ + border-collapse: collapse; + border: 2px solid #ccc; +} +.div-ballot #paper table table{ + border: none; +} +.div-ballot #paper table.main{ + padding: 2px; + background-color: #fff; + width: 234px; + +} +.div-ballot #paper table.main2{ + background-color: #fff; + /* min-width: 220px; */ + width: 234px; +} +.div-ballot #paper table.num{ + border: 2px solid #ccc; + min-width: 25px; +} +/* .div-ballot #paper table{ + min-width: unset; + width: unset +} */ +.div-ballot #paper td{ + padding-top: 0px; +} +.div-ballot #paper td td{ + padding: 2px; +} +.div-ballot #paper td.nameLabel{ + vertical-align: middle; + font-size: 1.4em; + white-space: nowrap; + padding-left: 5px; +} +.div-ballot #paper td.nameLabel svg{ + vertical-align: middle; +} +.div-ballot #paper td.nameLabel span.nameLabelName{ + vertical-align: middle; + font-weight: bold; + font-size: .8em; +} + +.div-ballot #paper td.tallyText{ + font-size: 1.11em; + padding: 2px; +} +.div-ballot #paper td.tallyText span.small{ + font-size: .9em; +} +.div-ballot #paper td.num{ + width: 1em; + height: 1em; + text-align: center; +} +.div-ballot #paper th.main{ + padding: 5px; + background-color: #eee; + text-align:center; +} +.div-ballot #paper td.main{ + padding: 10px; +} +.div-ballot #paper table.main, +.div-ballot #paper table.main2{ + margin: 10px auto; + margin-top: 0px; +} + +.div-ballot #paper table.main2 th{ + padding: 5px; + color: #302702; +} +.div-ballot #paper table.main2 td{ + padding: 4px; +} +.div-ballot #paper table.main2 > tbody > tr > td{ + padding: 15px; +} + +.div-ballot #paper table.main th span.small, +.div-ballot #paper table.main2 th span.small{ + font-size: .8em; + line-height: .8em; + font-weight: normal; +} + +div.circle { + width: .7em; + height: .7em; + -webkit-border-radius: .35em; + -moz-border-radius: .35em; + border-radius: .35em; + background: white; + border: .1em solid #ccc; +} + +.div-sandbox .div-ballot { + /* transform: scale(.5); */ + font-size: 55%; + line-height: 1.0em; +} + + +/* Extra Small Devices, Phones */ +@media (max-width : 656px) { + .div-ballot #b-right{ + margin-left: 10px; + } + + .div-ballot #ballot{ + margin-left: 0px; + } +} + +.div-ballot-parent{ + width: 230px; + margin: 0 auto; } \ No newline at end of file diff --git a/play/css/ballotInSandbox.css b/play/css/ballotInSandbox.css index 4f6a3ef0..019dda13 100644 --- a/play/css/ballotInSandbox.css +++ b/play/css/ballotInSandbox.css @@ -3,7 +3,7 @@ BALLOT DESIGN **********************/ -#ballot{ +.div-ballot-in-sandbox #ballot{ width: 210px; height: 195px; @@ -21,7 +21,7 @@ BALLOT DESIGN } -#ballot .ballot_sprite{ +.div-ballot-in-sandbox #ballot .ballot_sprite{ position: absolute; background-size: 100% auto; } diff --git a/play/css/ballot_original.css b/play/css/ballot_original.css new file mode 100644 index 00000000..f8d0d9ed --- /dev/null +++ b/play/css/ballot_original.css @@ -0,0 +1,30 @@ +/********************* +BALLOT DESIGN +**********************/ + +.div-ballot #ballot{ + + width: 375px; + height: 250px; + margin-left: 20px; + + overflow: hidden; + float:left; + + background: #F0EAD6; + color: #302707; + + background-size: 100% 100%; + border: 2px solid #fff; + + position: relative; + +} +.div-ballot #ballot .ballot_sprite{ + position: absolute; + background-size: 100% auto; +} + +.div-ballot .model{ + float:left; +} \ No newline at end of file diff --git a/play/css/codemirror/codemirror.css b/play/css/codemirror/codemirror.css new file mode 100644 index 00000000..a64f97c7 --- /dev/null +++ b/play/css/codemirror/codemirror.css @@ -0,0 +1,350 @@ +/* BASICS */ + +.CodeMirror { + /* Set height, width, borders, and global font properties here */ + font-family: monospace; + height: 300px; + color: black; + direction: ltr; +} + +/* PADDING */ + +.CodeMirror-lines { + padding: 4px 0; /* Vertical padding around content */ +} +.CodeMirror pre.CodeMirror-line, +.CodeMirror pre.CodeMirror-line-like { + padding: 0 4px; /* Horizontal padding of content */ +} + +.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { + background-color: white; /* The little square between H and V scrollbars */ +} + +/* GUTTER */ + +.CodeMirror-gutters { + border-right: 1px solid #ddd; + background-color: #f7f7f7; + white-space: nowrap; +} +.CodeMirror-linenumbers {} +.CodeMirror-linenumber { + padding: 0 3px 0 5px; + min-width: 20px; + text-align: right; + color: #999; + white-space: nowrap; +} + +.CodeMirror-guttermarker { color: black; } +.CodeMirror-guttermarker-subtle { color: #999; } + +/* CURSOR */ + +.CodeMirror-cursor { + border-left: 1px solid black; + border-right: none; + width: 0; +} +/* Shown when moving in bi-directional text */ +.CodeMirror div.CodeMirror-secondarycursor { + border-left: 1px solid silver; +} +.cm-fat-cursor .CodeMirror-cursor { + width: auto; + border: 0 !important; + background: #7e7; +} +.cm-fat-cursor div.CodeMirror-cursors { + z-index: 1; +} +.cm-fat-cursor-mark { + background-color: rgba(20, 255, 20, 0.5); + -webkit-animation: blink 1.06s steps(1) infinite; + -moz-animation: blink 1.06s steps(1) infinite; + animation: blink 1.06s steps(1) infinite; +} +.cm-animate-fat-cursor { + width: auto; + border: 0; + -webkit-animation: blink 1.06s steps(1) infinite; + -moz-animation: blink 1.06s steps(1) infinite; + animation: blink 1.06s steps(1) infinite; + background-color: #7e7; +} +@-moz-keyframes blink { + 0% {} + 50% { background-color: transparent; } + 100% {} +} +@-webkit-keyframes blink { + 0% {} + 50% { background-color: transparent; } + 100% {} +} +@keyframes blink { + 0% {} + 50% { background-color: transparent; } + 100% {} +} + +/* Can style cursor different in overwrite (non-insert) mode */ +.CodeMirror-overwrite .CodeMirror-cursor {} + +.cm-tab { display: inline-block; text-decoration: inherit; } + +.CodeMirror-rulers { + position: absolute; + left: 0; right: 0; top: -50px; bottom: 0; + overflow: hidden; +} +.CodeMirror-ruler { + border-left: 1px solid #ccc; + top: 0; bottom: 0; + position: absolute; +} + +/* DEFAULT THEME */ + +.cm-s-default .cm-header {color: blue;} +.cm-s-default .cm-quote {color: #090;} +.cm-negative {color: #d44;} +.cm-positive {color: #292;} +.cm-header, .cm-strong {font-weight: bold;} +.cm-em {font-style: italic;} +.cm-link {text-decoration: underline;} +.cm-strikethrough {text-decoration: line-through;} + +.cm-s-default .cm-keyword {color: #708;} +.cm-s-default .cm-atom {color: #219;} +.cm-s-default .cm-number {color: #164;} +.cm-s-default .cm-def {color: #00f;} +.cm-s-default .cm-variable, +.cm-s-default .cm-punctuation, +.cm-s-default .cm-property, +.cm-s-default .cm-operator {} +.cm-s-default .cm-variable-2 {color: #05a;} +.cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;} +.cm-s-default .cm-comment {color: #a50;} +.cm-s-default .cm-string {color: #a11;} +.cm-s-default .cm-string-2 {color: #f50;} +.cm-s-default .cm-meta {color: #555;} +.cm-s-default .cm-qualifier {color: #555;} +.cm-s-default .cm-builtin {color: #30a;} +.cm-s-default .cm-bracket {color: #997;} +.cm-s-default .cm-tag {color: #170;} +.cm-s-default .cm-attribute {color: #00c;} +.cm-s-default .cm-hr {color: #999;} +.cm-s-default .cm-link {color: #00c;} + +.cm-s-default .cm-error {color: #f00;} +.cm-invalidchar {color: #f00;} + +.CodeMirror-composing { border-bottom: 2px solid; } + +/* Default styles for common addons */ + +div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;} +div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;} +.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); } +.CodeMirror-activeline-background {background: #e8f2ff;} + +/* STOP */ + +/* The rest of this file contains styles related to the mechanics of + the editor. You probably shouldn't touch them. */ + +.CodeMirror { + position: relative; + overflow: hidden; + background: white; +} + +.CodeMirror-scroll { + overflow: scroll !important; /* Things will break if this is overridden */ + /* 50px is the magic margin used to hide the element's real scrollbars */ + /* See overflow: hidden in .CodeMirror */ + margin-bottom: -50px; margin-right: -50px; + padding-bottom: 50px; + height: 100%; + outline: none; /* Prevent dragging from highlighting the element */ + position: relative; +} +.CodeMirror-sizer { + position: relative; + border-right: 50px solid transparent; +} + +/* The fake, visible scrollbars. Used to force redraw during scrolling + before actual scrolling happens, thus preventing shaking and + flickering artifacts. */ +.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { + position: absolute; + z-index: 6; + display: none; + outline: none; +} +.CodeMirror-vscrollbar { + right: 0; top: 0; + overflow-x: hidden; + overflow-y: scroll; +} +.CodeMirror-hscrollbar { + bottom: 0; left: 0; + overflow-y: hidden; + overflow-x: scroll; +} +.CodeMirror-scrollbar-filler { + right: 0; bottom: 0; +} +.CodeMirror-gutter-filler { + left: 0; bottom: 0; +} + +.CodeMirror-gutters { + position: absolute; left: 0; top: 0; + min-height: 100%; + z-index: 3; +} +.CodeMirror-gutter { + white-space: normal; + height: 100%; + display: inline-block; + vertical-align: top; + margin-bottom: -50px; +} +.CodeMirror-gutter-wrapper { + position: absolute; + z-index: 4; + background: none !important; + border: none !important; +} +.CodeMirror-gutter-background { + position: absolute; + top: 0; bottom: 0; + z-index: 4; +} +.CodeMirror-gutter-elt { + position: absolute; + cursor: default; + z-index: 4; +} +.CodeMirror-gutter-wrapper ::selection { background-color: transparent } +.CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent } + +.CodeMirror-lines { + cursor: text; + min-height: 1px; /* prevents collapsing before first draw */ +} +.CodeMirror pre.CodeMirror-line, +.CodeMirror pre.CodeMirror-line-like { + /* Reset some styles that the rest of the page might have set */ + -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; + border-width: 0; + background: transparent; + font-family: inherit; + font-size: inherit; + margin: 0; + white-space: pre; + word-wrap: normal; + line-height: inherit; + color: inherit; + z-index: 2; + position: relative; + overflow: visible; + -webkit-tap-highlight-color: transparent; + -webkit-font-variant-ligatures: contextual; + font-variant-ligatures: contextual; +} +.CodeMirror-wrap pre.CodeMirror-line, +.CodeMirror-wrap pre.CodeMirror-line-like { + word-wrap: break-word; + white-space: pre-wrap; + word-break: normal; +} + +.CodeMirror-linebackground { + position: absolute; + left: 0; right: 0; top: 0; bottom: 0; + z-index: 0; +} + +.CodeMirror-linewidget { + position: relative; + z-index: 2; + padding: 0.1px; /* Force widget margins to stay inside of the container */ +} + +.CodeMirror-widget {} + +.CodeMirror-rtl pre { direction: rtl; } + +.CodeMirror-code { + outline: none; +} + +/* Force content-box sizing for the elements where we expect it */ +.CodeMirror-scroll, +.CodeMirror-sizer, +.CodeMirror-gutter, +.CodeMirror-gutters, +.CodeMirror-linenumber { + -moz-box-sizing: content-box; + box-sizing: content-box; +} + +.CodeMirror-measure { + position: absolute; + width: 100%; + height: 0; + overflow: hidden; + visibility: hidden; +} + +.CodeMirror-cursor { + position: absolute; + pointer-events: none; +} +.CodeMirror-measure pre { position: static; } + +div.CodeMirror-cursors { + visibility: hidden; + position: relative; + z-index: 3; +} +div.CodeMirror-dragcursors { + visibility: visible; +} + +.CodeMirror-focused div.CodeMirror-cursors { + visibility: visible; +} + +.CodeMirror-selected { background: #d9d9d9; } +.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } +.CodeMirror-crosshair { cursor: crosshair; } +.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; } +.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; } + +.cm-searching { + background-color: #ffa; + background-color: rgba(255, 255, 0, .4); +} + +/* Used to force a border model for a node */ +.cm-force-border { padding-right: .1px; } + +@media print { + /* Hide the cursor when printing */ + .CodeMirror div.CodeMirror-cursors { + visibility: hidden; + } +} + +/* See issue #2901 */ +.cm-tab-wrap-hack:after { content: ''; } + +/* Help users use markselection to safely style text background */ +span.CodeMirror-selectedtext { background: none; } diff --git a/play/css/election.css b/play/css/election.css index afb27238..3af9213c 100644 --- a/play/css/election.css +++ b/play/css/election.css @@ -6,69 +6,137 @@ Election/Sandbox design: ************/ -#left, #center, #right{ - float:left; - height: 320px; +.div-election{ + background-color: #ffffffd4; + border: 1px solid #ccc; + padding: 10px; + /* margin: -10px; */ } -#left{ - width:220px; - padding-right: 20px; - height: 371px; + +.div-election #left, +.div-election #center, +.div-election #right{ + /* float:left; */ + /* min-height: 320px; */ + text-align: left; + margin: 10px 10px 5px 10px; + display:inline-block; + vertical-align: top; +} +.div-election #left{ + max-width:220px; } -#center{ - width:320px; - height: 370px; +.div-election #center{ + text-align: center; + width:304px; } -#right{ - width: 220px; - padding-left: 20px; - overflow-y: auto; + +.div-election #right{ + min-width: 220px; +} + +/* Extra Small Devices, Phones */ +@media (max-width : 656px) { + .div-election{ + text-align:center; + display:block; + } + + .div-election #left, + .div-election #right, + .div-election #center{ + display:block; + margin: 10px auto; + /* float:none; + margin-left: 0px; + padding-left: 0px; + margin-right: 0px; + padding-right: 0px; */ + /* text-align: left; */ + } + .div-election #center{ + text-align: center; + } + .div-election #center{ + text-align: center; + + } } -#caption{ + +.div-election #caption, +.div-election #chart{ + margin: 0px auto; text-align: left; - font-size: 23px; + line-height: 1.0em; + font-size: 23px; + max-width: 220px; + padding: 5px; + background-color: white; + border: 2px solid #ccc; + width: 220px; +} + +.div-election #caption{ + padding: 14px; + width: 200px; +} + +.div-election #caption .small, +.div-election #chart .small{ + font-size: 14px; line-height: 1.0em; } -#caption .small{ - font-size: 15px; +.div-election #caption .xsmall, +.div-election #chart .xsmall{ + font-size: 12px; line-height: 1.0em; } -.buttonshape img{ + +.div-election #chart { + margin: 10px auto; + margin-top: 0px; +} + +.div-election .buttonshape img{ height: 1em; position: relative; top:0px; } -#right img{ +.div-election #right img{ height: 1em; position: relative; top:2px; } -.button-group{ +.div-election .button-group{ overflow: hidden; - margin-bottom: 5px; + margin-bottom: 0px; + margin-right: -4px; } -.button-group-label{ +.div-election .button-group-label{ font-weight: bold; font-size: 15px; margin-bottom: 2px; line-height: 1.1em; } -.button{ +.div-election .button{ float: left; cursor: pointer; - background: #bbb; - color: #555; + background: #fff; + border: 1px solid #888; + color: #888; height: 20px; line-height: 20px; - font-size: 20px; - font-weight: 300; + font-size: 15px; + font-weight: bold; text-align: center; margin-bottom: 3px; padding: 1px 0; + overflow:hidden; + position: relative; top:0; @@ -77,49 +145,250 @@ Election/Sandbox design: -o-transition: all 0.1s; transition: all 0.1s; } -.button:hover{ - background: #ddd; + +.div-election .button-group .smaller{ + font-size: 80%; } -.button:active{ - background: #aaa; + +.div-election .button svg{ + font-size: 20px; +} + + +.div-election .button:hover{ + background: #ddd; + border-color: #999; } -.button[on=yes]{ +.div-election .button:active{ background: #333; - color: #fff; + border-color: #000; } -.button[on=yes]:hover{ - background: #555; +.div-election .button[on=yes]{ + background: #ccc; + color: #000; +} +.div-election .button[on=yes]:hover{ + background: #ddd; } .button[on=yes]:active{ background: #111; } -::-webkit-scrollbar { +.div-election ::-webkit-scrollbar { -webkit-appearance: none; width: 7px; } -::-webkit-scrollbar-thumb { +.div-election ::-webkit-scrollbar-thumb { border-radius: 4px; background-color: rgba(0, 0, 0, .5); + box-shadow: 0 0 1px rgba(255, 255, 255, .5); -webkit-box-shadow: 0 0 1px rgba(255, 255, 255, .5); } -#ohno{ +.div-election #ohno{ font-size: 2em; position: relative; top:10px; } -input[type=range]{ - border: 0px solid white; - width: 22%; +.div-election input[type=range]{ + border: 1px solid #ccc; display:inline; - margin-left:10%; - margin-top: 20px; - margin-bottom: 25px; - transform: rotate(270deg); + /* transform: rotate(270deg); */ +} + +.div-election #containsliders{ + line-height: 0px; + font-size: 0px; } -#containsliders{ + +.div-election input[type="range"] { + margin: 0px; + -webkit-appearance: none; + position: relative; + overflow: hidden; + height: 20px; + width: 218px; + cursor: pointer; + border-radius: 0; /* iOS */ +} + +#savelink{ + margin: 5px 0px 0px 0px; + width:100%; +} + +.tinyURL{ + margin: 10px 9px 0 0; + font-size: .5em; + vertical-align: middle; + color: #0645AD +} + +.tinyURL img { + height: .7em; +} + + + +/* Beginning of Slider Stuff - note that the height is changed later in sandbox.css */ + +.div-election ::-webkit-slider-runnable-track { + background: #fff; +} + +/* + * 1. Set to 0 height and width, and remove border for a slider without a thumb + */ + .div-election ::-webkit-slider-thumb { + -webkit-appearance: none; + width: 20px; /* 1 */ + height: 20px; /* 1 */ + background: #fff; + box-shadow: -100vw 0 0 100vw #001; + border: 2px solid #555; /* 1 */ +} + +.div-election ::-moz-range-track { + height: 20px; + background: #fff; +} + +.div-election ::-moz-range-thumb { + background: #fff; + height: 20px; + width: 20px; + border: 3px solid #555; + border-radius: 0 !important; + box-shadow: -100vw 0 0 100vw #001; + box-sizing: border-box; +} + +.div-election ::-ms-fill-lower { + background: #001; +} + +.div-election ::-ms-thumb { + background: #fff; + border: 2px solid #555; + height: 20px; + width: 20px; + box-sizing: border-box; +} + +.div-election ::-ms-ticks-after { + display: none; +} + +.div-election ::-ms-ticks-before { + display: none; +} + +.div-election ::-ms-track { + background: #fff; + color: transparent; + height: 20px; + border: none; +} + +.div-election ::-ms-tooltip { + display: none; +} + +/* end of slider stuff */ + +.round { + border: 3px solid transparent; +} +.round:hover { + background: #eee; + border: 3px solid #aaa; + +} + +.pair { + border: 1px solid transparent; + +} +.pair:hover { + background: #ddd; + border: 1px solid #aaa; + +} + +svg { + width: 1em; + height: 1em; +} + +#chart svg { + width: unset; + height: unset; +} + +#chart { + border: 2px solid #ccc; +} + +#chart { + background-color: white; +} + +.node rect { + cursor: move; + fill-opacity: .9; + shape-rendering: crispEdges; +} + +.node text { + pointer-events: none; + text-shadow: 0 1px 0 #fff; +} + +.link { + fill: none; + /*stroke: #000;*/ + stroke-opacity: .5; +} + +.link:hover { + stroke-opacity: .8; +} + +.displayNoneClass { + display:none !important; +} + +.displayAboveClass { + display:block !important; +} + +.strategyTable { + background-color: white; +} +.strategyTable th { + text-align: center; +} +.strategyTable td { + text-align: right; + background-color: white; + +} +/* .nameLabelName { + opacity: 1; +} */ + +#pollChart{ + width: unset; + height: unset; +} + +/* fix google charts tooltip flicker */ +svg[aria-label="A chart."] > g > g:last-child { pointer-events: none } + +.filterMap { + margin: 7px 0px 0px 10px; + border: #ccc 2px solid; } \ No newline at end of file diff --git a/play/css/election_original.css b/play/css/election_original.css new file mode 100644 index 00000000..3aa01254 --- /dev/null +++ b/play/css/election_original.css @@ -0,0 +1,115 @@ +/************ + +Election/Sandbox design: +- buttons to the left +- caption to the right + +************/ + +.div-election{ + text-align: left; +} + +.div-election #left, .div-election #center, .div-election #right{ + float:left; + height: 320px; +} +.div-election #left{ + width:220px; + padding-right: 20px; +} +.div-election #center{ + width:320px; +} +.div-election #right{ + width:220px; + padding-left: 20px; + overflow-y: auto; +} +.div-election #caption{ + text-align: left; + font-size: 23px; + line-height: 1.0em; +} +.div-election #caption .small{ + font-size: 15px; + line-height: 1.0em; +} + +.div-election #right img{ + height: 1em; + position: relative; + top:2px; +} + +.div-election .button-group{ + overflow: hidden; + margin-bottom: 10px; + margin-right: -4px; +} +.div-election .button-group-label{ + font-weight: bold; + font-size: 19px; + margin-bottom: 5px; + line-height: 1.1em; +} +.div-election .button{ + float: left; + cursor: pointer; + background: #bbb; + color: #555; + font-size: 18px; + font-weight: 300; + text-align: center; + margin-bottom: 5px; + padding: 2px 0; + + position: relative; + top:0; + + -webkit-transition: all 0.1s; + -moz-transition: all 0.1s; + -o-transition: all 0.1s; + transition: all 0.1s; +} +.div-election .button:hover{ + background: #ddd; +} +.div-election .button:active{ + background: #aaa; +} +.div-election .button[on=yes]{ + background: #333; + color: #fff; +} +.div-election .button[on=yes]:hover{ + background: #555; +} +.div-election .button[on=yes]:active{ + background: #111; +} + +.div-election ::-webkit-scrollbar { + -webkit-appearance: none; + width: 7px; +} +.div-election ::-webkit-scrollbar-thumb { + border-radius: 4px; + background-color: rgba(0, 0, 0, .5); + -webkit-box-shadow: 0 0 1px rgba(255, 255, 255, .5); +} + +.div-election #ohno{ + font-size: 2em; + position: relative; + top:10px; +} + +#reset, #save{ + position:absolute; +} + +svg { + width: 1em; + height: 1em; +} \ No newline at end of file diff --git a/play/css/model.css b/play/css/model.css index 55d6f2a3..9fbf92d6 100644 --- a/play/css/model.css +++ b/play/css/model.css @@ -1,9 +1,17 @@ -body{ +:root { + --button-on: #ccc; + /* not using yet */ +} + +.div-model{ + display:inline-block; + position: relative; margin: 0px; font-family: Helvetica, Arial, sans-serif; font-size: 25px; line-height: 1.2em; color:#333; + text-align: center; -moz-user-select: none; -webkit-user-select: none; @@ -11,80 +19,129 @@ body{ user-select: none; } -/********************* -Preloading image assets... -**********************/ - -#offscreen{ - position:absolute; - top:-1000px; - left:-1000px; -} - /********************* THE MAIN MODEL ITSELF **********************/ -.model{ +.div-model .model{ + text-align: center; overflow: hidden; - float:left; } -.model .interactive{ +.model { + display: inline-block; +} + +.div-model-theme-Default .model .interactive{ + background-image: none; +} +.div-model-theme-Nicky .model .interactive{ background-image: url(../img/axis.png); - background-size: 100% 100%; +} +.div-model-theme-Bees .model .interactive{ + background-image: url(../img/honeycomb.png); +} +.div-model .model .interactive{ + background-size: 100% 100%; border: 2px solid #ccc; + background-color: white; } -.model .interactive[cursor=grab]{ + +.div-model .model .interactive[cursor=grab]{ cursor: -webkit-grab; cursor: -moz-grab; cursor: grab; } -.model .interactive[cursor=grabbing]{ +.div-model .model .interactive[cursor=grabbing]{ cursor: -webkit-grabbing; cursor: -moz-grabbing; cursor: grabbing; } -#caption{ +.div-model #caption{ cursor: default; - margin-top:10px; + /* margin-top:10px; */ text-align: center; + word-wrap: break-word; } -#caption .small{ + +.div-model #caption .small{ + vertical-align: top; font-size: 0.75em; } +.div-model .letter{ + -webkit-text-stroke: 0.5px black; +} +.div-model .letterBig{ + -webkit-text-stroke: 1px black; +} + + /********************* DAT RESET BUTTON **********************/ -#reset, #save{ +#reset, +.demonstrate, +.codeSave, +#save{ + display: inline-block; cursor: pointer; background: #aaa; color: #fff; font-size: 25px; font-weight: 300; text-align: center; - width: 100px; height: 30px; letter-spacing: 2px; padding: 2px 0; border-radius: 5px; border-bottom: 2px solid #888; - position: absolute; - margin-top:0px; + /* margin-top:10px; */ + /* margin-right: 9px; */ + margin: 0px 5px; -webkit-transition: all 0.1s; -moz-transition: all 0.1s; -o-transition: all 0.1s; transition: all 0.1s; } -#reset:hover, #save:hover{ + +.codeSave{ + margin-bottom: 10px; +} + +#reset, +#save{ + width: 100px; +} +.codeSave{ + width: 200px; +} +.demonstrate{ + width: 240px; + margin-bottom: 20px; +} + +#reset:hover, +.demonstrate:hover, +.codeSave:hover, + #save:hover{ margin-top:-2px; background: #bbb; border-bottom-color: #999; } -#reset:active, #save:active{ +#reset:active, +.demonstrate:active, +.codeSave:active, +#save:active{ margin-top:2px; background: #999; border-bottom-color: #777; +} + +span.percent { + font-stretch: ultra-condensed; + font-weight:lighter; + font-size: 80%; + opacity: .6; } \ No newline at end of file diff --git a/play/css/rbvote.css b/play/css/rbvote.css new file mode 100644 index 00000000..5adcf7e5 --- /dev/null +++ b/play/css/rbvote.css @@ -0,0 +1,34 @@ +.div-election h1 {font-size: 14px;} +td.against {background-color: #ffffff; font-family: monospace} +td.cfail1 {background-color: #ff5050} +td.cfail2 {background-color: #ff6060} +td.cfail3 {background-color: #ff7070} +td.cfail4 {background-color: #ff8080} +td.cfail5 {background-color: #ff9090} +td.cfail6 {background-color: #ffa0a0} +td.cfail7 {background-color: #ffb0b0} +td.cfail8 {background-color: #ffc0c0} +td.cfail9 {background-color: #ffd0d0} +td.cfaila {background-color: #ffe0e0} +td.cfailb {background-color: #fff0f0} +td.cpass1 {background-color: #50ff50} +td.cpass2 {background-color: #60ff60} +td.cpass3 {background-color: #70ff70} +td.cpass4 {background-color: #80ff80} +td.cpass5 {background-color: #90ff90} +td.cpass6 {background-color: #a0ffa0} +td.cpass7 {background-color: #b0ffb0} +td.cpass8 {background-color: #c0ffc0} +td.cpass9 {background-color: #d0ffd0} +td.cpassa {background-color: #e0ffe0} +td.cpassb {background-color: #f0fff0} +td.for {background-color: #ffffff; font-family: monospace} +td.loss {background-color: #ffc0c0} +td.win {background-color: #c0ffc0; font-weight: bold} +th.against {background-color: #ffc0c0; font-family: monospace} +th.for {background-color: #c0ffc0; font-family: monospace} +.cand {font-family: monospace} +.email {font-family: monospace} +.endtag {font-size: 60%; font-style: italic; margin-left: 2em} +.url {font-family: monospace} +/* .div-election #caption {font-size: 11px;} */ diff --git a/play/css/sandbox.css b/play/css/sandbox.css index 4ef477c9..db6c239a 100644 --- a/play/css/sandbox.css +++ b/play/css/sandbox.css @@ -1,47 +1,106 @@ -#reset, #save{ +.div-sandbox #reset, .demonstrate, .div-sandbox #save, .div-sandbox .codeSave , .roundChartButton , .weightChartsButton { background: #fff; color: #888; border: 1px solid #888; } -#reset:hover, #save:hover{ +.div-sandbox #reset:hover, .demonstrate:hover, .div-sandbox #save:hover, .div-sandbox .codeSave:hover, .roundChartButton:hover, .weightChartsButton:hover { margin-top:0px; background: #fff; color: #bbb; border-color: #bbb; border-bottom-color: #888; } -#reset:active, #save:active{ +.div-sandbox #reset:active, .demonstrate:active, .div-sandbox #save:active, .div-sandbox .codeSave:active, .roundChartButton:active, .weightChartsButton:active { margin-top:0px; - background: #fff; + background: #eee; border-bottom-color: #888; } -#description_container{ +.div-sandbox #description_container, +.div-sandbox #codeEditorText_container{ clear: both; overflow: hidden; - width: 800px; - height: 120px; - margin-bottom: 15px; + margin: 10px 0px; + width:100%; +} +.div-sandbox #description_container{ + height: 120px; } -textarea, input{ - display: block; +.div-sandbox #codeEditorText_container{ + height: 620px; +} +.div-sandbox textarea, .div-sandbox input, .div-sandbox .CodeMirror{ border: 1px solid #bbb; resize: none; +} +.div-sandbox textarea, .div-sandbox input{ font-family: Arial, sans-serif; } -#description_container textarea{ - width: 778px; - height: 97px; +.div-sandbox #description_container textarea, +.div-sandbox #codeEditorText_container textarea{ padding: 10px; font-size: 16px; + width: 100%; + margin: 0 0 0 -10px; +} +.div-sandbox #description_container textarea{ + height: 97px; +} +.div-sandbox #codeEditorText_container textarea{ + height: 597px; +} +.div-sandbox div#double_description_container{ + margin: 0px 20px; +} +.div-sandbox div#double_codeEditorText_container { + margin: 10px 10px; +} +.CodeMirror { + text-align: left; + height: 597px; + font-size: 15px; + line-height: 15px; +} +.CodeMirror { + /* This 2-line part makes the codemirror scale its width with its container. Comment it out to have codemirror scale width with it's longest line of code inside it. */ + position: absolute; + width: calc(100% - 45px); +} +.div-sandbox div.topMenuSpacer { + height: 0.5em; } -#savelink{ - position: absolute; - top: 471px; - left: 460px; - width: 82px; - height: 15px; - padding: 8px; +.div-sandbox #savelink{ + margin-top: 10px; + position: relative; + top: -3px; + width: 100%; + height: 19px; + padding: 0px; font-size: 12px; -} \ No newline at end of file +} + + + + + +/* Slider stuff - This modifies values in election.css */ +.div-sandbox input[type="range"] { + height: 20px; +} +.div-sandbox ::-webkit-slider-thumb { + height: 20px; /* 1 */ +} +.div-sandbox ::-moz-range-track { + height: 20px; +} +.div-sandbox ::-moz-range-thumb { + height: 20px; +} +.div-sandbox ::-ms-thumb { + height: 20px; +} +.div-sandbox ::-ms-track { + height: 20px; +} +/* end of slider stuff */ \ No newline at end of file diff --git a/play/css/sandbox_original.css b/play/css/sandbox_original.css new file mode 100644 index 00000000..e49800f3 --- /dev/null +++ b/play/css/sandbox_original.css @@ -0,0 +1,52 @@ +.div-sandbox #reset, .div-sandbox #save{ + background: #fff; + color: #888; + border: 1px solid #888; +} +.div-sandbox #reset:hover, .div-sandbox #save:hover{ + margin-top:0px; + background: #fff; + color: #bbb; + border-color: #bbb; + border-bottom-color: #888; +} +.div-sandbox #reset:active, .div-sandbox #save:active{ + margin-top:0px; + background: #fff; + border-bottom-color: #888; +} + +.div-sandbox #description_container{ + clear: both; + overflow: hidden; + height: 120px; + margin-bottom: 15px; + width:100%; +} +.div-sandbox textarea, .div-sandbox input{ + display: block; + border: 1px solid #bbb; + resize: none; + font-family: Arial, sans-serif; +} +.div-sandbox #description_container textarea{ + height: 97px; + padding: 10px; + font-size: 16px; + width: 100%; + margin: 0 0 0 -10px; +} +.div-sandbox div#double_description_container { + margin: 0 12px; +} + + +.div-sandbox #savelink{ + position: absolute; + top: 471px; + left: 240px; + width: 543px; + height: 15px; + padding: 8px; + font-size: 12px; +} \ No newline at end of file diff --git a/play/data/energy.json b/play/data/energy.json new file mode 100644 index 00000000..0dcc47c6 --- /dev/null +++ b/play/data/energy.json @@ -0,0 +1,120 @@ +{"nodes":[ +{"name":"Agricultural 'waste'"}, +{"name":"Bio-conversion"}, +{"name":"Liquid"}, +{"name":"Losses"}, +{"name":"Solid"}, +{"name":"Gas"}, +{"name":"Biofuel imports"}, +{"name":"Biomass imports"}, +{"name":"Coal imports"}, +{"name":"Coal"}, +{"name":"Coal reserves"}, +{"name":"District heating"}, +{"name":"Industry"}, +{"name":"Heating and cooling - commercial"}, +{"name":"Heating and cooling - homes"}, +{"name":"Electricity grid"}, +{"name":"Over generation / exports"}, +{"name":"H2 conversion"}, +{"name":"Road transport"}, +{"name":"Agriculture"}, +{"name":"Rail transport"}, +{"name":"Lighting & appliances - commercial"}, +{"name":"Lighting & appliances - homes"}, +{"name":"Gas imports"}, +{"name":"Ngas"}, +{"name":"Gas reserves"}, +{"name":"Thermal generation"}, +{"name":"Geothermal"}, +{"name":"H2"}, +{"name":"Hydro"}, +{"name":"International shipping"}, +{"name":"Domestic aviation"}, +{"name":"International aviation"}, +{"name":"National navigation"}, +{"name":"Marine algae"}, +{"name":"Nuclear"}, +{"name":"Oil imports"}, +{"name":"Oil"}, +{"name":"Oil reserves"}, +{"name":"Other waste"}, +{"name":"Pumped heat"}, +{"name":"Solar PV"}, +{"name":"Solar Thermal"}, +{"name":"Solar"}, +{"name":"Tidal"}, +{"name":"UK land based bioenergy"}, +{"name":"Wave"}, +{"name":"Wind"} +], +"links":[ +{"source":0,"target":1,"value":124.729}, +{"source":1,"target":2,"value":0.597}, +{"source":1,"target":3,"value":26.862}, +{"source":1,"target":4,"value":280.322}, +{"source":1,"target":5,"value":81.144}, +{"source":6,"target":2,"value":35}, +{"source":7,"target":4,"value":35}, +{"source":8,"target":9,"value":11.606}, +{"source":10,"target":9,"value":63.965}, +{"source":9,"target":4,"value":75.571}, +{"source":11,"target":12,"value":10.639}, +{"source":11,"target":13,"value":22.505}, +{"source":11,"target":14,"value":46.184}, +{"source":15,"target":16,"value":104.453}, +{"source":15,"target":14,"value":113.726}, +{"source":15,"target":17,"value":27.14}, +{"source":15,"target":12,"value":342.165}, +{"source":15,"target":18,"value":37.797}, +{"source":15,"target":19,"value":4.412}, +{"source":15,"target":13,"value":40.858}, +{"source":15,"target":3,"value":56.691}, +{"source":15,"target":20,"value":7.863}, +{"source":15,"target":21,"value":90.008}, +{"source":15,"target":22,"value":93.494}, +{"source":23,"target":24,"value":40.719}, +{"source":25,"target":24,"value":82.233}, +{"source":5,"target":13,"value":0.129}, +{"source":5,"target":3,"value":1.401}, +{"source":5,"target":26,"value":151.891}, +{"source":5,"target":19,"value":2.096}, +{"source":5,"target":12,"value":48.58}, +{"source":27,"target":15,"value":7.013}, +{"source":17,"target":28,"value":20.897}, +{"source":17,"target":3,"value":6.242}, +{"source":28,"target":18,"value":20.897}, +{"source":29,"target":15,"value":6.995}, +{"source":2,"target":12,"value":121.066}, +{"source":2,"target":30,"value":128.69}, +{"source":2,"target":18,"value":135.835}, +{"source":2,"target":31,"value":14.458}, +{"source":2,"target":32,"value":206.267}, +{"source":2,"target":19,"value":3.64}, +{"source":2,"target":33,"value":33.218}, +{"source":2,"target":20,"value":4.413}, +{"source":34,"target":1,"value":4.375}, +{"source":24,"target":5,"value":122.952}, +{"source":35,"target":26,"value":839.978}, +{"source":36,"target":37,"value":504.287}, +{"source":38,"target":37,"value":107.703}, +{"source":37,"target":2,"value":611.99}, +{"source":39,"target":4,"value":56.587}, +{"source":39,"target":1,"value":77.81}, +{"source":40,"target":14,"value":193.026}, +{"source":40,"target":13,"value":70.672}, +{"source":41,"target":15,"value":59.901}, +{"source":42,"target":14,"value":19.263}, +{"source":43,"target":42,"value":19.263}, +{"source":43,"target":41,"value":59.901}, +{"source":4,"target":19,"value":0.882}, +{"source":4,"target":26,"value":400.12}, +{"source":4,"target":12,"value":46.477}, +{"source":26,"target":15,"value":525.531}, +{"source":26,"target":3,"value":787.129}, +{"source":26,"target":11,"value":79.329}, +{"source":44,"target":15,"value":9.452}, +{"source":45,"target":1,"value":182.01}, +{"source":46,"target":15,"value":19.013}, +{"source":47,"target":15,"value":289.366} +]} diff --git a/play/election1.html b/play/election1.html deleted file mode 100644 index 476fe7ec..00000000 --- a/play/election1.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - -
-
- - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/play/election10.html b/play/election10.html deleted file mode 100644 index 476fe7ec..00000000 --- a/play/election10.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - -
-
- - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/play/election11.html b/play/election11.html deleted file mode 100644 index 476fe7ec..00000000 --- a/play/election11.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - -
-
- - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/play/election12.html b/play/election12.html deleted file mode 100644 index 476fe7ec..00000000 --- a/play/election12.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - -
-
- - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/play/election13.html b/play/election13.html deleted file mode 100644 index 476fe7ec..00000000 --- a/play/election13.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - -
-
- - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/play/election14.html b/play/election14.html deleted file mode 100644 index 476fe7ec..00000000 --- a/play/election14.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - -
-
- - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/play/election15.html b/play/election15.html deleted file mode 100644 index 476fe7ec..00000000 --- a/play/election15.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - -
-
- - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/play/election2.html b/play/election2.html deleted file mode 100644 index 476fe7ec..00000000 --- a/play/election2.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - -
-
- - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/play/election3.html b/play/election3.html deleted file mode 100644 index 476fe7ec..00000000 --- a/play/election3.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - -
-
- - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/play/election4.html b/play/election4.html deleted file mode 100644 index 476fe7ec..00000000 --- a/play/election4.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - -
-
- - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/play/election5.html b/play/election5.html deleted file mode 100644 index 476fe7ec..00000000 --- a/play/election5.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - -
-
- - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/play/election6.html b/play/election6.html deleted file mode 100644 index 476fe7ec..00000000 --- a/play/election6.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - -
-
- - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/play/election7.html b/play/election7.html deleted file mode 100644 index 476fe7ec..00000000 --- a/play/election7.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - -
-
- - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/play/election8.html b/play/election8.html deleted file mode 100644 index 476fe7ec..00000000 --- a/play/election8.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - -
-
- - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/play/election9.html b/play/election9.html deleted file mode 100644 index 476fe7ec..00000000 --- a/play/election9.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - -
-
- - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/play/examples/ballot1.html b/play/examples/ballot1.html new file mode 100644 index 00000000..af20b084 --- /dev/null +++ b/play/examples/ballot1.html @@ -0,0 +1,28 @@ +--- +--- + + + + + + + + + + + {% include js-1.html %} + + + + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/play/examples/ballot1_original.html b/play/examples/ballot1_original.html new file mode 100644 index 00000000..c59e1e88 --- /dev/null +++ b/play/examples/ballot1_original.html @@ -0,0 +1,28 @@ +--- +--- + + + + + + + + + + + {% include js-original.html %} + + + + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/play/examples/election1.html b/play/examples/election1.html new file mode 100644 index 00000000..8c3b751b --- /dev/null +++ b/play/examples/election1.html @@ -0,0 +1,22 @@ +--- +--- + + + + + + + + {% include css-1.html %} + + {% include js-1.html %} + + +
+
+ + + + \ No newline at end of file diff --git a/play/examples/model0.html b/play/examples/model0.html new file mode 100644 index 00000000..d18181b1 --- /dev/null +++ b/play/examples/model0.html @@ -0,0 +1,20 @@ +--- +--- + + + + + + + + + + {% include js-model.html %} + + +
+
+ + + + \ No newline at end of file diff --git a/play/examples/model1.html b/play/examples/model1.html new file mode 100644 index 00000000..5575accb --- /dev/null +++ b/play/examples/model1.html @@ -0,0 +1,18 @@ +--- +--- + + + + + + + + + {% include js-model.html %} + +
+
+ + + + \ No newline at end of file diff --git a/play/examples/sandbox.html b/play/examples/sandbox.html new file mode 100644 index 00000000..9997e29a --- /dev/null +++ b/play/examples/sandbox.html @@ -0,0 +1,18 @@ +--- +--- + + + + + + + + {% include css-1.html %} + + {% include js-1.html %} + + + {% include sandbox-min.html %} + + + \ No newline at end of file diff --git a/play/img/ballot5_approval.png b/play/img/ballot5_approval.png index 86452002..6a70f2cf 100644 Binary files a/play/img/ballot5_approval.png and b/play/img/ballot5_approval.png differ diff --git a/play/img/ballot5_range.png b/play/img/ballot5_range.png index 178aa16b..a9d203d1 100644 Binary files a/play/img/ballot5_range.png and b/play/img/ballot5_range.png differ diff --git a/play/img/ballot5_range3.png b/play/img/ballot5_range3.png index 50618665..55ec9a14 100644 Binary files a/play/img/ballot5_range3.png and b/play/img/ballot5_range3.png differ diff --git a/play/img/ballot_range_original.png b/play/img/ballot_range_original.png new file mode 100644 index 00000000..4dae4f60 Binary files /dev/null and b/play/img/ballot_range_original.png differ diff --git a/play/img/ballot_rate_original.png b/play/img/ballot_rate_original.png new file mode 100644 index 00000000..27296bed Binary files /dev/null and b/play/img/ballot_rate_original.png differ diff --git a/play/img/blue_bee.png b/play/img/blue_bee.png new file mode 100644 index 00000000..ddfd3df7 Binary files /dev/null and b/play/img/blue_bee.png differ diff --git a/play/img/bob.svg b/play/img/bob.svg new file mode 100644 index 00000000..e6aabacc --- /dev/null +++ b/play/img/bob.svg @@ -0,0 +1 @@ +Asset 4 \ No newline at end of file diff --git a/play/img/center.png b/play/img/center.png new file mode 100644 index 00000000..883dbc2d Binary files /dev/null and b/play/img/center.png differ diff --git a/play/img/external_link.svg b/play/img/external_link.svg new file mode 100644 index 00000000..f31b1f53 --- /dev/null +++ b/play/img/external_link.svg @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/play/img/gear.png b/play/img/gear.png new file mode 100644 index 00000000..e503f67b Binary files /dev/null and b/play/img/gear.png differ diff --git a/play/img/gear.svg b/play/img/gear.svg new file mode 100644 index 00000000..23b749ad --- /dev/null +++ b/play/img/gear.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/play/img/green_bee.png b/play/img/green_bee.png new file mode 100644 index 00000000..84ec18f5 Binary files /dev/null and b/play/img/green_bee.png differ diff --git a/play/img/hexagon.svg b/play/img/hexagon.svg new file mode 100644 index 00000000..5ca4df23 --- /dev/null +++ b/play/img/hexagon.svg @@ -0,0 +1 @@ +Asset 6 \ No newline at end of file diff --git a/play/img/honeycomb.png b/play/img/honeycomb.png new file mode 100644 index 00000000..094dc62d Binary files /dev/null and b/play/img/honeycomb.png differ diff --git a/play/img/orange_bee.png b/play/img/orange_bee.png new file mode 100644 index 00000000..1f7bc4a7 Binary files /dev/null and b/play/img/orange_bee.png differ diff --git a/play/img/pentagon.svg b/play/img/pentagon.svg new file mode 100644 index 00000000..c69e42d3 --- /dev/null +++ b/play/img/pentagon.svg @@ -0,0 +1 @@ +Asset 5 \ No newline at end of file diff --git a/play/img/plus.png b/play/img/plus.png new file mode 100644 index 00000000..e2f7e895 Binary files /dev/null and b/play/img/plus.png differ diff --git a/play/img/plusCandidate.png b/play/img/plusCandidate.png new file mode 100644 index 00000000..79c433b8 Binary files /dev/null and b/play/img/plusCandidate.png differ diff --git a/play/img/plusOneVoter.png b/play/img/plusOneVoter.png new file mode 100644 index 00000000..1e85521c Binary files /dev/null and b/play/img/plusOneVoter.png differ diff --git a/play/img/plusVoterGroup.png b/play/img/plusVoterGroup.png new file mode 100644 index 00000000..1ddff73c Binary files /dev/null and b/play/img/plusVoterGroup.png differ diff --git a/play/img/plus_bell.png b/play/img/plus_bell.png new file mode 100644 index 00000000..50ecd42f Binary files /dev/null and b/play/img/plus_bell.png differ diff --git a/play/img/plus_rectangle.png b/play/img/plus_rectangle.png new file mode 100644 index 00000000..b69c2eb0 Binary files /dev/null and b/play/img/plus_rectangle.png differ diff --git a/play/img/plus_sunflower.png b/play/img/plus_sunflower.png new file mode 100644 index 00000000..bcba5771 Binary files /dev/null and b/play/img/plus_sunflower.png differ diff --git a/play/img/red_bee.png b/play/img/red_bee.png new file mode 100644 index 00000000..28bc898d Binary files /dev/null and b/play/img/red_bee.png differ diff --git a/play/img/square.svg b/play/img/square.svg new file mode 100644 index 00000000..7811203e --- /dev/null +++ b/play/img/square.svg @@ -0,0 +1 @@ +Asset 7 \ No newline at end of file diff --git a/play/img/star.svg b/play/img/star.svg new file mode 100644 index 00000000..42a7da0f --- /dev/null +++ b/play/img/star.svg @@ -0,0 +1 @@ +Asset 9 \ No newline at end of file diff --git a/play/img/trash.png b/play/img/trash.png new file mode 100644 index 00000000..6868587e Binary files /dev/null and b/play/img/trash.png differ diff --git a/play/img/triangle.svg b/play/img/triangle.svg new file mode 100644 index 00000000..26b7068a --- /dev/null +++ b/play/img/triangle.svg @@ -0,0 +1 @@ +Asset 10 \ No newline at end of file diff --git a/play/img/viewMan.png b/play/img/viewMan.png new file mode 100644 index 00000000..27226017 Binary files /dev/null and b/play/img/viewMan.png differ diff --git a/play/img/viewMan2.png b/play/img/viewMan2.png new file mode 100644 index 00000000..1cfd0b1a Binary files /dev/null and b/play/img/viewMan2.png differ diff --git a/play/img/yellow_bee.png b/play/img/yellow_bee.png new file mode 100644 index 00000000..669eca1a Binary files /dev/null and b/play/img/yellow_bee.png differ diff --git a/play/js/Ballot.js b/play/js/Ballot.js index 9bdf7643..e3ba14a8 100644 --- a/play/js/Ballot.js +++ b/play/js/Ballot.js @@ -6,24 +6,19 @@ What does the base Ballot class need to do? ******************/ - -// are we in a sandbox or election? -var url = window.location.pathname; -var filename = url.substring(url.lastIndexOf('/')+1); -var firstletter = filename[0] -var inSandbox = (firstletter == "e" || firstletter == "s") -var sc = 1 // scale factor also is present in setting width and height in ballotInSandbox.css -if(inSandbox) {sc = 210/375} - -function ScoreBallot(config){ +function ScoreBallot(model){ var self = this; - config = config || {}; - config.bg = "img/ballot_range.png"; - if (inSandbox) { - config.bg = "img/ballot5_range.png"; + model = model || {}; + if (model.startAt1) { + model.bg = "play/img/ballot_range_original.png"; + } else { + model.bg = "play/img/ballot_range.png"; } - Ballot.call(self, config); + if (model.inSandbox) { + model.bg = "play/img/ballot5_range.png"; + } + Ballot.call(self, model); // BOXES! self.boxes = { @@ -31,7 +26,7 @@ function ScoreBallot(config){ triangle: self.createRate(133, 143, 3), hexagon: self.createRate(133, 184, 1) } - if (inSandbox) { + if (model.inSandbox) { self.boxes["pentagon"] = self.createRate(133, 226, 1) self.boxes["bob"] = self.createRate(133, 268, 1) }; @@ -39,27 +34,33 @@ function ScoreBallot(config){ // On update... self.update = function(ballot){ // Clear all - var vote = ballot.vote; for(var box in self.boxes){ self.boxes[box].gotoFrame(0); } - for(var cID in ballot){ - var score = ballot[cID]; - self.boxes[cID].gotoFrame(score+1); + for(var cID in ballot.scores){ + var score = ballot.scores[cID]; + + var okScore = (score < 6) + if (! (self._okCandidate(cID) && okScore)) continue + if (model.startAt1) { + self.boxes[cID].gotoFrame(score-1); + } else { + self.boxes[cID].gotoFrame(score+1); + } } }; } -function ThreeBallot(config){ +function ThreeBallot(model){ var self = this; - config = config || {}; - config.bg = "img/ballot_range3.png"; - if (inSandbox) { - config.bg = "img/ballot5_range3.png"; + model = model || {}; + model.bg = "play/img/ballot_range3.png"; + if (model.inSandbox) { + model.bg = "play/img/ballot5_range3.png"; } - Ballot.call(self, config); + Ballot.call(self, model); // BOXES! self.boxes = { @@ -67,7 +68,7 @@ function ThreeBallot(config){ triangle: self.createThree(133, 143, 3), hexagon: self.createThree(133, 184, 1) }; - if (inSandbox) { + if (model.inSandbox) { self.boxes["pentagon"] = self.createThree(133, 226, 1) self.boxes["bob"] = self.createThree(133, 268, 1) }; @@ -75,28 +76,29 @@ function ThreeBallot(config){ // On update... self.update = function(ballot){ // Clear all - var vote = ballot.vote; for(var box in self.boxes){ self.boxes[box].gotoFrame(0); } - for(var cID in ballot){ - var score = ballot[cID]; + for(var cID in ballot.scores){ + var score = ballot.scores[cID]; + var okScore = (score < 6) + if (! (self._okCandidate(cID) && okScore)) continue self.boxes[cID].gotoFrame(score+1); } }; } -function ApprovalBallot(config){ +function ApprovalBallot(model){ var self = this; - config = config || {}; - config.bg = "img/ballot_approval.png"; - if (inSandbox) { - config.bg = "img/ballot5_approval.png"; + model = model || {}; + model.bg = "play/img/ballot_approval.png"; + if (model.inSandbox) { + model.bg = "play/img/ballot5_approval.png"; } - Ballot.call(self, config); + Ballot.call(self, model); // BOXES! self.boxes = { @@ -104,7 +106,7 @@ function ApprovalBallot(config){ triangle: self.createBox(26, 140, 1), hexagon: self.createBox(26, 184, 0) }; - if (inSandbox) { + if (model.inSandbox) { self.boxes["pentagon"] = self.createBox(26, 228, 0) self.boxes["bob"] = self.createBox(26, 272, 0) }; @@ -113,30 +115,33 @@ function ApprovalBallot(config){ self.update = function(ballot){ // Clear all - var vote = ballot.vote; for(var box in self.boxes){ self.boxes[box].gotoFrame(0); } // Check all those who were approved - for(var i=0; i [] + } + self.makeData = config.makeData + self.buttonConfigs = self.makeData(); + self.doMakeData = true + } self.buttonConfigs = config.data; + + self.labelName = config.label + self.labelIsHTML = config.labelIsHTML || false + self.onChoose = config.onChoose; self.isCheckbox = config.isCheckbox || false; + self.isCheckboxBool = config.isCheckboxBool || false; self.justButton = config.justButton || false; + self.buttonHidden = config.buttonHidden || {} // DOM! self.dom = document.createElement("div"); self.dom.setAttribute("class", "button-group"); - // Label! - self.label = document.createElement("div"); - self.label.setAttribute("class", "button-group-label"); - self.label.innerHTML = config.label; - self.dom.appendChild(self.label); + self.buttonDOMByValue = [] + + self.init = function() { + + if (self.doMakeData) self.buttonConfigs = self.makeData(); + + // clear + self.dom.innerHTML = '' + self.buttons = []; + self.buttonsByName = {} + + // Label! + self.labelDOM = document.createElement("div"); + self.labelDOM.setAttribute("class", "button-group-label"); + self.draw() + self.dom.appendChild(self.labelDOM); + + // Create & place buttons! + for(var i=0; i button.turnOff() , 800); + } } // And send the data up self.onChoose(buttonData); @@ -47,6 +129,9 @@ function ButtonGroup(config){ // Highlight based on data... self.highlight = function(propName, propValue){ + // if we haven't set up the buttons yet, then don't do anything yet + if (self.buttons.length == 0) return + // Turn all off for(var i=0;i 1) { + self.id = self.icon + self.instance + } else { + self.id = self.icon + } + + // what order are we at + var chars = Candidate.graphics[model.theme] + var char = Candidate.graphicsByIcon[model.theme][self.icon] + var charIndex = char.i + var serial = charIndex + (self.instance - 1) * chars.length + self.serial = serial + + // The candidate's name is not his id. id is unique. + if (model.customNames == "Yes" && serial < model.namelist.length && model.namelist[serial] != "") { + self.name = model.namelist[serial] + } else { + // use specified name. If none specified, use alphabet. + if (Candidate.graphicsByIcon[model.theme][self.icon].hasOwnProperty("name")) { + self.name = Candidate.graphicsByIcon[model.theme][self.icon]["name"] + } else { + self.name = String.fromCharCode((serial % 26) + "A".charCodeAt(0)); + } + } + self.nameSelf = new Graphicon(self, "name",char,model,charIndex,chars) + + if (model.theme == "Letters") { + self.imageSelf = self.nameSelf + } else { + self.imageSelf = new Graphicon(self,"image",char,model,charIndex,chars) + } + self.fill = self.imageSelf.fill + } + + self.convertColor = function() { + var coloredSvgXml = svgXml.replace(/#3080d0/g,'#e05030'); + img.src = "data:image/svg+xml;charset=utf-8,"+coloredSvgXml; + } + self.sizeFromB = function(b) { + return b * 40 + } + self.bFromSize = function(b) { + return b / 40 + } - // GRAPHICS - var _graphics = Candidate.graphics[self.id]; - self.fill = _graphics.fill; - self.img = new Image(); - self.img.src = _graphics.img; + self.drawBackAnnotation = function(x,y,ctx) {}; // TO IMPLEMENT + self.drawAnnotation = function(x,y,ctx) {}; // TO IMPLEMENT + self.drawTie = function(ctx,arena) { + // ctx.textAlign = "center"; + // var p = arena.modelToArena(self) + // if (model.candidateIconsSet.includes("note")) { + // _drawStroked("TIE",p.x*2,p.y*2-35,40,ctx); + // } + } + self.drawWin = function(ctx,arena) { + // ctx.textAlign = "center"; + // var p = arena.modelToArena(self) + // if (model.candidateIconsSet.includes("note")) { + // _drawStroked("WIN",p.x*2,p.y*2-35,40,ctx); + // } + } + self.drawText = function(text,ctx,arena) { + ctx.textAlign = "center"; + var p = arena.modelToArena(self) + _drawStroked(text,p.x*2,p.y*2-35,40,ctx); + } + self.draw = function(ctx,arena,opt){ + if (model.nLoading > 0) return // still loading, will call lat - self.draw = function(ctx){ + opt = opt || {} + opt.rotate = opt.rotate || 0 // RETINA - var x = self.x*2; - var y = self.y*2; + var p = arena.modelToArena(self) + var x = p.x*2; + var y = p.y*2; var size = self.size*2; - + var hsize + + if (opt.rotate != 0) { + var angle = Math.PI/180 * -opt.rotate + ctx.save() + ctx.translate(x, y) + x = 0 + y = 0 + ctx.rotate(angle); + } + // Draw image instead! - ctx.drawImage(self.img, x-size/2, y-size/2, size, size); + //if(self.highlight) ctx.filter = "brightness(150%)" + if(self.highlight) var temp = ctx.globalAlpha + if(self.highlight) ctx.globalAlpha = 0.8 + if (model.candidateIconsSet.includes("note")) { + self.drawBackAnnotation(x,y,ctx) + } + + + // fade out the non-continuing candidates + if (model.roundCurrent !== undefined) { + var round = model.roundCurrent[self.iDistrict] + var elimSystem = (model.system == "IRV" || model.system == "STV") + if (round !== undefined && elimSystem && round > 0) { + var maxRound = model.result.continuing.length + if (round > maxRound) { + // final results.. forget about shading out candidate + round = maxRound + } else { + if (! model.result.continuing[round-1].includes(self.id)) { + ctx.globalAlpha = 0.3 + } + } + } + } + + if (model.customNames == "Yes") { + hsize = self.imageSelf.img.width / self.imageSelf.img.height * size + } + if (model.candidateIconsSet.includes("body")) { + + size *= 2/3 + + var iwon = self.winner + + // draw differently for each round + if (model.roundCurrent !== undefined) { + var round = model.roundCurrent[self.iDistrict] + // if (round > model.result.history.rounds.length) round = model.result.history.rounds.length - 1 + if (round !== undefined && (model.system == "STV")) { + if (round >= model.result.won.length) round = model.result.won.length - 1 + winners = model.result.won[round] + iwon = winners.includes(self.id) + // winnerIdxs = model.result.history.rounds[round].winners + // winners = winnerIdxs.map( x => district.candidates[x].id) + } + } + + if (iwon) { + _drawSpeckMan2(self.fill, self.fill, 4, ctx.globalAlpha, x/2, y/2, ctx) + } else { + _drawSpeckMan1(self.fill, self.fill, 4, ctx.globalAlpha, x/2, y/2, ctx) + } + } + if (model.candidateIconsSet.includes("image")) { + if (model.theme != "Nicky") { + if (model.theme == "Letters") { + var rectW = self.nameSelf.widthFracName * size + var rectH = self.nameSelf.heightFracName * size + } else { + var rectW = size + var rectH = size + } + + if (self.winner) { + _winBackgroundRectangle(ctx,x,y,rectW,rectH) + } else { + _backgroundRectangle(ctx,x,y,rectW,rectH) + } + + } + + hsize = self.imageSelf.img.width / self.imageSelf.img.height * size + ctx.drawImage(self.imageSelf.img, x-hsize/2, y-size/2, hsize, size); + } + if (model.candidateIconsSet.includes("name")) { + var rectW = self.nameSelf.widthFracName * size + var rectH = self.nameSelf.heightFracName * size + if (self.winner) { + _winBackgroundRectangle(ctx,x,y,rectW,rectH) + } else { + _backgroundRectangle(ctx,x,y,rectW,rectH) + } + + hsize = self.nameSelf.img.width / self.nameSelf.img.height * size + ctx.drawImage(self.nameSelf.img, x-hsize/2, y-size/2, hsize, size); + } + if (model.candidateIconsSet.includes("dots")) { + + var squareSize = 20 + if (self.winner) { + _winBackgroundRectangle(ctx,x,y,squareSize,squareSize) + } + + ctx.fillStyle = self.fill + ctx.strokeStyle = 'black' + ctx.lineWidth = 1 + + ctx.beginPath() + var side = 10 + ctx.rect(x-side/2, y-side/2, side, side) + ctx.fill() + ctx.stroke() + } + // } else if (model.votersAsCandidates) { + // ctx.rect(x-size/2, y-size/2, size, size); + // ctx.fillStyle = self.fill + // ctx.fill() + + if (model.candidateIconsSet.includes("note")) { + self.drawAnnotation(x,y,ctx) + if (self.selected) { + _drawStroked("SELECTED",x,y-5,40,ctx); + } + } + if(self.highlight) ctx.globalAlpha = temp + //if(self.highlight) ctx.filter = "brightness(100%)" + + if (opt.rotate != 0) { + ctx.restore() + } + ctx.globalAlpha = 1.0 }; } // CONSTANTS: the GRAPHICS! // id => img & fill + Candidate.graphics = { - square: { - img: "img/square.png", - fill: "hsl(240,80%,70%)" - }, - triangle: { - img: "img/triangle.png", - fill: "hsl(45,80%,70%)" - }, - hexagon: { - img: "img/hexagon.png", - fill: "hsl(0,80%,70%)" - }, - pentagon: { - img: "img/pentagon.png", - fill: "hsl(90,80%,70%)" - }, - bob: { - img: "img/bob.png", - fill: "hsl(30,80%,70%)" - } -}; \ No newline at end of file + Letters: [ + { + icon: "square", + fill: "hsl(240,80%,70%)", + }, + { + icon: "triangle", + fill: "hsl(45,80%,70%)", + }, + { + icon: "hexagon", + fill: "hsl(0,80%,70%)", + }, + { + icon: "pentagon", + url: "play/img/pentagon.svg", + fill: "hsl(90,80%,70%)", + }, + { + icon: "bob", + fill: "hsl(30,80%,70%)", + } + ], + Default: [ + { + icon: "square", + url: "play/img/square.svg", + fill: "hsl(240,80%,70%)", + name: "square", + }, + { + icon: "triangle", + url: "play/img/triangle.svg", + fill: "hsl(45,80%,70%)", + name: "triangle", + }, + { + icon: "hexagon", + url: "play/img/hexagon.svg", + fill: "hsl(0,80%,70%)", + name: "hexagon", + }, + { + icon: "pentagon", + url: "play/img/pentagon.svg", + fill: "hsl(90,80%,70%)", + name: "pentagon", + }, + { + icon: "bob", + url: "play/img/bob.svg", + fill: "hsl(30,80%,70%)", + name: "bob", + } + ], + Nicky: [ + { + icon: "square", + url: "play/img/square.png", + fill: "hsl(240,80%,70%)", + name: "square", + }, + { + icon: "triangle", + url: "play/img/triangle.png", + fill: "hsl(45,80%,70%)", + name: "triangle", + }, + { + icon: "hexagon", + url: "play/img/hexagon.png", + fill: "hsl(0,80%,70%)", + name: "hexagon", + }, + { + icon: "pentagon", + url: "play/img/pentagon.png", + fill: "hsl(90,80%,70%)", + name: "pentagon", + }, + { + icon: "bob", + url: "play/img/bob.png", + fill: "hsl(30,80%,70%)", + name: "bob", + } + ], + Bees: [ + { + icon: "square", + url: "play/img/blue_bee.png", + fill: "hsl(240,80%,70%)", + name: "blue", + }, + { + icon: "triangle", + url: "play/img/yellow_bee.png", + fill: "hsl(45,80%,70%)", + name: "yellow", + }, + { + icon: "hexagon", + url: "play/img/red_bee.png", + fill: "hsl(0,80%,70%)", + name: "red", + }, + { + icon: "pentagon", + url: "play/img/green_bee.png", + fill: "hsl(90,80%,70%)", + name: "green", + }, + { + icon: "bob", + url: "play/img/orange_bee.png", + fill: "hsl(30,80%,70%)", + name: "orange", + } + ] +}; + +// index by icon, as well, and have index stored and ready +Candidate.graphicsByIcon = {} +for (var themename in Candidate.graphics) { + var theme = Candidate.graphics[themename] + Candidate.graphicsByIcon[themename] = {} + for (var k = 0; k < theme.length; k++) { + var char = theme[k] + char.i = k + Candidate.graphicsByIcon[themename][char.icon] = char + } +} + +Candidate.idFromSerial = function (serial,theme) { + var chars = Candidate.graphics[theme] + var icon = serial % chars.length + var instance = (serial - icon) / chars.length +1 + if (instance == 1) instance = '' + var id = chars[icon].icon + instance + return id +} + +// put all the graphical stuff here... because it's difficult + +function Graphicon(candidate,option,char,model,charIndex,chars) { + var self = this + if (option == "name") { + // Make an image for the name + self.ext = "NA" + pickFillFromColorChooser() + if (candidate.dummy) return // don't make + makeNameImage() + } else { + + // Load the regular image + self.url = char.url + // var _graphics = Candidate.graphics[model.theme][self.icon]; + // self.url = _graphics.img + + // are we using an svg or an img? + // if the model doesn't have assets, then load the files the old way + if (model.assets) { + if (model.assets[self.url]) { + var asset = model.assets[self.url] + } else { + if (0) { // was for Letters + // skip + } else if (Loader) { + if (Loader.assets[self.url]) { + var asset = Loader.assets[self.url] + } + } + } + } + + // png or svg? + if (0) { // was for Letters + var ext = "NA" + } else { + var ext = char.url.split('.').pop(); + } + self.ext = ext + if (ext != "svg" && (1) ) { // 1 used to be !"Letters" theme + // if we are using png's then we have to repeat the fill // TODO: fix + self.fill = char.fill + } else { + pickFillFromColorChooser() + } + + + if (candidate.dummy) return // don't make + + // make an img for canvas + // make an embeddable text string for html + // This should be the last thing in this function because there are callbacks here. + if (asset) { + if (ext == "svg") { + processSVG(asset) + makeImg() + } else { + // png asset + // the img is already made + self.img = asset + + self.texticon = "" + } + } else { + // no asset, so load the img from a file + if (ext == "svg") { + downloadSVGandMakeImg( function() { + // callback + self.texticon = self.svg + + if (! candidate.dummy) model.update() // draw the UI.. maybe we don't need a whole bunch + }) + } else if (0) { // 0 used to be "Letters" theme + makeNameImage() + } else { + self.srcImg = self.url + makeImg() + + self.texticon = "" + } + } + + + if (model.theme == "Nicky") { + self.texticon = ""; + } + } + + function pickFillFromColorChooser() { + if (model.colorChooser == "pick and generate") { + if (candidate.instance > 1) { + // change fill for further rounds + self.fill = Color.generate(candidate.serial) + } else { + self.fill = char.fill + // self.fill = _graphics.fill; + } + } else if (model.colorChooser == "pick and repeat w/ offset") { + if (candidate.instance > 1) { + // change fill for further rounds + var fillIndex = (charIndex + candidate.instance - 1) % chars.length + self.fill = chars[fillIndex].fill + } else { + self.fill = char.fill + // self.fill = _graphics.fill; + } + } else if (model.colorChooser == "generate all") { + self.fill = Color.generate(candidate.serial) + } else { // "pick and repeat" + self.fill = char.fill + } + } + + function processSVG(asset) { + // make self.srcImg and self.svg from asset + + // svg processing + // create own class: replace cls-1 with cls-id + self.svg = asset + self.svg = self.svg.replace(/cls-1/g,"cls-"+candidate.id) + // set style color + var stylechange = "" + // add style to svg + self.svg = self.svg.replace("", stylechange + "") + // auto | optimizeSpeed | crispEdges | geometricPrecision + // shapeRender = 'shape-rendering="auto"' + // self.svg = self.svg.replace("[^<]*<\/title>/, "" + candidate.id + "") + } else { // no tooltext + self.svg = self.svg.replace(/[^<]*<\/title>/, "") + } + + // parse svg + var parser = new DOMParser(); + self.svgDoc = parser.parseFromString(self.svg, "text/xml"); + + // edit svg Doc + var root = self.svgDoc.getElementsByTagName("svg")[0] + root.setAttribute("width", "80"); + root.setAttribute("height", "80"); + + // serialize + var s = new XMLSerializer(); + self.svg = s.serializeToString(self.svgDoc); + + root.setAttribute("width", "10"); + root.setAttribute("height", "10"); + + self.svg_small = s.serializeToString(self.svgDoc); + + // make the image source + if (1) { // just trying different methods + self.srcImg = "data:image/svg+xml;base64," + btoa(self.svg); + // self.srcImg = "data:image/svg+xml;base64," + btoa(self.svg_small); + } else { + var svgblob = new Blob([self.svg], {type: 'image/svg+xml'}); + self.srcImg = URL.createObjectURL(svgblob); + } + self.texticon = self.svg + } + + function downloadSVGandMakeImg(cb) { + // save svg as text + var svgText + var request = new XMLHttpRequest(); + request.open("GET", self.url); + request.setRequestHeader("Content-Type", "image/svg+xml"); + request.onload = function(event) { // onload ... not load + svgText = event.target.responseText + processSVG(svgText) + makeImg() + cb() + } + // request.addEventListener("load", function(event) { // onload ... not load + // svgText = event.target.responseText + // loaded(src,svgText) + // }); + request.send(); + + } + + function makeImg() { + // is this an svg? + // img_svg, img_png, img_blob + + model.nLoading++ + + self.img1 = new Image() + self.img1.src = self.srcImg // This is either a base64 string of the svg text of a base64 png.. but we'd like to make it a png so it loads faster + self.img1.onload = function () { + self.png_b64 = _convertImageToDataURLviaCanvas(self.img1, 'png') + self.img = new Image() + if (1) { + self.img.src = self.png_b64 // base64 png + } else { + var bb = self.png_b64.split(',') + var svgblob = b64toBlob(bb[1], 'image/png'); + self.img.src = URL.createObjectURL(svgblob); + } + self.texticon_png = "<img src='"+self.img.src+"'/>" + self.img.onload = function () { + model.nLoading-- + if (model.nLoading == 0) { + if (candidate.dummy) return + model.draw() + } + } + } + } + + function b64toBlob(b64Data, contentType='', sliceSize=512) { + // str = b64Data + // str = str + '===' // pad + // newlen = str.length - str.length % 4 // round + // str = str.slice(0,newlen) // cut + // b64Data = str + const byteCharacters = atob(b64Data); + const byteArrays = []; + + for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) { + const slice = byteCharacters.slice(offset, offset + sliceSize); + + const byteNumbers = new Array(slice.length); + for (let i = 0; i < slice.length; i++) { + byteNumbers[i] = slice.charCodeAt(i); + } + + const byteArray = new Uint8Array(byteNumbers); + byteArrays.push(byteArray); + } + + const blob = new Blob(byteArrays, {type: contentType}); + return blob; + } + + function makeNameImage() { + model.nLoading++ + var nameImage = _convertNameToDataURLviaCanvas(candidate.name,self.fill, '.png') + self.png_b64 = nameImage.png_b64 + self.widthFracName = nameImage.widthFrac + self.heightFracName = nameImage.heightFrac + self.img = new Image() + self.img.src = self.png_b64 // base64 png + self.texticon_png = "<img src='"+self.img.src+"'/>" + self.img.onload = function () { + model.nLoading-- + if (model.nLoading == 0) { + if (candidate.dummy) return + model.draw() + } + } + } +} \ No newline at end of file diff --git a/play/js/Color.js b/play/js/Color.js new file mode 100644 index 00000000..afa4d746 --- /dev/null +++ b/play/js/Color.js @@ -0,0 +1,199 @@ +Color = new function() { + this.generate = function(colorIndex) { + var haltonList = halton(colorIndex+1) + var point = haltonList[colorIndex] + var a = 6 + switch (a) { + case 1: + var x = point[0] + var y = point[1] + var b = point[2] * 254 // color[2] // 1 to 254 + var rgb = cie_to_rgb(x,y,b).rgb + return _colorCodeFromRGB(rgb) + case 2: + var h = point[0] * 360 + var l = 50 + var s = point[1] * 80 + 20 + return hslToHex(h,s,l) + case 3: + var h = point[0] * 360 + var l = point[2] * 20 + 40 + var s = point[1] * 20 + 70 + return hslToHex(h,s,l) + case 4: + var h = normBetween(point[0], 0, 360) + var l = normBetween(point[2], 60, 80) + var s = normBetween(point[1], 70, 90) + return hsluv.hsluvToHex([h,s,l]) + case 5: + var h = normBetween(point[0], 0, 360) + var l = normBetween(point[1], 60, 95) + var s = normBetween(point[2], 80, 100) + return hsluv.hsluvToHex([h,s,l]) + case 6: + var h = normBetween(point[0], 0, 360) + var l = normBetween(point[1], 45, 85) + var s = normBetween(point[2], 80, 100) + return hsluv.hsluvToHex([h,s,l]) + case 7: + var k = 0 + for (var i = 0; i < 1000; i++) { + var haltonList = halton(i+1) + var point = haltonList[i] + var x = point[0] + var y = point[1] + var b = point[2] * 254 // color[2] // 1 to 254 + var result = cie_to_rgb(x,y,b) + var rgb = result.rgb + if (!result.error) { + k++ + } + if (k >= colorIndex) { + break + } + } + return _colorCodeFromRGB(rgb) + } + function normBetween(x,low,high) { + return x * (high - low) + low + } + } + + function vdc(n, base) { // gives teh nth entry of the Van der Corput sequence + var v = 0 + var denom = 1 + + while (n) { + denom *= base + // remainder = Math.floor(n % base); + remainder = n % base; + n = Math.floor(n/base); + v += remainder / denom + } + return v + } + + // https://en.wikipedia.org/wiki/Halton_sequence#Example_of_Halton_sequence_used_to_generate_points_in_(0,_1)_%C3%97_(0,_1)_in_R2 + function halton(n) { + // evenly spaced points in 2D + var a = [[0,0,0]] + for (var i = 1; i <= n; i++) { + a.push([vdc(i,2),vdc(i,3),vdc(i,7)]) + } + return a // an array of arrays [] ... [ [0.5, 0.33] , [0.25, 0.66] , [0.75, 0.11], ... ] + } + + function listVDC() { + for (var i = 1; i <= 10; i++) { + console.log(vdc(i,3)) + } + console.log(halton(10)) + } // for fun + + function hslToHex(h, s, l) { + h /= 360; + s /= 100; + l /= 100; + let r, g, b; + if (s === 0) { + r = g = b = l; // achromatic + } else { + const hue2rgb = (p, q, t) => { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1 / 6) return p + (q - p) * 6 * t; + if (t < 1 / 2) return q; + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; + return p; + }; + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + r = hue2rgb(p, q, h + 1 / 3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1 / 3); + } + const toHex = x => { + const hex = Math.round(x * 255).toString(16); + return hex.length === 1 ? '0' + hex : hex; + }; + return `#${toHex(r)}${toHex(g)}${toHex(b)}`; + } + + function _colorCodeFromRGB(a) { + return rgbToHex(a[0],a[1],a[2]) + } + + function componentToHex(c) { + var hex = c.toString(16); + return hex.length == 1 ? "0" + hex : hex; + } + + function rgbToHex(r, g, b) { + return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b); + } + + function cie_to_rgb(x, y, brightness) + { + //Set to maximum brightness if no custom value was given (Not the slick ECMAScript 6 way for compatibility reasons) + if (brightness === undefined) { + brightness = 254; + } + + var error = false + + var z = 1.0 - x - y; + var Y = (brightness / 254).toFixed(2); + var X = (Y / y) * x; + var Z = (Y / y) * z; + + //Convert to RGB using Wide RGB D65 conversion + var red = X * 1.656492 - Y * 0.354851 - Z * 0.255038; + var green = -X * 0.707196 + Y * 1.655397 + Z * 0.036152; + var blue = X * 0.051713 - Y * 0.121364 + Z * 1.011530; + + //If red, green or blue is larger than 1.0 set it back to the maximum of 1.0 + if (red > 1.0 || green > 1.0 || blue > 1.0) error = true + if (red < 0 || green < 0 || blue < 0) error = true + if (red > blue && red > green && red > 1.0) { + + green = green / red; + blue = blue / red; + red = 1.0; + } + else if (green > blue && green > red && green > 1.0) { + + red = red / green; + blue = blue / green; + green = 1.0; + } + else if (blue > red && blue > green && blue > 1.0) { + + red = red / blue; + green = green / blue; + blue = 1.0; + } + + //Reverse gamma correction + red = red <= 0.0031308 ? 12.92 * red : (1.0 + 0.055) * Math.pow(red, (1.0 / 2.4)) - 0.055; + green = green <= 0.0031308 ? 12.92 * green : (1.0 + 0.055) * Math.pow(green, (1.0 / 2.4)) - 0.055; + blue = blue <= 0.0031308 ? 12.92 * blue : (1.0 + 0.055) * Math.pow(blue, (1.0 / 2.4)) - 0.055; + + + //Convert normalized decimal to decimal + red = Math.round(red * 255); + green = Math.round(green * 255); + blue = Math.round(blue * 255); + + if (isNaN(red)) + red = 0; + + if (isNaN(green)) + green = 0; + + if (isNaN(blue)) + blue = 0; + + + return {rgb:[red, green, blue], error:error}; + } +} \ No newline at end of file diff --git a/play/js/Draggable.js b/play/js/Draggable.js index e355a0c0..621ed43f 100644 --- a/play/js/Draggable.js +++ b/play/js/Draggable.js @@ -1,30 +1,60 @@ -function Draggable(config){ +function Draggable(){ // Voter and Candidate classes are extended to make them draggable objects in an arena. + // sanity rules: Draggable class creation code cannot read attributes from model. var self = this; + // CONFIGURE DEFAULTS + self.x = 0 + self.y = 0 + self.size = 20 + self.grabsize = 20 + + self.touchAdd = 30 // add this to get bigger distances for touch events + self.radiusScale = 25/40 + + + self.init = function () { + self.offX = 0; + self.offY = 0; + } - // Passed properties - self.x = config.x; - self.y = config.y; - self.model = config.model; - self.radius = config.radius || 30; - - self.hitTest = function(x,y){ - var dx = x-self.x; - var dy = y-self.y; - var r = self.radius; - return((dx*dx+dy*dy) < r*r); + self.hitTest = function(x,y,arena){ + var a = arena.modelToArena(self) + var r = self.grabsize * self.radiusScale + if (arena.mouse.isTouch) r += self.touchAdd + return self.generalHitTest(r, x, y, a.x, a.y) + } + self.generalHitTest = function(radius,x1,y1,x2,y2){ + var dx = x1-x2; + var dy = y1-y2; + return((dx*dx+dy*dy) < radius*radius); }; - self.moveTo = function(x,y){ - self.x = x+self.offX; - self.y = y+self.offY; + self.objectMouseHitTest = function(radius, o, arena) { + var a = arena.modelToArena(o) + return self.generalHitTest(radius, a.x, a.y, arena.mouse.x, arena.mouse.y) + } + + self.hitDistance = function(x,y,arena){ + var a = arena.modelToArena(self) + var dx = x-a.x; + var dy = y-a.y; + return dx*dx+dy*dy; }; + self.newArenaPosition = function(x,y) { + // propose a new x and y, based only on the mouse + var a = {} + a.x = x+self.offX + a.y = y+self.offY + return a + } + self.offX = 0; self.offY = 0; - self.startDrag = function(){ - self.offX = self.x-Mouse.x; - self.offY = self.y-Mouse.y; + self.startDrag = function(arena){ + var a = arena.modelToArena(self) + self.offX = a.x-arena.mouse.x; + self.offY = a.y-arena.mouse.y; }; self.update = function(){ @@ -37,52 +67,176 @@ function Draggable(config){ } -function DraggableManager(model){ +function DraggableManager(arena,model){ var self = this; - self.model = model; // Helper: is Over anything? self.isOver = function(){ - for(var i=model.draggables.length-1; i>=0; i--){ // top DOWN. - var d = model.draggables[i]; - if(d.hitTest(Mouse.x, Mouse.y)){ - return d; + // make a list + var lowTie = [] + var middleTie = [] + var highTie = [] + var checklow = true + var checkmiddle = true + for(var i=arena.draggables.length-1; i>=0; i--){ // top DOWN. + var d = arena.draggables[i]; + if (d.isModify && arena.mouse.dragging && arena.mouse.dragging.isModify) continue // skip the mod gear, you're dragging it to a target and you want to find it + if(d.hitTest(arena.mouse.x, arena.mouse.y,arena)){ + if (d.isVoterCenter && arena.mouse.dragging && arena.mouse.dragging.isModify) continue // skip the voterCenter if we are the mod gear. + // low priority + if ( (d.isModify && d.active) || d.isright || d.isUp || d.isDown || ( model.yeeon && d == model.yeeobject ) ) { + highTie.push(d) + checkmiddle = false + checklow = false + } else if ( checkmiddle && (d.isCandidate || d.isVoter || d.isVoterCenter) ) { + middleTie.push(d) + checklow = false + } else if (checklow) { // (d.istrash || d.isplus || (d.isModify && ! d.active) ) + // middle priority + lowTie.push(d) + } + } + } + // there should be two priority classes, and we should pick among the high priorities first + if (highTie.length > 0) return _getClosest(highTie) + if (middleTie.length > 0) return _getClosest(middleTie) + if (lowTie.length > 0) return _getClosest(lowTie) + return null + + function _getClosest(tie) { + if (tie.length == 1) return tie[0] + // which of the tied objects is closest? + var min = Infinity + var closest = null + for( var i = 0; i < tie.length; i++) { + var d = tie[i].hitDistance(arena.mouse.x, arena.mouse.y,arena) + if (d < min) { + min = d + closest = tie[i] + } } + return closest } - return null; + } + + self.nearestVoterToMouse = function() { + var min = Infinity + var closest = null + + for (var voterGroup of model.voterGroups) { + for (var voterPerson of voterGroup.voterPeople) { + var a = arena.modelToArena(voterPerson) + let dx = a.x - arena.mouse.x + let dy = a.y - arena.mouse.y + var d2 = dx * dx + dy * dy + if (min > d2) { + min = d2 + closest = voterPerson + } + } + } + + return closest } // INTERFACING WITH THE *MOUSE* - subscribe(model.id+"-mousemove", function(){ - if(Mouse.pressed){ - model.update(); - }else if(self.isOver()){ + self.mousemove = function(event){ + var dragging = arena.mouse.dragging + if (dragging) { + event.preventDefault() + event.stopPropagation() + } + if(dragging && dragging.isViewMan) { + dragging.drag(arena) + } else if(arena.mouse.pressed && ! (dragging && dragging.isModify)){ + // open the trash can + if (dragging) { + if (model.arena.mouse.dragging) { + if (model.showToolbar == "on") { + // if we are in the main arena, then do the trash test + model.arena.trashes.test(); + } + } + model.update(); + } + }else if(self.isOver() || (dragging && dragging.isModify)){ // If over anything, grab cursor! - model.canvas.setAttribute("cursor", "grab"); + arena.canvas.setAttribute("cursor", "grab"); + // also, highlight one object + // set all objects to not highlight + for(var i=arena.draggables.length-1; i>=0; i--){ // top DOWN. + var d = arena.draggables[i]; + d.highlight = false + } + var flashydude = self.isOver() + if (flashydude) flashydude.highlight = true + if (dragging && dragging.isModify) model.update() + model.drawArenas() + self.lastwas = "hovering" }else{ // Otherwise no cursor - model.canvas.setAttribute("cursor", ""); + arena.canvas.setAttribute("cursor", ""); + if ( self.lastwas == "hovering"){ + for(var i=arena.draggables.length-1; i>=0; i--){ // top DOWN. + var d = arena.draggables[i]; + d.highlight = false + } + model.drawArenas() + } + self.lastwas = "nothovering" } - }); - subscribe(model.id+"-mousedown", function(){ + } + self.mousedown = function(){ // Didja grab anything? null if nothing. - Mouse.dragging = self.isOver(); + var d = self.isOver(); // If so... - if(Mouse.dragging){ - Mouse.dragging.startDrag(); + if(d){ + + // special case.. adding a candidate + if (d.istrash) return + if (d.isplus) { + n = d.doPlus(false) // plus stuff + d = n // switcheroo ... so the candidate pops out of the plus sign + } + + // move it + arena.mouse.dragging = d + d.startDrag(arena); + if (arena.mouse.ctrlclick) { // we toggled this guy as a winner + d.selected = ! d.selected + } model.update(); + if(d.isViewMan) d.drag(arena) // act as if we are already dragging viewMan + // GrabBING cursor! - model.canvas.setAttribute("cursor", "grabbing"); + arena.canvas.setAttribute("cursor", "grabbing"); } - }); - subscribe(model.id+"-mouseup", function(){ - Mouse.dragging = null; - }); + } + self.mouseup = function(){ + var dragging = arena.mouse.dragging + if (dragging) { // we are dragging something, not just air + model.onDrop() // drop in trash if there is one + + if (dragging.isViewMan) { + dragging.drop() + model.drawArenas() + } else if (dragging.isGear) { + var flashydude = self.isOver() + dragging.doModify(flashydude) + model.drawArenas() + } else { + model.update(); + } + arena.mouse.dragging = null; + model.update() + + } + } } \ No newline at end of file diff --git a/play/js/Election.js b/play/js/Election.js index 8e4a136d..dc8e7e52 100644 --- a/play/js/Election.js +++ b/play/js/Election.js @@ -7,19 +7,29 @@ and RENDER IT INTO THE CAPTION var Election = {}; -Election.score = function(model, options){ + +Election.score = function(district, model, options){ + + options = _electionDefaults(options) + var polltext = _beginElection(district,model,options,"score") + let cans = district.stages[model.stage].candidates // Tally the approvals & get winner! - var tally = _tally(model, function(tally, ballot){ - for(var candidate in ballot){ - tally[candidate] += ballot[candidate]; + var ballots = model.voterSet.getBallotsDistrict(district) + + var tally = _zeroTally(cans) + for(var ballot of ballots){ + for(var candidate in ballot.scores){ + tally[candidate] += ballot.scores[candidate]; } - }); - for(var candidate in tally){ - tally[candidate] /= model.getTotalVoters(); } + + + var maxscore = 5 + var winners = _countWinner(tally); - var color = _colorWinner(model, winners); + var result = _result(winners,model) + var color = result.color if (options.sidebar) { @@ -27,39 +37,96 @@ Election.score = function(model, options){ var winner = winners[0]; var text = ""; text += "<span class='small'>"; - text += "<b>highest average score wins</b><br>"; - for(var i=0; i<model.candidates.length; i++){ - var c = model.candidates[i].id; - text += _icon(c)+"'s score: "+(tally[c].toFixed(2))+" out of 5.00<br>"; + if ("Auto" == model.autoPoll) text += polltext; + text += "<b>score as % of max possible: </b><br>"; + if (model.doTallyChart) { + text += tallyChart(tally,cans,model,maxscore,ballots.length) + text += "<br>"; + } else { + for(var i=0; i<cans.length; i++){ + var c = cans[i].id; + text += model.icon(c)+"'s score: "+_percentFormat(district, tally[c] / maxscore)+"<br>"; + } } if(!winner | winners.length>=2){ // NO WINNER?! OR TIE?!?! - text += _tietext(winners); + text += _tietext(model,winners); + // text = "<b>TIE</b> <br> <br>" + text; } else { text += "<br>"; - text += _icon(winner)+" has the highest score, so...<br>"; + text += model.icon(winner)+" has the highest score, so...<br>"; text += "</span>"; text += "<br>"; - text += "<b style='color:"+color+"'>"+winner.toUpperCase()+"</b> WINS"; + text += "<b style='color:"+color+"'>"+model.nameUpper(winner)+"</b> WINS"; + // text = "<b style='color:"+color+"'>"+model.nameUpper(winner)+"</b> WINS <br> <br>" + text; } - model.caption.innerHTML = text; + result.text = text; } - if (model.dotop2) model.top2 = _sortTally(tally).slice(0,2) + if (model.doTop2) var theTop2 = _sortTally(tally).slice(0,2) + if (model.doTop2) result.theTop2 = theTop2 + return result; +}; + +Election.defaultCodeScore = Election.score.toString() + +Election.create = function(district, model, options){ + var code = model.codeEditorText + var wrappedCode = "(function(district, model, options) { return (" + code + ")(district, model, options) })(district, model, options)"; // code that will return a reference to the function typed by the user + var wrappedCode = "(" + code + ")(district, model, options)"; // code that will return a reference to the function typed by the user + + return eval(wrappedCode); }; -Election.star = function(model, options){ +function tallyChart(tally,cans,model,maxscore,nballots,opt) { + + opt = opt || {} + if(opt.percent == undefined) opt.percent = true + + var distList = makeDistListFromTally(tally, cans, maxscore, nballots) + var text = "" + // text += tBarChart("score",distList,model,{differentDisplay: true}) + text += tBarChart("score",distList,model,opt) + return text +} + +function lineChart(collectTallies,cans,model,maxscore,nballots,opt) { + + opt = opt || {} + if(opt.percent == undefined) opt.percent = true + var text = "" + var dls = [] + for (var i = 0; i < collectTallies.length; i++) { + var tally = collectTallies[i] + var distList = makeDistListFromTally(tally, cans, maxscore, nballots, {dontSort: true}) + // text += tBarChart("score",distList,model,{differentDisplay: true}) + dls.push(distList) + } + text += dLineChart("score",dls,model,opt) + text += tBarChart("score",dls[dls.length-1],model,opt) + return text +} + + + +Election.star = function(district, model, options){ + + options = _electionDefaults(options) + var polltext = _beginElection(district,model,options,"score") + let cans = district.stages[model.stage].candidates + + var maxscore = 5 // Tally the approvals & get winner! - var tally = _tally(model, function(tally, ballot){ - for(var candidate in ballot){ - tally[candidate] += ballot[candidate]; + var ballots = model.voterSet.getBallotsDistrict(district) + var tally = _zeroTally(cans) + for(var ballot of ballots){ + for(var candidate in ballot.scores){ + tally[candidate] += ballot.scores[candidate]; } - }); - for(var candidate in model.candidatesById){ - tally[candidate] /= model.getTotalVoters(); } + var frontrunners = []; for (var i in tally) { @@ -67,182 +134,295 @@ Election.star = function(model, options){ } frontrunners.sort(function(a,b){return tally[b]-tally[a]}) - var ballots = model.getBallots(); var aWins = 0; var bWins = 0; for(var k=0; k<ballots.length; k++){ var ballot = ballots[k]; - if(ballot[frontrunners[0]]>ballot[frontrunners[1]]){ + if(ballot.scores[frontrunners[0]]>ballot.scores[frontrunners[1]]){ aWins++; // a wins! - } else if(ballot[frontrunners[0]]<ballot[frontrunners[1]]){ + } else if(ballot.scores[frontrunners[0]]<ballot.scores[frontrunners[1]]){ bWins++; // b wins! } } - var winner = frontrunners[0] if (bWins > aWins) { - winner = frontrunners[1] + var winners = [frontrunners[1]] + } else if (aWins > bWins) { + var winners = [frontrunners[0]] + } else { + var winners = frontrunners // tie } - var color = _colorWinner(model, [winner]); + var result = _result(winners,model) + var color = result.color + - if (model.dotop2) model.top2 = frontrunners.slice(0,2) + if (model.doTop2) var theTop2 = frontrunners.slice(0,2) + if (model.doTop2) result.theTop2 = theTop2 - if (!options.sidebar) return + if (!options.sidebar) return result // NO WINNER?! OR TIE?!?! - if(!winner){ + if(winners.length > 1){ - var text = "<b>NOBODY WINS</b>"; - model.caption.innerHTML = text; + var text = "<b>TIE</b>"; + result.text = text; }else{ // Caption var text = ""; text += "<span class='small'>"; + if ("Auto" == model.autoPoll) text += polltext; text += "<b>pairwise winner of two highest average scores wins</b><br>"; - for(var i=0; i<model.candidates.length; i++){ - var c = model.candidates[i].id; - text += _icon(c)+"'s score: "+(tally[c].toFixed(2))+" out of 5.00<br>"; + if (model.doTallyChart) { + text += tallyChart(tally,cans,model,maxscore,ballots.length) + } else { + for(var i=0; i<cans.length; i++){ + var c = cans[i].id; + text += model.icon(c)+":"+_percentFormat(district, tally[c] / maxscore)+"<br>"; + } + } + if (frontrunners.length >= 2) { + text += "<br>"; + text += "<b>Final Round between the top two:<br></b>"; + if (model.doTallyChart) { + var runoffTally = {} + runoffTally[frontrunners[0]] = aWins + runoffTally[frontrunners[1]] = bWins + text += tallyChart(runoffTally,cans,model,1,ballots.length) + } else { + text += model.icon(frontrunners[0])+_percentFormat(district, aWins)+". "+model.icon(frontrunners[1]) +_percentFormat(district, bWins) + "<br>"; + } + text += "</span>"; } text += "<br>"; - text += _icon(frontrunners[0])+" and "+_icon(frontrunners[1]) +" have the highest score, and...<br>"; - text += "...their pairwise counts are "+aWins+" to "+bWins+", so...<br>"; - text += "</span>"; - text += "<br>"; - text += "<b style='color:"+color+"'>"+winner.toUpperCase()+"</b> WINS"; - model.caption.innerHTML = text; + text += "<b style='color:"+color+"'>"+model.nameUpper(winners[0])+"</b> WINS"; + // text = "<b style='color:"+color+"'>"+model.nameUpper(winners[0])+"</b> WINS <br> <br>" + text; + result.text = text; } + return result; }; -Election.three21 = function(model, options){ +Election.three21 = function(district, model, options){ - var ballots = model.getBallots(); - // Tally the approvals & get winner! - var tallies = _tallies(model, 3); + options = _electionDefaults(options) + var polltext = _beginElection(district,model,options,"3-2-1") + let cans = district.stages[model.stage].candidates + + var ballots = model.voterSet.getBallotsDistrict(district) + + // Create the tally + var tallies = []; + for (var level=0; level < 3; level++) { + var tally = _zeroTally(cans); + tallies.push(tally) + } + + // Count 'em up + for(var i=0; i<ballots.length; i++){ + var ballot = ballots[i] + for(var candidate in ballot.scores){ + tallies[ballot.scores[candidate]][candidate] += 1; + } + } var semifinalists = []; - for (var i in model.candidatesById) { - semifinalists.push(i); + for (var c of cans) { + semifinalists.push(c.id); } semifinalists.sort(function(a,b){return tallies[2][b]-tallies[2][a]}) + semifinalists = semifinalists.slice(0,3) - var finalists = semifinalists.slice(0,3); + var finalists = _jcopy(semifinalists) finalists.sort(function(a,b){return tallies[0][a]-tallies[0][b]}) - var ballots = model.getBallots(); var aWins = 0; var bWins = 0; for(var k=0; k<ballots.length; k++){ var ballot = ballots[k]; - if(ballot[finalists[0]]>ballot[finalists[1]]){ + if(ballot.scores[finalists[0]]>ballot.scores[finalists[1]]){ aWins++; // a wins! - } else if(ballot[finalists[0]]<ballot[finalists[1]]){ + } else if(ballot.scores[finalists[0]]<ballot.scores[finalists[1]]){ bWins++; // b wins! } } - var winner = finalists[0] + if (bWins > aWins) { - winner = finalists[1] + var winners = [finalists[1]] + } else if (aWins > bWins) { + var winners = [finalists[0]] + } else { + var winners = finalists // tie } - var color = _colorWinner(model, [winner]); + var result = _result(winners,model) + var color = result.color - if (model.dotop2) model.top2 = finalists.slice(0,2) + if (model.doTop2) var theTop2 = finalists.slice(0,2) + if (model.doTop2) result.theTop2 = theTop2 - if (!options.sidebar) return + if (!options.sidebar) return result // NO WINNER?! OR TIE?!?! - if(!winner){ + if(winners.length > 1){ - var text = "<b>NOBODY WINS</b>"; - model.caption.innerHTML = text; + var text = "<b>TIE</b>"; + result.text = text; }else{ // Caption var text = ""; text += "<span class='small'>"; - text += "<b>Semifinalists: 3 most good. Finalists: 2 least bad. Winner: more preferred.</b><br>"; - text += "<b>Semifinalists:</b><br>"; - for(var i=0; i<semifinalists.length; i++){ - var c = semifinalists[i]; - text += _icon(c)+"'s 'good': "+tallies[2][c]+"<br>"; + if ("Auto" == model.autoPoll) text += polltext; + // text += "Semifinalists: 3 most good. <br>Finalists: 2 least bad. <br>Winner: more preferred.<br><br>"; + text += "<b>Pick semifinalists:</b> 3 most good<br>"; + + if (model.doTallyChart) { + text += tallyChart(tallies[2],cans,model,1,ballots.length) + } else { + for(var i=0; i<semifinalists.length; i++){ + var c = semifinalists[i]; + text += model.icon(c)+"'s 'good': "+ _percentFormat(district, tallies[2][c]) +"<br>"; + } + } + text += "<br>"; + text += "<b>Pick finalists:</b> 2 least bad<br>"; + if (model.doTallyChart) { + var semiTally = {} + for(var i=0; i<semifinalists.length; i++){ + var c = semifinalists[i]; + semiTally[c] = tallies[0][c] + } + text += tallyChart(semiTally,cans,model,1,ballots.length,{distLine:true}) + } else { + for(var i=0; i<finalists.length; i++){ + var c = finalists[i]; + text += model.icon(c)+"'s 'bad': "+_percentFormat(district, tallies[0][c])+"<br>"; + } } - text += "<b>Finalists:</b><br>"; - for(var i=0; i<finalists.length; i++){ - var c = finalists[i]; - text += _icon(c)+"'s 'bad': "+tallies[0][c]+"<br>"; + text += "<br>"; + text += "<b>Pick winner:</b> 1 more preferred<br>"; + if (model.doTallyChart) { + var runoffTally = {} + runoffTally[finalists[0]] = aWins + runoffTally[finalists[1]] = bWins + text += tallyChart(runoffTally,cans,model,1,ballots.length) + } else { + text += model.icon(finalists[0])+": "+_percentFormat(district, aWins)+"; "+model.icon(finalists[1]) +": "+_percentFormat(district, bWins)+", so...<br>"; } - text += "<b>Winner:</b><br>"; - text += _icon(finalists[0])+": "+aWins+"; "+_icon(finalists[1]) +": "+bWins+", so...<br>"; text += "</span>"; text += "<br>"; - text += "<b style='color:"+color+"'>"+winner.toUpperCase()+"</b> WINS"; - model.caption.innerHTML = text; + text += "<b style='color:"+color+"'>"+model.nameUpper(winners[0])+"</b> WINS"; + // text = "<b style='color:"+color+"'>"+model.nameUpper(winners[0])+"</b> WINS <br> <br>" + text; + result.text = text; } + return result; }; -Election.approval = function(model, options){ +Election.approval = function(district, model, options){ + + options = _electionDefaults(options) + var polltext = _beginElection(district,model,options,"approval") + let cans = district.stages[model.stage].candidates // Tally the approvals & get winner! - var tally = _tally(model, function(tally, ballot){ - var approved = ballot.approved; - for(var i=0; i<approved.length; i++) tally[approved[i]]++; - }); + var ballots = model.voterSet.getBallotsDistrict(district) + var tally = _zeroTally(cans) + for(var ballot of ballots){ + for(var cid in ballot.scores){ + tally[cid] += ballot.scores[cid]; + } + } + var winners = _countWinner(tally); - var color = _colorWinner(model, winners); + var result = _result(winners,model) + var color = result.color + - if (model.dotop2) model.top2 = _sortTally(tally).slice(0,2) + if (model.doTop2) var theTop2 = _sortTally(tally).slice(0,2) + if (model.doTop2) result.theTop2 = theTop2 - if (!options.sidebar) return + + + if (!options.sidebar) return result // Caption var winner = winners[0]; var text = ""; text += "<span class='small'>"; - text += "<b>most approvals wins</b><br>"; - for(var i=0; i<model.candidates.length; i++){ - var c = model.candidates[i].id; - text += _icon(c)+" got "+tally[c]+" approvals<br>"; + if ("Auto" == model.autoPoll) text += polltext; + text += "<b>most approvals wins (%)</b><br>"; + if (model.doTallyChart) { + text += tallyChart(tally,cans,model,1,ballots.length) + } else { + for(var i=0; i<cans.length; i++){ + var c = cans[i].id; + text += model.icon(c)+" got "+_percentFormat(district, tally[c])+"<br>"; + } } if(!winner | winners.length>=2){ // NO WINNER?! OR TIE?!?! - text += _tietext(winners); + text += _tietext(model,winners); + // text = "<b>TIE</b> <br> <br>" + text; } else { text += "<br>"; - text += _icon(winner)+" is most approved, so...<br>"; + text += model.icon(winner)+" is most approved, so...<br>"; text += "</span>"; text += "<br>"; - text += "<b style='color:"+color+"'>"+winner.toUpperCase()+"</b> WINS"; + text += "<b style='color:"+color+"'>"+model.nameUpper(winner)+"</b> WINS"; + // text = "<b style='color:"+color+"'>"+model.nameUpper(winner)+"</b> WINS <br> <br>" + text; } - model.caption.innerHTML = text; + result.text = text; + return result; }; -Election.condorcet = function(model, options){ +Election.condorcet = function(district, model, options){ + + options = _electionDefaults(options) + var polltext = _beginElection(district,model,options,"nopoll") + let cans = district.stages[model.stage].candidates var text = ""; - text += "<span class='small'>"; - text += "<b>who wins each one-on-one?</b><br>"; - var ballots = model.getBallots(); + var ballots = model.voterSet.getBallotsDistrict(district) // Create the WIN tally - var tally = {}; - for(var candidateID in model.candidatesById) tally[candidateID] = 0; + var tallyWins = {} + var tallyWinsOrTies = {} + var tallyLosses = {} + var beatsMe = {} + var beatsOrTiesMe = {} + for(var candidateID in model.candidatesById) { + tallyWins[candidateID] = 0 + tallyWinsOrTies[candidateID] = 0 + tallyLosses[candidateID] = 0 + beatsMe[candidateID] = [] + beatsOrTiesMe[candidateID] = [] + } + + // make a list of mouseover events + let eventsToAssign = [] + + if (options.sidebar) { + + text += "<span class='small'>"; + text += "<b>who wins each one-on-one?</b><br>"; + text += pairChart(ballots,district,model) + } // For each combination... who's the better ranking? - for(var i=0; i<model.candidates.length-1; i++){ - var a = model.candidates[i]; - for(var j=i+1; j<model.candidates.length; j++){ - var b = model.candidates[j]; + for(var i=0; i<cans.length-1; i++){ + var a = cans[i]; + for(var j=i+1; j<cans.length; j++){ + var b = cans[j]; // Actually figure out who won. var aWins = 0; @@ -258,9 +438,23 @@ Election.condorcet = function(model, options){ // WINNER? var winner = (aWins>bWins) ? a : b; - if (aWins != bWins) { - tally[winner.id]++; + var loser = (aWins<=bWins) ? a : b; + var tie = aWins==bWins + + let eventID = 'pair_' + winner.id + '_' + loser.id + '_' + _rand5() + let e = { + eventID: eventID, + f: pairDraw(model,district,winner.id,loser.id,tie) + } + eventsToAssign.push(e) + text += '<div id="' + eventID + '">' + if ( ! tie ) { + tallyWins[winner.id]++; + tallyWinsOrTies[winner.id]++; + tallyLosses[loser.id]++; + beatsMe[loser.id].push(winner.id) + beatsOrTiesMe[loser.id].push(winner.id) // Text. var by,to; if(winner==a){ @@ -270,43 +464,88 @@ Election.condorcet = function(model, options){ by = bWins; to = aWins; } - text += _icon(a.id)+" vs "+_icon(b.id)+": "+_icon(winner.id)+" wins by "+by+" to "+to+"<br>"; + text += model.icon(winner.id) + " beats " + model.icon(loser.id) + ", "+_percentFormat(district, by)+" to "+_percentFormat(district, to)+"<br>"; + // text += model.icon(a.id)+" vs "+model.icon(b.id)+": "+model.icon(winner.id)+" wins, "+_percentFormat(district, by)+" to "+_percentFormat(district, to)+"<br>"; } else { //tie - tally[a.id]++; - tally[b.id]++; - text += _icon(a.id)+" vs "+_icon(b.id)+": "+"TIE"+"<br>"; + tallyWinsOrTies[a.id]++; + tallyWinsOrTies[b.id]++; + beatsOrTiesMe[a.id].push(b.id) + beatsOrTiesMe[b.id].push(a.id) + text += " TIE between " + model.icon(a.id)+" "+model.icon(b.id) + "<br>"; + // text += model.icon(a.id)+" vs "+model.icon(b.id)+": "+"TIE"+"<br>"; } + text += '</div>' + + } } - // Was there one who won all???? + // Was there one who won all? var topWinners = []; - - for(var id in tally){ - if(tally[id]==model.candidates.length-1){ + for(var id in tallyWins){ + if(tallyWins[id]==cans.length-1){ topWinners.push(id); } } - // probably it would be better to find the smith set but this is okay for now - topWinners = _countWinner(tally); - var color = _colorWinner(model, topWinners); - if (model.dotop2) model.top2 = _sortTally(tally).slice(0,2) - if (!options.sidebar) return + + // was there one who lost none? + if (topWinners.length === 0) { + for(var id in tallyLosses){ + if(tallyLosses[id]==0){ + topWinners.push(id); + } + } + } + + // if there are multiple, then they tied each other + // if there are none, then there's a cycle + // find the Schwartz set = nobody beats the set + if (topWinners.length == 0) { + // ties + + // sort list + var idSorted = Object.keys(tallyWins).sort(function(x,y) {return -tallyWins[x]+tallyWins[y]}) // reverse? + var indexOfId = {} + for (var i = 0; i < idSorted.length; i++) indexOfId[idSorted[i]] = i + var divider = 1 + for (var i = 0; i < divider; i++) { + var b = beatsMe[idSorted[i]] + for (var j = 0; j < b.length; j++) { + divider = Math.max(divider, indexOfId[b[j]] + 1) + } + } + topWinners = idSorted.slice(0,divider) + var usedSchwartz = true + } + + var result = _result(topWinners,model) +    var color = result.color + if (model.doTop2) var theTop2 = _sortTally(tallyWins).slice(0,2) + if (model.doTop2) result.theTop2 = theTop2 + if (!options.sidebar) return result + var topWinner = topWinners[0]; // Winner... or NOT!!!! text += "<br>"; if (topWinners.length == 1) { - text += _icon(topWinner)+" beats all other candidates in one-on-one races.<br>"; + if (usedSchwartz) { + text += model.icon(topWinner)+" beats or ties all other candidates in one-on-one races.<br>"; + } else { + text += model.icon(topWinner)+" beats all other candidates in one-on-one races.<br>"; + } text += "</span>"; text += "<br>"; - text += "<b style='color:"+color+"'>"+topWinner.toUpperCase()+"</b> WINS"; + text += "<b style='color:"+color+"'>"+model.nameUpper(topWinner)+"</b> WINS"; + // text = "<b style='color:"+color+"'>"+model.nameUpper(topWinner)+"</b> WINS <br> <br>" + text; }else if (topWinners.length >= 2) { - for(var i=0; i<model.candidates.length; i++){ - var c = model.candidates[i].id; - text += _icon(c)+" got "+tally[c]+" wins<br>"; + for(var i=0; i<cans.length; i++){ + var c = cans[i].id; + text += model.icon(c)+" got "+tallyWins[c]+" wins<br>"; } - text += _tietext(topWinners); + text += "<br>"; + text += _tietext(model,topWinners); + // text = "<b>TIE</b> <br> <br>" + text; } else { text += "NOBODY beats everyone else in one-on-one races.<br>"; text += "</span>"; @@ -317,233 +556,5874 @@ Election.condorcet = function(model, options){ // what's the loop? - model.caption.innerHTML = text; + result.text = text; + result.eventsToAssign = eventsToAssign + return result; }; -Election.borda = function(model, options){ - - // Tally the approvals & get winner! - var tally = _tally(model, function(tally, ballot){ - for(var i=0; i<ballot.rank.length; i++){ - var candidate = ballot.rank[i]; - tally[candidate] += i; // the rank! +function pairChart(ballots,district,model,hh) { + var text = "" + text += "<span class='small'>" + let opt = {entity:"winner",doSort:true,triangle:true,light:true} + if (hh == undefined) { + if (model.ballotType == "Ranked") { + var hh = head2HeadTally(model, district,ballots) + } else { + var hh = head2HeadScoreTally(model, district,ballots) } - }); - var winners = _countLoser(tally); // LOWER score is best! - var color = _colorWinner(model, winners); - if (model.dotop2) model.top2 = _sortTallyRev(tally).slice(0,2) - if (!options.sidebar) return - - // Caption - var text = ""; - text += "<span class='small'>"; - text += "<b>lower score is better</b><br>"; - for(var i=0; i<model.candidates.length; i++){ - var c = model.candidates[i].id; - text += _icon(c)+"'s total score: "+tally[c]+"<br>"; } - if(winners.length>=2){ - // NO WINNER?! OR TIE?!?! - text += _tietext(winners); - }else{ - var winner = winners[0]; - text += "<br>"; - text += _icon(winner)+" has the <i>lowest</i> score, so...<br>"; - text += "</span>"; - text += "<br>"; - text += "<b style='color:"+color+"'>"+winner.toUpperCase()+"</b> WINS"; + text += pairwiseTable(hh,district,model,opt) + text += "</span><br>" + return text +} + +function squarePairChart(ballots,district,model,hh) { + var text = "" + text += "<span class='small'>" + let opt = {entity:"winner",light:true,diagonal:true} + if (hh == undefined) { + if (model.ballotType == "Ranked") { + var hh = head2HeadTally(model, district,ballots) + } else { + var hh = head2HeadScoreTally(model, district,ballots) + } } - model.caption.innerHTML = text; -}; + text += pairwiseTable(hh,district,model,opt) + text += "</span><br>" + return text +} -Election.irv = function(model, options){ +// PairElimination +Election.schulze = function(district, model, options){ // Pairs of candidates are sorted by their win margin. Then we eliminate the weakest wins until there is a Condorcet winner. A condorcet winner has 0 losses. + + options = _electionDefaults(options) + var polltext = _beginElection(district,model,options,"nopoll") + let cans = district.stages[model.stage].candidates + + var reverseExplanation = true var text = ""; - text += "<span class='small'>"; - var resolved = null; - var roundNum = 1; + var ballots = model.voterSet.getBallotsDistrict(district) - var candidates = []; - for(var i=0; i<model.candidates.length; i++){ - candidates.push(model.candidates[i].id); + + if (options.sidebar) { + + text += "<span class='small'>"; + text += "<b>who wins each one-on-one?</b><br>"; + text += pairChart(ballots,district,model) + + if (reverseExplanation) { + text += "<b>who lost the least, one-on-one?</b><br>"; + } else { + text += "<b>who had the strongest wins, one-on-one?</b><br>"; + } } - var loserslist = [] - while(!resolved){ - text += "<b>round "+roundNum+":</b><br>"; - text += "who's voters' #1 choice?<br>"; + // Create the WIN tally + var tally = {}; + var losses = {}; + for(var candidateID in model.candidatesById) tally[candidateID] = 0; + for(var candidateID in model.candidatesById) losses[candidateID] = 0; - // Tally the approvals & get winner! - var pre_tally = _tally(model, function(tally, ballot){ - var first = ballot.rank[0]; // just count #1 - tally[first]++; - }); + // For each combination... who's the better ranking? + pairs = [] + for(var i=0; i<cans.length-1; i++){ + var a = cans[i]; + for(var j=i+1; j<cans.length; j++){ + var b = cans[j]; - // ONLY tally the remaining candidates... - var tally = {}; - for(var i=0; i<candidates.length; i++){ - var cID = candidates[i]; - tally[cID] = pre_tally[cID]; - } + // Actually figure out who won. + var aWins = 0; + var bWins = 0; + for(var k=0; k<ballots.length; k++){ + var rank = ballots[k].rank; + if(rank.indexOf(a.id)<rank.indexOf(b.id)){ + aWins++; // a wins! + }else{ + bWins++; // b wins! + } + } - // Say 'em... - for(var i=0; i<candidates.length; i++){ - var c = candidates[i]; - text += _icon(c)+":"+tally[c]; - if(i<candidates.length-1) text+=", "; - } - text += "<br>"; + // WINNER? + var winner = (aWins>bWins) ? a : b; + var loser = (aWins>bWins) ? b : a; + if (aWins != bWins) { + tally[winner.id]++; + losses[loser.id]++; - // Do they have more than 50%? - var winners = _countWinner(tally); - var winner = winners[0]; - var ratio = tally[winner]/model.getTotalVoters(); - if(ratio>0.5){ - if (winners.length >= 2) { // won't happen bc ratio > .5 - resolved = "tie"; - break; + // Text. + var by,to; + if(winner==a){ + by = aWins; + to = bWins; + pairs.push({winI:i,loseI:j,winN:aWins,loseN:bWins,margin:aWins-bWins,tie:false}) + }else{ + by = bWins; + to = aWins; + pairs.push({winI:j,loseI:i,winN:bWins,loseN:aWins,margin:bWins-aWins,tie:false}) + } + //text += model.icon(a.id)+" vs "+model.icon(b.id)+": "+model.icon(winner.id)+" wins by "+by+" to "+to+"<br>"; + } else { //tie + tally[a.id]++; + tally[b.id]++; + pairs.push({winI:i,loseI:j,winN:aWins,loseN:bWins,margin:aWins-bWins,tie:true}) + //text += model.icon(a.id)+" vs "+model.icon(b.id)+": "+"TIE"+"<br>"; } - resolved = "done"; - text += _icon(winner)+" has more than 50%<br>"; - break; } + } - // Otherwise... runoff... - var losers = _countLoser(tally); - var loser = losers[0]; - if (losers.length >= candidates.length) { - resolved = "tie"; - break; + // Was there one who won all???? + var topWinners = []; + + for(var id in tally){ + if(tally[id]==cans.length-1){ + topWinners.push(id); } - loserslist = loserslist.concat(losers) + } + var unanimousWin = topWinners.length == 1 + - // ACTUALLY ELIMINATE + pairs = pairs.sort(function(x,y) {return y.margin - x.margin}) // sort in descending order - text += "nobody's more than 50%. "; - for (var li = 0; li < losers.length ; li++ ) { - loser = losers[li]; - text += "eliminate loser, "+_icon(loser)+". next round!<br>"; - candidates.splice(candidates.indexOf(loser), 1); // remove from candidates... - var ballots = model.getBallots(); - for(var i=0; i<ballots.length; i++){ - var rank = ballots[i].rank; - rank.splice(rank.indexOf(loser), 1); // REMOVE THE LOSER - } - // And repeat! - roundNum++; + + // if there was a tie, then try to break the tie + if (! unanimousWin) { + + // switch to indexing the candidates by numbers instead of names + var lossesI=[] + for (var j = 0; j < cans.length; j++) { + //lossesI[j] = losses[cans[j]] + lossesI.push(losses[cans[j].id]) } - text += "<br>" - - } - if (model.dotop2) { - loserslist = loserslist.concat(_sortTallyRev(tally)) - var ll = loserslist.length - model.top2 = loserslist.slice(ll-1,ll).concat(loserslist.slice(ll-2,ll-1)) - } - - - var color = _colorWinner(model, winners); - if (!options.sidebar) return + // find the Schwartz set + schwartz = [] - if (resolved == "tie") { - text += _tietext(winners); - } else { - // END! - text += "</span>"; - text += "<br>"; - text += "<b style='color:"+color+"'>"+winner.toUpperCase()+"</b> WINS"; - } + // find the lowest loss candidates and add them to the schwarz set - model.caption.innerHTML = text; + max3 = cans.length + for(var j = 0; j < cans.length; j++){ // see who wins + if(lossesI[j]<max3){ + max3 = lossesI[j] + schwartz = [] + schwartz.push(j) + } else if (lossesI[j]==max3){ + schwartz.push(j) + } + } + // add anybody that beat them (not tied) + var jp + for (var j = 0; j < pairs.length; j++) { + jp = pairs[j] + if ( schwartz.includes(jp.loseI) && ! jp.tie && ! schwartz.includes(jp.winI) ) { + schwartz.push(jp.winI) + j = -1 // restart loop + } + } + schwartzFirst = (Array.from(schwartz)).map(x => cans[x].id) + -}; -Election.plurality = function(model, options){ - options = options || {}; + var tieBreakerWinners = [] + for (var i = pairs.length - 1; i >= 0; i--) { // i represents the strongest pair to be eliminated + - // Tally the approvals & get winner! - var tally = _tally(model, function(tally, ballot){ - tally[ballot.vote]++; - }); - var winners = _countWinner(tally); - var color = _colorWinner(model, winners); - if (model.dotop2) model.top2 = _sortTally(tally).slice(0,2) - if (!options.sidebar) return + if (! pairs[i].tie) { + losses[cans[pairs[i].loseI].id] -- // eliminate loss + lossesI[pairs[i].loseI] -- // eliminate loss + } + if (i > 0 && pairs[i].margin == pairs[i-1].margin) { // check if there is a tie for weakest win + continue + } - // Caption - var winner = winners[0]; - var text = ""; - text += "<span class='small'>"; - text += "<b>most votes wins</b><br>"; - for(var i=0; i<model.candidates.length; i++){ - var c = model.candidates[i].id; - text += _icon(c)+" got "+tally[c]+" votes<br>"; - } - // Caption text for winner, or tie - if (winners.length == 1) { - if(options.sidebar){ - text += "<br>"; - text += _icon(winner)+" has most votes, so...<br>"; - } - text += "</span>"; - text += "<br>"; - text += "<b style='color:"+color+"'>"+winner.toUpperCase()+"</b> WINS"; - } else { - text += _tietext(winners); - } - model.caption.innerHTML = text; -}; -var _tally = function(model, tallyFunc){ + // find the Schwartz set + schwartz = [] - // Create the tally + // find the lowest loss candidates and add them to the schwarz set + + max3 = cans.length + for(var j = 0; j < cans.length; j++){ // see who wins + if(lossesI[j]<max3){ + max3 = lossesI[j] + schwartz = [] + schwartz.push(j) + } else if (lossesI[j]==max3){ + schwartz.push(j) + } + } + // add anybody that beat them (not tied) + var jp + for (var j = 0; j < i; j++) { // don't look at pairs that are already eliminated + jp = pairs[j] + if ( schwartz.includes(jp.loseI) && ! jp.tie && ! schwartz.includes(jp.winI) ) { + schwartz.push(jp.winI) + j = -1 // restart loop + } + } + + // store schwartz set to display later + pairs[i].schwartz = (Array.from(schwartz)).map(x => cans[x].id) + + // count losses + + var schwartzlosses = [] + for(var j = 0; j < cans.length; j++){ + schwartzlosses[j] = 0 + } + + + for (var j = 0; j < pairs.length; j++) { + jp = pairs[j] + if ( schwartz.includes(jp.loseI) && ! jp.tie && schwartz.includes(jp.winI) ) { + schwartzlosses[jp.loseI]++ + } + } + + //////////////////////// doesn't work yet + + + + // finda winner in the Schwartz set + + for(var j in schwartz){ // see who wins + var guy = schwartz[j] + if(lossesI[guy]==0){ + tieBreakerWinners.push(cans[guy].id); + } + } + if (tieBreakerWinners.length > 0) break; // stop if someone won + + } + topWinners = tieBreakerWinners + var strongestElimination = i + } + + + +    var result = _result(topWinners,model) +    var color = result.color + if (model.doTop2) var theTop2 = _sortTally(tally).slice(0,2) + if (model.doTop2) result.theTop2 = theTop2 + if (!options.sidebar) return result + + if (unanimousWin) { + text += model.icon(topWinners[0])+" beats all other candidates in one-on-one races.<br>"; + } else { + schwartztext = "" + for (var j in model.candidatesById) { // go through the candidate names in order and display the ones that are in the schwartz set. + if( schwartzFirst.includes(j)) { + schwartztext = schwartztext + model.icon(j) + } + } + schwartztext += " is Schwartz set.<br>" + text += schwartztext + } + + // add text + for (var i in pairs) { + if (reverseExplanation) i = pairs.length - i - 1 + var a = cans[pairs[i].winI] + var b = cans[pairs[i].loseI] + + if (i >= strongestElimination) { + var begintext = "<del>" + var endtext = "</del><br>" + } else { + var begintext = "" + var endtext = "<br>" + } + + + if (! unanimousWin) { + schwartztext = "" + if (pairs[i].hasOwnProperty("schwartz")) { + for (var j in model.candidatesById) { // go through the candidate names in order and display the ones that are in the schwartz set. + if( pairs[i].schwartz.includes(j)) { + schwartztext = schwartztext + model.icon(j) + } + } + var extraspace = cans.length - pairs[i].schwartz.length + var spaces = Math.round(extraspace * 3.4 + 0) + for (var j = 0; j < spaces; j++) { + schwartztext = schwartztext + " " + } + schwartztext += "←" + } else { + var spacelength = Math.round(cans.length * 3.4 + 4) + for (var j = 0; j < spacelength; j++) { + schwartztext = schwartztext + " " + } + } + begintext = schwartztext + begintext + } + + if (pairs[i].tie) { + if(reverseExplanation) { + text += begintext + model.icon(a.id)+"&"+model.icon(b.id) + " tie" + endtext + } else { + text += begintext + "Tie for " + model.icon(a.id)+"&"+model.icon(b.id) + endtext + //text += model.icon(b.id)+" ties "+model.icon(a.id) + endtext + } + } else { + if(reverseExplanation) { + text += begintext + model.icon(b.id)+" lost to "+model.icon(a.id)+" by " + _percentFormat(district, pairs[i].margin) + endtext + } else { + text += begintext + model.icon(a.id)+" beats "+model.icon(b.id)+" by " + _percentFormat(district, pairs[i].margin) + endtext + } + } + + } + + // sort losses + var sortedlosses = [] + for(var i = 0; i < cans.length; i++) sortedlosses.push({name:cans[i].id,losses:losses[cans[i].id]}) + sortedlosses.sort(function(a,b) {return a.losses - b.losses}) + + text += "<br>"; + if (topWinners.length >= 2) { + text += "<b>Eliminate the weakest losses until someone in the Schwartz set has 0 losses.</b><br>" + for(var i=0; i<sortedlosses.length; i++){ + var c = sortedlosses[i].name; + text += model.icon(c)+" got "+losses[c]+" strong losses<br>"; + } + text += _tietext(model,topWinners); + // text = "<b>TIE</b> <br> <br>" + text; + } else if (topWinners.length == 0) { // this shouldn't happen + text = "<b>TIE</b> <br> <br>" + text; + } else { + topWinner = topWinners[0] + if (unanimousWin) { + + } else { + text += "<b>Eliminate the weakest wins until someone in the Schwartz set has 0 losses.<br>" + for(var i=0; i<sortedlosses.length; i++){ + var c = sortedlosses[i].name; + text += model.icon(c)+" got "+losses[c]+" strong losses<br>"; + } + } + text += "</span>"; + text += "<br>"; + text += "<b style='color:"+color+"'>"+model.nameUpper(topWinner)+"</b> WINS"; + // text = "<b style='color:"+color+"'>"+model.nameUpper(topWinner)+"</b> WINS <br> <br>" + text; + } + + // what's the loop? + + result.text = text; + + return result; +}; + + +function pairDraw(model,district,winid,loseid,tie,weightcopy,pastwinnerscopy) { // a function is returned, so that i has a new scope + return function() { + // we have a backup in the "general" stage + // so we can edit the ballots directly + + // model.stage = "pair" + // model.voterSet.copyDistrictBallotsToStage(district,"pair") + // // I could also copy candidates over, but I don't need to. + + model.voterSet.copyDistrictBallotsToStage(district,"backup") + + // leave only the pair in the ballot + for (let voterPerson of district.voterPeople) { + voterPerson.stages[model.stage].ballot.rank = voterPerson.stages[model.stage].ballot.rank.filter(cid => [winid,loseid].includes(cid)) + } + + if (weightcopy) { // we should really use a proper data structure like .round (similar to .stage) + var backupWC = [] + for(var j=0; j<district.voterPeople.length; j++){ + var v = district.voterPeople[j] + backupWC[j] = model.voterGroups[v.iGroup].voterPeople[v.iPoint].weight + let wini = model.candidatesById[winid].i + let losei = model.candidatesById[loseid].i + model.voterGroups[v.iGroup].voterPeople[v.iPoint].weight = weightcopy[j][wini][losei] + } + } + + + model.dontdrawwinners = true + + // draw - the main action + model.drawArenas() + + + // restore backups + model.dontdrawwinners = false + + if (weightcopy) { + for(var j=0; j<district.voterPeople.length; j++){ + var v = district.voterPeople[j] + model.voterGroups[v.iGroup].voterPeople[v.iPoint].weight = backupWC[j] + } + } + + // model.stage = "general" + model.voterSet.loadDistrictBallotsFromStage(district,"backup") + + // also draw past winners + + if (weightcopy) { + for (var i = 0; i < pastwinnerscopy.length; i++) { + var p = pastwinnerscopy[i] + model.candidatesById[p].draw(model.arena.ctx,model.arena) + model.candidatesById[p].drawText("WON",model.arena.ctx,model.arena) + } + } + // draw this pair's better half + if (! tie) { + model.candidatesById[winid].drawText("Better",model.arena.ctx,model.arena) + } else { + model.candidatesById[winid].drawText("Tie",model.arena.ctx,model.arena) + model.candidatesById[loseid].drawText("Tie",model.arena.ctx,model.arena) + } + } +} + +// PairElimination +Election.minimax = function(district, model, options){ // Pairs of candidates are sorted by their win margin. Then we eliminate the weakest wins until there is a Condorcet winner. A condorcet winner has 0 losses. + + options = _electionDefaults(options) + var polltext = _beginElection(district,model,options,"nopoll") + let cans = district.stages[model.stage].candidates + var ballots = model.voterSet.getBallotsDistrict(district) + + var reverseExplanation = true + + + var text = ""; + + + + // Create the WIN tally + var tally = {}; + var losses = {}; + for(var i=0; i<cans.length; i++){ + cID = cans[i].id + tally[cID] = 0 + losses[cID] = 0 + } + + // For each combination... who's the better ranking? + pairs = [] + head2head = {} + + for(var i=0; i<cans.length; i++){ + var a = cans[i]; + head2head[a.id] = {} + } + + for(var i=0; i<cans.length-1; i++){ + var a = cans[i]; + for(var j=i+1; j<cans.length; j++){ + var b = cans[j]; + + // Actually figure out who won. + var aWins = 0; + var bWins = 0; + for(var k=0; k<ballots.length; k++){ + var rank = ballots[k].rank; + if (options.ballotweight) { + var inc = options.ballotweight[k][i][j] + } else if (options.ballotweight2) { + var inc = options.ballotweight2[k] + } else { + var inc = 1 + } + if(rank.indexOf(a.id)<rank.indexOf(b.id)){ + aWins+=inc; // a wins! + }else{ + bWins+=inc; // b wins! + } + } + head2head[a.id][b.id] = aWins + head2head[b.id][a.id] = bWins + + // WINNER? + var winner = (aWins>bWins) ? a : b; + var loser = (aWins>bWins) ? b : a; + if (aWins != bWins) { + tally[winner.id]++; + losses[loser.id]++; + + // Text. + var by,to; + if(winner==a){ + by = aWins; + to = bWins; + pairs.push({winI:i,loseI:j,winN:aWins,loseN:bWins,margin:aWins-bWins,tie:false}) + }else{ + by = bWins; + to = aWins; + pairs.push({winI:j,loseI:i,winN:bWins,loseN:aWins,margin:bWins-aWins,tie:false}) + } + //text += model.icon(a.id)+" vs "+model.icon(b.id)+": "+model.icon(winner.id)+" wins by "+by+" to "+to+"<br>"; + } else { //tie + tally[a.id]++; + tally[b.id]++; + pairs.push({winI:i,loseI:j,winN:aWins,loseN:bWins,margin:aWins-bWins,tie:true}) + //text += model.icon(a.id)+" vs "+model.icon(b.id)+": "+"TIE"+"<br>"; + } + } + } + + if (options.sidebar) { + + text += "<span class='small'>"; + text += "<b>who wins each one-on-one?</b><br>"; + text += pairChart(ballots,district,model,head2head) + + if (reverseExplanation) { + text += "<b>who lost the least, one-on-one?</b><br>"; + } else { + text += "<b>who had the strongest wins, one-on-one?</b><br>"; + } + } + // Was there one who won all???? + var topWinners = []; + + for(var id in tally){ + if(tally[id]==cans.length-1){ + topWinners.push(id); + } + } + var unanimousWin = topWinners.length == 1 + + + pairs = pairs.sort(function(x,y) {return y.margin - x.margin}) // sort in descending order + + + // if there was a tie, then try to break the tie + if (! unanimousWin) { + var tieBreakerWinners = [] + for (var i = pairs.length - 1; i >= 0; i--) { // i represents the strongest pair to be eliminated + + if (! pairs[i].tie) { + losses[cans[pairs[i].loseI].id] -- // eliminate loss + } + + if (i > 0 && pairs[i].margin == pairs[i-1].margin) { // check if there is a tie for weakest win + continue + } + + for(var id in tally){ // see who wins + if(losses[id]==0){ + tieBreakerWinners.push(id); + } + } + if (tieBreakerWinners.length > 0) break; // stop if someone won + } + topWinners = tieBreakerWinners + var strongestElimination = i + } + + if (topWinners.length == 2) { // handle tie + if (head2head[topWinners[0]][topWinners[1]] > head2head[topWinners[1]][topWinners[0]]) { + var tieBrokenText = `In tiebreaker, ${model.icon(topWinners[0])} beats ${model.icon(topWinners[1])} head to head<br>` + topWinners = [topWinners[0]] + } else { + var tieBrokenText = `In tiebreaker, ${model.icon(topWinners[1])} beats ${model.icon(topWinners[0])} head to head<br>` + topWinners = [topWinners[1]] + } + } + + +    var result = _result(topWinners,model) +    var color = result.color + if (model.doTop2) var theTop2 = _sortTally(tally).slice(0,2) + if (model.doTop2) result.theTop2 = theTop2 + if (!options.sidebar) return result + + if (unanimousWin) { + text += model.icon(topWinners[0])+" beats all other candidates in one-on-one races.<br>"; + } + + // add text + var eventsToAssign = [] + + for (var i in pairs) { + if (reverseExplanation) i = pairs.length - i - 1 + var a = cans[pairs[i].winI] + var b = cans[pairs[i].loseI] + + if (i >= strongestElimination) { + var begintext = "<del>" + var endtext = "</del> . weak<br>" + } else { + var begintext = "" + var endtext = "<br>" + } + + + var eventID = 'pair_' + a.id + '_' + b.id + '_' + _rand5() + if (options.round) eventID += '_round' + options.round + eventID += '_district' + district.i + text += '<div id="' + eventID + '" class="pair">' // onmouseover="showOnlyPair(' + a.id + ',' + b.id + ')">' + + if (options.ballotweight) { + var weightcopy = _jcopy(options.ballotweight) + var pastwinnerscopy = _jcopy(options.pastwinners) + } + + eventsToAssign.push({eventID,f:pairDraw(model,district,a.id,b.id,pairs[i].tie,weightcopy,pastwinnerscopy)}) + if (pairs[i].tie) { + if(reverseExplanation) { + text += begintext + model.icon(a.id)+"&"+model.icon(b.id) + " tie" + endtext + } else { + text += begintext + "Tie for " + model.icon(a.id)+"&"+model.icon(b.id) + endtext + //text += model.icon(b.id)+" ties "+model.icon(a.id) + endtext + } + } else { + if(reverseExplanation) { + text += begintext + model.icon(b.id)+" lost to "+model.icon(a.id)+" by " + _percentFormat(district, pairs[i].margin) + endtext + } else { + text += begintext + model.icon(a.id)+" beats "+model.icon(b.id)+" by " + _percentFormat(district, pairs[i].margin) + endtext + } + } + text += '</div>' + + } + result.eventsToAssign = eventsToAssign + + // sort losses + var sortedlosses = [] + for(var i = 0; i < cans.length; i++) sortedlosses.push({name:cans[i].id,losses:losses[cans[i].id]}) + sortedlosses.sort(function(a,b) {return a.losses - b.losses}) + + text += "<br>"; + if (topWinners.length >= 2) { + text += "<b>Eliminate the weakest losses until someone has 0 losses.</b><br>" + for(var i=0; i<sortedlosses.length; i++){ + var c = sortedlosses[i].name; + text += model.icon(c)+" got "+losses[c]+" strong losses<br>"; + } + text += _tietext(model,topWinners); + // text = "<b>TIE</b> <br> <br>" + text; + } else if (topWinners.length == 1) { + topWinner = topWinners[0] + if (unanimousWin) { + + } else { + text += "<b>Eliminate the weakest wins until someone has 0 losses.</b><br>" + for(var i=0; i<sortedlosses.length; i++){ + var c = sortedlosses[i].name; + text += model.icon(c)+" got "+losses[c]+" strong losses<br>"; + } + } + if (tieBrokenText) text += tieBrokenText + text += "</span>"; + text += "<br>"; + text += "<b style='color:"+color+"'>"+model.nameUpper(topWinner)+"</b> WINS"; + // text = "<b style='color:"+color+"'>"+model.nameUpper(topWinner)+"</b> WINS <br> <br>" + text; + } else { + text += "No Candidates <br>" + } + + // what's the loop? + + result.text = text; + + return result; +}; + +// PairElimination +Election.rankedPairs = function(district, model, options){ // Pairs of candidates are sorted by their win margin. Then we eliminate the weakest wins until there is a Condorcet winner. A condorcet winner has 0 losses. + + options = _electionDefaults(options) + var polltext = _beginElection(district,model,options,"nopoll") + let cans = district.stages[model.stage].candidates + + var reverseExplanation = false + + var text = ""; + + var ballots = model.voterSet.getBallotsDistrict(district) + + if (options.sidebar) { + + text += "<span class='small'>"; + text += "<b>who wins each one-on-one?</b><br>"; + text += pairChart(ballots,district,model) + + if (reverseExplanation) { + text += "<b>who lost the least, one-on-one?</b><br>"; + } else { + text += "<b>who had the strongest wins, one-on-one?</b><br>"; + } + } + + // Create the WIN tally var tally = {}; + var losses = {}; for(var candidateID in model.candidatesById) tally[candidateID] = 0; + for(var candidateID in model.candidatesById) losses[candidateID] = 0; + + // For each combination... who's the better ranking? + pairs = [] + for(var i=0; i<cans.length-1; i++){ + var a = cans[i]; + for(var j=i+1; j<cans.length; j++){ + var b = cans[j]; + + // Actually figure out who won. + var aWins = 0; + var bWins = 0; + for(var k=0; k<ballots.length; k++){ + var rank = ballots[k].rank; + if(rank.indexOf(a.id)<rank.indexOf(b.id)){ + aWins++; // a wins! + }else{ + bWins++; // b wins! + } + } + + // WINNER? + var winner = (aWins>bWins) ? a : b; + var loser = (aWins>bWins) ? b : a; + if (aWins != bWins) { + tally[winner.id]++; + losses[loser.id]++; + + // Text. + var by,to; + if(winner==a){ + by = aWins; + to = bWins; + pairs.push({winI:i,loseI:j,winN:aWins,loseN:bWins,margin:aWins-bWins,tie:false}) + }else{ + by = bWins; + to = aWins; + pairs.push({winI:j,loseI:i,winN:bWins,loseN:aWins,margin:bWins-aWins,tie:false}) + } + //text += model.icon(a.id)+" vs "+model.icon(b.id)+": "+model.icon(winner.id)+" wins by "+by+" to "+to+"<br>"; + } else { //tie + tally[a.id]++; + tally[b.id]++; + pairs.push({winI:i,loseI:j,winN:aWins,loseN:bWins,margin:aWins-bWins,tie:true}) + //text += model.icon(a.id)+" vs "+model.icon(b.id)+": "+"TIE"+"<br>"; + } + } + } + + // Was there one who won all???? + var topWinners = []; + + for(var id in tally){ + if(tally[id]==cans.length-1){ + topWinners.push(id); + } + } + var unanimousWin = topWinners.length == 1 + + + pairs = pairs.sort(function(x,y) {return y.margin - x.margin}) // sort in descending order + + + // if there was a tie, then try to break the tie + if (! unanimousWin) { + var showdead = true // option: should we show the dead candidates? + var tieBreakerWinners = [] + var hadawin = new Set() + var dead = new Set() + var surviving = new Set() + var survivedq = function (w3) { + if (! dead.has(w3)) { // this guy hasn't lost yet, so make sure he's in the survivor list + surviving.add(w3) + } + return + } + for (var i = 0; i < pairs.length; i++) { // i represents the strongest pair to be eliminated + var w3 = pairs[i].winI + var l3 = pairs[i].loseI + if (pairs[i].tie) { + survivedq(w3) + survivedq(l3) + } else if (surviving.size == 1 && surviving.has(l3) && dead.has(w3)) { + // check if there is a conflict + // if we're about to remove our last survivor, don't do it + pairs[i].conflict = true + } else { + dead.add(l3) + surviving.delete(l3) + survivedq(w3) + } + pairs[i].survivors = (Array.from(surviving)).map(x => cans[x].id) + if (showdead) pairs[i].dead = (Array.from(dead)).map(x => cans[x].id) + } + topWinners = (Array.from(surviving)).map(x => cans[x].id) + } + + + + + +    var result = _result(topWinners,model) +    var color = result.color + if (model.doTop2) var theTop2 = _sortTally(tally).slice(0,2) + if (model.doTop2) result.theTop2 = theTop2 + + if (!options.sidebar) return result + + if (unanimousWin) { + text += model.icon(topWinners[0])+" beats all other candidates in one-on-one races.<br>"; + } + + // add text + + if (! unanimousWin) { + text += "(also, cross out the conflicts)<br>"; + text += "(left list: had a win & no loss)<br>" + var keepShowingSurvivors = true + } + for (var i in pairs) { + if (reverseExplanation) i = pairs.length - i - 1 + var a = cans[pairs[i].winI] + var b = cans[pairs[i].loseI] + + if (pairs[i].conflict) { + var begintext = "<del>" + var endtext = "</del><br>" // "conflict" + } else { + var begintext = "" + var endtext = "<br>" + } + if (! unanimousWin) { // this is the block that shows the survivors + survivorstext = "" + if (keepShowingSurvivors) { + for (var ic in pairs[i].survivors) { + var c = pairs[i].survivors[ic] + survivorstext = survivorstext + model.icon(c) + } + if (showdead) { + survivorstext = survivorstext + ">" + var deadtext = "" + for (var ic in pairs[i].dead) { + var c = pairs[i].dead[ic] + deadtext = model.icon(c) + deadtext + } + survivorstext = survivorstext + deadtext + var extraspace = cans.length - pairs[i].survivors.length - pairs[i].dead.length + survivorstext = survivorstext + "   " + } else { + var extraspace = cans.length - pairs[i].survivors.length + } + if (pairs[i].survivors.length == 1 && pairs[i].dead.length + 1 == cans.length) keepShowingSurvivors = false + + var spaces = Math.round(extraspace * 3.4 + 0) + for (var j = 0; j < spaces; j++) { + survivorstext = survivorstext + " " + } + } else { + var spacelength = Math.round(cans.length * 3.4 + 6) + for (var j = 0; j < spacelength; j++) { + survivorstext = survivorstext + " " + } + } + begintext = survivorstext + begintext + } + + if (pairs[i].tie) { + if(reverseExplanation) { + text += begintext + model.icon(a.id)+"&"+model.icon(b.id) + " tie" + endtext + } else { + text += begintext + "Tie for " + model.icon(a.id)+"&"+model.icon(b.id) + endtext + //text += model.icon(b.id)+" ties "+model.icon(a.id) + endtext + } + } else { + if(reverseExplanation) { + text += begintext + model.icon(b.id)+" lost to "+model.icon(a.id)+" by " + _percentFormat(district, pairs[i].margin) + endtext + } else { + text += begintext + model.icon(a.id)+" beats "+model.icon(b.id)+" by " + _percentFormat(district, pairs[i].margin) + endtext + } + } + + } + + text += "<br>"; + if (topWinners.length >= 2) { + text += _tietext(model,topWinners); + // text = "<b>TIE</b> <br> <br>" + text; + } else if (topWinners.length == 1) { + topWinner = topWinners[0] + text += "</span>"; + text += "<br>"; + text += "<b style='color:"+color+"'>"+model.nameUpper(topWinner)+"</b> WINS"; + // text = "<b style='color:"+color+"'>"+model.nameUpper(topWinner)+"</b> WINS <br> <br>" + text; + } else { + text += "<br> No Candidates <br>" + } + + // what's the loop? + + result.text = text; + + return result; +}; + +Election.rbvote = function(district, model, options){ // Use the RBVote from Rob Legrand + + + options = _electionDefaults(options) + _beginElection_rbvote(district,model) + let cans = district.stages[model.stage].candidates + + if (model.checkRunTextBallots()) { + // var filler = {candidates:[]} + // result = _check01(filler,model) + // if (! result.good) return result + var text = "<span class='small'>"; + rbvote.setreturnstring() // tell rbvote that we might want return strings (unless we're not doing the sidebar) + document.rbform = { + rvote:{ + value:model.textBallotInput + }, + tiebreak:{ + value:"" + }, + ignore:{ + value: "" + }, + reverse:{ + checked: false + } + } + if (! rbvote.readvotes()) return + resultRB = model.rbelection(options.sidebar) // e.g. result = rbvote.calctide() // having a sidebar display means we want to construct explanation strings + + if (resultRB.str) { // e.g. when the sidebar is on + // replace some of the html in the output of rbvote to make it match the style of betterballot + var rbvote_string = (resultRB.str).replace("style.css","./play/css/rbvote.css").replace() + rbvote_string = rbvote_string.replace('<th rowspan="5">for</th>',) + text += rbvote_string + } + text += "</span>"; + result = {text:text}; + + return result; + } + + var reverseExplanation = false + + + result = _check01(district,model) + if (! result.good) return result + + var text = ""; + + var ballots = model.voterSet.getBallotsDistrict(district) + + + if (options.sidebar) { + + text += "<span class='small'>"; + text += "<b>who wins each one-on-one?</b><br>"; + text += pairChart(ballots,district,model) + } + + rbvote.setreturnstring() // tell rbvote that we might want return strings (unless we're not doing the sidebar) + rbvote.readballots(ballots,district,model) + resultRB = model.rbelection(options.sidebar) // e.g. result = rbvote.calctide() // having a sidebar display means we want to construct explanation strings + + + + topWinners = [resultRB.winner] + + + +    var result = _result(topWinners,model) +    var color = result.color + + if (!options.sidebar) return result + + // replace some of the html in the output of rbvote to make it match the style of betterballot + var rbvote_string = (resultRB.str).replace("style.css","./play/css/rbvote.css").replace() + var intext = [] + var outtext = [] + for(var i = model.candidates.length - 1; i >= 0; i--) { + var c = model.candidates[i] + var t = '[^{](' + c.id + ")" + intext.push(t) + outtext.push(model.icon(c.id)) + } + // var outtext = Object.keys(model.candidatesById).map(x => model.icon(x)) + for (var i in intext) { + + rbvote_string = rbvote_string.replace(new RegExp(intext[i],"g"), (match, $1) => { + return match.slice(0,1) + outtext[i] + }) + // rbvote_string = rbvote_string.replace(new RegExp(intext[i],"g"),outtext[i]) + } + rbvote_string = rbvote_string.replace('<th rowspan="5">for</th>',) + + text += rbvote_string + topWinner = topWinners[0] + text += "</span>"; + text += "<br>"; + text += "<b style='color:"+color+"'>"+model.nameUpper(topWinner)+"</b> WINS"; + // text = "<b style='color:"+color+"'>"+model.nameUpper(topWinner)+"</b> WINS <br> <br>" + text; + + result.text = text; + + return result; +}; + +Election.rrv = function(district, model, options){ + + options = _electionDefaults(options) + var polltext = _beginElection(district,model,options,"nopoll") + let cans = district.stages[model.stage].candidates + + var numreps = model.seats + var maxscore = 5 + + if (options.sidebar) { + var text = "" + var history = {} + history.rounds = [] + var v = model.voterSet.getDistrictVoterArray(district) + history.v = v + history.seats = numreps + history.maxscore = maxscore + model.round = -1 + + var weightUsed = v.map( () => 0 ) + var beforeWeightUsed = v.map( () => 0 ) + var beforeWeight = v.map( () => 1 ) + var powerUsed = v.map( () => 0 ) + var beforePowerUsed = v.map( () => 0 ) + } + + var invmaxscore = 1/maxscore + var ballots = model.voterSet.getBallotsDistrict(district) + var ballotweight = [] + var ballotsum = [] + for(var i=0; i<ballots.length; i++){ + ballotweight[i] = 1 + ballotsum[i] = 0 + } + var resolved = false + var tallies = [] + var winnerslist = [] + + var candidates = []; + for(var i=0; i<cans.length; i++){ + candidates.push(cans[i].id); + } + + for(var j=0; j<numreps;j++) { + // Tally the approvals & get winner! + var tally = _zeroTally(cans) + for(var i=0; i<ballots.length; i++){ + var ballot = ballots[i] + for(var k=0; k<candidates.length; k++){ + var candidate = candidates[k]; + tally[candidate] += ballot.scores[candidate] * ballotweight[i] + } + } + tallies.push(tally) + + var winners = _countWinner(tally); + var winner = winners[0] // really we should have a tree of scenarios form here because a whole different group can be chosen from a tiebreaker TODO + winnerslist.push(winner) + + + //reweight + for(var i=0; i<ballots.length; i++){ + var ballot = ballots[i] + var votetotal = tally[winner] + ballotsum[i] += ballot.scores[winner] + var bw = 1/(1+ballotsum[i]*invmaxscore) + + if (options.sidebar) { + weightUsed[i] = ballotweight[i] - bw + } + ballotweight[i] = bw + } + + if (options.sidebar) { + powerUsed = _calcPowerFromWeight(weightUsed,numreps) + } + + if (options.sidebar) { + var roundHistory = { + winners:[model.candidatesById[winner].i], + beforeWeight:_jcopy(beforeWeight), + weightUsed:_jcopy(weightUsed), + beforeWeightUsed:_jcopy(beforeWeightUsed), + powerUsed:_jcopy(powerUsed), + beforePowerUsed:_jcopy(beforePowerUsed), + tally:_jcopy(tally), + } + history.rounds.push(roundHistory) + for(var i=0; i<ballots.length; i++){ + beforeWeightUsed[i] += weightUsed[i] + beforePowerUsed[i] += powerUsed[i] + } + beforeWeight = _jcopy(ballotweight) + + } + // remove winner from candidates + candidates.splice(candidates.indexOf(winner),1) + } + + var result = _result(winnerslist.concat().sort(),model) + + if (options.sidebar) { + + // Caption + var text = ""; + for(j=0; j<winnerslist.length;j++){ + text += '<div id="district'+district.i+'round' + (j+1) + '" class="round">' + text += "<span class='small'>"; + text += "Round " + (j+1); + var tally = tallies[j] + var winner = winnerslist[j]; + if (j>0) text += "<br><b>After votes go to winner,</b>" + text += "<br><b>score as %:</b><br>"; + if (model.doTallyChart) { + text += tallyChart(tally,cans,model,maxscore,ballots.length) + } else { + for(var i=0; i<cans.length; i++){ + var c = cans[i].id; + //text += model.icon(c)+"'s score: "+((tally[c]/district.voterPeople.length).toFixed(2))+" out of 5.00<br>"; + text += model.icon(c)+": "+_percentFormat(district, tally[c] / maxscore) + if (winner == c) text += " ←"//" <--" + text += "<br>"; + } + } + var color = _colorsWinners([winner],model)[0] + text += ""; + //text += model.icon(winner)+" has the highest score, so..."; + text += ""; + text += '<br>' + text += "<b style='color:"+color+"'>"+model.nameUpper(winner)+"</b> WINS <br>"; + // text = "<b style='color:"+color+"'>"+model.nameUpper(winner)+"</b> WINS <br>" + text; + text += "</span>"; + text += '</div>' + text += '<br>' + } + + if(options.sidebar) { + text += '<div id="district'+district.i+'round' + (winnerslist.length+1) + '" class="round">' + text += "Final Winners:"; + text += "<br>"; + for(var j=0; j<winnerslist.length; j++){ + var c = winnerslist[j] + text += model.icon(c)+" "; + } + text += "<br>"; + text += "<br>"; + text += '</div>' + + result.history = history + result.eventsToAssign = [] // we have an interactive caption + result.text = text; + + // attach caption hover functions + for (var i=0; i < winnerslist.length+1; i++) { + var cbDraw = function(i) { // a function is returned, so that i has a new scope + return function() { + model.round = i+1 + model.drawArenas() + model.round = -1 + } + } + var e = { + eventID: "district"+district.i+"round" + (i+1), + f: cbDraw(i) + } + result.eventsToAssign.push(e) + } + } + + } + + // if (model.doTop2) var theTop2 = _sortTally(tally).slice(0,2) + if (model.doTop2) var theTop2 = winnerslist.slice(0,2) /// TODO: see if this actually works + if (model.doTop2) result.theTop2 = theTop2 + + return result; +}; + +function _calcPowerFromWeight(weightUsed,numreps) { + var totalWeightUsed = weightUsed.reduce((p,c) => p + c) // sum + var totalPower = weightUsed.length / numreps // voters per seat + powerUsed = weightUsed.map( x => totalPower * (x / totalWeightUsed) ) + return powerUsed +} + +Election.rav = function(district, model, options){ + + options = _electionDefaults(options) + var polltext = _beginElection(district,model,options,"nopoll") + let cans = district.stages[model.stage].candidates + + var numreps = model.seats + var maxscore = 1 + + if (options.sidebar) { + var text = "" + var history = {} + history.rounds = [] + var v = model.voterSet.getDistrictVoterArray(district) + history.v = v + history.seats = numreps + history.maxscore = maxscore + model.round = -1 + + var weightUsed = v.map( () => 0 ) + var beforeWeightUsed = v.map( () => 0 ) + var beforeWeight = v.map( () => 1 ) + var powerUsed = v.map( () => 0 ) + var beforePowerUsed = v.map( () => 0 ) + } + + var invmaxscore = 1/maxscore + var ballots = model.voterSet.getBallotsDistrict(district) + var ballotweight = [] + var ballotsum = [] + for(var i=0; i<ballots.length; i++){ + ballotweight[i] = 1 + ballotsum[i] = 0 + } + var resolved = false + var tallies = [] + var winnerslist = [] + + var candidates = []; + for(var i=0; i<cans.length; i++){ + candidates.push(cans[i].id); + } + + for(var j=0; j<numreps;j++) { + // Tally the approvals & get winner! + var tally = _zeroTally(cans) + for(var i=0; i<ballots.length; i++){ + var ballot = ballots[i] + for(var k=0; k<candidates.length; k++){ + var candidate = candidates[k]; + tally[candidate] += ballot.scores[candidate] * ballotweight[i] + } + } + tallies.push(tally) + + var winners = _countWinner(tally); + var winner = winners[0] // really we should have a tree of scenarios form here because a whole different group can be chosen from a tiebreaker TODO + winnerslist.push(winner) + + + //reweight + for(var i=0; i<ballots.length; i++){ + var ballot = ballots[i] + var approved = ballot.scores[winner]; + if (approved) { + ballotsum[i]++ + bw = 1/(1+ballotsum[i]*invmaxscore) + if (options.sidebar) { + weightUsed[i] = ballotweight[i] - bw + } + ballotweight[i] = bw + } else { + if (options.sidebar) { + weightUsed[i] = 0 + } + } + } + if (options.sidebar) { + powerUsed = _calcPowerFromWeight(weightUsed,numreps) + } + + if (options.sidebar) { + var roundHistory = { + winners:[model.candidatesById[winner].i], + beforeWeight:_jcopy(beforeWeight), + weightUsed:_jcopy(weightUsed), + beforeWeightUsed:_jcopy(beforeWeightUsed), + powerUsed:_jcopy(powerUsed), + beforePowerUsed:_jcopy(beforePowerUsed), + tally:_jcopy(tally) + + } + history.rounds.push(roundHistory) + for(var i=0; i<ballots.length; i++){ + beforeWeightUsed[i] += weightUsed[i] + beforePowerUsed[i] += powerUsed[i] + } + beforeWeight = _jcopy(ballotweight) + } + + // remove winner from candidates + candidates.splice(candidates.indexOf(winner),1) + } + + var result = _result(winnerslist.concat().sort(),model) + + if (options.sidebar) { + + // Caption + var text = ""; + for(j=0; j<winnerslist.length;j++){ + text += '<div id="district'+district.i+'round' + (j+1) + '" class="round">' + text += "<span class='small'>"; + text += "Round " + (j+1); + var tally = tallies[j] + var winner = winnerslist[j]; + if (j>0) text += "<br><b>After votes go to winner,</b>" + text += "<br><b>score as %:</b><br>"; + if (model.doTallyChart) { + text += tallyChart(tally,cans,model,maxscore,ballots.length) + } else { + for(var i=0; i<cans.length; i++){ + var c = cans[i].id; + //text += model.icon(c)+"'s score: "+((tally[c]/district.voterPeople.length).toFixed(2))+" out of 5.00<br>"; + text += model.icon(c)+": "+_percentFormat(district, tally[c]) + if (winner == c) text += " ←"//" <--" + text += "<br>"; + } + } + var color = _colorsWinners([winner],model)[0] + text += ""; + //text += model.icon(winner)+" has the highest score, so..."; + text += ""; + text += '<br>' + text += "<b style='color:"+color+"'>"+model.nameUpper(winner)+"</b> WINS <br>"; + // text = "<b style='color:"+color+"'>"+model.nameUpper(winner)+"</b> WINS <br>" + text; + text += "</span>"; + text += '</div>' + text += '<br>' + } + + if(options.sidebar) { + text += '<div id="district'+district.i+'round' + (winnerslist.length+1) + '" class="round">' + text += '</span>' + text += "Final Winners:"; + text += "<br>"; + for(var j=0; j<winnerslist.length; j++){ + var c = winnerslist[j] + text += model.icon(c)+" "; + } + text += "<br>"; + text += "<br>"; + text += '</div>' + } + + result.history = history + result.text = text; + // attach caption hover functions + result.eventsToAssign = [] // we have an interactive caption + for (var i=0; i < winnerslist.length+1; i++) { + var cbDraw = function(i) { // a function is returned, so that i has a new scope + return function() { + model.round = i+1 + model.drawArenas() + model.round = -1 + } + } + var e = { + eventID: "district"+district.i+"round" + (i+1), + f: cbDraw(i) + } + result.eventsToAssign.push(e) + } + } + + // if (model.doTop2) var theTop2 = _sortTally(tally).slice(0,2) + if (model.doTop2) var theTop2 = winnerslist.slice(0,2) /// TODO: see if this actually works + if (model.doTop2) result.theTop2 = theTop2 + + return result; +}; + +Election.borda = function(district, model, options){ + + options = _electionDefaults(options) + var polltext = _beginElection(district,model,options,"nopoll") + let cans = district.stages[model.stage].candidates + + // Tally the approvals & get winner! + var numcan = cans.length + var ballots = model.voterSet.getBallotsDistrict(district) + var tally = _zeroTally(cans) + for(var ballot of ballots){ + for(var i=0; i<numcan; i++){ + var candidate = ballot.rank[i]; + tally[candidate] += numcan - i - 1; // reverse the rank and subtract 1 because nobody's going to rank their least favorite. + } + } + var winners = _countWinner(tally); + var result = _result(winners,model) + var color = result.color + + if (model.doTop2) var theTop2 = _sortTally(tally).slice(0,2) + if (model.doTop2) result.theTop2 = theTop2 + if (!options.sidebar) return result + + // Caption + var text = ""; + text += "<span class='small'>"; + // text += "<b>higher score is better</b><br>"; + if (model.doTallyChart) { + text += tallyChart(tally,cans,model,numcan-1,ballots.length) + } else { + for(var i=0; i<cans.length; i++){ + var c = cans[i].id; + text += model.icon(c)+"'s total score: "+tally[c]+" = "+_percentFormat(district, tally[c] / (numcan-1))+"<br>"; + } + } + if(winners.length>=2){ + // NO WINNER?! OR TIE?!?! + text += _tietext(model,winners); + // text = "<b>TIE</b> <br> <br>" + text; + }else{ + var winner = winners[0]; + text += "<br>"; + text += model.icon(winner)+" has the <i>highest</i> score, so...<br>"; + text += "</span>"; + text += "<br>"; + text += "<b style='color:"+color+"'>"+model.nameUpper(winner)+"</b> WINS"; + // text = "<b style='color:"+color+"'>"+model.nameUpper(winner)+"</b> WINS <br> <br>" + text; + } + result.text = text; + return result; +}; + +Election.irv = function(district, model, options){ + + options = _electionDefaults(options) + var polltext = _beginElection(district,model,options,"irv") + let cans = district.stages[model.stage].candidates + + var drawFlows = (model.ballotConcept != "off" || model.arena.viewMan.active) && ( ! options.yeefast ) + drawFlows = options.sidebar // quick temporary fix + if (drawFlows) { + var transfers = [] + var coalitions = [] + var topChoice = [] + var coalitionInRound = [] + var lastlosers = [] + var losers = [] + var tallies = [] + var continuing = [] + } + var ballots = model.voterSet.getBallotsDistrict(district) + cBallots = _jcopy(ballots) + + if (options.sidebar) { + var text = ""; + text += "<span class='small'>"; + text += polltext; + + var history = {} + history.rounds = [] + + var startText = "Who's voters' top choice?"; + history.startText = startText + } + + var resolved = null; + var roundNum = 1; + + var candidates = []; + var startingCandidates = [] + for(var i=0; i< cans.length; i++){ + var cid = cans[i].id + candidates.push(cid); + startingCandidates.push(cid) + } + var loserslist = [] + + if(options.sidebar) { + // var ov = model.voterGroups // original voters + // var temp = JSON.parse(JSON.stringify(model.voterGroups)) // save the voters before changing them + // model.voterGroups = temp + // var vt = [] + // for (var i=0; i<model.voterGroups.length; i++) { + // vt[i]=[] + // for (varj=0; j < model.voterGroups[i].voterPeople.length; j++) + // vt[i][j] = [] + // Object.create(model.voterGroups[i].ballots) // new copy + // } + // model.voterGroups = vt + // // model.voterGroups = model.voterGroups.map( x => Object.create(x)) // new copy + } + while(!resolved){ + + // There are three stages, + // 1. Tallying votes + // 2. Deciding whether to continue + // 3. Eliminating candidates + + // 1. Tally + + // Tally first choices + var pre_tally = _zeroTally(cans) + for(var ballot of cBallots){ + var top = ballot.rank[0]; // just count #1 + pre_tally[top]++; + } + + // Filter + // ONLY list the remaining candidates + var tally = {}; + for(var i=0; i<candidates.length; i++){ + var cID = candidates[i]; + tally[cID] = pre_tally[cID]; + } + + // Explanation Text + if (options.sidebar) + { + text += "<b>round "+roundNum+":</b><br>"; + text += "who's voters' top choice?<br>"; + if (model.doTallyChart) { + text += tallyChart(tally,cans,model,1,ballots.length) + } else { + for(var i=0; i<candidates.length; i++){ + var c = candidates[i]; + text += model.icon(c)+":"+_percentFormat(district, tally[c]) + if(i<candidates.length-1) text+=", "; + } + } + text += "<br>"; + + var roundHistory = {} + } + + if (drawFlows) { + + // find top choices + topChoice[roundNum-1] = cBallots.map(x => x.rank[0]) + + // count coalition members + coalitionInRound[roundNum-1] = {} + for(var i=0; i<candidates.length; i++){ + var cid = candidates[i]; + coalitionInRound[roundNum-1][cid] = {} + for (var m = 0; m < startingCandidates.length; m++) { + var cid2 = startingCandidates[m] + coalitionInRound[roundNum-1][cid][cid2] = 0 + } + } + // for all the ballots + // add to their current top choice's coalition + for(var k=0; k<cBallots.length; k++){ + cid = cBallots[k].rank[0] + first = topChoice[0][k] + coalitionInRound[roundNum-1][cid][first] ++ + } + tallies.push(_jcopy(tally)) + } + + + // 2. DECIDE WHETHER TO CONTINUE + + // Do they have more than 50%? + var winners = _countWinner(tally); + var winner = winners[0]; + var ratio = tally[winner] / district.voterPeople.length; + var option100 = model.opt.irv100 + if (option100) { + if (candidates.length == 1) { + resolved = "done"; + if (options.sidebar) { + var roundText = model.icon(winner)+" is the last candidate standing"; + text += roundText + text += "<br>" + roundHistory.roundText = roundText + } + break; + } + + } else if(ratio>0.5){ + if (winners.length >= 2) { // won't happen bc ratio > .5 + resolved = "tie"; + break; + } + resolved = "done"; + if (options.sidebar) { + var roundText = model.icon(winner)+" has more than 50%"; + text += roundText + text += "<br>" + roundHistory.roundText = roundText + } + break; + } + // Otherwise... runoff... + var losers = _countLoser(tally); + var loser = losers[0]; + if (options.sidebar) var roundText = "" + if (model.opt.breakEliminationTiesIRV && losers.length > 1) { + + loser = losers[Math.floor(losers.length * Math.random())] + + if (options.sidebar) { + + for (var li = 0; li < losers.length ; li++ ) { + cid = losers[li] + roundText += model.icon(cid) + } + var eTieText = " tie for lowest. Break tie.<br>" + // eTieText += model.icon(loser) + " chosen to lose by tiebreaker." + // eTieText += "<br>" + roundText += eTieText + text += eTieText + roundHistory.roundText = roundText + } + + losers = [loser] + } + if (losers.length >= candidates.length) { + resolved = "tie"; + break; + } + if (0 && candidates.length > 2 && candidates.length - losers.length === 1) { + // There's only one candidate left + if (options.sidebar) { + var elimText = model.icon(winner)+" wins because the others tied in this elimination round." + roundText += elimText; + text += elimText + text += "<br>" + roundHistory.roundText = roundText + } + break; + } + loserslist = loserslist.concat(losers) + + if (drawFlows) { + + // keep a list of the most recent losers + lastlosers = losers + + + // assign coalitions for the losers + for (var li = 0; li < losers.length ; li++ ) { + cid = losers[li] + var coalition = { + id: cid, + list: coalitionInRound[roundNum-1][cid] + } + coalitions.push(coalition) + } + + } + + // 3. ELIMINATE + + //text += "nobody's more than 50%. "; + + if (drawFlows) {transfers[roundNum-1] = []} + + for (var li = 0; li < losers.length ; li++ ) { + loser = losers[li]; + if (options.sidebar) { + var eText = "Eliminate loser, "+model.icon(loser)+"."; + roundText += eText + text += eText + text += "<br>" + roundHistory.roundText = roundText + } + + // REMOVE THE LOSER + candidates.splice(candidates.indexOf(loser), 1); // remove from candidates... + for(var i=0; i<cBallots.length; i++){ // remove from ballots + var ranking = cBallots[i].rank; + var loserIndex = ranking.indexOf(loser) + ranking.splice(loserIndex, 1); + } + + } + if (drawFlows) { + // calculate transferred voters + for (var li = 0; li < losers.length ; li++ ) { + loser = losers[li]; + + // make empty data structure for losing candidate + transfer = {from:loser, flows:{}} + for (var k = 0; k < candidates.length; k++) { + var cid = candidates[k] + transfer.flows[cid] = {} + for (var m = 0; m < startingCandidates.length; m++) { + var cid2 = startingCandidates[m] + transfer.flows[cid][cid2] = 0 + } + } + + for(var i=0; i<cBallots.length; i++){ + var old = topChoice[roundNum-1][i] + if (old === loser) { + var first = topChoice[0][i] + var now = cBallots[i].rank[0]; + transfer.flows[now][first] ++ + } + } + transfers[roundNum-1].push(transfer) + + continuing.push(_jcopy(candidates)) + } + } + roundNum++ + if (options.sidebar) { + text += "<br>" + + history.rounds.push(_jcopy(roundHistory)) + } + } + + if (drawFlows) { + + } + + if (model.doTop2 || drawFlows) { + // add the rest of the candidates to the list of "losers" + loserslist = loserslist.concat(_sortTallyRev(tally)) + } + + if (model.doTop2) { + var ll = loserslist.length + var theTop2 = loserslist.slice(ll-1,ll).concat(loserslist.slice(ll-2,ll-1)) + } + + + var result = _result(winners,model) + var color = result.color + + if (model.doTop2) result.theTop2 = theTop2 + + if (drawFlows) { + + // add the rest of the candidates onto the end of the loserslist + // and find their coalitions + for(var i=0; i<candidates.length; i++){ + var cid = candidates[i]; + + if (1) { + var rounds = coalitionInRound.length + var coalition = { + id: cid, + list: coalitionInRound[rounds-1][cid] + } + } else { + var coalition = {id: cid, list:{}} + for (var m = 0; m < startingCandidates.length; m++) { + var cid2 = startingCandidates[m] + coalition.list[cid2] = 0 + } + + // record the coalition + for(var k=0; k<cBallots.length; k++){ + var ranking = cBallots[k].rank; + if (ranking[0] === cid) { + first = topChoice[0][k] + coalition.list[first] ++ + } + } + } + coalitions.push(coalition) + } + + result.loserslist = loserslist + result.canIdByDecision = loserslist + result.transfers = transfers + result.topChoice = topChoice + result.coalitions = coalitions + result.lastlosers = lastlosers + result.nBallots = cBallots.length + result.tallies = tallies + result.continuing = continuing + result.coalitionInRound = coalitionInRound + } + + // district.pollResults = undefined // clear polls for next time + + if (!options.sidebar) return result + + + + if (resolved == "tie") { + var tieText = _tietext(model,winners) + text += tieText + var finalText = tieText + // text = "<b>TIE</b> <br> <br>" + text; + } else { + // END! + text += "</span>"; + text += "<br>"; + text += "<b style='color:"+color+"'>"+model.nameUpper(winner)+"</b> WINS"; + // text = "<b style='color:"+color+"'>"+model.nameUpper(winner)+"</b> WINS <br> <br>" + text; + + var finalText = "Final Winner: "; + finalText += model.icon(winner) + } + + history.rounds.push(roundHistory) + history.afterFinalRound = {finalText:finalText} + result.history = history + + result.text = text; + + return result; +}; + +Election.stv = function(district, model, options){ + + options = _electionDefaults(options) + var polltext = _beginElection(district,model,options,"nopoll") + let cans = district.stages[model.stage].candidates + + var numreps = model.seats + + var drawFlows = (model.ballotConcept != "off" || model.arena.viewMan.active) && ( ! options.yeefast ) + if (drawFlows) { + var transfers = [] + var coalitions = [] + var topChoice = [] + var coalitionInRound = [] + var lastlosers = [] + var losers = [] + var canIdByDecision = [] + var tallies = [] + var continuing = [] + var won = [] + } + + if (options.sidebar) { + var text = "" + var history = {} + history.rounds = [] + var v = model.voterSet.getDistrictVoterArray(district) + history.v = v + history.seats = numreps + history.maxscore = 5 + model.round = -1 + var beforeWeightUsed = v.map( () => 0) + var powerUsed = v.map( () => 0 ) + var beforePowerUsed = v.map( () => 0 ) + } + + var quota = 1/(numreps+1) + + if (options.sidebar) { + var quotapercent = Math.round(quota * 100) + var startText = ""; + startText += "Find " + numreps + " winners.<br>" + startText += "Set quota at 1/(1+" + numreps + ") = " + quotapercent + "%.<br>" + + var text = ""; + text += "<span class='small'>"; + text += startText + text += "<br>" + + startText += "Who's voters' top choice?"; + history.startText = startText + + var hsid = "hide-show-detail-" + _rand5() + + if (0) { + text += `<button onclick='var x = document.getElementById("${hsid}"); + if (x.style.display === "none") { + x.style.display = "block"; + } else { + x.style.display = "none"; + };' + ">Hide/show detailed results</button>` + } + text += `<div id="${hsid}" >` // style="display:none;" + } + var resolved = null; + var roundNum = 1; + + var candidates = []; + var startingCandidates = [] + for(var i=0; i<cans.length; i++){ + var cid = cans[i].id + candidates.push(cid); + startingCandidates.push(cid) + } + var loserslist = [] + var winnerslist = [] + var top = [] + var ballots = model.voterSet.getBallotsDistrict(district) + var cBallots = _jcopy(ballots) + var ballotweight = [] + for(var i=0; i<cBallots.length; i++){ + ballotweight[i] = 1 + } + while(!resolved){ + + + if (options.sidebar) { + + + var stillin = [] + for (var i=0; i<candidates.length; i++) { + stillin.push(model.candidatesById[candidates[i]].i) + } + + var roundHistory = { + beforeWeight:_jcopy(ballotweight), + beforeWeightUsed: _jcopy(beforeWeightUsed), + ballots:_jcopy(cBallots), + stillin: stillin + } + roundHistory.weightUsed = v.map( () => 0) + + text += '<div id="district'+district.i+'round' + (roundNum) + '" class="round">' + text += "<b>round "+roundNum+":</b><br>"; + text += "who's voters' top choice?<br>"; + } + + var pre_tally = _zeroTally(cans) + top = [] + for(var i=0; i<cBallots.length; i++){ + var ballot = cBallots[i] + var first = ballot.rank[0]; // just count #1 + pre_tally[first] += ballotweight[i]; + if (options.sidebar) top.push(first) + } + + + // ONLY tally the remaining candidates... + var tally = {}; + for(var i=0; i<candidates.length; i++){ + var cID = candidates[i]; + tally[cID] = pre_tally[cID]; + } + + + if (options.sidebar) { + + roundHistory.tally = tally + roundHistory.top = top + + // Say 'em... + if (model.doTallyChart) { + text += tallyChart(tally,cans,model,1,ballots.length) + } else { + for(var i=0; i<candidates.length; i++){ + var c = candidates[i]; + // text += model.icon(c)+":"+Math.round(tally[c]); + text += model.icon(c)+":"+_percentFormat(district,tally[c]); + + if(i<candidates.length-1) text+=",<br>"; + } + } + text += "<br>"; + } + + + if (drawFlows) { + + // find top choices + topChoice[roundNum-1] = [] + for(var i=0; i<cBallots.length; i++){ + var top = cBallots[i].rank[0]; + topChoice[roundNum-1][i] = top + } + + // count coalition members + coalitionInRound[roundNum-1] = {} + for(var i=0; i<candidates.length; i++){ + var cid = candidates[i]; + coalitionInRound[roundNum-1][cid] = {} + for (var m = 0; m < startingCandidates.length; m++) { + var cid2 = startingCandidates[m] + coalitionInRound[roundNum-1][cid][cid2] = 0 + } + } + // for all the ballots + // add to their current top choice's coalition + for(var k=0; k<cBallots.length; k++){ + cid = cBallots[k].rank[0] + first = topChoice[0][k] + coalitionInRound[roundNum-1][cid][first] += ballotweight[k] + } + tallies.push(_jcopy(tally)) + } + + + // 2. DECIDE WHETHER TO CONTINUE + + // Do they have more than 50%? + var winners = _countWinner(tally); + var winner = winners[0]; // there needs to be a better tiebreaker here. TODO + var ratio = tally[winner]/district.voterPeople.length; + + // show all the transfers if the 100% option is chosen + var option100 = model.opt.irv100 + var lastwin = numreps - winnerslist.length == 1 // this could be the last winner + var oneleft = candidates.length == 1 // there is only one candidate left + var wait = option100 && lastwin & !oneleft // don't name the last winner unless he's the only one left + + if(ratio>quota && ! wait){ + // if (winners.length >= 2) { // won't happen bc ratio > .5 + // resolved = "tie"; + // break; + // } + var reweight = 1-quota/ratio + winnerslist.push(winner) + + if (options.sidebar) { + var roundText = model.icon(winner)+" has more than " + quotapercent + "%<br>"; + roundText += "select winner, "+model.icon(winner)+".<br>"; + text += roundText + text += "<br>" + roundHistory.roundText = roundText + } + + if (drawFlows) { + var coalition = { + id: winner, + list: coalitionInRound[roundNum-1][winner] + } + coalitions.push(coalition) + } + + // 3. Remove winner from candidates + + candidates.splice(candidates.indexOf(winner), 1); // remove from candidates... + // var ballots = model.voterSet.getBallotsDistrict(district) + for(var i=0; i<cBallots.length; i++){ + var rank = cBallots[i].rank; + if (rank[0] === winner) { + if (options.sidebar) { + var weightUsed = ballotweight[i] * (1-reweight) + roundHistory.weightUsed[i] = weightUsed + beforeWeightUsed[i] += weightUsed + } + ballotweight[i] *= reweight + } + rank.splice(rank.indexOf(winner), 1); // REMOVE THE winner + } + if (options.sidebar) { + powerUsed = _calcPowerFromWeight(roundHistory.weightUsed,numreps) + roundHistory.powerUsed = _jcopy(powerUsed) + roundHistory.beforePowerUsed = _jcopy(beforePowerUsed) + for ( var i = 0; i < powerUsed.length; i++) { + beforePowerUsed[i] += powerUsed[i] + } + } + if (drawFlows) { + canIdByDecision = canIdByDecision.concat(winner) + var transferFrom = [winner] + } + + if (winnerslist.length == numreps) { + resolved = "done" + + break + } + if (candidates.length == 0) break + } else { + winners = [] + winner = null + // Otherwise... runoff... + var losers = _countLoser(tally); + var loser = losers[0]; + if (model.opt.breakEliminationTiesIRV && losers.length > 1) { + loser = losers[Math.floor(losers.length * Math.random())] + losers = [loser] + } + if (losers.length >= candidates.length) { + resolved = "tie"; + winnerslist = winnerslist.concat(losers) + var tiedlosers = losers + break; + } + loserslist = loserslist.concat(losers) + if (drawFlows) canIdByDecision = canIdByDecision.concat(losers) + + if (drawFlows) { + + // keep a list of the most recent losers + lastlosers = losers + + + // assign coalitions for the losers + for (var li = 0; li < losers.length ; li++ ) { + cid = losers[li] + var coalition = { + id: cid, + list: coalitionInRound[roundNum-1][cid] + } + coalitions.push(coalition) + } + + } + + // 3. ELIMINATE + + //text += "nobody's more than 50%. "; + + + for (var li = 0; li < losers.length ; li++ ) { + loser = losers[li]; + + if (options.sidebar) { + var roundText = "eliminate loser, "+model.icon(loser)+"."; + text += roundText + roundHistory.roundText = roundText + text += "<br>" + } + candidates.splice(candidates.indexOf(loser), 1); // remove from candidates... + for(var i=0; i<cBallots.length; i++){ + var ranking = cBallots[i].rank; + var loserIndex = ranking.indexOf(loser) + ranking.splice(loserIndex, 1); // REMOVE THE LOSER + } + } + var transferFrom = losers + } + + + if (drawFlows) { + // calculate transferred voters + + transfers[roundNum-1] = [] + + for (var li = 0; li < transferFrom.length ; li++ ) { + from = transferFrom[li]; + + // make empty data structure for losing candidate + transfer = {from:from, flows:{}} + for (var k = 0; k < candidates.length; k++) { + var cid = candidates[k] + transfer.flows[cid] = {} + for (var m = 0; m < startingCandidates.length; m++) { + var cid2 = startingCandidates[m] + transfer.flows[cid][cid2] = 0 + } + } + + for(var i=0; i<cBallots.length; i++){ + var old = topChoice[roundNum-1][i] + if (old === from) { + var first = topChoice[0][i] + var now = cBallots[i].rank[0]; + transfer.flows[now][first] += ballotweight[i] + } + } + transfers[roundNum-1].push(transfer) + continuing.push(_jcopy(candidates)) + won.push(_jcopy(winnerslist)) + } + } + + + if (candidates.length == 0) { + // we ran out of candidates, everybody won already + resolved = "done" + break + } + + // And repeat! + roundNum++; // TODO: clarify what the round number means, w.r.t ties + + if (options.sidebar) { + + if (winner) { + roundHistory.winners = [model.candidatesById[winner].i] + } else { + roundHistory.winners = [] + } + history.rounds.push(roundHistory) + + text += "<br>" + text += '</div>' + } + } + + if (options.sidebar) { + + if (winner) { + roundHistory.winners = [model.candidatesById[winner].i] + } else { + roundHistory.winners = [] + } + history.rounds.push(roundHistory) + + + if (drawFlows) won.push(_jcopy(winnerslist)) + + text += "<br>" + text += '</div>' + text += '</div>' + + // push out a final count just to evaluate how well the method worked + if (1) { + var stillin = [] + for (var i=0; i<candidates.length; i++) { + stillin.push(model.candidatesById[candidates[i]].i) + } + + var roundHistory = { + beforeWeight:_jcopy(ballotweight), + beforeWeightUsed: _jcopy(beforeWeightUsed), + ballots:_jcopy(cBallots), + stillin: stillin + } + + var pre_tally = _zeroTally(cans) + var top = [] + for(var i=0; i<cBallots.length; i++){ + var ballot = cBallots[i] + var f1 = ballot.rank[0]; // just count #1 + pre_tally[f1] += ballotweight[i]; + top.push(f1) + } + + // ONLY tally the remaining candidates... + var tally = {}; + for(var i=0; i<candidates.length; i++){ + var cID = candidates[i]; + tally[cID] = pre_tally[cID]; + } + + + var winners = _countWinner(tally); + var winner = winners[0]; // there needs to be a better tiebreaker here. TODO + + roundHistory.top = top + roundHistory.tally = tally + roundHistory.ballots = _jcopy(cBallots) + if (winner) { + roundHistory.winners = [model.candidatesById[winner].i] + } else { + roundHistory.winners = [] + } + history.afterFinalRound = roundHistory + + } + } + + if (options.sidebar) { + text += '</div>' + } + + winners = winnerslist.sort() + + if (model.doTop2) { /// TODO: see if this actually works + loserslist = loserslist.concat(_sortTallyRev(tally)) + var ll = loserslist.length + var theTop2 = loserslist.slice(ll-1,ll).concat(loserslist.slice(ll-2,ll-1)) + } + + + var result = _result(winners,model) + var color = result.color + if (model.doTop2) result.theTop2 = theTop2 + + + if (drawFlows) { + + // add the rest of the candidates onto the end of the list of candidates sorted by decision order + // and find their coalitions + for(var i=0; i<candidates.length; i++){ + var cid = candidates[i]; + canIdByDecision.push(cid) + + if (1) { + var rounds = coalitionInRound.length + var coalition = { + id: cid, + list: coalitionInRound[rounds-1][cid] + } + } else { + var coalition = {id: cid, list:{}} + for (var m = 0; m < startingCandidates.length; m++) { + var cid2 = startingCandidates[m] + coalition.list[cid2] = 0 + } + + // record the coalition + for(var k=0; k<cBallots.length; k++){ + var ranking = cBallots[k].rank; + if (ranking[0] === cid) { + first = topChoice[0][k] + coalition.list[first] ++ + } + } + } + coalitions.push(coalition) + } + + result.loserslist = loserslist + result.canIdByDecision = canIdByDecision + result.transfers = transfers + result.topChoice = topChoice + result.coalitions = coalitions + result.lastlosers = lastlosers + result.nBallots = cBallots.length + result.tallies = tallies + result.continuing = continuing + result.won = won + result.coalitionInRound = coalitionInRound + } + + if (options.sidebar) { + text += '<div id="district'+district.i+'round' + (roundNum+1) + '" class="round">' + var finalText = "" + if (resolved == "tie") { + finalText += _tietext(model,tiedlosers); + text += _tietext(model,tiedlosers); + // text = "<b>TIE</b> <br> <br>" + text; + } + text = "<br>" + text + for (var i in winners) { + var winner = winners[i] + var color = _colorsWinners([winner],model)[0] + // END! + text += "</span>"; + text += "<br>" + text += "<b style='color:"+color+"'>"+model.nameUpper(winner)+"</b> WINS "; + // text = "<b style='color:"+color+"'>"+model.nameUpper(winner)+"</b> WINS <br>" + text; + } + text += '</div>' + + + finalText += "Final Winners:"; + finalText += "<br>"; + for(var i=0; i<winners.length; i++){ + var c = winners[i] + finalText += model.icon(c)+" "; + } + + history.afterFinalRound.finalText = finalText + result.history = history + result.eventsToAssign = [] // we have an interactive caption + result.text = text; + + // attach caption hover functions + for (var i=0; i < roundNum+1; i++) { + var cbDraw = function(i) { // a function is returned, so that i has a new scope + return function() { + model.round = i+1 + model.drawArenas() + model.round = -1 + } + } + var e = { + eventID: "district"+district.i+"round" + (i+1), + f: cbDraw(i) + } + result.eventsToAssign.push(e) + } + } + + return result; +}; + +Election.stvMinimax = function(district, model, options){ + + options = _electionDefaults(options) + var polltext = _beginElection(district,model,options,"nopoll") + let cans = district.stages[model.stage].candidates + + var numreps = model.seats + + var drawFlows = (model.ballotConcept != "off" || model.arena.viewMan.active) && ( ! options.yeefast ) + if (drawFlows) { + var transfers = [] + var coalitions = [] + var topChoice = [] + var coalitionInRound = [] + var lastlosers = [] + var losers = [] + var canIdByDecision = [] + var tallies = [] + var continuing = [] + var won = [] + } + + if (options.sidebar) { + var text = "" + var history = {} + history.rounds = [] + var v = model.voterSet.getDistrictVoterArray(district) + history.v = v + history.seats = numreps + history.maxscore = 5 + model.round = -1 + var beforeWeightUsed = v.map( () => 0) + var powerUsed = v.map( () => 0 ) + var beforePowerUsed = v.map( () => 0 ) + } + + var quota = 1/(numreps) + + if (options.sidebar) { + var quotapercent = Math.round(quota * 100) + var startText = ""; + startText += "Find " + numreps + " winners.<br>" + startText += "Set quota at 1/" + numreps + " = " + quotapercent + "%.<br>" + + var text = ""; + text += "<span class='small'>"; + text += startText + text += "<br>" + + startText += "Who's voters' top choice?"; + history.startText = startText + + var hsid = "hide-show-detail-" + _rand5() + + if (0) { + text += `<button onclick='var x = document.getElementById("${hsid}"); + if (x.style.display === "none") { + x.style.display = "block"; + } else { + x.style.display = "none"; + };' + ">Hide/show detailed results</button>` + } + text += `<div id="${hsid}" >` // style="display:none;" + } + var resolved = null; + var roundNum = 1; + + var candidates = []; + var startingCandidates = [] + for(var i=0; i<cans.length; i++){ + var cid = cans[i].id + candidates.push(cid); + startingCandidates.push(cid) + } + var loserslist = [] + var winnerslist = [] + var trueWinnerslist = [] + var top = [] + var ballots = model.voterSet.getBallotsDistrict(district) + var cBallots = _jcopy(ballots) + var ballotweight = [] + for(var i=0; i<cBallots.length; i++){ + ballotweight[i] = 1 + } + while(!resolved){ + + + if (options.sidebar) { + + + var stillin = [] + for (var i=0; i<candidates.length; i++) { + stillin.push(model.candidatesById[candidates[i]].i) + } + + var roundHistory = { + beforeWeight:_jcopy(ballotweight), + beforeWeightUsed: _jcopy(beforeWeightUsed), + ballots:_jcopy(cBallots), + stillin: stillin + } + roundHistory.weightUsed = v.map( () => 0) + + text += '<div id="district'+district.i+'round' + (roundNum) + '" class="round">' + text += "<b>round "+roundNum+":</b><br>"; + text += "who's voters' top choice?<br>"; + } + + var pre_tally = _zeroTally(cans) + top = [] + for(var i=0; i<cBallots.length; i++){ + var ballot = cBallots[i] + var first = ballot.rank[0]; // just count #1 + pre_tally[first] += ballotweight[i]; + if (options.sidebar) top.push(first) + } + + + // ONLY tally the remaining candidates... + var tally = {}; + for(var i=0; i<candidates.length; i++){ + var cID = candidates[i]; + tally[cID] = pre_tally[cID]; + } + + + if (options.sidebar) { + + roundHistory.tally = tally + roundHistory.top = top + + // Say 'em... + if (model.doTallyChart) { + text += tallyChart(tally,cans,model,1,ballots.length) + } else { + for(var i=0; i<candidates.length; i++){ + var c = candidates[i]; + // text += model.icon(c)+":"+Math.round(tally[c]); + text += model.icon(c)+":"+_percentFormat(district,tally[c]); + + if(i<candidates.length-1) text+=",<br>"; + } + } + text += "<br>"; + } + + + if (drawFlows) { + + // find top choices + topChoice[roundNum-1] = [] + for(var i=0; i<cBallots.length; i++){ + var top = cBallots[i].rank[0]; + topChoice[roundNum-1][i] = top + } + + // count coalition members + coalitionInRound[roundNum-1] = {} + for(var i=0; i<candidates.length; i++){ + var cid = candidates[i]; + coalitionInRound[roundNum-1][cid] = {} + for (var m = 0; m < startingCandidates.length; m++) { + var cid2 = startingCandidates[m] + coalitionInRound[roundNum-1][cid][cid2] = 0 + } + } + // for all the ballots + // add to their current top choice's coalition + for(var k=0; k<cBallots.length; k++){ + cid = cBallots[k].rank[0] + first = topChoice[0][k] + coalitionInRound[roundNum-1][cid][first] += ballotweight[k] + } + tallies.push(_jcopy(tally)) + } + + + // 2. DECIDE WHETHER TO CONTINUE + + // Do they have more than 50%? + var winners = _countWinner(tally); + var winner = winners[0]; // there needs to be a better tiebreaker here. TODO + var ratio = tally[winner]/district.voterPeople.length; + + // show all the transfers if the 100% option is chosen + var option100 = model.opt.irv100 + var lastwin = numreps - winnerslist.length == 1 // this could be the last winner + var oneleft = candidates.length == 1 // there is only one candidate left + var wait = option100 && lastwin & !oneleft // don't name the last winner unless he's the only one left + + if( (ratio>=quota && ! wait) || (winnerslist.length + candidates.length == numreps) ){ + // if (winners.length >= 2) { // won't happen bc ratio > .5 + // resolved = "tie"; + // break; + // } + var reweight = 1-quota/ratio + winnerslist.push(winner) + + if (options.sidebar) { + var roundText = model.icon(winner)+" has more than " + quotapercent + "%<br>"; + roundText += "select winning coalition of "+model.icon(winner)+".<br>"; + text += roundText + text += "<br>" + roundHistory.roundText = roundText + } + + if (drawFlows) { + var coalition = { + id: winner, + list: coalitionInRound[roundNum-1][winner] + } + coalitions.push(coalition) + } + + // 3. Remove winner from candidates + + candidates.splice(candidates.indexOf(winner), 1); // remove from candidates... + // var ballots = model.voterSet.getBallotsDistrict(district) + var weightsForMinimax = cBallots.map( () => 0) + for(var i=0; i<cBallots.length; i++){ + var rank = cBallots[i].rank; + if (rank[0] === winner) { + var weightUsed = ballotweight[i] * (1-reweight) + weightsForMinimax[i] = weightUsed + if (options.sidebar) { + roundHistory.weightUsed[i] = weightUsed + beforeWeightUsed[i] += weightUsed + } + + ballotweight[i] *= reweight + } + rank.splice(rank.indexOf(winner), 1); // REMOVE THE winner + } + + var tempOptions = _jcopy(options) // send new options to minimax + tempOptions.ballotweight2 = weightsForMinimax + tempOptions.round = roundNum + // tempOptions.pastwinners = winnerslist + tempOptions.justCount = true + + var roundResult = Election.minimax(district, model,tempOptions) + + var trueWinners = roundResult.winners + var trueWinner = trueWinners[0]; // there needs to be a better tiebreaker here. TODO + trueWinnerslist.push(trueWinner) + + if (options.sidebar) { + text += '</div>' + text += roundResult.text + } + + if (options.sidebar) { + powerUsed = _calcPowerFromWeight(roundHistory.weightUsed,numreps) + roundHistory.powerUsed = _jcopy(powerUsed) + roundHistory.beforePowerUsed = _jcopy(beforePowerUsed) + for ( var i = 0; i < powerUsed.length; i++) { + beforePowerUsed[i] += powerUsed[i] + } + } + if (drawFlows) { + canIdByDecision = canIdByDecision.concat(winner) + var transferFrom = [winner] + } + + if (winnerslist.length == numreps) { + resolved = "done" + + break + } + if (candidates.length == 0) break + } else { + winners = [] + winner = null + // Otherwise... runoff... + var losers = _countLoser(tally); + var loser = losers[0]; + if (model.opt.breakEliminationTiesIRV && losers.length > 1) { + loser = losers[Math.floor(losers.length * Math.random())] + losers = [loser] + } + if (losers.length >= candidates.length) { + resolved = "tie"; + winnerslist = winnerslist.concat(losers) + var tiedlosers = losers + break; + } + loserslist = loserslist.concat(losers) + if (drawFlows) canIdByDecision = canIdByDecision.concat(losers) + + if (drawFlows) { + + // keep a list of the most recent losers + lastlosers = losers + + + // assign coalitions for the losers + for (var li = 0; li < losers.length ; li++ ) { + cid = losers[li] + var coalition = { + id: cid, + list: coalitionInRound[roundNum-1][cid] + } + coalitions.push(coalition) + } + + } + + // 3. ELIMINATE + + //text += "nobody's more than 50%. "; + + + for (var li = 0; li < losers.length ; li++ ) { + loser = losers[li]; + + if (options.sidebar) { + var roundText = "eliminate loser, "+model.icon(loser)+"."; + text += roundText + roundHistory.roundText = roundText + text += "<br>" + } + candidates.splice(candidates.indexOf(loser), 1); // remove from candidates... + for(var i=0; i<cBallots.length; i++){ + var ranking = cBallots[i].rank; + var loserIndex = ranking.indexOf(loser) + ranking.splice(loserIndex, 1); // REMOVE THE LOSER + } + } + var transferFrom = losers + } + + + if (drawFlows) { + // calculate transferred voters + + transfers[roundNum-1] = [] + + for (var li = 0; li < transferFrom.length ; li++ ) { + from = transferFrom[li]; + + // make empty data structure for losing candidate + transfer = {from:from, flows:{}} + for (var k = 0; k < candidates.length; k++) { + var cid = candidates[k] + transfer.flows[cid] = {} + for (var m = 0; m < startingCandidates.length; m++) { + var cid2 = startingCandidates[m] + transfer.flows[cid][cid2] = 0 + } + } + + for(var i=0; i<cBallots.length; i++){ + var old = topChoice[roundNum-1][i] + if (old === from) { + var first = topChoice[0][i] + var now = cBallots[i].rank[0]; + transfer.flows[now][first] += ballotweight[i] + } + } + transfers[roundNum-1].push(transfer) + continuing.push(_jcopy(candidates)) + won.push(_jcopy(winnerslist)) + } + } + + + if (candidates.length == 0) { + // we ran out of candidates, everybody won already + resolved = "done" + break + } + + // And repeat! + roundNum++; // TODO: clarify what the round number means, w.r.t ties + + if (options.sidebar) { + + if (trueWinner) { + roundHistory.winners = [model.candidatesById[trueWinner].i] + } else { + roundHistory.winners = [] + } + history.rounds.push(roundHistory) + + text += "<br>" + text += '</div>' + } + } + + if (options.sidebar) { + + if (trueWinner) { + roundHistory.winners = [model.candidatesById[trueWinner].i] + } else { + roundHistory.winners = [] + } + history.rounds.push(roundHistory) + + + if (drawFlows) won.push(_jcopy(trueWinnerslist)) + + text += "<br>" + text += '</div>' + text += '</div>' + + // push out a final count just to evaluate how well the method worked + if (1) { + var stillin = [] + for (var i=0; i<candidates.length; i++) { + stillin.push(model.candidatesById[candidates[i]].i) + } + + var roundHistory = { + beforeWeight:_jcopy(ballotweight), + beforeWeightUsed: _jcopy(beforeWeightUsed), + ballots:_jcopy(cBallots), + stillin: stillin + } + + var pre_tally = _zeroTally(cans) + var top = [] + for(var i=0; i<cBallots.length; i++){ + var ballot = cBallots[i] + var f1 = ballot.rank[0]; // just count #1 + pre_tally[f1] += ballotweight[i]; + top.push(f1) + } + + // ONLY tally the remaining candidates... + var tally = {}; + for(var i=0; i<candidates.length; i++){ + var cID = candidates[i]; + tally[cID] = pre_tally[cID]; + } + + + var winners = _countWinner(tally); + var winner = winners[0]; // there needs to be a better tiebreaker here. TODO + + roundHistory.top = top + roundHistory.tally = tally + roundHistory.ballots = _jcopy(cBallots) + if (winner) { + roundHistory.winners = [model.candidatesById[winner].i] + } else { + roundHistory.winners = [] + } + history.afterFinalRound = roundHistory + + } + } + + if (options.sidebar) { + text += '</div>' + } + + trueWinners = trueWinnerslist.sort() + + if (model.doTop2) { /// TODO: see if this actually works + loserslist = loserslist.concat(_sortTallyRev(tally)) + var ll = loserslist.length + var theTop2 = loserslist.slice(ll-1,ll).concat(loserslist.slice(ll-2,ll-1)) + } + + + var result = _result(trueWinners,model) + var color = result.color + if (model.doTop2) result.theTop2 = theTop2 + + + if (drawFlows) { + + // add the rest of the candidates onto the end of the list of candidates sorted by decision order + // and find their coalitions + for(var i=0; i<candidates.length; i++){ + var cid = candidates[i]; + canIdByDecision.push(cid) + + if (1) { + var rounds = coalitionInRound.length + var coalition = { + id: cid, + list: coalitionInRound[rounds-1][cid] + } + } else { + var coalition = {id: cid, list:{}} + for (var m = 0; m < startingCandidates.length; m++) { + var cid2 = startingCandidates[m] + coalition.list[cid2] = 0 + } + + // record the coalition + for(var k=0; k<cBallots.length; k++){ + var ranking = cBallots[k].rank; + if (ranking[0] === cid) { + first = topChoice[0][k] + coalition.list[first] ++ + } + } + } + coalitions.push(coalition) + } + + result.loserslist = loserslist + result.canIdByDecision = canIdByDecision + result.transfers = transfers + result.topChoice = topChoice + result.coalitions = coalitions + result.lastlosers = lastlosers + result.nBallots = cBallots.length + result.tallies = tallies + result.continuing = continuing + result.won = won + result.coalitionInRound = coalitionInRound + } + + if (options.sidebar) { + text += '<div id="district'+district.i+'round' + (roundNum+1) + '" class="round">' + var finalText = "" + if (resolved == "tie") { + finalText += _tietext(model,tiedlosers); + text += _tietext(model,tiedlosers); + // text = "<b>TIE</b> <br> <br>" + text; + } + text = "<br>" + text + for (var i in trueWinners) { + var trueWinner = trueWinners[i] + var color = _colorsWinners([trueWinner],model)[0] + // END! + text += "</span>"; + text += "<br>" + text += "<b style='color:"+color+"'>"+model.nameUpper(trueWinner)+"</b> WINS "; + // text = "<b style='color:"+color+"'>"+model.nameUpper(winner)+"</b> WINS <br>" + text; + } + text += '</div>' + + + finalText += "Final Winners:"; + finalText += "<br>"; + for(var i=0; i<trueWinners.length; i++){ + var c = trueWinners[i] + finalText += model.icon(c)+" "; + } + + history.afterFinalRound.finalText = finalText + result.history = history + result.eventsToAssign = [] // we have an interactive caption + result.text = text; + + // attach caption hover functions + for (var i=0; i < roundNum+1; i++) { + var cbDraw = function(i) { // a function is returned, so that i has a new scope + return function() { + model.round = i+1 + model.drawArenas() + model.round = -1 + } + } + var e = { + eventID: "district"+district.i+"round" + (i+1), + f: cbDraw(i) + } + result.eventsToAssign.push(e) + } + } + + return result; +}; + +Election.quotaMinimax = function(district, model, options){ + + options = _electionDefaults(options) + var polltext = _beginElection(district,model,options,"nopoll") + let cans = district.stages[model.stage].candidates + + var numreps = model.seats + + var pairEventsToAssign = [] + + if (options.sidebar) { + var text = "" + var history = {} + history.rounds = [] + var v = model.voterSet.getDistrictVoterArray(district) + history.v = v + history.seats = numreps + history.maxscore = 5 + model.round = -1 + } + + var quota = 1/numreps + + if (options.sidebar) { + var quotapercent = Math.round(quota * 100) + var text = ""; + text += "<span class='small'>"; + text += "Find " + numreps + " winners.<br>" + text += "Set quota at 1/" + numreps + " = " + quotapercent + "%.<br><br>" + } + + var candidates = []; + for(var i=0; i<cans.length; i++){ + candidates.push(cans[i].id); + } + var winnerslist = [] + var ballots = model.voterSet.getBallotsDistrict(district) + var cBallots = _jcopy(ballots) + var oldballots = _jcopy(ballots) + var ballotweight = [] + var numcan = cans.length + for(var i=0; i<cBallots.length; i++){ + ballotweight[i] = [] + for(var j=0; j < cans.length; j++) { + ballotweight[i][j] = [] + for(var k=0; k < cans.length; k++) { + ballotweight[i][j][k] = 1 + } + } + } + var all1 = _jcopy(ballotweight) + var ballotcounted = _jcopy(all1) + + + + // model.stage = "working" + // model.voterSet.copyDistrictBallotsToStage(district,"working") + // var workingCandids = district.stages[model.stage].candidates + var oldCandidates = cans + district.stages[model.stage].candidates = _jcopy(oldCandidates) + for ( var roundNum = 1; roundNum <= numreps; roundNum++) { + + + if (options.sidebar) { + + + var stillin = [] + for (var i=0; i<candidates.length; i++) { + stillin.push(model.candidatesById[candidates[i]].i) + } + + var roundHistory = { + ballotweight:_jcopy(ballotweight), + ballots:_jcopy(cBallots), + stillin: stillin + } + + text += '<div id="district'+district.i+'round' + (roundNum) + '" class="round">' + text += "<b>round "+roundNum+":</b><br>"; + if (roundNum>1) { + text += "Since we already counted the winner's supporters, only count the remaining votes.<br>"; + } + } + + var tempOptions = _jcopy(options) // send new options to minimax + tempOptions.ballotweight = ballotweight + tempOptions.round = roundNum + tempOptions.pastwinners = winnerslist + tempOptions.justCount = true + // need to change candidates + var roundResult = Election.minimax(district, model,tempOptions) + + + pairEventsToAssign = pairEventsToAssign.concat(roundResult.eventsToAssign) + + var winners = roundResult.winners + var winner = winners[0]; // there needs to be a better tiebreaker here. TODO + winnerslist.push(winner) + + if (options.sidebar) { + text += '</div>' + text += roundResult.text + } + + + if (options.sidebar) { + + roundHistory.tally = tally + roundHistory.ballots = _jcopy(cBallots) + if (winner) { + roundHistory.winners = [model.candidatesById[winner].i] + } else { + roundHistory.winners = [] + } + history.rounds.push(roundHistory) + // text += '</div>' + text += "<br>" + } + + + var support = [] + for(var j=0; j < numcan; j++) { + support[j] = [] + for(var k=0; k < numcan; k++) { + support[j][k] = 0 + } + } + + var sequential = false + if (sequential) { + // add up support for the winner + for(var i=0; i<cBallots.length; i++){ + var rank = cBallots[i].rank; + var winpos = rank.indexOf(winner) + for(j=0; j<ballotweight[i].length; j++) { + for(k=0; k<ballotweight[i][j].length; k++) { + if (j==k) continue // skip + var jr = rank.indexOf(oldCandidates[j].id) + var kr = rank.indexOf(oldCandidates[k].id) + if (jr >= winpos && kr >= winpos) { + support[j][k] += ballotweight[i][j][k] + } + } + } + } + // figure out how much to reweight + reweight = [] + for(var j=0; j < numcan; j++) { + reweight[j] = [] + for(var k=0; k < numcan; k++) { + var ratio = support[j][k] / (roundNum * quota * cBallots.length) + // reweight[j][k] = Math.max(0,1-ratio) + if (ratio == 0) { + reweight[j][k] = 1 + } else { + reweight[j][k] = Math.max(0,1-1/ratio) + } + } + } + // reweight and eliminate winner + for(var i=0; i<cBallots.length; i++){ + var rank = cBallots[i].rank; + var winpos = rank.indexOf(winner) + for(j=0; j<ballotweight[i].length; j++) { + for(k=0; k<ballotweight[i][j].length; k++) { + if (j==k) continue // skip + var jr = rank.indexOf(oldCandidates[j].id) + var kr = rank.indexOf(oldCandidates[k].id) + if (jr >= winpos && kr >= winpos) { + ballotweight[i][j][k] *= reweight[j][k] + } + } + } + rank.splice(rank.indexOf(winner), 1); // REMOVE THE winner + } + } else { + // add up support for a the winning candidates so far + for(var i=0; i<oldballots.length; i++){ + var rank = oldballots[i].rank; + for(j=0; j<ballotweight[i].length; j++) { + for(k=0; k<ballotweight[i][j].length; k++) { + if (j==k) continue // skip + var jr = rank.indexOf(oldCandidates[j].id) + var kr = rank.indexOf(oldCandidates[k].id) + var add = 0 + for (var l=0; l<winnerslist.length;l++){ + var w = winnerslist[l] + var winpos = rank.indexOf(w) + if (jr >= winpos && kr >= winpos) { + add = 1 + } + } + ballotcounted[i][j][k] = add + support[j][k] += add + } + } + } + // figure out how much to reweight + // if there is a lot of support for the winning candidates, then the voters can still have positive weight. + // if there is zero support for the winners, then the voters are at full weight. + reweight = [] + for(var j=0; j < numcan; j++) { + reweight[j] = [] + for(var k=0; k < numcan; k++) { + var ratio = support[j][k] / (roundNum * quota * cBallots.length) + if (ratio == 0) { + reweight[j][k] = 1 + } else { + reweight[j][k] = Math.max(0,1-1/ratio) // don't let reweighting go negative + } + } + } + ballotweight = _jcopy(all1) + // reweight and eliminate winner + for(var i=0; i<cBallots.length; i++){ + var rank = cBallots[i].rank; + var winpos = rank.indexOf(winner) + for(j=0; j<ballotweight[i].length; j++) { + for(k=0; k<ballotweight[i][j].length; k++) { + if (ballotcounted[i][j][k]) { + ballotweight[i][j][k] = reweight[j][k] + } + } + } + rank.splice(rank.indexOf(winner), 1); // REMOVE THE winner + } + } + var cids = [] // candidate id's + for (var i=0; i < candidates.length; i++) { + cids.push(candidates[i].id) + } + candidates.splice(cids.indexOf(winner), 1); // remove from candidates... + + } + district.stages[model.stage].candidates = oldCandidates + + if (options.sidebar) { + + roundHistory.tally = tally + roundHistory.ballots = _jcopy(cBallots) + if (winner) { + roundHistory.winners = [model.candidatesById[winner].i] + } else { + roundHistory.winners = [] + } + history.rounds.push(roundHistory) + + text += "<br>" + text += '</div>' + + // push out a final reweight just to evaluate how well the method worked + if (0) { + var stillin = [] + for (var i=0; i<candidates.length; i++) { + stillin.push(model.candidatesById[candidates[i]].i) + } + + var roundHistory = { + ballotweight:_jcopy(ballotweight), + ballots:_jcopy(cBallots), + stillin: stillin + } + + var pre_tally = _zeroTally(cans) + for(var i=0; i<cBallots.length; i++){ + var ballot = cBallots[i] + var f1 = ballot.rank[0]; // just count #1 + pre_tally[f1] += ballotweight[i]; + } + + // ONLY tally the remaining candidates... + var tally = {}; + for(var i=0; i<candidates.length; i++){ + var cID = candidates[i]; + tally[cID] = pre_tally[cID]; + } + + + var winners = _countWinner(tally); + var winner = winners[0]; // there needs to be a better tiebreaker here. TODO + + roundHistory.tally = tally + roundHistory.ballots = _jcopy(cBallots) + if (winner) { + roundHistory.winners = [model.candidatesById[winner].i] + } else { + roundHistory.winners = [] + } + history.rounds.push(roundHistory) + + } + } + + // TODO: fill in last votes + + if (options.sidebar) { + text += '</div>' + } + + winners = winnerslist.sort() + + if (model.doTop2) { /// TODO: see if this actually works + loserslist = loserslist.concat(_sortTallyRev(tally)) + var ll = loserslist.length + var theTop2 = loserslist.slice(ll-1,ll).concat(loserslist.slice(ll-2,ll-1)) + } + + + var result = _result(winners,model) + var color = result.color + + if (model.doTop2) result.theTop2 = theTop2 + + if (options.sidebar) { + text += '<div id="district'+district.i+'round' + (roundNum) + '" class="round">' + text = "<br>" + text + for (var i in winners) { + var winner = winners[i] + var color = _colorsWinners([winner],model)[0] + // END! + text += "</span>"; + text += "<br>"; + // text += "<b style='color:"+color+"'>"+model.nameUpper(winner)+"</b> WINS"; + // text = "<b style='color:"+color+"'>"+model.nameUpper(winner)+"</b> WINS <br>" + text; + } + text += '</div>' + + result.history = history + result.eventsToAssign = [] // we have an interactive caption + result.text = text; + + // attach caption hover functions + for (var i=0; i < roundNum; i++) { + var cbDraw = function(i) { // a function is returned, so that i has a new scope + return function() { + model.round = i+1 + model.drawArenas() + model.round = -1 + } + } + var e = { + eventID: "district"+district.i+"round" + (i+1), + f: cbDraw(i) + } + result.eventsToAssign.push(e) + } + // attach more + for (var i=0; i < pairEventsToAssign.length; i++) { + var e = pairEventsToAssign[i] + result.eventsToAssign.push(e) + } + } + + return result; +}; + +Election.quotaApproval = function(district, model, options){ + + options = _electionDefaults(options) + var polltext = _beginElection(district,model,options,"nopoll") + let cans = district.stages[model.stage].candidates + + var v = model.voterSet.getDistrictVoterArray(district) + + var seats = model.seats + var winners = [] + var winnersIndexes = [] + + if (options.sidebar) { + var text = "" + text += "<span class='small'>"; + var history = {} + history.rounds = [] + history.v = v + history.seats = seats + history.maxscore = 1 + model.round = -1 + + var weightUsed = v.map( () => 0 ) + var beforeWeightUsed = v.map( () => 0 ) + var beforeWeight = v.map( () => 1 ) + var powerUsed = v.map( () => 0 ) + var beforePowerUsed = v.map( () => 0 ) + } + + var q = [] + for (var i=0; i < v.length; i++) { + q.push(1) + } + for (var n = 0; n < cans.length; n++) { + if (winners.length >= seats) { + break + } + var tally = [] + for (var k = 0; k < cans.length; k++) { + tally[k] = 0 + } + for (var i = 0; i < v.length; i++) { + var b = v[i].b + var weight = Math.max(q[i],0) + + for (var k = 0; k < b.length; k++) { + if (winnersIndexes.includes(k)) continue + if (b[k] == 1) { + // add up the number of votes + tally[k] += weight + } + } + } + if(options.sidebar) { + text += '<div id="district'+district.i+'round' + (n+1) + '" class="round">' + text += "Round " + (n+1); + text += "<br>"; + if (model.doTallyChart) { + var cidTally = {} + for(var i=0; i<cans.length; i++){ + var cid = cans[i].id; + cidTally[cid] = tally[i] + } + text += tallyChart(cidTally,cans,model,1,v.length) + } else { + for(var i=0; i<cans.length; i++){ + var c = cans[i].id; + text += model.icon(c)+" got "+_percentFormat(district, tally[i])+"<br>"; + } + } + text += "<br>"; + text += '</div>' + } + // who won this round? + var roundWinners = _countWinner(tally) // need to exclude twice-winners + if (model.opt.breakWinTiesMultiSeat) { + roundWinners = roundWinners[Math.floor(Math.random() * roundWinners.length)] + roundWinners = [roundWinners] + } + roundWinners = roundWinners.map(x => Number(x)) + roundWinners.forEach(x => winnersIndexes.push(Number(x))) + roundWinnersId = roundWinners.map( x => cans[x].id) + roundWinnersId.forEach(x => winners.push(x)) + + // subtract off the quota + for (var i=0; i < roundWinners.length; i++) { + var winnerIndex = roundWinners[i] + var sum = tally[winnerIndex] + var rep = v.length / sum / seats + for (var k=0; k < v.length; k++) { + var b = v[k].b + + var wu = q[k] * rep * b[winnerIndex] // the weight of your contribution toward electing a candidate, weight used + q[k] -= wu // we could just multiply by b[wI] + if (options.sidebar) { + weightUsed[k] = wu + } + } + } + if (options.sidebar) { + powerUsed = _calcPowerFromWeight(weightUsed,seats) + var roundHistory = { + winners: _jcopy(roundWinners), + beforeWeight:_jcopy(beforeWeight), + weightUsed:_jcopy(weightUsed), + beforeWeightUsed:_jcopy(beforeWeightUsed), + powerUsed:_jcopy(powerUsed), + beforePowerUsed:_jcopy(beforePowerUsed), + tally:_jcopy(tally), + } + history.rounds.push(roundHistory) + for(var i=0; i<v.length; i++){ + beforeWeightUsed[i] += weightUsed[i] + beforePowerUsed[i] += powerUsed[i] + } + beforeWeight = _jcopy(q) + } + } + + + if(options.sidebar) { + text += '<div id="district'+district.i+'round' + (n+1) + '" class="round">' + text += "Final Winners:"; + text += "<br>"; + for(var i=0; i<winners.length; i++){ + var c = winners[i] + text += model.icon(c)+" "; + } + text += "<br>"; + text += "<br>"; + text += '</div>' + } + + var result = _result(winners,model) + if (options.sidebar) { + result.history = history + + result.text = text + + result.eventsToAssign = [] // we have an interactive caption + // attach caption hover functions + for (var i=0; i < n+1; i++) { + var cbDraw = function(i) { // a function is returned, so that i has a new scope + return function() { + model.round = i+1 + model.drawArenas() + model.round = -1 + } + } + var e = { + eventID: "district"+district.i+"round" + (i+1), + f: cbDraw(i) + } + result.eventsToAssign.push(e) + } + } + return result +} + +Election.quotaScore = function(district, model, options){ + + options = _electionDefaults(options) + var polltext = _beginElection(district,model,options,"nopoll") + let cans = district.stages[model.stage].candidates + + var v = model.voterSet.getDistrictVoterArray(district) + + var seats = model.seats + var winners = [] + var winnersIndexes = [] + var maxscore = model.voterGroups[0].voterModel.maxscore + + if (options.sidebar) { + var text = "" + text += "<span class='small'>"; + var history = {} + history.rounds = [] + history.v = v + history.seats = seats + history.maxscore = maxscore + model.round = -1 + + var weightUsed = v.map( () => 0 ) + var beforeWeightUsed = v.map( () => 0 ) + var beforeWeight = v.map( () => 1 ) + var powerUsed = v.map( () => 0 ) + var beforePowerUsed = v.map( () => 0 ) + } + + var q = [] + for (var i=0; i < v.length; i++) { + q.push(1) + } + for (var n = 0; n < cans.length; n++) { + if (winners.length >= seats) { + break + } + var tally = [] + for (var k = 0; k < cans.length; k++) { + tally[k] = 0 + } + for (var i = 0; i < v.length; i++) { + var b = v[i].b + var weight = Math.max(q[i],0) + + for (var k = 0; k < b.length; k++) { + if (winnersIndexes.includes(k)) continue + // add up the number of votes + tally[k] += weight * b[k] / maxscore + } + } + if(options.sidebar) { + text += '<div id="district'+district.i+'round' + (n+1) + '" class="round">' + text += "Round " + (n+1); + text += "<br>"; + if (model.doTallyChart) { + var cidTally = {} + for(var i=0; i<cans.length; i++){ + var cid = cans[i].id; + cidTally[cid] = tally[i] + } + text += tallyChart(cidTally,cans,model,1,v.length) + } else { + for(var i=0; i<cans.length; i++){ + var c = cans[i].id; + text += model.icon(c)+" got "+_percentFormat(district, tally[i])+"<br>"; + } + } + text += "<br>"; + text += '</div>' + } + // who won this round? + var roundWinners = _countWinner(tally) // need to exclude twice-winners + if (model.opt.breakWinTiesMultiSeat) { + roundWinners = roundWinners[Math.floor(Math.random() * roundWinners.length)] + roundWinners = [roundWinners] + } + roundWinners = roundWinners.map(x => Number(x)) + roundWinners.forEach(x => winnersIndexes.push(Number(x))) + roundWinnersId = roundWinners.map( x => cans[x].id) + roundWinnersId.forEach(x => winners.push(x)) + + // subtract off the quota + for (var i=0; i < roundWinners.length; i++) { + var winnerIndex = roundWinners[i] + var sum = tally[winnerIndex] + var rep = v.length / sum / seats + for (var k=0; k < v.length; k++) { + var b = v[k].b + var wu = q[k] * rep * b[winnerIndex] / maxscore + q[k] -= wu // we could just multiply by b[wI] + if (options.sidebar) { + weightUsed[k] = wu + } + } + } + if (options.sidebar) { + powerUsed = _calcPowerFromWeight(weightUsed,seats) + var roundHistory = { + winners: _jcopy(roundWinners), + beforeWeight:_jcopy(beforeWeight), + weightUsed:_jcopy(weightUsed), + beforeWeightUsed:_jcopy(beforeWeightUsed), + powerUsed:_jcopy(powerUsed), + beforePowerUsed:_jcopy(beforePowerUsed), + tally:_jcopy(tally), + } + history.rounds.push(roundHistory) + for(var i=0; i<v.length; i++){ + beforeWeightUsed[i] += weightUsed[i] + beforePowerUsed[i] += powerUsed[i] + } + beforeWeight = _jcopy(q) + } + } + + + if(options.sidebar) { + text += '<div id="district'+district.i+'round' + (n+1) + '" class="round">' + text += "Final Winners:"; + text += "<br>"; + for(var i=0; i<winners.length; i++){ + var c = winners[i] + text += model.icon(c)+" "; + } + text += "<br>"; + text += "<br>"; + text += '</div>' + } + + var result = _result(winners,model) + if (options.sidebar) { + result.history = history + + result.text = text + + result.eventsToAssign = [] // we have an interactive caption + // attach caption hover functions + for (var i=0; i < n+1; i++) { + var cbDraw = function(i) { // a function is returned, so that i has a new scope + return function() { + model.round = i+1 + model.drawArenas() + model.round = -1 + } + } + var e = { + eventID: "district"+district.i+"round" + (i+1), + f: cbDraw(i) + } + result.eventsToAssign.push(e) + } + } + return result +} + +Election.monroeSequentialRange = function(district, model, options){ + // Monroe like + // sort scores + // sum only the top quota of ballots + // top means highest ballotweight * score + // or top means highest score + // highest sum wins + // sequential rounds + + options = _electionDefaults(options) + var polltext = _beginElection(district,model,options,"nopoll") + let cans = district.stages[model.stage].candidates + + var v = model.voterSet.getDistrictVoterArray(district) + + var seats = model.seats + var winners = [] + var winnersIndexes = [] + var maxscore = model.voterGroups[0].voterModel.maxscore + + if (options.sidebar) { + var text = "" + text += "<span class='small'>"; + var history = {} + history.rounds = [] + history.v = v + history.seats = seats + history.maxscore = maxscore + model.round = -1 + var powerUsed = v.map( () => 0 ) + var beforePowerUsed = v.map( () => 0 ) + } + + var q = [] + for (var i=0; i < v.length; i++) { + q.push(1) + } + var qPrevious = _jcopy(q) + for (var n = 0; n < cans.length; n++) { + if (winners.length >= seats) { + break + } + var tally = [] + for (var k = 0; k < cans.length; k++) { + tally[k] = 0 + } + + // how many in quota? + var quota = Math.ceil(v.length / seats) // number of voters in a quota + + // TODO: Add weighting so that we can assign all the voters that gave a score or higher. Rather than just choosing them by whatever sort order the sorting algorithm placed them in. + + // make sorted arrays of index and score for each candidate + var bByCan = [] // going to be a sorted array of voters for each candidate + var tally = [] + var lasti = [] // the number of voters needed to complete a quota (based on ballotweight and sort order) + for (var k = 0; k < cans.length; k++) { + bByCan[k] = v.map( (x,i) => [x.b[k],i] ).sort( (a,b) => a[0]-b[0] ).reverse() + // only get q's adding up to quota + var sumq = 0 + lasti[k] = v.length - 1 // default value, all of them are in quota + for (var i = 0; i < v.length; i++) { + var idx = bByCan[k][i][1] + sumq += q[idx] + if (sumq > quota) { + lasti[k] = i + break // this is the i value we want + } + } + + if (options.allocatedScore) { + var iStop = bByCan[k].length - 1 // add up all the scores + } else { + var iStop = lasti[k] + } + + var talsum = 0 + for (var i = 0; i <= iStop; i++) { + var voter = bByCan[k][i] + talsum += voter[0] * q[voter[1]]// sum, score * q + } + tally[k] = talsum/ maxscore + // tally[k] = bByCan[k].slice(0,lasti[k]).reduce( (p,c) => p + c[0]*q[c[1]]) // sum, score * q + if (winnersIndexes.includes(k)) tally[k] = 0 + } + + // display winner + if(options.sidebar) { + text += '<div id="district'+district.i+'round' + (n+1) + '" class="round">' + text += "Round " + (n+1); + text += "<br>"; + if (model.doTallyChart) { + var cidTally = {} + for(var i=0; i<cans.length; i++){ + var cid = cans[i].id; + cidTally[cid] = tally[i] + } + text += tallyChart(cidTally,cans,model,1,v.length) + } else { + for(var i=0; i<cans.length; i++){ + var c = cans[i].id; + text += model.icon(c)+" got "+_percentFormat(district, tally[i])+"<br>"; + } + } + text += "<br>"; + text += '</div>' + } + + // who won this round? + + if (options.star) { + var frontrunners = []; + + for (var i in tally) { + frontrunners.push(i); + } + frontrunners.sort(function(a,b){return tally[b]-tally[a]}) + + if (frontrunners.length >= 2) { + var aWins = 0; + var bWins = 0; + for (var i = 0; i <= iStop; i++) { + var aScore = bByCan[frontrunners[0]][i] + var bScore = bByCan[frontrunners[1]][i] + if(aScore > bScore){ + aWins++; // a wins! + } else if(bScore > aScore){ + bWins++; // b wins! + } + } + + if (bWins > aWins) { + var roundWinners = [frontrunners[1]] + } else if (aWins > bWins) { + var roundWinners = [frontrunners[0]] + } else { + var roundWinners = frontrunners // tie + } + + if (options.sidebar) { + text += "Final Round between top two:<br>"; + if (model.doTallyChart) { + var runoffTally = {} + runoffTally[cans[frontrunners[0]].id] = aWins + runoffTally[cans[frontrunners[1]].id] = bWins + text += tallyChart(runoffTally,cans,model,1,iStop+1) + } else { + text += model.icon(frontrunners[0])+_percentFormat(district, aWins)+". "+model.icon(frontrunners[1]) +_percentFormat(district, bWins) + "<br>"; + } + text += "<br>"; + } + } else { + var roundWinners = frontrunners + } + } else { + var roundWinners = _countWinner(tally) // TODO: need to exclude twice-winners + } + + if (model.opt.breakWinTiesMultiSeat) { + roundWinners = roundWinners[Math.floor(Math.random() * roundWinners.length)] + roundWinners = [roundWinners] + } + roundWinners = roundWinners.map(x => Number(x)) + roundWinners.forEach(x => winnersIndexes.push(Number(x))) + roundWinnersId = roundWinners.map( x => cans[x].id) + roundWinnersId.forEach(x => winners.push(x)) + + // subtract off the quota + for (var k=0; k < roundWinners.length; k++) { + var winnerIndex = roundWinners[k] + // var sum = tally[winnerIndex] + // var rep = v.length / sum / seats + + var bByCanWin = bByCan[winnerIndex] + var lastiWin = lasti[winnerIndex] + var smallestScore = bByCanWin[lastiWin][0] + var cutset = bByCanWin.filter( x => x[0] == smallestScore) // for the voters that gave the minimum score required to be part of the quota, reweight differently + var fullset = bByCanWin.filter( x => x[0] > smallestScore) // for the voters that gave more than the minimum score, use full weight + + var qFull = 0 + for (var i=0; i < fullset.length; i++) { + var idx = fullset[i][1] + qFull += q[idx] + q[idx] = 0 // zero out all within quota + } + + var qCut = 0 + for (var i=0; i < cutset.length; i++) { + var idx = cutset[i][1] + qCut += q[idx] + } + var needTotal = quota - qFull + var needPer = needTotal / cutset.length + for (var i=0; i < cutset.length; i++) { + var idx = cutset[i][1] + q[idx] -= needPer * q[idx] + } + } + var qUsed = [] + var beforeWeightUsed = qPrevious.map( x => 1-x ) + for (var i = 0; i < q.length; i++) { + qUsed.push(qPrevious[i] - q[i]) + } + + if (options.sidebar) { + if (options.allocatedScore) { + var weightCounted = [] + for (var i = 0; i < q.length; i++) { + var idx = bByCan[winnerIndex][i][1] + weightCounted[idx] = qPrevious[idx] * bByCan[winnerIndex][i][0] // calculate weight counted toward a candidate + } + powerUsed = _calcPowerFromWeight(weightCounted,seats) + } else { + powerUsed = _calcPowerFromWeight(qUsed,seats) + } + var roundHistory = { + winners: _jcopy(roundWinners), + beforeWeightUsed: _jcopy(beforeWeightUsed), + weightUsed:_jcopy(qUsed), + beforeWeight:_jcopy(qPrevious), + powerUsed:_jcopy(powerUsed), + beforePowerUsed:_jcopy(beforePowerUsed), + qUsed:_jcopy(qUsed), + tally:_jcopy(tally) + } + history.rounds.push(roundHistory) + for (var i = 0; i < powerUsed.length; i++) { + beforePowerUsed[i] += powerUsed[i] + } + qPrevious = _jcopy(q) + } + } + + + if(options.sidebar) { + text += '<div id="district'+district.i+'round' + (n+1) + '" class="round">' + text += "Final Winners:"; + text += "<br>"; + for(var i=0; i<winners.length; i++){ + var c = winners[i] + text += model.icon(c)+" "; + } + text += "<br>"; + text += "<br>"; + text += '</div>' + } + + var result = _result(winners,model) + if (options.sidebar) { + result.history = history + + result.text = text + + result.eventsToAssign = [] // we have an interactive caption + // attach caption hover functions + for (var i=0; i < n+1; i++) { + var cbDraw = function(i) { // a function is returned, so that i has a new scope + return function() { + model.round = i+1 + model.drawArenas() + model.round = -1 + } + } + var e = { + eventID: "district"+district.i+"round" + (i+1), + f: cbDraw(i) + } + result.eventsToAssign.push(e) + } + } + return result +} + +Election.allocatedScore = function(district, model, options){ + var newOptions = _jcopy(options) // don't modify options object + newOptions.allocatedScore = true + return Election.monroeSequentialRange(district,model, newOptions) // there's only one line that's different between these methods +} + + +Election.starPR = function(district, model, options){ + var newOptions = _jcopy(options) // don't modify options object + newOptions.allocatedScore = true + newOptions.star = true + return Election.monroeSequentialRange(district,model, newOptions) // there's only one line that's different between these methods +} + + +Election.phragmenSequentialRange = function(district, model, options){ + // sequential Phragmen after KP transform + // KP transform means from score to approval in a certain way. It's a particular way of counting support. + + // do KP transform + + // sequential rounds start + // group by ballot weight remaining + // groups[weight class].voters[i] = {b:ballot, i:original index} + // groups[weight class].weight + // for each candidate + // keep track of total ballot weight used, start at 0. This is the same across all supporting ballots. + // add up number of supporting ballots, starting with ballots with most weight, m = 0 + // weight required = divide remaining quota by number of supporting ballots + // if this is the last weight class, then + // update the total ballot weight used by adding the weight required + // exit the loop + // else + // if the weight required is greater than the jump to the next weight class, then + // weight diff = the difference between this weight class and the next weight class + // update the total ballot weight used by adding the weight diff + // update the quota remaining + // continue the loop + // else + // update the total ballot weight used by adding the weight required + // exit the loop + // now we know the total ballot weight used for all the voters in the included ballot weight groups + // the included ballot weight groups are 0 through m + // the last weight group is m + // find the candidate with the lowest total ballot weight used + // For display later, calculate ballot weight used for this candidate from each voter. + // This is just the "total ballot weight used" minus each voters's already used ballot weight. + // sequential rounds end + + options = _electionDefaults(options) + var polltext = _beginElection(district,model,options,"nopoll") + let cans = district.stages[model.stage].candidates + + var v = model.voterSet.getDistrictVoterArray(district) + + var seats = model.seats + var winners = [] + var winnersIndexes = [] + var maxscore = model.voterGroups[0].voterModel.maxscore + + // do KP transform + var va = [] + var t = 0 + for (var i = 0; i < v.length; i++) { + // did the voter give at least this score? + for (var s = 1; s <= maxscore; s++) { + var va0 = [] + for (var k = 0; k < cans.length; k++) { + if (v[i].b[k] >= s) { + va0.push(1) + } else { + va0.push(0) + } + } + va.push({b:va0,i:i,t:t}) // i is voter id, n is transformed voter id + t++ + } + } + + if (options.sidebar) { + var text = "" + text += "<span class='small'>"; + var history = {} + history.rounds = [] + history.v = va + history.seats = seats + history.maxscore = maxscore + model.round = -1 + var powerUsed = v.map( () => 0 ) + var beforePowerUsed = v.map( () => 0 ) + } + + var q = va.map(() => 1) + + var qPrevious = _jcopy(q) + + var qi = v.map( () => 1 ) + var qiPrevious = _jcopy(qi) + var beforeWeightUsed = v.map( () => 0 ) + var tBeforeWeightUsed = [] + + // group by ballot weight remaining + // groups[weight class].voters[i] = {b:ballot, i:original index} + // groups[weight class].weight + var groups = [] + groups[0] = { voters:va, weight:1 } + + for (var n = 0; n < cans.length; n++) { + if (winners.length >= seats) { + break + } + var tally = [] + for (var k = 0; k < cans.length; k++) { + tally[k] = 0 + } + + // how many in quota? + var quota = Math.ceil(va.length / seats) // number of transformed voters in a quota + var allused = [] + var lastgroup = [] // the last group of supporters needed to complete a quota + for (var k = 0; k < cans.length; k++) { + // keep track of total ballot weight used, start at 0. This is the same across all supporting ballots. + var used = 0 + // add up number of supporting ballots, starting with ballots with most weight, m = 0 + var supporting = 0 + var remaining = quota // initial value + + for (var m = 0; m < groups.length; m++) { + var group = groups[m] + // count how many voters support the candidate + var groupvoters = group.voters + var thisGroupSupport = 0 + for (var i = 0; i < groupvoters.length; i++) { + if (groupvoters[i].b[k] === 1) { + thisGroupSupport += 1 + } + } + supporting += thisGroupSupport + // weight required = divide remaining quota by number of supporting ballots + var required = remaining / supporting + + // if this is the last weight class, then + if (m == groups.length - 1) { + // update the total ballot weight used by adding the weight required + used += required + // exit the loop + break + } else { + // weight jump = the difference between this weight class and the next weight class + var jump = groups[m].weight - groups[m+1].weight + // if the weight required is greater than the jump to the next weight class, then + if (required > jump) { + // update the total ballot weight used by adding the weight jump + used += jump + // update the quota remaining + remaining -= jump * supporting + // continue the loop + continue + } else { + // update the total ballot weight used by adding the weight required + used += required + // exit the loop + break + } + } + } + // used = now we know the total ballot weight used for all the voters in the included ballot weight groups + // the included ballot weight groups are 0 through m + // the last weight group is m + allused.push(used) + lastgroup.push(m) + + // tally is just a vestigial functionality for now + // find the candidate with the lowest total ballot weight used + tally[k] = (1-used) * v.length + if (winnersIndexes.includes(k)) tally[k] = -Infinity + } + + // display winner + if(options.sidebar) { + text += '<div id="district'+district.i+'round' + (n+1) + '" class="round">' + text += "Round " + (n+1); + text += "<br>"; + if (0 && model.doTallyChart) { + var cidTally = {} + for(var i=0; i<cans.length; i++){ + var cid = cans[i].id; + cidTally[cid] = tally[i] + } + text += tallyChart(cidTally,cans,model,1,v.length) + } else { + for(var i=0; i<cans.length; i++){ + var c = cans[i].id; + text += model.icon(c)+" uses "+_percentFormat(district, allused[i] * v.length)+"<br>"; + } + } + text += "<br>"; + text += '</div>' + } + + // who won this round? + // find the candidate with the lowest total ballot weight used + + var roundWinners = _countWinner(tally) // need to exclude twice-winners + if (model.opt.breakWinTiesMultiSeat) { + roundWinners = roundWinners[Math.floor(Math.random() * roundWinners.length)] + roundWinners = [roundWinners] + } + roundWinners = roundWinners.map(x => Number(x)) + roundWinners.forEach(x => winnersIndexes.push(Number(x))) + roundWinnersId = roundWinners.map( x => cans[x].id) + roundWinnersId.forEach(x => winners.push(x)) + + // subtract off the quota + for (var k=0; k < roundWinners.length; k++) { + var winnerIndex = roundWinners[k] + // fill up a new group with voters + // if they supported the winner + // otherwise, keep them in the old group + var newVoterGroup = [] + var oldGroups = [] + for (var m = 0; m <= lastgroup[winnerIndex]; m++) { + var group = groups[m] + var groupvoters = group.voters + oldGroups[m] = { voters:[] , weight:groups[m].weight } // make a replacement group + oldVoterGroup = oldGroups[m].voters + for (var i = 0; i < groupvoters.length; i++) { + if (groupvoters[i].b[winnerIndex] === 1) { + var t = group.voters[i].t + q[t] = 1 - allused[winnerIndex] // use up quota + newVoterGroup.push(groupvoters[i]) + } else { + oldVoterGroup.push(groupvoters[i]) + } + } + } + // TODO: This probably won't work with multiple winners in one round. + // remove old groups + groups.splice(0,lastgroup[winnerIndex]+1) + // add old groups back, but only non-supporters of the winning candidate + groups = oldGroups.concat(groups) + // add new group with supporters of the winning candidate + var newGroup = { voters:newVoterGroup, weight:1-allused[winnerIndex] } + groups.push(newGroup) + } + // sort groups by weight remaining, descending + groups.sort( (a,b) => -(a.weight - b.weight) ) + + if (options.sidebar) { + // For display later, calculate ballot weight used for this candidate from each voter. + // This is just the "total ballot weight used" minus each voters's already used ballot weight. + var qUsed = [] + for (var t = 0; t < q.length; t++) { + qUsed.push(qPrevious[t] - q[t]) + tBeforeWeightUsed[t] = 1 - qPrevious[t] // The total weight used before this round. + } + + var qi = [] + var qiUsed = [] + var t = 0 + for (var i = 0; i < v.length; i++) { + // loop through all transformed ballots for this voter + qi[i] = 0 + for (var s = 1; s <= maxscore; s++) { + qi[i] += q[t] // sum up the used fraction + t++ + } + qi[i] *= 1/maxscore // average the used fraction + qiUsed[i] = qiPrevious[i] - qi[i] + beforeWeightUsed[i] = 1 - qiPrevious[i] + } + powerUsed = _calcPowerFromWeight(qiUsed,seats) + var roundHistory = { + winners: _jcopy(roundWinners), + beforeWeightUsed: _jcopy(beforeWeightUsed), + weightUsed: _jcopy(qiUsed), + beforeWeight: _jcopy(qiPrevious), + tBeforeWeightUsed: _jcopy(tBeforeWeightUsed), + tBeforeWeight:_jcopy(qPrevious), // qt is the ballot weight for the transformed ballots. I'm not using this yet. + tWeightUsed:_jcopy(qUsed), + powerUsed:_jcopy(powerUsed), + beforePowerUsed:_jcopy(beforePowerUsed), + tally:_jcopy(tally) + } + for (var i = 0; i < powerUsed.length; i++) { + beforePowerUsed[i] += powerUsed[i] + } + qPrevious = _jcopy(q) + qiPrevious = _jcopy(qi) + history.rounds.push(roundHistory) + } + } + + + if(options.sidebar) { + text += '<div id="district'+district.i+'round' + (n+1) + '" class="round">' + text += "Final Winners:"; + text += "<br>"; + for(var i=0; i<winners.length; i++){ + var c = winners[i] + text += model.icon(c)+" "; + } + text += "<br>"; + text += "<br>"; + text += '</div>' + } + + var result = _result(winners,model) + if (options.sidebar) { + result.history = history + + result.text = text + + result.eventsToAssign = [] // we have an interactive caption + // attach caption hover functions + for (var i=0; i < n+1; i++) { + var cbDraw = function(i) { // a function is returned, so that i has a new scope + return function() { + model.round = i+1 + model.drawArenas() + model.round = -1 + } + } + var e = { + eventID: "district"+district.i+"round" + (i+1), + f: cbDraw(i) + } + result.eventsToAssign.push(e) + } + } + return result +} + + +Election.phragmenMax = function(district, model, options){ + + return lpGeneral(_solvePhragmenMaxKP,district,model,options) + +} + +function lpGeneral(_solver,district,model,options) { + + options = _electionDefaults(options) + var polltext = _beginElection(district,model,options,"score") + let cans = district.stages[model.stage].candidates + + // Tally the approvals & get winner! + var ballots = model.voterSet.getBallotsDistrict(district) + + if (cans.length == 0 || ballots.length == 0) return _result([],model) + + var b = _getBallotsAsB(ballots, cans) + + if (model.system == "PAV") { + var maxscore = 1 + } else { + var maxscore = 5 + } + + var phragmenResult = _solver(b,model.seats,maxscore) + district.stages[model.stage].lpResult = phragmenResult.results + district.stages[model.stage].assignments = phragmenResult.assignments + + var winners = _getWinnersFromPhragmenResult(phragmenResult,cans) + var iWinners = _getIWinnersFromPhragmenResult(phragmenResult,cans) + + + // var winners = _countWinner(tally); + var result = _result(winners,model) + var color = result.color + + var text = ""; + if (options.sidebar) { + text += "<span class='small'>"; + } + if (0 && options.sidebar) { + + // Caption + var winner = winners[0]; + if ("Auto" == model.autoPoll) text += polltext; + text += "<b>score as % of max possible: </b><br>"; + for(var i=0; i<cans.length; i++){ + var c = cans[i].id; + text += model.icon(c)+"'s score: "+_percentFormat(district, tally[c] / maxscore)+"<br>"; + } + } + + if (options.sidebar) { + if(!winner | winners.length>=2){ + // NO WINNER?! OR TIE?!?! + text += _tietext(model,winners); + // text = "<b>TIE</b> <br> <br>" + text; + } else { + text += "<br>"; + text += model.icon(winner)+" has the highest score, so...<br>"; + text += "</span>"; + text += "<br>"; + text += "<b style='color:"+color+"'>"+model.nameUpper(winner)+"</b> WINS"; + // text = "<b style='color:"+color+"'>"+model.nameUpper(winner)+"</b> WINS <br> <br>" + text; + } + + result.text = text; + + result.iWinners = iWinners // district candidate indexes of winners + result.history = {} + result.history.rounds = [] + for (var r = 0; r < iWinners.length; r++) { + var k = iWinners[r] + if (r == 0) { + var beforeWeightUsed = b.map( () => 0) + var beforePowerUsed = b.map( () => 0) + } else { + for (var i = 0; i < weightUsed.length; i++) { + beforeWeightUsed[i] += weightUsed[i] + beforePowerUsed[i] += powerUsed[i] + } + } + if (model.system == "PAV") { + if (r == 0) { + var beforeSumElected = b.map( () => 0) + } else { + beforeSumElected = afterSumElected + } + var afterSumElected = [] + var afterWeightUsed = [] + var weightUsed = [] + for (var i = 0; i < b.length; i++) { + // var weightUsed = phragmenResult.assignments.map( x => x[k]) + var additionalElected = (phragmenResult.assignments[i][k] > 0) ? 1 : 0 + afterSumElected[i] = additionalElected + beforeSumElected[i] + var weightRemaining = (afterSumElected[i] == 0) ? 1 : (1 / (1+afterSumElected[i])) + afterWeightUsed[i] = 1 - weightRemaining + weightUsed[i] = afterWeightUsed[i] - beforeWeightUsed[i] + } + } else { + var weightUsed = phragmenResult.assignments.map( x => x[k]) + } + var powerUsed = phragmenResult.assignments.map( x => x[k]) + var winners = [district.candidates[k].i] + var round = { + weightUsed:_jcopy(weightUsed), + beforeWeightUsed:_jcopy(beforeWeightUsed), + powerUsed:_jcopy(powerUsed), + beforePowerUsed:_jcopy(beforePowerUsed), + winners:_jcopy(winners), + } + result.history.rounds.push(round) + } + result.history.maxscore = maxscore + + } + + if (model.doTop2) var theTop2 = _sortTally(tally).slice(0,2) + if (model.doTop2) result.theTop2 = theTop2 + return result; +}; + + +function _getBallotsAsB(ballots,cans) { + var b = [] + for(var i = 0; i < ballots.length; i++ ){ + var ballot = ballots[i] + b[i] = [] + for(var k = 0; k < cans.length; k++){ + var cid = cans[k].id + b[i][k] = ballot.scores[cid] + } + } + return b +} + +function _solvePhragmenMaxKP(b,seats,maxscore) { + + + // do KP transform + var nk = b[0].length + var ni = b.length + var ba = [] + var baInfo = [] + var t = 0 + for (var i = 0; i < ni; i++) { + // did the voter give at least this score? + for (var s = 1; s <= maxscore; s++) { + var ba0 = [] + for (var k = 0; k < nk; k++) { + if (b[i][k] >= s) { + ba0.push(1) + } else { + ba0.push(0) + } + } + ba.push(ba0) + baInfo.push({b:ba0,i:i,t:t}) // i is voter id, n is transformed voter id + t++ + } + } + maxscore1 = 1 + + phragmenResult = _solvePhragmenMax(ba,seats,maxscore1) + + // reverse KP transform + var a = [] + if (0) { + var t = 0 + for (var i = 0; i < ni; i++) { + a[i] = [] + for (var k = 0; k < nk; k++) { + // loop through all transformed ballots for this voter + a[i][k] = 0 + for (var s = 1; s <= maxscore; s++) { + var t = s-1 + i * maxscore + a[i][k] += phragmenResult.assignments[t][k] // sum up the used fraction + } + a[i][k] *= 1/maxscore // average the used fraction + } + } + } else { + var a = b.map( () => [] ) // empty i by k array + for (var i = 0; i < ni; i++) { + for (var k = 0; k < nk; k++) { + a[i][k] = 0 // zero + } + } + var nt = ba.length + for (var t = 0; t < nt; t++) { + var i = baInfo[t].i + for (var k = 0; k < nk; k++) { + var x = phragmenResult.assignments[t][k] + if (x > 0) { + a[i][k] += x // sum + } + } + } + for (var i = 0; i < ni; i++) { + for (var k = 0; k < nk; k++) { + a[i][k] *= 1/maxscore // average + } + } + } + + testAssignment(phragmenResult.assignments) + testAssignment(a) + + testAssignment(phragmenResult.assignments,ba) + testAssignmentB(a,b) + + phragmenResult.assignments = a + + + return phragmenResult +} + + +function testAssignment(a) { + // test + + var ni = a.length + var nk = a[0].length + + var avg = [] + for (var k = 0; k < nk; k++) { + avg[k] = 0 + } + for (var i = 0; i < ni; i++) { + for (var k = 0; k < nk; k++) { + if (a[i][k] > 0) { + avg[k] += a[i][k] + } + } + } + for (var k = 0; k < nk; k++) { + avg[k] /= nk + } + console.log(avg) +} + +function testAssignmentB(a,b) { + // test + + var ni = a.length + var nk = a[0].length + + var avg = [] + for (var k = 0; k < nk; k++) { + avg[k] = 0 + } + for (var i = 0; i < ni; i++) { + for (var k = 0; k < nk; k++) { + if (a[i][k] > 0) { + avg[k] += a[i][k] * b[i][k] + } + } + } + for (var k = 0; k < nk; k++) { + avg[k] /= nk + } + console.log(avg) +} + +function _solvePhragmenMax(b,seats,maxscore) { + + // doesn't work right now due to limitations of the solver. + + var lb = false + + var nk = b[0].length + var ni = b.length + var nw = seats + + var con = {} + var va = {} + var ints = {} + + var kset = [] + for (var k = 0; k < nk; k++) { + kset.push(k) + } + var iset = [] + for (var i = 0; i < ni; i++) { + iset.push(i) + } + + // variables x for selection of candidates + for (var k of kset) { + va['x' + k] = {} + } + + // candidate is either selected or not, 1 or 0 + for (var k of kset) { + con['x' + k] = {"max": 1} + va['x' + k]['x' + k] = 1 + ints['x' + k] = 1 + } + + // n winners + con["winners"] = {"equal": nw} + for (var k of kset) { + va['x' + k]['winners'] = 1 + } + + // variables for representation assignment + for (var k of kset) { + for (var i of iset) { + va["y" + i + "_" + k] = {} + // con["y" + i + "_" + k] = {"min": 0} // no negative assignment allowed + // va["y" + i + "_" + k]["y" + i + "_" + k] = 1 // no negative assignment allowed + } + } + + // support must be the same for all winning candidates + for (var k of kset) { + con['xby' + k] = {"equal": 0} + va['x' + k]["xby" + k] = ni / seats // the y values are weights and should be 1 on average, so they sum to the number of voters divided by the number of seats. + for (var i of iset) { + va["y" + i + "_" + k]["xby" + k] = -b[i][k] / maxscore + // va["y" + i + "_" + k]["xby" + k] = -1 // or.. same assignment level for all winning candidates + } + } + + // find upper bound of assignment level for voters + va["z"] = {} + for (var i of iset) { + con["zoy" + i] = {"min": 0} + va["z"]["zoy" + i] = 1 + for (var k of kset) { + va["y" + i + "_" + k]["zoy" + i] = -1 + // va["y" + i + "_" + k]["zoy" + i] = -b[i][k] // or.. find ub of support level + } + } + + // if x is not elected, then y is 0 + // Not sure why I needed this. I think there is a bug in the linear programming solver. + if (0) { + for ( var i of iset) { + for (var k of kset) { + var conName = "xy_i" + i + "_k" + k + con[conName] = {"min": 0} + va["x" + k][conName] = 1000 // the limit is 1 / smallest value of b . So, 1/(1/5)) = 5 or anything greater should be good. + va["y" + i + "_" + k][conName] = -1 + } + } + } else if (0) { + for ( var i of iset) { + var conName = "xy_k" + k + con[conName] = {"min": 0} + va["x" + k][conName] = 100000 // the limit is ni / smallest value of b . So, 100 * 5 /(1/5)) = 2500 or anything greater should be good. + for (var k of kset) { + va["y" + i + "_" + k][conName] = -1 + } + } + } + + // minimize highest assignment level + var solver = window.solver, + results, + model = { + "optimize": "z", + "opType": "min", + "constraints": con, + "variables": va, + "ints": ints, + "options": { + "timeout": 30, + "tolerance": 0.05 + } + }; + + // put lower bound on assignment level + if (lb) { + va["w"] = {} + for (var i of iset) { + con["woy" + i] = {"max": 0}, + va["w"]["woy" + i] = 1 + for (var k of kset) { + va["y" + i + "_" + k]["woy" + i] = -1 + } + } + + // minimize difference between upper and lower bounds + va["d"] = {} + con["dzw"] = {"equal": 0} + va["z"]["dzw"] = 1 + va["d"]["dzw"] = -1 + va["w"]["dzw"] = -1 + + model.optimize = "d" + } + + console.log(model) + results = solver.Solve(model); + console.log(results) + console.log(results.z) + var canResult = [] + for (var k = 0; k < nk; k++) { + canResult[k] = results['x' + k] + } + + var assignments = _getAssignmentsFromLP(results, ni, nk) + + var phragmenResult = { results:results, canResult:canResult, assignments:assignments} + + return phragmenResult +} + +// for reference regarding above +// model = { +// "optimize": "z", +// "opType": "min", +// "constraints": { +// "zoyI": {"min": 0}, +// "xbyK": {"equal": 0}, +// "winners": {"equal": 1}, +// "xK": {"max": 1}, +// // "yIK": {"max": 1}, +// "yIK": {"min": 0}, +// }, +// "variables": { +// "z": { +// "zoyI": 1, +// }, +// "yIK": { +// "zoyI": -1, +// "xbyK": -b11, +// }, +// "xK": { +// "xbyK": 1, +// // "xI": 1, +// "winners":1, +// }, +// }, +// "ints": { +// "xK": 1, +// } +// }; + + +function _getWinnersFromPhragmenResult(phragmenResult,cans) { + var winners = [] + for (var k = 0; k < cans.length; k++) { + if (phragmenResult.canResult[k] == 1) { + winners.push(cans[k].id) + } + } + return winners +} + +function _getIWinnersFromPhragmenResult(phragmenResult,cans) { + var winners = [] + for (var k = 0; k < cans.length; k++) { + if (phragmenResult.canResult[k] == 1) { + winners.push(k) + } + } + return winners +} + + +function _getAssignmentsFromLP(results, ni, nk) { + + var a = [] + for(var i = 0; i < ni; i++ ){ + a[i] = [] + for(var k = 0; k < nk; k++){ + var x = results["y" + i + "_" + k] + if (typeof x !== "number") { + x = 0 + } + a[i][k] = x + } + } + return a +} + +Election.equalFacilityLocation = function(district, model, options){ + + var result = lpGeneral(_solveEqualFacilityLocation,district,model,options) + + var lpr = district.stages[model.stage].lpResult + var maxscore = model.voterGroups[0].voterModel.maxscore + var numvoters = district.voterPeople.length + var comboScore = lpr.result / maxscore / numvoters + + var makeIcons = x => x ? x.map(a => model.icon(a)) : "" + + if (options.sidebar) { + var text = ""; + text += ` + <span class='small'> + The computer has determined that with a total combined score of + <b>${_textPercent(comboScore)}</b>, the winners are + ${makeIcons(result.winners)} + <span> + ` + result.text = text + + } + return result + +} + +function _solveEqualFacilityLocation(b,seats,maxscore) { + + var nk = b[0].length + var ni = b.length + var nw = seats + + var con = {} + var va = {} + var ints = {} + + var kset = [] + for (var k = 0; k < nk; k++) { + kset.push(k) + } + var iset = [] + for (var i = 0; i < ni; i++) { + iset.push(i) + } + + // variables x for selection of candidates + for (var k of kset) { + va['x' + k] = {} + } + + // candidate is either selected or not, 1 or 0 + for (var k of kset) { + con['x' + k] = {"max": 1} + va['x' + k]['x' + k] = 1 + ints['x' + k] = 1 + } + + + // n winners + con["winners"] = {"equal": nw} + for (var k of kset) { + va['x' + k]['winners'] = 1 + } + + // variables for representation assignment + for (var k of kset) { + for (var i of iset) { + va["y" + i + "_" + k] = {} + // con["y" + i + "_" + k] = {"min": 0} // no negative assignment allowed + // va["y" + i + "_" + k]["y" + i + "_" + k] = 1 // no negative assignment allowed + } + } + + // voter is either assigned to a candidate or not, 1 or 0 + for (var k of kset) { + for (var i of iset) { + con["y" + i + "_" + k] = {"max": 1} + va["y" + i + "_" + k]["y" + i + "_" + k] = 1 + ints["y" + i + "_" + k] = 1 + } + } + + // voters are assigned exactly once + for (var i of iset) { + con["y" + i] = {"equal":1} + for (var k of kset) { + va["y" + i + "_" + k]["y" + i] = 1 + } + } + + + + + + // assignments must be the same for all winning candidates + // within rounding error + lbSumY = Math.floor( ni / seats ) + ubSumY = Math.ceil( ni / seats ) + for (var k of kset) { + con['lyx' + k] = {"min": 0} // lower bound + con['uyx' + k] = {"max": 0} // upper bound + va['x' + k]["lyx" + k] = -lbSumY + va['x' + k]["uyx" + k] = -ubSumY + for (var i of iset) { + va["y" + i + "_" + k]["lyx" + k] = 1 // -b[i][k] * .2 + va["y" + i + "_" + k]["uyx" + k] = 1 + // va["y" + i + "_" + k]["xby" + k] = -1 // or.. same assignment level for all winning candidates + } + } + + + if (1) { + va["z"] = {} + for (var i of iset) { + for (var k of kset) { + va["y" + i + "_" + k]["z"] = b[i][k] + } + } + } else { + // this doesn't work, we have to set variables to contribute directly to the objective. It's a limitation of the solver, it seems. + // find upper bound of assignment level for voters + con["zby"] = {"equal": 0}, + va["z"] = {} + va["z"]["zby"] = -1 + for (var i of iset) { + for (var k of kset) { + va["y" + i + "_" + k]["zby"] = maxscore-b[i][k] + // va["y" + i + "_" + k]["zby" + i] = -b[i][k] // or.. find ub of support level + } + } + } + + + // minimize highest assignment level + var solver = window.solver, + results, + model = { + "optimize": "z", + "opType": "max", + "constraints": con, + "variables": va, + "ints": ints, + "options": { + "tolerance": 0.05 + } + }; + + console.log(model) + results = solver.Solve(model); + console.log(results) + var canResult = [] + for (var k = 0; k < nk; k++) { + canResult[k] = results['x' + k] + } + + var assignments = _getAssignmentsFromLP(results, ni, nk) + + var phragmenResult = { results:results, canResult:canResult, assignments:assignments} + + return phragmenResult +} + + +Election.pav = function(district, model, options){ + + return lpGeneral(_solvePAV,district,model,options) + +} + +function _solvePAV(b,seats,maxscore) { + + var nk = b[0].length + var ni = b.length + var nw = seats + + // b[i][k] // the vote, whether i voted for k + + var array = Array.from(Array(nk).keys()) // array from 0 to k-1 + // var combos = combine( array, nw, 0) + var combos = makeCombos(array,nw) + + // loop through all combos + var merits = [] + var maxMerit = 0 + var cMax = null + for ( var c = 0; c < combos.length; c++) { + var combo = combos[c] + // add up all the votes for this combo + var merit = 0 + for (var i = 0; i < ni; i++) { + // how many winners did the voter choose? + var denom = 1 + for (var m = 0; m < combo.length; m++) { + var k = combo[m] + if (b[i][k] == 1) { + merit += 1/denom + denom ++ + } + } + + } + merits.push(merit) + if (maxMerit < merit) { + maxMerit = merit + cMax = c + } + } + // we have the winning combo + + var combo = combos[cMax] + var canResult = array.map( (k) => combo.includes(k)) + + var normalize = ni / maxMerit + + var assignments = [] + for (var i = 0; i < ni; i++) { + // how many winners did the voter choose? + var merit = 0 + var denom = 1 + for (var m = 0; m < combo.length; m++) { + var k = combo[m] + if (b[i][k] == 1) { + merit += 1/denom + denom ++ + } + } + // split the merit equally over the choices + if (merit > 0) { + var weight = merit / (denom-1) + } + var iAssignments = Array(nk).map( () => 0) + for (var m = 0; m < combo.length; m++) { // assign weight equally to each winner that the voter voted for. + var k = combo[m] + iAssignments[k] = b[i][k] * weight * normalize + } + assignments.push(iAssignments) + } + + + var results = "dummy" + + return { results:results, canResult:canResult, assignments:assignments} + +} + +function combine(input, len, start) { + if(len === 0) { + return result; + } + for (let i = start; i <= input.length - len; i++) { + result[result.length - len] = input[i]; + combine(input, len-1, i+1 ); + } +} + +function makeCombos(array, nk) { + if (nk > array.length) return []; + + var combos = []; + + makeNextCombos([], 0, nk); + return combos; + + function makeNextCombos(combo, iStart, todo) { + + for (let i = iStart; i < array.length; i++) { + var next = [ ...combo, array[i] ]; + + if (todo == 1) { + combos.push(next); + } + else { + makeNextCombos(next, i + 1, todo - 1); + } + } + } + +} + + +Election.toptwo = function(district, model, options){ // not to be confused with finding the top2 in a poll, which I already made as a variable + + options = _electionDefaults(options) + var polltext = _beginElection(district,model,options,"plurality") + let cans = district.stages[model.stage].candidates + + // Tally the approvals & get winner! + var ballots = model.voterSet.getBallotsDistrict(district) + var tally1 = _zeroTally(cans) + for(var ballot of ballots){ + tally1[ballot.vote]++; + } + var sortedtally = _sortTally(tally1) + var toptwo = sortedtally.slice(0,2) + + // ties. Who matched the scores of the top two? (I'm not sure of the official way to break ties.) + var winningscores = toptwo.map(x => tally1[x]) + toptwo = sortedtally.filter(x => winningscores.includes(tally1[x])) + + // only do 2 candidates + model.stage = "runoff" + district.stages["runoff"] = {candidates: cans.filter( x => toptwo.includes(x.id)) } + + model.updateDistrictBallots(district); + + var ballots2 = model.voterSet.getBallotsDistrict(district) + var tally = _zeroTally(cans) + for(var ballot of ballots2){ + tally[ballot.vote]++; + } + + model.stage = "general" // set to general for display purposes + model.voterSet.loadDistrictBallotsFromStage(district,"general") + + + var winners = _countWinner(tally); + var result = _result(winners,model) + var color = result.color + + if (model.doTop2) var theTop2 = _sortTally(tally).slice(0,2) + if (model.doTop2) result.theTop2 = theTop2 + + + district.pollResults = undefined // clear polls for next time + + if (!options.sidebar) return result + + // Caption + var winner = winners[0]; + var text = ""; + text += "<span class='small'>"; + if ("Auto" == model.autoPoll) text += polltext; + text += "<b>top two move to 2nd round</b><br>"; + if (model.doTallyChart) { + text += tallyChart(tally1,cans,model,1,ballots.length) + } else { + for(var i=0; i<cans.length; i++){ + var c = cans[i].id; + text += model.icon(c)+" got "+_percentFormat(district, tally1[c])+"<br>"; + } + } + text += "<br><b>2nd round</b><br>"; + if (model.doTallyChart) { + var tally2 = {} + for (var cid of toptwo) { + tally2[cid] = tally[cid] + } + text += tallyChart(tally2,cans,model,1,ballots.length) + text += "<br>"; + } else { + for(var i=0; i<cans.length; i++){ + var c = cans[i].id; + if (toptwo.includes(c)) text += model.icon(c)+" got "+_percentFormat(district, tally[c])+"<br>"; + } + } + // Caption text for winner, or tie + if (winners.length == 1) { + if(options.sidebar){ + text += "<br>"; + text += model.icon(winner)+" has the most votes, so...<br>"; + } + text += "</span>"; + text += "<br>"; + text += "<b style='color:"+color+"'>"+model.nameUpper(winner)+"</b> WINS"; + // text = "<b style='color:"+color+"'>"+model.nameUpper(winner)+"</b> WINS <br> <br>" + text; + } else { + text += _tietext(model,winners); + // text = "<b>TIE</b> <br> <br>" + text; + } + result.text = text; + return result +}; + +Election.pluralityWithPrimary = function(district, model, options){ + + options = _electionDefaults(options) + var polltext = _beginElection_pluralityWithPrimary(district,model,options) + let cans = district.stages[model.stage].candidates + + // Look at each voter group and get tallies for all the candidates + let ptallies = _getBallotsAndTallyPrimary(district, model, function(tally, ballot){ + tally[ballot.vote]++; + }); + + // Who won each primary? + let pwinners = [] + for (var i in ptallies) { + let tally = ptallies[i] + pwinners = pwinners.concat(_countWinner(tally)) + } + + // Now we vote in the general election. + // only do 2 candidates + model.stage = "general" + var cans2 = cans.filter( x => pwinners.includes(x.id)) + district.stages["general"] = {candidates: cans2 } + + + if ( ! options.justCount ) { + // fresh polls for general election + district.pollResults = undefined + var generalPollText = runPoll(district,model,options,"plurality") + + model.updateDistrictBallots(district); + } + + var ballots2 = model.voterSet.getBallotsDistrict(district) + var tally = _zeroTally(cans2) + for(var ballot of ballots2){ + tally[ballot.vote]++; + } + + // return original candidates and update voters' ballots + model.stage = "primary" // for display purposes + + // cleanup + // clear the old poll results. we're done with casting ballots. + district.pollResults = undefined + district.primaryPollResults = undefined + + + + var winners = _countWinner(tally); + var result = _result(winners,model) + var color = result.color + if (model.doTop2) var theTop2 = _sortTally(tally).slice(0,2) + if (model.doTop2) result.theTop2 = theTop2 + if (!options.sidebar) return result + + // Caption + var winner = winners[0]; + var text = ""; + text += polltext + text += "<span class='small'>"; + for (let i in ptallies) { + var tally1 = ptallies[i] + let totalPeopleInPrimary = district.parties[i].voterPeople.length + var ip1 = i*1+1 + text += "<b>primary for group " + ip1 + ":</b><br>"; + var pwin = _countWinner(tally1) + + if (model.doTallyChart) { + text += tallyChart(tally1,cans,model,1,totalPeopleInPrimary) + } else { + for(let k=0; k<cans.length; k++){ + let c = cans[k] + let cid = c.id + if (district.parties[i].candidates.includes(c)) { + text += model.icon(cid)+" got "+_primaryPercentFormat(tally1[cid], totalPeopleInPrimary); + if (pwin.includes(cid)) text += " ←" + text += "<br>" + } + } + } + text += "<br>" + } + text += "<b>general election:</b><br>"; + + if (pwinners.length == 1) { + text += "There was only one nominee. Win by default.<br>" + } + + text += generalPollText + + if (model.doTallyChart) { + text += tallyChart(tally,cans,model,1,ballots2.length) + } else { + for(var i=0; i<cans.length; i++){ + var c = cans[i].id; + if (pwinners.includes(c)) text += model.icon(c)+" got "+_percentFormat(district, tally[c])+"<br>"; + } + } + // Caption text for winner, or tie + if (winners.length == 1) { + if(options.sidebar){ + text += "<br>"; + text += model.icon(winner)+" has most votes, so...<br>"; + } + text += "</span>"; + text += "<br>"; + text += "<b style='color:"+color+"'>"+model.nameUpper(winner)+"</b> WINS"; + // text = "<b style='color:"+color+"'>"+model.nameUpper(winner)+"</b> WINS <br> <br>" + text; + } else { + text += _tietext(model,winners); + // text = "<b>TIE</b> <br> <br>" + text; + } + result.text = text; + return result +} + +function _beginElection(district,model,options,polltype) { + + var polltext = "" + + if ( ! options.justCount ) { + + model.stage = "general" + + district.stages = {} + district.stages["general"] = {candidates: district.candidates } + + for ( let voterPerson of district.voterPeople) { + voterPerson.stages = {} + } + + if ("Auto" == model.autoPoll && ! options.dontpoll && polltype !== "nopoll") { + district.pollResults = undefined + polltext += runPoll(district,model,options,polltype) + } + + model.updateDistrictBallots(district) + + } + + return polltext +} + +function _beginElection_pluralityWithPrimary(district,model,options) { + + model.stage = "primary" + + district.stages = {} + district.stages["primary"] = {candidates: district.candidates} + + for ( let voterPerson of district.voterPeople) { + voterPerson.stages = {} + } + + polltext = "" + // Take polls and vote + if ( ! options.justCount ) { + district.primaryPollResults = undefined + polltext += runPrimaryPoll(district,model,options,"plurality") + district.pollResults = undefined + polltext += runPoll(district,model,options,"plurality") + + model.updateDistrictBallots(district) + } + return polltext +} + +function _beginElection_rbvote(district,model) { + + model.stage = "general" + + district.stages = {} + district.stages["general"] = {candidates: district.candidates } + + for ( let voterPerson of district.voterPeople) { + voterPerson.stages = {} + } + + model.updateDistrictBallots(district) +} + +Election.plurality = function(district, model, options){ + + options = _electionDefaults(options) + var polltext = _beginElection(district,model,options,"plurality") + let cans = district.stages[model.stage].candidates + + + // if (model.primaries == "Yes"){ + // Election.pluralityWithPrimary(model, options) + // return + // } + var result = {} + + // Tally the approvals & get winner! + var ballots = model.voterSet.getBallotsDistrict(district) + var tally = _zeroTally(cans) + for(var ballot of ballots){ + tally[ballot.vote]++; + } + var winners = _countWinner(tally); + var result = _result(winners,model) +    var color = result.color + + if (model.doTop2) var theTop2 = _sortTally(tally).slice(0,2) + if (model.doTop2) result.theTop2 = theTop2 + if (!options.sidebar) { + return result + } + + + // Caption + var winner = winners[0]; + + if(options.verbose) { + text = "<span class='small'>"; + for(var i=0; i<cans.length; i++){ + var c = cans[i].id; + text += model.icon(c)+": "+tally[c]; + text+=" votes"; + if(i<cans.length-1) text+=", "; + } + text += "</span>"; + text += "<br>"; + text += "<b style='color:"+color+"'>"+model.nameUpper(winner)+"</b> WINS"; + + result.text = text; + return result + } else if (options.original) { + text = "<span class='small'>"; + for(var i=0; i<cans.length; i++){ + var c = cans[i].id; + text += c+": "+tally[c]; + if(i<cans.length-1) text+=", "; + } + text += "</span>"; + text += "<br>"; + text += "<b style='color:"+color+"'>"+model.nameUpper(winner)+"</b> WINS"; + + result.text = text; + return result + + } + var text = ""; + text += "<span class='small'>"; + if ("Auto" == model.autoPoll) text += polltext; + text += "<b>most votes wins</b><br>"; + if (model.doTallyChart) { + text += tallyChart(tally,cans,model,1,ballots.length) + text += "<br>"; + } else { + for(var i=0; i<cans.length; i++){ + var c = cans[i].id; + text += model.icon(c)+" got "+_percentFormat(district, tally[c])+"<br>"; + } + } + // Caption text for winner, or tie + if (winners.length == 1) { + if(options.sidebar){ + text += "<br>"; + text += model.icon(winner)+" has most votes, so...<br>"; + } + text += "</span>"; + text += "<br>"; + text += "<b style='color:"+color+"'>"+model.nameUpper(winner)+"</b> WINS"; + // text = "<b style='color:"+color+"'>"+model.nameUpper(winner)+"</b> WINS <br> <br>" + text; + } else { + text += _tietext(model,winners); + // text = "<b>TIE</b> <br> <br>" + text; + } + result.text = text; + + return result +}; + + +// HELPERS: + +function _electionDefaults(options) { + options = options || {} + _fillInDefaults(options, { + justCount: false, + dontpoll: false, + }) + return options +} + +function head2HeadTally(model,district,ballots) { + + var cans = district.stages[model.stage].candidates + + head2head = {} + // For each combination... who's the better ranking? + for(var i=0; i<cans.length; i++){ + var a = cans[i]; + head2head[a.id] = {} + for(var j=0; j<cans.length; j++){ + var b = cans[j]; + // How many votes did A get? + var aWins = 0; + for(var m=0; m<ballots.length; m++){ + var rank = ballots[m].rank; + if(rank.indexOf(a.id)<rank.indexOf(b.id)){ + aWins++; // a wins! + } + } + head2head[a.id][b.id] = aWins + } + } + return head2head +} + +function head2HeadScoreTally(model,district,ballots) { + + var cans = district.stages[model.stage].candidates + + head2head = {} + // For each combination... who's the better ranking? + for(var i=0; i<cans.length; i++){ + var a = cans[i]; + head2head[a.id] = {} + for(var j=0; j<cans.length; j++){ + var b = cans[j]; + // How many votes did A get? + var aWins = 0; + for(var m=0; m<ballots.length; m++){ + var ballot = ballots[m] + var ba = ballot.scores[a.id] + var bb = ballot.scores[b.id] + if (ba > bb) { + aWins++; // a wins! + } else if (ba == bb) { + aWins += .5 // ties count as half .. not sure if this is the best way to do this + } + } + head2head[a.id][b.id] = aWins + } + } + return head2head +} + +function runPoll(district,model,options,electiontype){ + + // check to see if there is a need for polling + + if ( ! model.checkRunPoll() ) return "" + + var cans = district.stages[model.stage].candidates + + polltext = "" + + + if (options.sidebar) { + polltext += '<span class="small" >' + if (electiontype=="irv") { + polltext += "A low-risk strategy in IRV is to look at who wins and make a compromise if you're not winning. Voters look down their ballot and pick the first one that defeats the current winner head to head." + polltext += " <br> <br>" + polltext += "Reporting results is done with both head-to-head and instant runoff tallies." + polltext += " <br> <br>" + polltext += "<b>Here are polls for first preferences: </b></br>" + // this strategy could be further refined by voting for people who will be eliminated but who we like better + } else { + polltext += "<b>polling for viable candidates: </b><br>"; + //polltext += "<b>(score > " + (100*threshold/district.voterPeople.length).toFixed(0) + " = half max)</b><br>" + } + } + + var lastTally = {} + var collectTallies = [] + for (var k=0;k<5;k++) { // do the polling many times + + // get polling information + model.updateDistrictBallots(district); + + + // count the votes in the poll + var ballots = model.voterSet.getBallotsDistrict(district) + + if (electiontype == "score") { + // Tally + var tally = _zeroTally(cans) + for(var ballot of ballots){ + for(var candidate in ballot.scores){ + tally[candidate] += ballot.scores[candidate]; + } + } + } else if (electiontype == "3-2-1") { + // Create the tally + // not bad + var nb = _zeroTally(cans) + var tallies = []; + for (var level=0; level < 3; level++) { + tallies.push(_zeroTally(cans)) + } + // Count 'em up + for(var i=0; i<ballots.length; i++){ + var ballot = ballots[i] + for(var candidate in ballot.scores){ + tallies[ballot.scores[candidate]][candidate] += 2; // 2 because we normalize later + if (ballot.scores[candidate]) { + nb[candidate] += 2 + } + } + } + var tally = tallies[2] + // var tally = nb + } else if (electiontype=="approval"){ + // Tally the approvals & get winner! + var tally = _zeroTally(cans) + for(var ballot of ballots){ + for(var candidate in ballot.scores){ + tally[candidate] += ballot.scores[candidate]; + } + } + } else if (electiontype=="plurality"){ + var tally = _zeroTally(cans) + for(var ballot of ballots){ + tally[ballot.vote]++; + } + } else if (electiontype=="irv"){ + + // for the report, get the first preferences + var tally = _zeroTally(cans) + for(var ballot of ballots){ + var first = ballot.rank[0]; // just count #1 + tally[first]++; + } + + var options2 = {dontpoll:true, sidebar:true, justCount:true} + var result = Election.irv(district,model,options2) // do an IRV election to find out who wins + var winners = result.winners + + if (1) { + /// Get really good polling results. + temp1 = district.pollResults // doing a poll without strategy. not sure if this would work + district.pollResults = undefined + + model.updateDistrictBallots(district); + + var ballots2 = model.voterSet.getBallotsDistrict(district) + let head2head = head2HeadTally(model, district,ballots2) + district.pollResults = temp1 + } else { + let head2head = head2HeadTally(model, district,ballots) + } + + var results = {head2head:head2head, firstpicks:tally, winners:winners} + + } + + if (electiontype == 'irv') { + district.pollResults = results + } else { + district.pollResults = tally + } + + if(options.sidebar) { + + + if (model.stage == "primary") { + let ptallies = _getBallotsAndTallyPrimary(district, model, function(tally, ballot){ + tally[ballot.vote]++; + }); + collectTallies.push(ptallies) + } else { + + if (model.doTallyChart) { + collectTallies.push(_jcopy(tally)) + } else { + for(var i=0; i<cans.length; i++){ + var c = cans[i].id; + if (electiontype == "irv"){ + polltext += model.icon(c)+""+_padAfter(3,_percentFormat(district, tally[c]) + ". ") + " " + }else { + polltext += model.icon(c)+""+ _padAfter(3,_percentFormat(district, tally[c]/model.voterGroups[0].voterModel.maxscore) + ".") + " " + //if (tally[c] > threshold) polltext += " ←"//" <--" + //polltext += "<br>" + } + } + polltext += "<br>" + } + } + } + + // end of one poll + + // check if the results are the same as the previous poll, then quit if they are the same + if (_isEquivalent(lastTally,tally)) { + break + } + // update last tally + lastTally = _jcopy(tally) + + } + + // not yet needed + district.stages[model.stage].pollResults = district.pollResults + + if (options.sidebar) { + var maxscore = model.voterGroups[0].voterModel.maxscore + if (model.stage == "primary") { + for (let i in collectTallies[0]) { + text = "" + text += "<span class='small'>"; + var ip1 = i*1+1 + text += "<b>group " + ip1 + ":</b><br>"; + + let totalPeopleInPrimary = district.parties[i].voterPeople.length + text += `${collectTallies.length} rounds of polling<br>` + if (model.doTallyChart) { + var partyCollect = [] + for (var ptallies of collectTallies) { + var tally1 = ptallies[i] + partyCollect.push(tally1) + } + text += lineChart(partyCollect,cans,model,maxscore,totalPeopleInPrimary) + // TODO: add primary head2head polling for irv + text += "<br>" + } else { + for (var ptallies of collectTallies) { + var tally1 = ptallies[i] + var pwin = _countWinner(tally1) + for(let k=0; k<cans.length; k++){ + let c = cans[k] + let cid = c.id + if (district.parties[i].candidates.includes(c)) { + + text += model.icon(cid)+""+ _padAfter(3,_primaryPercentFormat(tally1[cid]/model.voterGroups[0].voterModel.maxscore, totalPeopleInPrimary) + ".") + " " + } + } + text += "<br>" + } + } + polltext += text + } + } else if (model.doTallyChart) { + polltext += `${collectTallies.length} rounds of polling<br>` + var nballots = district.voterPeople.length + polltext += lineChart(collectTallies,cans,model,maxscore,nballots) + if (electiontype == 'irv') { + var hh = district.pollResults.head2head + polltext += "<br>" + polltext += "<b>Head-to-head polls:</b><br>" + polltext += pairChart(ballots,district,model,hh) + } + } + } + + if (options.sidebar){ + polltext += "</span><br>" + // model.draw() // not sure why this was here + } + + + if ( cans.length < 3 ) return "" // don't show poll text if the poll was not interesting + + return polltext +} + + +function cellText(model,opt,hh,a,b) { + var pairText1 = pairText(model,opt,hh,a,b) + var winnerColor1 = winnerColor(hh,a,b) + var margin1 = margin(hh,a,b) + if (opt.light) { + var cellText = `<td style='background-color:${winnerColor1}; opacity: ${margin1*.5 + .5};'><div class='nameLabelName' >${pairText1}</div></td>` + } else { + var cellText = "<td><span class='nameLabelName' style='color:"+winnerColor1+"'>" + pairText1 + "</span></td>" + } + // row += '<td bgcolor="' + winnerColor + '">' + cellText + '</td>' + return cellText +} + +function winnerColor(hh,a,b) { + let win = hh[a.id][b.id] + let loss = hh[b.id][a.id] + + var winnerColor = (win == loss) ? "#ccc" : (win > loss) ? a.fill : b.fill + return winnerColor +} + + +function margin(hh,a,b) { + let win = hh[a.id][b.id] + let loss = hh[b.id][a.id] + + var margin = Math.abs(win - loss) / (win + loss) + return margin +} + +function pairText(model,opt,hh,a,b) { + let win = hh[a.id][b.id] + let loss = hh[b.id][a.id] + if (opt.entity == "winner") { + let winnerTally = Math.max(win,loss) + var frac = winnerTally / (win+loss) + } else { // opt.entity == "row" + // let pairText = win + '-' + loss + // let pairText = win + var frac = win / (win+loss) + } + // var pairText = Math.round(100*frac) + var pairText = _textPercent(frac) + return pairText +} + +function strategyTable(district,model,opt) { + + let a = district.parties[0].candidates + let b = district.parties[1].candidates + let text = "" + let header = ` + <table class="strategyTable"> + <tbody> + <tr> + <th> + ${ (opt.entity == "row") ? "+" : " " } + </th> + ` + for (let i = 0; i < b.length; i++) { + header += '<th>' + model.icon(b[i].id) + '</th>' + } + header += '</tr>' + + text += header + + let hh = district.primaryPollResults.head2head + + for (let k = 0; k < a.length; k++) { + let row = "<tr>" + row += '<td>' + model.icon(a[k].id) + '</td>' + for (let i = 0; i < b.length; i++) { + row += cellText(model,opt,hh,a[k],b[i]) + } + row += "</tr>" + + text += row + } + + let footer = "</body></table>" + + text += footer + + return text +} + +function pairwiseTable(hh,district,model,opt) { + + let cans = district.stages[model.stage].candidates + + + if (opt.doSort) { + var a = cans.map( x => x ) // copy + a.sort( (a,b) => hh[b.id][a.id] - hh[a.id][b.id] ) // might be kinda random for cycles + } else { + var a = cans + } + + let text = "" + text += `<table class="strategyTable"><tbody>` + + var square = opt.triangle == undefined + if (square) { + let header = ` + <tr> + <th> + ${ (opt.entity == "row") ? "+" : " " } + </th> + ` + for (let i = 0; i < a.length; i++) { + header += '<th>' + model.icon(a[i].id) + '</th>' + } + header += '</tr>' + + text += header + } + + for (let k = 0; k < a.length; k++) { + let row = "<tr>" + if (square) { + row += '<td>' + model.icon(a[k].id) + '</td>' + } + for (let i = 0; i < a.length; i++) { + if (i === k) { + if (opt.triangle) { + row += '<td style="text-align:center;">' + model.icon(a[k].id) + ' </td>' + } else { + if (opt.diagonal) { + row += `<td style='background-color:${a[k].fill}; '> </td>` + } else { + row += '<td> </td>' + } + } + } else if (opt.triangle && i > k) { + row += '<td> </td>' // skip + } else { + row += cellText(model,opt,hh,a[k],a[i]) + } + } + row += "</tr>" + + text += row + } + + let footer = "</body></table>" + + text += footer + + return text +} + + +var runPrimaryPoll = function(district,model,options,electiontype){ - // Count 'em up - var ballots = model.getBallots(); - for(var i=0; i<ballots.length; i++){ - tallyFunc(tally, ballots[i]); - } + // do head to head polling to find electable candidates - // Return it. - return tally; + let polltext = "" + district.primaryPollResults = {} -} + let numParties = district.parties.length + if (! model.doElectabilityPolls || numParties < 2) { + return "" + } -var _tallies = function(model, levels){ + if (options.sidebar) { + polltext += "<span class='small'>" + polltext += "<b>polling for electable candidates: </b><br>"; + } - // Create the tally - var tallies = []; - for (var level=0; level<levels; level++) { - var tally = {}; - for(var candidateID in model.candidatesById) tally[candidateID] = 0; - tallies.push(tally) + // ask voters to cast ranked ballots + var ballots = [] + let rankedVM = new VoterModel(model,"Ranked") + for (let voterPerson of district.voterPeople) { + let ballot = rankedVM.castBallot(voterPerson) + ballots.push(ballot) } - // Count 'em up - var ballots = model.getBallots(); - for(var i=0; i<ballots.length; i++){ - var ballot = ballots[i] - for(var candidate in ballot){ - tallies[ballot[candidate]][candidate] += 1; + // tally the ranked ballots + let head2head = head2HeadTally(model,district,ballots) + district.primaryPollResults.head2head = head2head + + // not yet needed + district.stages[model.stage].primaryPollResults = district.primaryPollResults + + + // display results + if(options.sidebar) { + // let opt = {entity:"row"} + let opt = {entity:"winner",light:true} + // let opt = {entity:"winner",doSort:true,triangle:true,light:true} + if (opt.entity == "row") { + polltext += "Vote % for Row Nominee<br>" + } else { // opt.entity == "winner" + polltext += "Vote % for Winning Nominee<br>" + } + + let hh = district.primaryPollResults.head2head + if (numParties == 2) { + polltext += strategyTable(district,model,opt) + } else { + polltext += pairwiseTable(hh,district,model,opt) } + + polltext += "</span><br>" + } + + return polltext +} + +function _zeroTally(cans) { + // Create the tally + var tally = {}; + for (let c of cans) { + tally[c.id] = 0 } + return tally +} - // Return it. - return tallies; +var _getBallotsAndTallyPrimary = function(district, model, tallyFunc){ + var primaries_tallies = [] + // look at only the candidates in the party + + var numParties = district.parties.length + for ( var j = 0; j < numParties; j++){ + let ballots = model.voterSet.getBallotsPartyAndDistrict(j,district) + var tally = {} + var candidates = district.parties[j].candidates + for (let c of candidates) { + tally[c.id] = 0 + } + for(let ballot of ballots){ + tallyFunc(tally, ballot) + } + primaries_tallies.push(tally) + } + return primaries_tallies } var _countWinner = function(tally){ // TO DO: TIES as an array?!?! // attempted - var highScore = -1; + var highScore = -Infinity; var winners = []; for(var candidate in tally){ @@ -605,31 +6485,36 @@ var _countLoser = function(tally){ return winners; } -var _colorWinner = function(model, winners){ - if (winners.length > 1) { - var color = "#ccc"; // grey - var colors = [] - for (i in winners) { - var c1 = (winners[i]) ? Candidate.graphics[winners[i]].fill : ""; - colors.push(c1) - } - model.colors = colors; +var _colorsWinners = function(winners,model){ + return winners.map( x => (x) ? model.candidatesById[x].fill : "" ); +} + +function _oneColor(colors){ + if (colors.length > 1) { + return "#ccc"; // grey } else { - var color = (winners[0]) ? Candidate.graphics[winners[0]].fill : ""; + return colors[0] } - model.canvas.style.borderColor = color; - model.winners = winners; - model.color = color; - return color; } -function _tietext(winners) { +function _result(winners,model) { + result = {} +    var colors = _colorsWinners(winners,model) +    var color = _oneColor(colors) +    result.winners = winners +    result.colors = colors +    result.color = color + return result +} + + +function _tietext(model,winners) { text = ""; for ( var i=0; i < winners.length; i++) { if(i) { text += " and "; } - text += _icon(winners[i]); + text += model.icon(winners[i]); } text += " tie<br>"; text += "</span>"; @@ -637,3 +6522,890 @@ function _tietext(winners) { text += "<b>TIE</b>"; return text; } + +function _percentFormat(district,count) { + var f = count/(district.voterPeople.length) + return _textPercent(f) +} + +function _primaryPercentFormat(count,total) { + var f = count/total + return _textPercent(f) +} + +function _textPercent(f) { + var a = "" + (100 * f).toFixed(0) + var dopadding = false + if (dopadding) { + for (var i = a.length; i < 2; i ++) { + a = "  " + a + } + } + a += "<span class='percent'>%</span>" + return a +} + +function _padBefore(padding,a){ + for (var i = a.length; i < padding; i ++) { + a = "  " + a + } + return a +} +function _padAfter(padding,a){ + for (var i = a.length; i < padding; i ++) { + a = a + "  " + } + return a +} + +// break up this massive drawing function into smaller units with documentation + + +function _drawBars(iDistrict, arena, model, round) { + + // get only the sorted voters for this district. + var v = model.getSortedVoters() + v = v.filter(x => x.iDistrict == iDistrict) + + // There are two sorts here... one for all voters and one for the district + // we want to use the one for all voters... and we want to get data that is based on the one for the districtq[] + // definitions + // weightUsed[iAll] or weightUsed[iDistSort] -- not sure about this one, maybe works for selectedRoundBeforeWeight + // district[].voterPeople[iDistSort] + // iAll = district[].voterPeople[iDistSort].iAll + // v[iTSP] + // iAll = orderOfVoters[iTSP] + // iDistSort = model.districtIndexOfVoter[iAll] + + + // for votes cast, draw rectangles + // need to make a rectangle for displaying. + var barOptions = {} + barOptions.width = arena.canvas.width + barOptions.widthRectangle = barOptions.width / v.length + barOptions.heightRectangle = 100 + barOptions.base = 600 + barOptions.baralpha = .8 + barOptions.fontSize = 32 + + drawWeightUsed(model,arena,barOptions,v,round) + + barOptions.pos = 200 + barOptions.heightRectangle2 = Math.min(200 / model.candidates.length, 200/5) + + drawWeight(model,arena,barOptions,v,round) + +} + +function _rLimitFrom(model,round) { + if (round == -1) { + var rLimit = model.result.history.rounds.length // Draw the weight used in all rounds. + } else if (round > -1) { // round is never 0 + var rLimit = round-1 // Only draw the weight used in previous rounds. + } + return rLimit +} + +function _type1Get(model) { + var type1 = model.system == "Phragmen Seq S" || model.system == "Monroe Seq S" || model.system == "Allocated Score" || model.system == "STAR PR" || model.system == "QuotaApproval" || model.system == "QuotaScore" // not sure why .. also not sure if STV is type 1 or not + return type1 +} + +function drawWeightUsed(model,arena,barOptions,v,round) { + + var rLimit = _rLimitFrom(model,round) + var type1 = _type1Get(model) + + drawBackGroundPowerChart(model,arena,barOptions,rLimit) + + arena.ctx.globalAlpha = barOptions.baralpha + + // drawing functions are separated out to simplify the code + // this loop does a calculation of support that really should already be done in the election function. + + // loop through rounds + // build one layer at a time + // for the last layer, keep track of the ballot weight remaining (unUsed) after the round + // rLimit is the upper bound for the index of the rounds. It lets us loop through all the rounds that ran previously. + + if (barOptions.doSatisfaction) var beforeSatisfaction = v.map( () => 0) + for (var r=0; r < rLimit; r++) { + var thisround = model.result.history.rounds[r] + if (thisround.winners.length == 0) continue + var winnerIndex = thisround.winners[0] + for (var i=0; i < v.length; i++) { + if (type1) { + var idx = model.orderOfVoters[i] + } else { + var idx = model.districtIndexOfVoter[model.orderOfVoters[i]] + } + if (barOptions.doPowerUsed) { // doPowerUsed + var beforePowerUsed = thisround.beforePowerUsed[idx] + var powerUsed = thisround.powerUsed[idx] + drawOneLayerOfWeightUsed(model,arena,i,beforePowerUsed,powerUsed,winnerIndex,barOptions) + } else if (barOptions.doSatisfaction) { + var powerUsed = thisround.powerUsed[idx] + var support = v[i].b[winnerIndex] / model.result.history.maxscore + var satisfaction = powerUsed * support + drawOneLayerOfWeightUsed(model,arena,i,beforeSatisfaction[idx],satisfaction,winnerIndex,barOptions) + beforeSatisfaction[idx] += satisfaction + } else { + var beforeWeightUsed = thisround.beforeWeightUsed[idx] + var weightUsed = thisround.weightUsed[idx] + drawOneLayerOfWeightUsed(model,arena,i,beforeWeightUsed,weightUsed,winnerIndex,barOptions) + } + } + // q[i] = 1 - (lastround.beforeWeightUsed[idx] + lastround.weightUsed[idx]) // the weight remaining after the round + } + + arena.ctx.globalAlpha = 1 + + if (model.showPowerChart) { + if (barOptions.doPowerUsed) { + _drawText("Voter Weight Contributed to Candidates",10,barOptions.base - 120,barOptions.fontSize,arena.ctx,"start") + } else if (barOptions.doSatisfaction) { + _drawText("Voter Support for Assigned Candidate",10,barOptions.base - 120,barOptions.fontSize,arena.ctx,"start") // used to be _drawStroked + } else { + _drawText("Voter Weighting Used by Method",10,barOptions.base - 120,barOptions.fontSize,arena.ctx,"start") // used to be _drawStroked + } + } + +} + +function drawKWeightUsed(model,arena,barOptions,v,round) { + + // do KP transform on v + + // the rest just is a modification of the drawWeightUsed function + + var vk = [] + var t = 0 + var nk = v[0].b.length + var maxscore = model.district[0].result.history.maxscore + for (var i = 0; i < v.length; i++) { + // did the voter give at least this score? + for (var s = 1; s <= maxscore; s++) { + var va0 = [] + for (var k = 0; k < nk; k++) { + if (v[i].b[k] >= s) { + va0.push(1) + } else { + va0.push(0) + } + } + vk.push({b:va0,i:i,t:t,s:s}) // i is voter id, n is transformed voter id + t++ + } + } + + barOptions.widthRectangle = barOptions.width / vk.length + + var rLimit = _rLimitFrom(model,round) + var type1 = _type1Get(model) + + drawBackGroundPowerChart(model,arena,barOptions,rLimit) + + arena.ctx.globalAlpha = barOptions.baralpha + + // drawing functions are separated out to simplify the code + // this loop does a calculation of support that really should already be done in the election function. + + // loop through rounds + // build one layer at a time + // for the last layer, keep track of the ballot weight remaining (unUsed) after the round + // rLimit is the upper bound for the index of the rounds. It lets us loop through all the rounds that ran previously. + var maxscore = model.result.history.maxscore + for (var r=0; r < rLimit; r++) { + var thisround = model.result.history.rounds[r] + if (thisround.winners.length == 0) continue + var winnerIndex = thisround.winners[0] + for (var i=0; i < vk.length; i++) { + if (type1) { + var idx = model.orderOfVoters[vk[i].i] + } else { + var idx = model.districtIndexOfVoter[model.orderOfVoters[vk[i].i]] + } + var idxk = idx * maxscore + vk[i].s - 1 // KP transform changes the indices + // console.log(idxk) + if (barOptions.doPowerUsed) { // doPowerUsed + var beforePowerUsed = thisround.tBeforePowerUsed[idxk] + var powerUsed = thisround.tPowerUsed[idxk] + drawOneLayerOfWeightUsed(model,arena,i,beforePowerUsed,powerUsed,winnerIndex,barOptions) + } else { + var beforeWeightUsed = thisround.tBeforeWeightUsed[idxk] + var weightUsed = thisround.tWeightUsed[idxk] + drawOneLayerOfWeightUsed(model,arena,i,beforeWeightUsed,weightUsed,winnerIndex,barOptions) + } + } + // q[i] = 1 - (lastround.beforeWeightUsed[idx] + lastround.weightUsed[idx]) // the weight remaining after the round + } + + arena.ctx.globalAlpha = 1 + + if (model.showPowerChart) { + if (barOptions.doPowerUsed) { + _drawText("Voter Weight Contributed to Candidates",10,barOptions.base - 120,barOptions.fontSize,arena.ctx,"start") + } else { + _drawText("Voter Weighting Used by Method, KP",10,barOptions.base - 120,barOptions.fontSize,arena.ctx,"start") // used to be _drawStroked + } + } + +} + +function drawWeight(model,arena,barOptions,v,round) { + + var rLimit = _rLimitFrom(model,round) + var type1 = _type1Get(model) + + if (0) { // this was a first attempt to draw the ballots. It is no longer needed. + barOptions.heightRectangle3 = Math.min(300 / model.candidates.length, 300/10) + for (var i = 0; i < v.length; i++) { + var b = v[i].b + for (var k = 0; k < b.length; k++) { + if (b[k] == 1) { + firstAttemptDrawBallots(model,arena,i,k,barOptions) + } + } + } + } + + // calculate the weight remaining before the current round + // calculate weight not used, selectedRoundBeforeWeight + var selectedRoundBeforeWeight = v.map( () => 1) + // If we're looking at the results after the final round, then calculate the weight remaining after the final round + var doAfterFinalRound = (round == -1) || (round == model.result.history.rounds.length + 1) // show the weight after the final round + for (var i=0; i < v.length; i++) { + if (type1) { + var idx = model.orderOfVoters[i] + } else { + var idx = model.districtIndexOfVoter[model.orderOfVoters[i]] + } + if (doAfterFinalRound) { + var lastIdx = model.result.history.rounds.length - 1 + var lastround = model.result.history.rounds[lastIdx] + selectedRoundBeforeWeight[i] = 1 - (lastround.beforeWeightUsed[idx] + lastround.weightUsed[idx]) + } else { + var lastIdx = round - 1 + var lastround = model.result.history.rounds[lastIdx] + selectedRoundBeforeWeight[i] = 1 - lastround.beforeWeightUsed[idx] + } + } + + + // I should make it clear the difference between weight and power. This may be different for different systems. + + + if (0){ // don't draw the weight.. for now + drawWeightGrey(model,arena,selectedRoundBeforeWeight,rLimit,barOptions) + } + + + // draw votes for each candidate in this round + + if (model.system == "STV") { + var rowFunction = "rounds" + // var rowFunction = "candidates" + + // grey out the ones that were eliminated that we voted for + if (rowFunction == "rounds") { + // loop through all the rounds we want to see + for (var r = 0 ; r <= rLimit; r++) { + if (r == model.result.history.rounds.length) { + var thisround = model.result.history.afterFinalRound + } else { + var thisround = model.result.history.rounds[r] + } + for (var i = 0; i < v.length; i++) { + var idx = model.districtIndexOfVoter[model.orderOfVoters[i]] + var beforeWeight = thisround.beforeWeight[idx] + var top = thisround.top[idx] + var color = model.candidatesById[top].fill + drawOneRoundSquareSTV(model,arena,beforeWeight,i,r,color,barOptions) + } + } + } else { + if (rLimit == model.result.history.rounds.length) { + var thisround = model.result.history.afterFinalRound + } else { + var thisround = model.result.history.rounds[rLimit] + } + var stillin = thisround.stillin + // who is still in the race + for (var i = 0; i < v.length; i++) { + var b = v[i].b + var idx = model.districtIndexOfVoter[model.orderOfVoters[i]] + var beforeWeight = thisround.beforeWeight[idx] + + for (var k = 0; k < b.length; k++) { + var c = b[k] + drawOneCandidateSquareSTV(model,arena,beforeWeight,i,c,stillin,barOptions) + if (stillin.includes(c)) { + break + // go through the list of people the voter voted for in order until we get to one that is still in the race + } + } + } + } + } else { // not STV + + // var rowFunction = "rounds" + var rowFunction = "candidates" + + if (rowFunction == "rounds") { + barOptions.heightRectangle4 = barOptions.heightRectangle2 / (rLimit+1) + for (var r = 0 ; r <= rLimit; r++) { + // var thisround = model.result.history.rounds[r] + for (var i = 0; i < v.length; i++) { + var b = v[i].b + // var idx = model.districtIndexOfVoter[model.orderOfVoters[i]] + // var beforeWeight = Math.max(1 - thisround.beforeWeightUsed,0) + var beforeWeight = Math.max(selectedRoundBeforeWeight[i],0) + for (var k = 0; k < b.length; k++) { + var support = b[k] / model.result.history.maxscore + drawOneCandidateSquareScore(model,arena,beforeWeight,support,i,k,r,rLimit,barOptions) + } + } + } + + } else { + for (var i = 0; i < v.length; i++) { + var b = v[i].b + // beforeWeight is just the weight at the beginning of the round + // support is the score the voter gave to each candidate + + // var thisround = model.result.history.rounds[rLimit] + // var idx = model.districtIndexOfVoter[model.orderOfVoters[i]] + // var beforeWeight = Math.max(0, 1 - thisround.beforeWeightUsed) + + var beforeWeight = Math.max(selectedRoundBeforeWeight[i],0) + for (var k = 0; k < b.length; k++) { + var support = b[k] / model.result.history.maxscore + drawOneRoundSquareScore(model,arena,beforeWeight,support,i,k,barOptions) + } + } + } + + } + + // labels + var pos = barOptions.pos + if (rowFunction == "rounds") { + _drawText("Votes by Round",10,pos-20,40,arena.ctx,"start") // used to be _drawStroked + } else { + _drawText("Votes",10,pos-20,40,arena.ctx,"start") + } + +} + +function drawKWeight(model,arena,barOptions,v,round) { + + + // do KP transform on v + + // the rest just is a modification of the drawWeight function + + var vk = [] + var t = 0 + var nk = v[0].b.length + var maxscore = model.district[0].result.history.maxscore + for (var i = 0; i < v.length; i++) { + // did the voter give at least this score? + for (var s = 1; s <= maxscore; s++) { + var va0 = [] + for (var k = 0; k < nk; k++) { + if (v[i].b[k] >= s) { + va0.push(1) + } else { + va0.push(0) + } + } + vk.push({b:va0,i:i,t:t,s:s}) // i is voter id, n is transformed voter id + t++ + } + } + + barOptions.widthRectangle = barOptions.width / vk.length + + var maxscore = model.result.history.maxscore + + var rLimit = _rLimitFrom(model,round) + var type1 = _type1Get(model) + + + // calculate the weight remaining before the current round + // calculate weight not used, selectedRoundBeforeWeight + var selectedRoundBeforeWeight = vk.map( () => 1) + // If we're looking at the results after the final round, then calculate the weight remaining after the final round + var doAfterFinalRound = (round == -1) || (round == model.result.history.rounds.length + 1) // show the weight after the final round + for (var i=0; i < vk.length; i++) { + if (type1) { + var idx = model.orderOfVoters[vk[i].i] + } else { + var idx = model.districtIndexOfVoter[model.orderOfVoters[vk[i].i]] + } + var idxk = idx * maxscore + vk[i].s - 1 // KP transform changes the indices + if (doAfterFinalRound) { + var lastIdx = model.result.history.rounds.length - 1 + var lastround = model.result.history.rounds[lastIdx] + selectedRoundBeforeWeight[i] = 1 - (lastround.tBeforeWeightUsed[idxk] + lastround.tWeightUsed[idxk]) + } else { + var lastIdx = round - 1 + var lastround = model.result.history.rounds[lastIdx] + selectedRoundBeforeWeight[i] = 1 - lastround.tBeforeWeightUsed[idxk] + } + } + + + // I should make it clear the difference between weight and power. This may be different for different systems. + + + if (0){ // don't draw the weight.. for now + drawWeightGrey(model,arena,selectedRoundBeforeWeight,rLimit,barOptions) + } + + + // draw votes for each candidate in this round + + if (0) { + } else { // not STV + + // var rowFunction = "rounds" + var rowFunction = "candidates" + + if (rowFunction == "rounds") { + barOptions.heightRectangle4 = barOptions.heightRectangle2 / (rLimit+1) + for (var r = 0 ; r <= rLimit; r++) { + // var thisround = model.result.history.rounds[r] + for (var i = 0; i < vk.length; i++) { + var b = vk[i].b + var beforeWeight = Math.max(selectedRoundBeforeWeight[i],0) + for (var k = 0; k < b.length; k++) { + var support = b[k] + drawOneCandidateSquareScore(model,arena,beforeWeight,support,i,k,r,rLimit,barOptions) + } + } + } + + } else { + for (var i = 0; i < vk.length; i++) { + var b = vk[i].b + // beforeWeight is just the weight at the beginning of the round + // support is the score the voter gave to each candidate + + var beforeWeight = Math.max(selectedRoundBeforeWeight[i],0) + for (var k = 0; k < b.length; k++) { + var support = b[k] + drawOneRoundSquareScore(model,arena,beforeWeight,support,i,k,barOptions) + } + } + } + + } + + // labels + var pos = barOptions.pos + if (rowFunction == "rounds") { + _drawText("Votes by Round",10,pos-20,40,arena.ctx,"start") // used to be _drawStroked + } else { + _drawText("Votes KP Transform",10,pos-20,40,arena.ctx,"start") + } + +} + + +function firstAttemptDrawBallots(model,arena,i,k,barOptions) { + var widthRectangle = barOptions.widthRectangle + var heightRectangle = barOptions.heightRectangle3 + + var lineHeight = 6 + + var left = Math.round(i * widthRectangle) + var right = Math.round((i+1) * widthRectangle) + + var top = Math.round((k+1/2) * heightRectangle - lineHeight / 2) + var bottom = Math.round((k+1/2) * heightRectangle + lineHeight / 2) + + var color = model.candidates[k].fill + arena.ctx.fillStyle = color + arena.ctx.fillRect(left,top,right-left,bottom-top) + arena.ctx.fill() +} + + +function drawDottedVoterLine(y,barOptions,v,ctx) { + ctx.save() + heightRectangle = barOptions.heightRectangle2 + for (var i=0; i < v.length; i++) { + drawDotted(ctx,i,barOptions,heightRectangle,y) + } + ctx.restore() +} + +function drawDotted(ctx,i,barOptions,heightRectangle,y) { + var widthRectangle = barOptions.widthRectangle + + var left = Math.round(i * widthRectangle) + var right = Math.round((i+1) * widthRectangle) + + var top = Math.round(y - heightRectangle * .5) + var bottom = Math.round(y + heightRectangle * .5) + + var color = (i % 2) ? "#fff" : "#ccc" + ctx.fillStyle = color + ctx.fillRect(left,top,right-left,bottom-top) + ctx.fill() +} + +function drawBackGroundPowerChart(model,arena,barOptions) { + + var width = barOptions.width + var heightRectangle = barOptions.heightRectangle + var base = barOptions.base + + if (model.showPowerChart) { + + // draw background for quota and build + var left = 0 + var right = width + var top = base - heightRectangle + var bottom = base + if (0) { + var color = "#492" + arena.ctx.fillStyle = color + } else { + var g = arena.ctx.createLinearGradient(0,top,0,bottom) + g.addColorStop(0,"#ccc") + g.addColorStop(1,"black") + arena.ctx.fillStyle = g + } + arena.ctx.fillRect(left,top,right-left,bottom-top) + //arena.ctx.fill() + } +} + +function drawOneLayerOfWeightUsed(model,arena,i,beforeWeightUsed,weightUsed,winnerIndex,barOptions) { + if (model.showPowerChart) { + + var baralpha = barOptions.baralpha + var widthRectangle = barOptions.widthRectangle + var heightRectangle = barOptions.heightRectangle + var base = barOptions.base + + var left = Math.round(i * widthRectangle) + var right = Math.round((i+1) * widthRectangle) + var top = Math.round(base-(beforeWeightUsed + weightUsed) * heightRectangle) + var bottom = Math.round(base-beforeWeightUsed * heightRectangle) + var bucket = Math.round(base- heightRectangle) // where the shadows start + + // white background for bar + arena.ctx.globalAlpha = 1 + arena.ctx.fillStyle = "white" + arena.ctx.fillRect(left,top,right-left,bottom-top) + arena.ctx.fill() + arena.ctx.globalAlpha = baralpha + + var color = model.candidates[winnerIndex].fill + arena.ctx.fillStyle = color + + var greyedout = .7 + if (bottom > bucket) { // bottom is in bucket + if (top < bucket) { // top is out of bucket + arena.ctx.fillRect(left,bucket,right-left,bottom-bucket) + arena.ctx.fill() + arena.ctx.globalAlpha = greyedout + arena.ctx.fillRect(left,top,right-left,bucket-top) + arena.ctx.fill() + } else { // entirely in bucket + arena.ctx.fillRect(left,top,right-left,bottom-top) + arena.ctx.fill() + } + } else { // entirely out of bucket + arena.ctx.globalAlpha = greyedout + arena.ctx.fillRect(left,top,right-left,bottom-top) + arena.ctx.fill() + } + arena.ctx.globalAlpha = baralpha + } +} + + +function drawWeightGrey(model,arena,selectedRoundBeforeWeight,r,barOptions) { + + var widthRectangle = barOptions.widthRectangle + var heightRectangle = barOptions.heightRectangle + + if (model.system == "RRV" || model.system == "RAV") { + var startpos = 450 + // draw the beforeWeight + for (var i=0; i < selectedRoundBeforeWeight.length; i++) { + var beforeWeight = Math.max(selectedRoundBeforeWeight[i],0) + var left = Math.round(i * widthRectangle) + var right = Math.round((i+1) * widthRectangle) + var top = Math.round(startpos - 1 * heightRectangle) + var bottom = Math.round(startpos - (1-beforeWeight) * heightRectangle) + arena.ctx.fillStyle = "#ccc" + arena.ctx.fillRect(left,top,right-left,bottom-top) + arena.ctx.fill() + + } + // draw line at bottom + var yLine = bottom + var ctx = arena.ctx + ctx.beginPath(); + ctx.moveTo(0,yLine*2); + ctx.lineTo(ctx.canvas.width,yLine*2); + ctx.lineWidth = 2; + ctx.strokeStyle = "#888"; + ctx.stroke(); + + _drawStroked("Weight",70,400,40,arena.ctx) + } +} + + +function drawOneRoundSquareSTV(model,arena,beforeWeight,i,r,color,barOptions) { + + var widthRectangle = barOptions.widthRectangle + var heightRectangle = barOptions.heightRectangle2 + var pos = barOptions.pos + + // determine where to draw + var left = Math.round(i * widthRectangle) + var right = Math.round((i+1) * widthRectangle) + var middle = Math.round(pos+(r+beforeWeight*1) * heightRectangle) + var middle2 = Math.round(pos+(r+1-beforeWeight) * heightRectangle) + var top = Math.round(pos+(r) * heightRectangle) + var bottom = Math.round(pos+(r+1) * heightRectangle) + + // draw candidate color + arena.ctx.fillStyle = color + arena.ctx.fillRect(left,top,right-left,bottom-top) + arena.ctx.fill() + + // draw amount of support + arena.ctx.fillStyle = "white" + supportMethod = "useTransparency" + if (supportMethod == "useTransparency") { + arena.ctx.globalAlpha = 1-beforeWeight + var width = right-left + var height = bottom-top + arena.ctx.fillRect(left,top,width,height) + } else { // "useVertical" + arena.ctx.globalAlpha = .7 + var topdown = true + if (topdown) { + arena.ctx.fillRect(left,middle,right-left,bottom-middle) + } else { + arena.ctx.fillRect(left,top,right-left,middle2-top) + } + } + + arena.ctx.globalAlpha = 1 +} + +function drawOneCandidateSquareSTV(model,arena,beforeWeight,i,c,stillin,barOptions) { + + var widthRectangle = barOptions.widthRectangle + var heightRectangle = barOptions.heightRectangle2 + var pos = barOptions.pos + + // determine where to draw + var left = Math.round(i * widthRectangle) + var right = Math.round((i+1) * widthRectangle) + + var middle = Math.round(pos+(c+beforeWeight*1) * heightRectangle) + var middle2 = Math.round(pos+(c+1-beforeWeight) * heightRectangle) + var top = Math.round(pos+(c) * heightRectangle) + var bottom = Math.round(pos+(c+1) * heightRectangle) + + var color = model.candidates[c].fill + if (stillin.includes(c)) { + // draw support in color + } else { + // draw support in grey + // var color = "#ccc" + middle = top // just solid grey, no beforeWeight stuff + } + + arena.ctx.fillStyle = color + arena.ctx.fillRect(left,top,right-left,bottom-top) + arena.ctx.fill() + + arena.ctx.globalAlpha = .7 + arena.ctx.fillStyle = "white" + if (1) { + arena.ctx.fillRect(left,middle,right-left,bottom-middle) + } else { + arena.ctx.fillRect(left,top,right-left,middle2-top) + } + + arena.ctx.globalAlpha = 1 +} + +function drawOneCandidateSquareScore(model,arena,beforeWeight,support,i,k,r,rLimit,barOptions) { + + if (support > 0) { + + var widthRectangle = barOptions.widthRectangle + var heightRectangle = barOptions.heightRectangle4 + var pos = barOptions.pos + + var left = Math.round(i * widthRectangle) + var right = Math.round((i+1) * widthRectangle) + + var interleaveCandidates = false + if (interleaveCandidates) { + var g = r * b.length + k + } else { + var g = r + k * (rLimit+1) + } + var middle = Math.round(pos+(g+beforeWeight*support) * heightRectangle) + var middle2 = Math.round(pos+(g+1-beforeWeight) * heightRectangle) + var top = Math.round(pos+(g) * heightRectangle) + var useHeight = true + if (useHeight) { // for STV, support = 1 + var bottom = Math.round(pos+(g+support) * heightRectangle) + } else { + var bottom = Math.round(pos+(g+1) * heightRectangle) + } + + var color = model.candidates[k].fill + arena.ctx.fillStyle = color + arena.ctx.fillRect(left,top,right-left,bottom-top) + arena.ctx.fill() + + // draw amount of support + arena.ctx.fillStyle = "white" + supportMethod = "useTransparency" + if (supportMethod == "useTransparency") { + arena.ctx.globalAlpha = (1-beforeWeight) * support + var width = right-left + var height = bottom-top + arena.ctx.fillRect(left,top,width,height) + } else { // "useVertical" + arena.ctx.globalAlpha = .7 + var topdown = true + if (topdown) { + arena.ctx.fillRect(left,middle,right-left,bottom-middle) + } else { + arena.ctx.fillRect(left,top,right-left,middle2-top) + } + } + + arena.ctx.globalAlpha = 1 + } +} + + +function drawOneRoundSquareScore(model,arena,beforeWeight,support,i,k,barOptions) { + if (support > 0) { + + var widthRectangle = barOptions.widthRectangle + var heightRectangle = barOptions.heightRectangle2 + var pos = barOptions.pos + + var left = Math.round(i * widthRectangle) + var right = Math.round((i+1) * widthRectangle) + + var middle = Math.round(pos+(k+beforeWeight*support) * heightRectangle) + var middle2 = Math.round(pos+(k+1-beforeWeight) * heightRectangle) + var top = Math.round(pos+(k) * heightRectangle) + var bottom = Math.round(pos+(k+support) * heightRectangle) + + var color = model.candidates[k].fill + arena.ctx.fillStyle = color + arena.ctx.fillRect(left,top,right-left,bottom-top) + arena.ctx.fill() + + // draw amount of support + arena.ctx.fillStyle = "white" + // var supportMethod = "useTransparency" + var supportMethod = "useVertical" + if (supportMethod == "useTransparency") { + arena.ctx.globalAlpha = (1-beforeWeight) * support + var width = right-left + var height = bottom-top + arena.ctx.fillRect(left,top,width,height) + } else { // "useVertical" + arena.ctx.globalAlpha = .7 + var topdown = true + if (topdown) { + arena.ctx.fillRect(left,middle,right-left,bottom-middle) + } else { + arena.ctx.fillRect(left,top,right-left,middle2-top) + } + } + + arena.ctx.globalAlpha = 1 + } +} + + + +function _percentileToX(percentile,model) { + var v = model.getSortedVoters() + if (v.length == 0) return 0 + var indexPercentile = (v.length-1) * percentile/100 // fine tuning TODO + // linear interpolation + var indexLeft = Math.floor(indexPercentile) + var frac = indexPercentile - indexLeft + var indexRight = indexLeft + 1 + indexRight = Math.min(indexRight,v.length-1) + var left = v[indexLeft].x + var right = v[indexRight].x + var xNew = left + frac * (right - left) + return xNew +} + +function _percentileToY(percentile,model) { + var v = model.getSortedVoters() + if (v.length == 0) return 0 + var indexPercentile = (v.length-1) * percentile/100 // fine tuning TODO + // linear interpolation + var indexLeft = Math.floor(indexPercentile) + var frac = indexPercentile - indexLeft + var indexRight = indexLeft + 1 + indexRight = Math.min(indexRight,v.length-1) + var left = v[indexLeft].y + var right = v[indexRight].y + var yNew = left + frac * (right - left) + return yNew +} + +function _xToPercentile(x,model) { + var v = model.getSortedVoters() + if (v.length == 0) return 0 + for (var k = 0; k < v.length; k++) { + if (x < v[k].x) break + } + var iLeft = k - 1 + iLeft = Math.max(iLeft,0) + var iRight = k + iRight = Math.min(iRight,v.length-1) + var left = v[iLeft].x + var right = v[iRight].x + if (right == left) { + var frac = 1 + } else { + var frac = (x - left) / (right - left) + } + var indexP = iLeft + frac * (iRight - iLeft) + var percentile = indexP / v.length * 100 + return percentile +} + +_check01 = function(district,model) { + let cans = district.stages[model.stage].candidates + + result = {good:false} + if (cans.length === 0) { +     result = _result([],model) + result.text = "Nobody ran."; + } else if (cans.length === 1) { +     result = _result([cans[0].id],model) + result.text = "Uncontested."; + } else { + result.good = true + } + return result +} diff --git a/play/js/Loader.js b/play/js/Loader.js index 5b932bd8..0d006579 100644 --- a/play/js/Loader.js +++ b/play/js/Loader.js @@ -1,20 +1,89 @@ -window.Loader = {}; -Loader.load = function(imagePaths){ +// Each sandbox() makes a new Loader. The Loader is configured with an .onload and updated with .load. +// Then all the Loaders call the LoaderManager to say what files they need and to give a countdown function _onAssetLoad +// LoaderManager calls the countdown for each Loader each time an image loads. +// When the countdown reaches 0, the .onload's run - // When all images loaded, call dat callback - var assetsToLoad = imagePaths.length; - var _onAssetLoad = function(){ - assetsToLoad--; - if(assetsToLoad==0){ - Loader.onload(); - } - }; - // Load 'em all - for(var i=0;i<imagePaths.length;i++){ - var img = new Image(); - img.onload = _onAssetLoad; - img.src = imagePaths[i]; +var LoaderManager = new function() { + var self = this; + var alreadyLoaded = [] + var alreadyRequested = [] + var assets = {} + var onloads = {} // list of callbacks to do once the image loads + + self.request = function(src,f) { + if (alreadyLoaded.includes(src)) { + f(src,assets[src]) // do the call back now + } else if (alreadyRequested.includes(src)) { + onloads[src].push(f) // do the callback later + } else { + alreadyRequested.push(src) + onloads[src] = [] + onloads[src].push(f) // do the callback later + + // now do the loading + // check filetype + var ext = src.split('.').pop(); + if (ext == "svg") { + // save svg as text + var svgText + var request = new XMLHttpRequest(); + request.open("GET", src); + request.setRequestHeader("Content-Type", "image/svg+xml"); + request.onload = function(event) { // onload ... not load + svgText = event.target.responseText + loaded(src,svgText) + } + // request.addEventListener("load", function(event) { // onload ... not load + // svgText = event.target.responseText + // loaded(src,svgText) + // }); + request.send(); + } else { + // save image as image + var img = new Image(); + img.onload = function() {loaded(src,img)} // send the network request for the image. + img.src = src; + } + } + + } + function loaded(src,img) { + alreadyLoaded.push(src) + assets[src] = img + for (var i=0; i < onloads[src].length; i++) { + onloads[src][i](src,img) // do the callback + } } +} + + +function Loader() { + var self = this; + + self.load = function(imagePaths){ + + // When all images loaded, call dat callback + var assetsToLoad = imagePaths.length; + var ONLY_ONCE = false; + var allAssets = {} + var _onAssetLoad = function(src,img){ + allAssets[src] = img + assetsToLoad--; + if(assetsToLoad==0){ + // ONCE. + if(ONLY_ONCE) return; + ONLY_ONCE=true; + self.onload(allAssets); + } + }; + + // Load 'em all + for(var i=0;i<imagePaths.length;i++){ + // send a request to LoaderManager to get an image and call back when it's done + LoaderManager.request(imagePaths[i], _onAssetLoad) + } + + }; -}; \ No newline at end of file +} diff --git a/play/js/Model.js b/play/js/Model.js index d0605e8b..8bc38bde 100644 --- a/play/js/Model.js +++ b/play/js/Model.js @@ -6,349 +6,3062 @@ A MODEL: ***************************/ -function Model(config){ +function Model(idModel){ var self = this; - // Properties - config = config || {}; - self.id = config.id || "model"; - self.size = config.size || 300; - self.scale = config.scale || 1; // TO DO: actually USE this. - self.border = config.border || 10; - - // RETINA canvas, whatever. - var canvas = document.createElement("canvas"); - canvas.setAttribute("class", "interactive"); - canvas.width = canvas.height = self.size*2; // retina! - canvas.style.width = canvas.style.height = self.size+"px"; - canvas.style.borderWidth = self.border+"px"; - var ctx = canvas.getContext("2d"); - self.canvas = canvas; - self.ctx = ctx; - - // My DOM: title + canvas + caption - self.dom = document.createElement("div"); - self.dom.setAttribute("class", "model"); - self.dom.style.width = (self.size+2*self.border)+"px"; // size+2*borders! - self.title = document.createElement("div"); - self.title.id = "title"; - self.caption = document.createElement("div"); - self.caption.id = "caption"; - self.caption.style.width = self.dom.style.width; - self.dom.appendChild(self.title); - self.dom.appendChild(self.canvas); - self.dom.appendChild(self.caption); - - // MAH MOUSE - self.mouse = new Mouse(self.id, self.canvas); - - // Draggables - self.draggables = []; - self.draggableManager = new DraggableManager(self); - - // Candidates & Voter(s) + // CREATE DATA STRUCTURE + self.voterGroups = []; + self.voterSet = new VoterSet(self) + self.district = [] + self.dm = new DistrictManager(self) + self.voterManager = new VoterManager(self) self.candidates = []; - self.candidatesById = {}; - self.voters = []; - self.addCandidate = function(id, x, y){ - var candidate = new Candidate({ - model: self, - id:id, x:x, y:y - }); - self.candidates.push(candidate); - self.draggables.push(candidate); - self.candidatesById[id] = candidate; - }; - self.addVoters = function(config){ - config.model = self; - var DistClass = config.dist; - var voters = new DistClass(config); - self.voters.push(voters); - self.draggables.push(voters); + self.dom = document.createElement("div"); + self.arena = new Arena("arena",self) + self.tarena = new Arena("tarena",self) + self.nLoading = 0 // counter for drawing after everything is loaded + // the only thing to be done after loading the images is drawing the images + self.randomSeed = 0 + + // CONFIGURE DEFAULTS + // helper + var all_candidate_names = Object.keys(Candidate.graphicsByIcon["Default"]) + var yes_all_candidates = {} + for (var i = 0; i < all_candidate_names.length; i++) { + var a = all_candidate_names[i] + yes_all_candidates[a] = true + } + Object.assign(self,{ + // values used in init + id:idModel, + size:300, + scale:1, + border:10, + optionsForElection:{sidebar:true}, + // values used later + // defaults that are also in main_sandbox.js in the cleanConfig function + system: "FPTP", + rbsystem: "Tideman", + numOfCandidates: 3, + spread_factor_voters: 1, + arena_size: 300, + median_mean: 1, + utility_shape: "linear", + dimensions: "2D", + nDistricts: 1, + colorChooser: "pick and generate", + colorSpace: "hsluv with dark", + arena_border: 2, + preFrontrunnerIds: ["square","triangle"], + autoPoll: "Manual", + // primaries: "No", + firstStrategy: "zero strategy. judge on an absolute scale.", // maybe should be for voter, not model + secondStrategy: "zero strategy. judge on an absolute scale.", // maybe should be for voter, not model + doTwoStrategies: true,// maybe should be for voter, not model + yeefilter: yes_all_candidates, + computeMethod: "ez", + pixelsize: 60, + // + nVoterGroupsRealName: "Single Voter", + yeeobject: undefined, + yeeon: false, + // + // VoterType: PluralityVoter, + election: Election.plurality, + BallotType: PluralityBallot, + ballotType: "Plurality", + rbelection: rbvote.calctide, + opt: { + irv100: true, // show the final transfer to the winner (to reach 100%) + IRVShowdown: false, // show a reverse-direction transfer to represent the winner + showIRVTransfers: false, // show lines representing transfers between rounds + breakWinTiesMultiSeat: true, // break ties for winning candidates in multi-winner methods + breakEliminationTiesIRV: true, // break ties for eliminations of candidates in IRV + doDrawIRVCandidates: false, // TODO: make a button for this + }, + ballotVis: true, // turn on or off the visuals that show where the ballots go + visSingleBallotsOnly: false, // only show the single ballots as part of the ballotVis + beatMap: "auto", + keyyee: "newcan", + kindayee: "newcan", + ballotConcept: "auto", + roundChart: "auto", + voterIcons: "circle", + voterCenterIcons: "off", + candidateIconsSet: ["image","note"], + placeHoldDuringElection: false, + doPlaceHoldDuringElection: true, + pairwiseMinimaps: "off", + doTextBallots: false, + behavior:"stand", + showVoters:true, + showToolbar: "off", + rankedVizBoundary: "atWinner", + useBeatMapForRankedBallotViz: false, + doMedianDistViz: false, + drawSliceMethod: "circleNicky", // "circleBunch" or "old" + allCan: false, + useBorderColor: true, + pairOrderByCandidate: true, + squareFirstChoice: true, + doVoterMapGPU: false, + devOverrideShowAllFeatures: false, + doElectabilityPolls: true, + partyRule: 'crowd', + stage: "general", + showPowerChart: true, + centerPollThreshold: .5, + howBadlyDefeatedThreshold: 1.1, + doTallyChart: true, + codeEditorText: Election.defaultCodeScore, + createStrategyType: "score", + createBallotType: "Score", + showUtilityChart: false, + enableTArena: false, + voterGroupCustomNames: "No", + voterGroupNameList: [], + drawNameSingleVoter: false, + onlyVoterMapViewMan: true, // only show the viewMan's voter map when dragging him. + }) + + self.viz = new Viz(self); + + self.createDOM = function() { + self.arena.createDOM() + self.tarena.createDOM() + self.tarena.canvas.hidden = true + + // My DOM: title + canvas + caption + self.dom = document.createElement("div"); + self.dom.setAttribute("class", "model"); + self.title = document.createElement("div"); + self.title.id = "title"; + self.caption = document.createElement("div"); + self.caption.id = "caption"; + + self.dom.appendChild(self.title); + self.dom.appendChild(self.arena.canvas); + self.dom.appendChild(self.tarena.canvas); + self.dom.appendChild(self.caption); + } + + self.initDOM = function() { + self.arena.initDOM() + self.tarena.initDOM() + + self.dom.style.width = (self.size+2*self.border)+"px"; // size+2*borders! + // self.caption.style.width = self.dom.style.width; + } + + self.initMODEL = function() { + + self.arena.initARENA() + self.tarena.initARENA() + + self.candidatesById = {}; + self.candidatesBySerial = {}; + for (var i=0; i<self.candidates.length; i++) { + var c = self.candidates[i] + c.i = i + self.candidatesById[c.id] = c; + self.candidatesBySerial[c.serial] = c; + } + + self.yeefilter = {} + for (var c of self.candidates) { + self.yeefilter[c.id] = true + } + + + var expYeeObject = function() { + // Yee diagram + if ( ! self.yeeon) { + return undefined + } else if (self.kindayee == "can") { + return self.candidatesById[self.keyyee] + } else if (self.kindayee=="voter") { + return self.voterGroups[self.keyyee] + } else if (self.kindayee=="center") { + return self.voterCenter + } else if (self.kindayee=="newcan") { + return undefined + } else { // if yeeobject is not defined + return undefined + } + } + self.yeeobject = expYeeObject() + self.onInitModel() + } + self.onInitModel = function() {} // a hook for a caller + + self.election = function(){ + self.stage = "general" + for (let district of self.district) { + district.stages = {} + district.stages["general"] = {candidates: district.candidates } + self.updateDistrictBallots(district) + } }; - // Init! - self.onInit = function(){}; // TO IMPLEMENT - self.init = function(){ - self.onInit(); - self.update(); + self.initPlugin = function(){ // TO IMPLEMENT FURTHER IN CALLER + self.initMODEL() // replace this with more }; // Reset! - self.reset = function(noInit){ + self.reset = function(){ + // RE-CREATE DATA STRUCTURE self.candidates = []; - self.candidatesById = {}; - self.voters = []; - self.draggables = []; - if(!noInit) self.init(); + self.voterGroups = []; + // START - combination of CREATE, CONFIGURE, INIT, UPDATE + self.initPlugin(); }; // Update! self.onUpdate = function(){}; // TO IMPLEMENT - - self.calculateYee = function(){ - self.pixelsize= 30.0; - var pixelsize = self.pixelsize; - WIDTH = ctx.canvas.width; - HEIGHT = ctx.canvas.height; - doArrayWay = self.computeMethod != "ez" - var winners - if (doArrayWay) { - // put candidate information into arrays - var canAid = [], xc = [], yc = [], fillc = [] //, canA = [], revCan = {} // candidates - var f=[] // , fA = [], fAid = [], xf = [], yf = [], fillf = [] // frontrunners - var movethisidx, whichtypetomove - var i = 0 - for (can in self.candidatesById) { - var c = self.candidatesById[can] - canAid.push(can) - // canA.push(c) - // revCan[c] = i - xc.push(c.x*2) // remember the 2 - yc.push(c.y*2) - fillc.push(c.fill) - if (model.preFrontrunnerIds.includes(c.id)) { - // fAid.push(can) - // fA.push(c) - f.push(i) - // xf.push(c.x*2) - // yf.push(c.y*2) - // fillf.push(c.fill) // maybe don't need - } - if (self.yeeobject == c){ - movethisidx = i - whichtypetomove = "candidate" - } - i++ - } - // now we have xc,yc,fillc,xf,yf - // maybe we don't need fillf, fA, canA, canAid, fAid, but they might help - - // put voter information into arrays - var av = [], xv = [], yv = [] , vg = [] , xvcenter = [] , yvcenter = []// candidates - var movethisidx, whichtypetomove - var i = 0 - for (vidx in self.voters) { - v = self.voters[vidx] - av.push(v) - xvcenter.push(v.x*2) - yvcenter.push(v.y*2) - if (self.yeeobject == v){ - movethisidx = i - whichtypetomove = "voter" - } - for (j in v.points) { - p = v.points[j] - xv.push((p[0] + v.x)*2) - yv.push((p[1] + v.y)*2) - vg.push(i) - } - i++ - } - // now we have xv,yv, - // we might not need av - - // need to compile yee and decide when to recompile - // basically the only reason to recompile is when the number of voters or candidates changes - - lv = xv.length - lc = xc.length - self.fastyeesettings = [lc,lv,WIDTH,HEIGHT,pixelsize] - function arraysEqual(arr1, arr2) { - arr1 = arr1 || [0] - arr2 = arr2 || [0] - if(arr1.length !== arr2.length) - return false; - for(var i = arr1.length; i--;) { - if(arr1[i] !== arr2[i]) - return false; - } - - return true; - } - recompileyee = !arraysEqual(self.fastyeesettings,self.oldfastyeesettings) - //(self.fastyeesettings || 0) != (self.oldfastyeesettings || 0)) - self.oldfastyeesettings = self.fastyeesettings - if (recompileyee) { - fastyee = createKernelYee(lc,lv,WIDTH,HEIGHT,pixelsize) - } - //method = "gpu" - //method = "js" - method = self.computeMethod - winners = fastyee(xc,yc,f,xv,yv,vg,xvcenter,yvcenter,movethisidx,whichtypetomove,method) - + self.getSortedVoters = function() { + var v = self.voterSet.getVoterArray() + var x = v.map( (d,i) => v[self.orderOfVoters[i]] ) + return x + } + self.onDrop = function() { + if (self.showToolbar == "on") { + if (self.arena.trashes.overTrash) { + self.arena.trashes.tossInTrash() + } + } + } + + self.onAddCandidate = function() {} // callback + self.update = function(){ + + + if (self.checkRunTextBallots() && self.textBallotInput !== "" ){ // TODO: make text ballots work for every method, so we don't have to do this step and we can + self.textBallotUpdate() + return + } + + // if (self.nLoading > 0) return // the loading function will call update() + + // update positions of draggables + self.arena.update(); + self.tarena.update(); + + // update the position of the voter center + if (typeof self.voterCenter !== 'undefined') { // does the voterCenter exist? If so then calculate it. + self.voterCenter.update() + } + + // do an analysis kind of election + if (self.votersAsCandidates) { + self.updateVC() + self.votersAsCandidates = false + } + + // get the ballots for this election + for(var i=0; i<self.voterGroups.length; i++){ + var voter = self.voterGroups[i]; + voter.updatePeople(); + } + + self.viz.calculateBeforeElection() + + + for(var i=0; i<self.candidates.length; i++){ + var c = self.candidates[i]; + c.update(); // doesn't do anything... yet + } + + // did we manually select the winners? (with ctrl-click) + var selected = self.selection() + + if (selected.winners.length > 0) { + self.result = selected + if (self.doTop2) self.result.theTop2 = selected + self.updateBallots() + } else { + self.doTheElection() + } + + self.sortVoters() + + self.viz.calculateAfterElection() + + + // Update! + self.onUpdate(); + publish(self.id+"-update"); + + self.draw() + }; + + + self.updateFromModel = function() {} // hook + + self.onDraw = function(){}; // TO IMPLEMENT + self.draw = function() { + + if (self.nLoading > 0) return // still loading, will call later and save some computing cycles + // The drawing system and the loading assets system are connected here. + + if (self.checkRunTextBallots()) { + self.arena.canvas.hidden = true + self.drawSidebar() + return + } else { + self.arena.canvas.hidden = false + } + + // three things need to be drawn. The arenas, the sidebar, and maybe more, like the main_sandbox or the main_ballot or whatever else calls new Model + self.drawArenas() + self.drawSidebar() + self.onDraw() + } + + self.textBallotUpdate = function() { + // run an election with RBVote + if (self.system === "RBVote") { + self.result = self.election(self.district[0] ,self,self.optionsForElection) + self.district[0].result = self.result + self.drawSidebar() } - self.gridx = []; - self.gridy = []; - self.gridl = []; - self.gridb = []; - saveo = {} - saveo.x = self.yeeobject.x; - saveo.y = self.yeeobject.y; - var i=0 - for(var x=.5*pixelsize, cx=0; x<=WIDTH; x+= pixelsize, cx++) { - for(var y=.5*pixelsize, cy=0; y<=HEIGHT; y+= pixelsize, cy++) { - if (doArrayWay) { - var winner = Math.round(winners[i]) - if (winner > lc) { // we have a set of winners to decode - //winner = 3 + lc* (2+lc*(4)) - //var decode = function (winner) { - wl = [] - for (var s = 0; s < lc; s++) { - if (winner <= lc) {break} - wl.push(winner % lc) - winner = Math.floor(winner / lc) + // self.onDraw() + // self.onUpdate() + publish(self.id+"-update"); + return + } + + self.sortVoters = function() { + + if (self.checkDoSort()) // find order of voters + { + var v = self.voterSet.getVoterArray() + for (var i = 0; i < v.length; i++) { + v[i].i = i + } + // if (self.system == "STV") { + // for (var voter of v) { + // var newb = [] + // for (var [r,c] of Object.entries(voter.b)) { + // newb[c] = r + // } + // voter.b = newb + // } + // } + if (self.ballotType == "Ranked") { + var iList = self.candidates.map( x => x.i) + for (var i = 0; i < v.length; i++) { + v[i].b = pairList(v[i].b,iList) + } + } + if (v.length > 0) { + if (self.dimensions == "1D+B" || self.dimensions == "1D") { + // easy sort in 1D + var m = v.map( function(d, i) { return { i: i, d: d }; } ) // add an index to each voter + m.sort(function(a,b){return a.d.x - b.d.x}) // sort voters + self.orderOfVoters = m.map( a => a.i) // get original indices of sorted voters + } else { + // 2D + var draggingSomething = (self.arena.mouse.dragging || self.tarena.mouse.dragging) + var changedNumVoters = (self.orderOfVoters != undefined) && (self.orderOfVoters.length != v.length) + if (changedNumVoters || !draggingSomething ) { + var algorithmForTSP = 4 + if (algorithmForTSP == 4) { // fast way + var out = clusterTheVoters(v,{sortCluster:false,sortAll:false}) + var order = out.map(x => x.i) + } else if (algorithmForTSP == 3) { + var out = clusterTheVoters(v,{sortCluster:true,sortAll:true}) + var order = out.map(x => x.i) + } else if (algorithmForTSP == 2) { + + if (v.length > 2) { + var out = order_by_distance(v,nodeDist2, {crossover: true, points: true}) + } else { + var out = v } - wl.push(winner) - // return wl - //} - colorlist = [] - for (w in wl) {colorlist.push(Candidate.graphics[canAid[wl[w]] || "square"].fill)} - self.gridb[i] = colorlist - var a = "#ccc" // grey is actually a code for "look for more colors" - } else { - var a = Candidate.graphics[canAid[winner] || "square"].fill + var order = out.map(x => x.i) + } else { + var tsp = new TravelingSalesman(); + tsp.runOnSet(v) + var order = tsp.getOrder() + } + self.orderOfVoters = order + } - // if (a == "#ccc") {a = "#ddd"} // hack for now, but will deal with ties later - self.gridx.push(x); - self.gridy.push(y); - self.gridl.push(a); - i++; - continue; - } - self.yeeobject.x = x * .5; - self.yeeobject.y = y * .5; - for(var j=0; j<self.voters.length; j++){ - self.voters[j].update(); } - self.election(self, {sidebar:false}); - - var a = self.color; // updated color - if (a == "#ccc") {self.gridb[i] = self.colors;} - self.gridx.push(x); - self.gridy.push(y); - self.gridl.push(a); - // model.caption.innerHTML = "Calculating " + Math.round(x/WIDTH*100) + "%"; // doesn't work yet - i++ } } - self.yeeobject.x = saveo.x; - self.yeeobject.y = saveo.y; + } - self.update = function(){ - // calculate yee if its turned on and we haven't already calculated it ( we aren't dragging the yee object) - if (self.yeeon && Mouse.dragging != self.yeeobject) self.calculateYee() + // helpers for the immediately above code for self.sortVoters + function pairList(b,iList) { + var p = [] + for (var i = 0; i < b.length; i++ ) { + p[b[i]] = [] + for (var j = 0; j < i; j ++) { + var weight = b.length - j // set to 1 maybe + var weight = 1 + p[b[i]][b[j]] = weight // set all beats to 1 + } + } + var pairs = [] + for (var m = 0; m < iList.length; m++ ) { + var i = iList[m] + for (var n = 0; n < iList.length; n ++) { + var j = iList[n] + if (p[i] && p[i][j]) { + pairs.push(p[i][j]) + } else { + pairs.push(0) + } + } + } + return pairs + } + function clusterTheVoters(v,opt) { + // basically, we're creating clusters based only on the ballots, not the voter positions. + // Then we're sorting the center of each cluster by it's similarity in ballot and in position to other clusters. + // Also, there is no sorting within the clusters. + + opt = opt || {} + + // identify clusters + var clusters = {} + var clusterNames = [] + for (var i = 0; i < v.length; i++ ) { + var clusterName = JSON.stringify(v[i].b) + if (clusters[clusterName] == undefined) { + clusters[clusterName] = [] + clusterNames.push(clusterName) + } + clusters[clusterName].push(v[i]) + } + // find center of clusters + clusterPositions = [] + for (var i = 0; i < clusterNames.length; i++) { + var clusterName = clusterNames[i] + var cluster = clusters[clusterName] + var avgx = cluster.map( a => a.x).reduce( (a,b) => a + b) / cluster.length // average voter positions + var avgy = cluster.map( a => a.y).reduce( (a,b) => a + b) / cluster.length // average voter positions + clusterPositions.push( {x:avgx , y:avgy, i:i, b:cluster[0].b}) + } + // tsp on cluster positions + var sortedClusterPositions = order_by_distance(clusterPositions,nodeDist2,{crossover: true, points: true}) + var clusterOrder = sortedClusterPositions.map( x => x.i) + // clustered voters + var clusteredVoters = [] + for ( var i = 0; i < clusterOrder.length; i++) { + var index = clusterOrder[i] + var clusterName = clusterNames[index] + var cluster = clusters[clusterName] + if (opt.sortCluster && cluster.length > 2) { + cluster = order_by_distance(cluster, dist2xy) + } + for ( var k = 0; k < cluster.length; k++) { + clusteredVoters.push(cluster[k]) + } + } + if (opt.sortAll) { + crosssOverAndPoints(clusteredVoters, nodeDist2, {crossover: true, points: true}) + } + return clusteredVoters + } + function nodeDist2(m,n) { + var bWeight = 739 + return (m.x - n.x) ** 2 + (m.y - n.y) ** 2 + euclidian2(m.b, n.b) * bWeight * bWeight + } + function euclidian2(a, b) { + return a.map((_, i) => (a[i] - b[i]) ** 2).reduce((a, b) => a + b); // sum of squares of differences + } + function dist2xy(a,b) { + return (a.x - b.x) ** 2 + (a.y - b.y) ** 2 + } + + self.selection = function() { + var selected = {} + selected.winners = [] + selected.colors = [] + for (var i=0; i < self.candidates.length; i++) { + var c = self.candidates[i] + if (c.selected) { + selected.winners.push(c.id) + selected.colors.push(c.fill) + } + } + if (selected.winners.length == 1) { + selected.winner = selected.winners[0] + selected.color = selected.colors[0] + } else if (selected.winners.length > 1) { + selected.winner = selected.winners[0] + selected.color = "#ccc" + } + return selected + } + + self.doTheElection = function() { - // Clear it all! - ctx.clearRect(0,0,canvas.width,canvas.height); - // Move the one that's being dragged, if any - if(Mouse.dragging){ - Mouse.dragging.moveTo(Mouse.x, Mouse.y); + // for the moment, this works, but ideally there would be separate components of the STV, RRV etc elections for the roundCharts + // we make sure that we generate the data now so we have it later. + self.optionsForElection.sidebar = self.optionsForElection.sidebar || self.checkGotoTarena() + + if (self.nDistricts > 1) { + for (var i = 0; i < self.nDistricts; i++) { + self.placeHoldDuringElection = self.doPlaceHoldDuringElection + self.district[i].result = self.election(self.district[i], self, self.optionsForElection); + self.placeHoldDuringElection = false + } + + // put all the results together + self.result = { + winners: [], + colors: [], + color: [], + history: [], + eventsToAssign: [], + text: '' + } + if (self.doTop2) self.result.theTop2 = [] + for (var i = 0; i < self.nDistricts; i++) { + if (self.result) { + self.result.text += "<br>" + self.result.text += "<br>" + self.result.text += "District " + (i+1) + self.result.text += "<br>" + self.result.text += self.district[i].result.text + self.result.winners = [].concat(self.result.winners , self.district[i].result.winners) + self.result.colors = [].concat(self.result.colors , self.district[i].result.colors) + self.result.color = [].concat(self.result.color , self.district[i].result.color) + if (self.district[i].result.eventsToAssign) self.result.eventsToAssign = [].concat(self.result.eventsToAssign , self.district[i].result.eventsToAssign) + if (self.doTop2) self.result.theTop2 = [].concat(self.result.theTop2 , self.district[i].result.theTop2) + } + } + // for now, just give the history for the first district with candidates + for (var i = 0; i < self.nDistricts; i++) { + if (self.result) { + self.result.history = self.district[i].result.history + break + } + } + + // for (var i = 0; i < self.nDistricts; i++) { + // self.result.history =[].concat(self.result.history , self.district[i].result.history) + // self.result.history.push =[].concat(self.result.history , self.district[i].result.history) + // } + + // TODO: visualize many districts results in tarena + + + } else { + self.placeHoldDuringElection = self.doPlaceHoldDuringElection + self.result = self.election(self.district[0], self,self.optionsForElection); + self.placeHoldDuringElection = false + self.district[0].result = self.result } - // DRAW 'EM ALL. - // Draw voters' BG first, then candidates, then voters. + } + + self.drawArenas = function() { + + self.arena.clear() + + var doTarena = self.checkGotoTarena() + if (doTarena) { + self.tarena.clear() + } // Draw axes //var background = new Image(); //background.src = "../play/img/axis.png"; - //ctx.drawImage(background,0,0); - - if(self.yeeon){ - ctx.globalAlpha = .9 - var pixelsize = self.pixelsize; - for(var k=0;k<self.gridx.length;k++) { - var ca = self.gridl[k] - if (ca=="#ccc") { // make stripes instead of gray - var cb = self.gridb[k] - var xb = self.gridx[k]-pixelsize*.5 - var yb = self.gridy[k]-pixelsize*.5 - var wb = pixelsize - var hb = pixelsize - var hh = 5; // height of stripe - for (var j=0; j< pixelsize/hh; j++) { - ctx.fillStyle = cb[j % cb.length] - ctx.fillRect(xb,yb+j*hh,wb,hh); + //self.ctx.drawImage(background,0,0); + self.viz.drawBackground() + + if (doTarena) { + if (self.nDistricts > 1) { + // TODO later + // for (var i = 0; i < self.nDistricts; i++) { + // if (self.district[i].candidates.length == 0) continue + // _drawBars(i,self.tarena,self,self.round) + // } + } else { + if (self.optionsForElection.sidebar) { + _drawBars(0,self.tarena,self,self.round) + _drawText("Candidates in Voter Space",10,35,40,self.tarena.ctx,"start") + } + } + } + + if (self.dimensions == "1D+B") self.arena.drawHorizontal(self.arena.yDimOne + self.arena.yDimBuffer) + if (self.dimensions == "1D" && 0) { + self.arena.drawHorizontal(self.arena.yDimOne) + self.arena.drawHorizontal(self.size - self.arena.yDimOne) + } + + if (self.dimensions == "2D") { + for (var i=0; i < self.district.length - 1; i++) { + self.arena.drawHorizontal(self.district[i].upperBound) + } + } + + self.arena.draw() + if (doTarena) { + self.tarena.draw() + } + + + } + self.drawSidebar = function () { + if (self.result) { + if(self.result.text) { + // the doPlaceHoldDuringElection option helps make it easier to draw + // because we can use a placeholder for the drawing during the calculation phase of the election + // and then substitute the image on the draw step. + if (self.placeHolding || self.doPlaceHoldDuringElection) { + if (self.nLoading > 0) { + // will do on next draw + return + } else { + // ready to replace + self.result.textSubs = self.replacePlaceholder(self.result.text) } } else { - ctx.fillStyle = self.gridl[k]; - ctx.fillRect(self.gridx[k]-pixelsize*.5, self.gridy[k]-pixelsize*.5, pixelsize, pixelsize); + self.result.textSubs = self.result.text + } + if (! (self.optionsForElection.originalCaption == true) ) { + var title = '<div style="text-align:center;"><span class="small" > Election Results </span></div>' + } else { + var title = '' + } + self.caption.innerHTML = title + self.result.textSubs; + if (self.result.eventsToAssign) { + for (var i=0; i < self.result.eventsToAssign.length; i++) { + var e = self.result.eventsToAssign[i] + self.caption.querySelector("#" + e.eventID).addEventListener("mouseover", e.f) + self.caption.querySelector("#" + e.eventID).addEventListener("mouseleave", ()=>self.drawArenas()) + } + } + if (! (self.optionsForElection.originalCaption == true) ) { + // self should really be ui and this should be moved out of model as a plugin to model like sandbox + if (self.minusControl == undefined) self.minusControl = {} + if (self.minusControl.caption == undefined) self.minusControl.caption = {} + addMinusButtonC(self.caption,self.minusControl.caption, {caption:true}) } } - ctx.globalAlpha = 1 - // Draw axes - //var background = new Image(); - //background.src = "../play/img/axis.png"; - // ctx.drawImage(background,0,0); // eh, I don't like the axis. } + } - // make the candidate that is moving say "yee-yee!" - if(self.yeeon){ - var x = self.yeeobject.x; - var y = self.yeeobject.y; - ctx.beginPath(); - ctx.arc(x*2, y*2, 60, 0, Math.TAU, true); - ctx.strokeStyle = "white"; - ctx.lineWidth = 8; - ctx.fillStyle = 'white'; - ctx.globalAlpha = 0.3 - ctx.fill(); - ctx.stroke(); - ctx.globalAlpha = 1 - } + + var finding = false + var seed = 1 + var goal = [] + var bounce = undefined + self.buzz = function() { + + if (self.behavior == "goal") { + //find goal + if (!finding) { + finding = true + for(var i=0; i<self.candidates.length; i++){ + var can = self.candidates[i] + goal[i] = self.viz.yee.winSeek(can) + } + finding = false + } + + // move toward goal or center + for(var i=0; i<self.candidates.length; i++){ + var c = self.candidates[i]; + if (self.result) { + if (self.result.winners.includes(c.id)) + { + continue // skip this guy because he's winning + } + } + if (goal[i]) { + var g = goal[i] + } else { + if (1) { + continue // skip this guy + } else { + // move toward center + var g = { + x: self.canvas.width * .5, + y: self.canvas.height * .5 + } + } + } + var diff = { + x: g.x * .5 - c.x, + y: g.y * .5 - c.y + } + lenDiff = Math.sqrt(diff.x**2 + diff.y**2) + var unit = { + x: diff.x / lenDiff, + y: diff.y / lenDiff + } + var speed = 5 + c.x += unit.x * speed + c.y += unit.y * speed + } + bumble() + } else if (self.behavior == "bounce") { + // bouncing + + if (bounce == undefined || bounce.length != self.candidates.length) { + bounce = [] + + for(var i=0; i<self.candidates.length; i++){ + var c = self.candidates[i]; + var x = c.x - self.arena.canvas.width * .25 + var y = c.y - self.arena.canvas.height * .25 + + var lenC = Math.sqrt(x**2 + y**2) + bounce[i] = { + x: y / lenC, + y: -x / lenC + } + } + } + var speed = 10 + for(var i=0; i<self.candidates.length; i++){ + var c = self.candidates[i]; + c.x += bounce[i].x * speed + c.y += bounce[i].y * speed + // r = Math.sqrt(c.x**2 + c.y**2) + // if (r > modelName.size * .5) { + // // reverse the radial component of the bounce + // theta_approx = c.y/c.x + // c.x = r * theta_approx + // c.y = r * + // } + if (c.x < 0) { + bounce[i].x = Math.abs(bounce[i].x) + } else if (c.x > self.arena.canvas.width * .5) { + bounce[i].x = - Math.abs(bounce[i].x) + } + if (c.y < 0) { + bounce[i].y = Math.abs(bounce[i].y) + } else if (c.y > self.arena.canvas.height * .5) { + bounce[i].y = - Math.abs(bounce[i].y) + } + } + // radial - for(var i=0; i<self.voters.length; i++){ - var voter = self.voters[i]; - voter.update(); - voter.draw(ctx); + } else if (self.behavior == "buzz") { + bumble() } - for(var i=0; i<self.candidates.length; i++){ - var c = self.candidates[i]; - c.update(); - c.draw(ctx); - } - - if(self.yeeon){ - function drawStroked(text, x, y) { - ctx.font = "40px Sans-serif" - ctx.strokeStyle = 'black'; - ctx.lineWidth = 4; - ctx.strokeText(text, x, y); - ctx.fillStyle = 'white'; - ctx.fillText(text, x, y); - } - ctx.textAlign = "center"; - ctx.globalAlpha = 0.9 - drawStroked("yee-yee!",x*2,y*2+50); - ctx.globalAlpha = 1 + function bumble() { + // random movement + seed++ + Math.seedrandom(seed); + var stepsize = 1 + for(var i=0; i<self.candidates.length; i++){ + var c = self.candidates[i]; + c.x += Math.round((Math.random()*2-1)*stepsize) + c.y += Math.round((Math.random()*2-1)*stepsize) + } } + } + self.icon = function(id) { + if (self.placeHoldDuringElection) { + return "^Placeholder{" + id + "}" - // Update! - self.onUpdate(); - publish(self.id+"-update"); + } else if (self.nLoading > 0) { + // if the images haven't loaded yet, then put a placeholder here and flag a task to replace the text during the redraw + // (The redraw happens after images are loaded) + self.placeHolding = true + return "^Placeholder{" + id + "}" - }; + } else if (self.theme == "Letters") { + var c = self.candidatesById[id] + return "<span class='letter' style='color:"+c.fill+";'><b>"+c.name.toUpperCase()+"</b></span>" + } else { + return self.candidatesById[id].imageSelf.texticon + // return self.candidatesById[id].imageSelf.texticon_png + } + } + + self.replacePlaceholder = function(text) { + var filled = text.replace(/\^Placeholder{(.*?)}/g, (match, $1) => { + return self.icon($1) + }); + // https://stackoverflow.com/a/49262416 + var filled2 = filled.replace(/\^PlaceholderNameUpper{(.*?)}/g, (match, $1) => { + return self.nameUpper($1) + }); + return filled2 + } + + self.nameUpper = function(id) { + if (self.placeHoldDuringElection) { + return "^PlaceholderNameUpper{" + id + "}" - // HELPERS: - self.getBallots = function(){ - var ballots = []; - for(var i=0; i<self.voters.length; i++){ - var voter = self.voters[i]; - ballots = ballots.concat(voter.ballots); + } else if (self.nLoading > 0) { + // if the images haven't loaded yet, then put a placeholder here and flag a task to replace the text during the redraw + // (The redraw happens after images are loaded) + self.placeHolding = true + return "^PlaceholderNameUpper{" + id + "}" + + } else if (self.candidateIconsSet.includes("name")) { + var c = self.candidatesById[id] + // return "<span class='letterBig' style='color:"+c.fill+";'>"+c.name.toUpperCase()+"</span>" + return "<span class='letterBig'>"+c.name.toUpperCase()+"</span>" + } else { + return "<span class='letterBig'>" + self.candidatesById[id].name.toUpperCase()+"</span>" } - return ballots; - }; - self.getTotalVoters = function(){ - var count = 0; - for(var i=0; i<self.voters.length; i++){ - var voter = self.voters[i]; - count += voter.points.length; + + + } + + self.checkGotoTarena = function() { + // checks to see if we want to add the additional arena for displaying the bar charts that we use for multi-winner systems + // right now, we don't have a good visual of these for multiple districts, just one + return (self.nDistricts < 2) && self.checkSystemWithBarChart() && ! (self.roundChart == "off") && (self.enableTArena) + } + self.checkDoMultiWinnerBarCharts = function() { + // checks to see if we want to add the additional arena for displaying the bar charts that we use for multi-winner systems + // right now, we don't have a good visual of these for multiple districts, just one + return self.checkSystemWithBarChart() && ! (self.roundChart == "off") + } + self.checkSystemWithBarChart = function () { + return self.system == "QuotaApproval" || self.system == "QuotaScore" || self.system == "Monroe Seq S" || self.system == "Allocated Score" || self.system == "STAR PR" || self.system == "Phragmen Seq S" || self.system == "RRV" || self.system == "RAV" || self.system == "STV" || self.system == "equalFacilityLocation" || self.system == "PAV" + } + self.checkSystemWithRoundButtons = function() { + return self.checkSystemWithBarChart() || self.system == "IRV" + } + self.checkDoSort = function() { + if (self.orderOfVoters == undefined || self.behavior == "stand") { + return self.checkDoMultiWinnerBarCharts() || ["IRV","STV"].includes(self.system) || self.showUtilityChart + } else { + return false } - return count; - }; + } + + self.checkDoBeatMap = function() { + // ranked voter and not (original or IRV or Borda) + var autoBeatMap = (self.beatMap == "auto") && (self.ballotType == "Ranked") && ! (self.doOriginal || self.system == "IRV" || self.system == "STV" || self.system == "Borda") + var on = (self.beatMap == "on") || autoBeatMap + var doBeatMap = on && ( ! self.doTextBallots) + doBeatMap = doBeatMap && ! (self.arena.viewMan.active && self.onlyVoterMapViewMan) + return doBeatMap + } + self.checkDrawCircle = function() { + return self.yeeon || self.checkDoBeatMap() + } + self.checkDoMultiBallotConcept = function() { + // ranked voter and not (original or IRV or Borda) + var p1 = ! self.doOriginal + var p2 = self.ballotConcept != "off" + var p3 = ! self.doTextBallots + return p1 && p2 && p3 + + } + self.checkDoIRVConcept = function() { + var go = (self.system == "IRV" || self.system == "STV") && self.dimensions == "2D" && self.result + go = go && self.checkDoMultiBallotConcept() + return go + } + + self.checkDoStarStrategy = function(strategy) { + var starOr321 = ["STAR","3-2-1"].includes(self.system) + var doStar = ( starOr321 && strategy != "zero strategy. judge on an absolute scale.") || self.doStarStrategy + return doStar + } + + self.checkRunTextBallots = function() { + return self.system == "RBVote" && self.doTextBallots + } + + self.checkRunPoll = function() { + var not_f = ["zero strategy. judge on an absolute scale.","normalize"] + var skipthis = true + for(var i=0;i<self.voterGroups.length;i++){ // someone is looking at frontrunners, then don't skipthis + if (! not_f.includes(self.firstStrategy) && self.voterGroups[0].percentSecondStrategy != 100) skipthis = false + if (! not_f.includes(self.secondStrategy) && self.voterGroups[0].percentSecondStrategy != 0) skipthis = false + } //not_f.includes(config.firstStrategy) && not_f.includes(config.secondStrategy) + return ! skipthis + } + + self.checkMultiWinner = function(system) { + return (system == "QuotaApproval" || system == "QuotaScore" || system == "Monroe Seq S" || system == "Allocated Score" || system == "STAR PR" || system == "Phragmen Seq S" || system == "RRV" || system == "RAV" || system == "STV" || system == "QuotaMinimax" || system == "stvMinimax" || system == "PhragmenMax" || system == "PAV" || system == "equalFacilityLocation") + } + + self.updateVC = function() { + + // make candidates in the positions of the voters + var vs = self.voterSet.getVoterArrayXY() + self.candidates = [] + self.preFrontrunnerIds = [] + for (var k = 0; k < vs.length; k ++) { + var v = vs[k] + + var n = new Candidate(self) + n.x = v.x + n.y = v.y + + // generate a new id] + var c = Object.keys(Candidate.graphicsByIcon[self.theme]) + var x = c.length + var i = Math.floor(k/x) + 1 + var b = k % x + var icon = c[b] + n.icon = icon + n.instance = i + self.candidates.push(n) + + // INIT + n.init() + } + self.initMODEL() + self.dm.redistrictCandidates() + // update the GUI + self.onAddCandidate() + + } + + self.updateDistrictBallots = function(district) { + self.voterSet.updateDistrictBallots(district) + } + self.updateBallots = function() { + self.voterSet.updateBallots() + } + + self.random = function() { + Math.seedrandom(self.randomSeed) + self.randomSeed = Math.random() + return self.randomSeed + } }; + +function Arena(arenaName, model) { + var self = this + self.id = arenaName + + self.createDOM = function() { + self.canvas = document.createElement("canvas"); + self.canvas.setAttribute("class", "interactive"); + self.ctx = self.canvas.getContext("2d"); + // if (self.id == "arena") self.draggableManager = new DraggableManager(self,model); // only allow dragging for the main arena... for now.. TODO + self.draggableManager = new DraggableManager(self,model); + self.mouse = new Mouse(model.id + "-" + self.id, self.canvas, self.draggableManager); // MAH MOUSE + self.plusCandidate = new Plus(model) + self.plusOneVoter = new Plus(model) + self.plusVoterGroup = new Plus(model) + self.plusXVoterGroup = new Plus(model) + self.plusRectangle = new Plus(model) + self.plusCandidate.isPlusCandidate = true + self.plusOneVoter.isPlusOneVoter = true + self.plusVoterGroup.isPlusVoterGroup = true + self.plusXVoterGroup.isPlusXVoterGroup = true + self.plusRectangle.isPlusRectangle = true + self.trashes = new Trashes(model) + self.modify = new Modify(model) + self.viewMan = new ViewMan(model) + } + self.initDOM = function() { + // RETINA canvas, whatever. + self.canvas.width = self.canvas.height = model.size*2; // retina! + self.canvas.style.width = self.canvas.style.height = model.size+"px"; + self.canvas.style.borderWidth = model.border+"px"; + //self.canvas.style.margin = (2-self.border)+"px"; // use margin instead of border + + self.plusCandidate.init() + self.plusOneVoter.init() + self.plusVoterGroup.init() + self.plusXVoterGroup.init() + self.plusRectangle.init() + self.trashes.init() + self.modify.init() + self.viewMan.init() + } + + + function Plus(model){ + + var self = this; + Draggable.call(self); + self.isplus = true // when we try to drag the plus, we'll find out it's a plus and make a new candidate + + // CONFIGURE DEFAULTS + self.size = 20; + self.isPlusCandidate = false + self.isPlusOneVoter = false + self.isPlusVoterGroup = false + self.isPlusXVoterGroup = false + self.isPlusRectangle = false + + self.init = function() { + self.y = model.size - 20 + var between = 35 + if (self.isPlusCandidate) { + self.x = model.size - between * 5.5 + var srcPlus = "play/img/plusCandidate.png" + } else if (self.isPlusOneVoter) { + self.x = model.size - between * 4.5 + var srcPlus = "play/img/plusOneVoter.png" + } else if (self.isPlusVoterGroup) { + self.x = model.size - between * 3.5 + var srcPlus = "play/img/plusVoterGroup.png" + } else if (self.isPlusXVoterGroup) { + self.x = model.size - between * 1.5 + var srcPlus = "play/img/plus_bell.png" + } else if (self.isPlusRectangle) { + self.x = model.size - between * 2.5 + var srcPlus = "play/img/plus_rectangle.png" + } + // if (Loader) { + // if (Loader.assets[srcPlus]) { + // self.img = Loader.assets[srcPlus] + // } + // } + self.img = new Image(); + self.img.src = srcPlus + model.nLoading++ + self.img.onload = onLoadTool + } + self.draw = function(ctx,arena){ + + // RETINA + var p = self + var x = p.x*2; + var y = p.y*2; + var size = self.size*2; + + if(self.highlight) { + var temp = ctx.globalAlpha + ctx.globalAlpha = 0.8 + size *= 2 + // y -= size/4 + } + ctx.drawImage(self.img, x-size/2, y-size/2, size, size); + if(self.highlight) { + ctx.globalAlpha = temp + } + }; + + self.doPlus = function(doDummy) { + if (self.isPlusCandidate) { + // add candidate + var n = new Candidate(model) + n.x = self.x + n.y = self.y + // generate a new id + // look for the first one that isn't taken + for (var i=1; i < 10000000; i++) { // million is more than enough candidates + var c = Candidate.graphicsByIcon[model.theme] + for (var icon in c) { + if (i == 1) { + var newId = icon + } else { + var newId = icon + i + } + if (model.candidatesById[newId] != undefined) { + // already done + } else { + var doBreak = true + break + } + } + if (doBreak) break + } + n.icon = icon + n.instance = i + n.dummy = doDummy + model.candidates.push(n) + + // once model.candidates is updated, we call the usual functions + + + // INIT + n.init() + model.initMODEL() + // UPDATE + model.dm.redistrictCandidates() + model.onAddCandidate() + // model.update will happen later + + return n + } else if (self.isPlusOneVoter || self.isPlusVoterGroup || self.isPlusXVoterGroup || self.isPlusRectangle) { + if (self.isPlusOneVoter) { + var n = new SingleVoter(model) + } else if (self.isPlusVoterGroup || self.isPlusXVoterGroup || self.isPlusRectangle) { + var n = new GaussianVoters(model) + } + n.x = self.x + n.y = self.y + if (self.isPlusXVoterGroup) { + n.x_voters = true + n.crowdShape = "gaussian sunflower" + } else if (self.isPlusRectangle) { + n.crowdShape = "rectangles" + } else { + n.crowdShape = "circles" + } + var max = 0 + for (var i = 0; i < model.voterGroups.length; i++) { + var a = model.voterGroups[i].vid + if (a > max) max = a + } + n.vid = max + 1 + n.typeVoterModel = model.ballotType // needs init + n.firstStrategy = model.firstStrategy + n.secondStrategy = model.secondStrategy + n.spread_factor_voters = model.spread_factor_voters + model.voterGroups.push(n) + // INIT + model.initMODEL() + model.voterManager.initVoters() + return n + } + } + + } + + function Trashes(model) { + var self = this + self.init = function() { + self.t = [new Trash(model)] + if (model.dimensions != "2D") { + self.t.push (new Trash(model)) + } + for (var i=0; i<self.t.length; i++) { + self.t[i].init() + } + if (model.dimensions == "1D+B") { + self.t[1].y = model.arena.yDimOne + } else if (model.dimensions == "1D") { + self.t[0].y = model.size - model.arena.yDimOne + self.t[1].y = model.arena.yDimOne + } + } + self.tossInTrash = function() { + for (var i=0; i<self.t.length; i++) { + if (self.t[i].overTrash) self.t[i].tossInTrash() + } + } + self.test = function() { + var r = false + for (var i=0; i<self.t.length; i++) { + self.t[i].test() + r = r || self.t[i].overTrash + } + self.overTrash = r + } + self.draw = function(ctx,a) { + for (var i=0; i<self.t.length; i++) { + self.t[i].draw(ctx,a) + } + } + } + + function Trash(model){ + + var self = this; + Draggable.call(self); + self.istrash = true + self.isArenaObject = true + + // CONFIGURE DEFAULTS + self.size = 20; + self.img = new Image(); + self.img.src = "play/img/trash.png" + model.nLoading++ + self.img.onload = onLoadTool + self.init = function() { // these x & y are with respect to the arena, rather than the model + self.x = model.size - 20 + self.y = model.size - 20 + } + self.init() + self.draw = function(ctx,arena){ + + // RETINA + var x = self.x*2; + var y = self.y*2; + var size = self.size*2; + + if(self.highlight) { + var temp = ctx.globalAlpha + ctx.globalAlpha = 0.8 + } + ctx.drawImage(self.img, x-size/2, y-size/2, size, size); + if(self.highlight) { + ctx.globalAlpha = temp + } + + // TODO: make the trashcan open + }; + + self.tossInTrash = function() { + self.overTrash = false + + var d = model.arena.mouse.dragging + // find the candidate in the candidate list + for (var i=0; i < model.candidates.length; i++) { + if (model.candidates[i] == d) { + // delete candidate + model.candidates.splice(i,1) + + break + } + } + for (var i=0; i < model.preFrontrunnerIds.length; i++) { + if (model.preFrontrunnerIds[i] == d.id) { + // delete candidate + model.preFrontrunnerIds.splice(i,1) + break + } + } + model.preFrontrunnerIds[i] + + // find the voter in the list + for (var i=0; i < model.voterGroups.length; i++) { + if (model.voterGroups[i] == d) { + // delete candidate + model.voterGroups.splice(i,1) + // need to init voterSet + model.voterSet.init() + // remove name from customNames, if there was one + if (model.voterGroupNameList && model.voterGroupNameList[i] !== undefined) { + model.voterGroupNameList.splice(i,1) + } + model.voterManager.initNames() + model.voterManager.onDeleteVoterGroup() + + // also, check if the viewMan was on a voter in the group + var viewMan = model.arena.viewMan + if (viewMan.active && i == viewMan.focus.iGroup) { + viewMan.unInit() + viewMan.configure() + } + + break + } + } + + + // INIT + model.initMODEL() + // update the GUI + model.onAddCandidate() + model.dm.redistrict() + + self.size = 20 + return + } + + self.test = function() { + var d = model.arena.mouse.dragging + var p = d.newArenaPosition(model.arena.mouse.x,model.arena.mouse.y); + var dx = p.x - self.x + var dy = p.y - self.y + var r = self.size * self.radiusScale + if (model.arena.mouse.isTouch) r += d.touchAdd + if (dx * dx + dy * dy < r * r) { + // we have trash + self.size = 40 + self.overTrash = true + } else { + self.size = 20 + self.overTrash = false + } + } + + } + + function Modify(model) { + var self = this; + Draggable.call(self); + self.isModify = true // might help later + self.isGear = true + self.isArenaObject = true + + // CONFIGURE DEFAULTS + self.size = 20; + + self.init = function() { + var srcMod = "play/img/gear.png" + // if (Loader) { + // if (Loader.assets[srcMod]) { + // self.img = Loader.assets[srcMod] + // } + // } + self.img = new Image(); + self.img.src = srcMod + model.nLoading++ + self.img.onload = onLoadTool + self.configure() + } + self.configure = function() { + if (self.active) { + var f = model.arena.modelToArena(self.focus) + self.x = f.x + self.y = f.y + } else { + self.y = model.size - 20 + var between = 35 + self.x = model.size - between * 6.5 + } + } + self.draw = function(ctx,arena){ + // if it is near a candidate, then draw it on that candidate + // when the mouse is let go, the coordinates will snap to the candidate + + if (self.active) { + var f = model.arena.modelToArena(self.focus) + self.x = f.x + self.y = f.y + } + + // RETINA + var x = self.x*2; + var y = self.y*2; + var size = self.size*2; + + if(self.highlight) { + var temp = ctx.globalAlpha + ctx.globalAlpha = 0.8 + size *= 2 + // y -= size/4 + } + ctx.drawImage(self.img, x-size/2, y-size/2, size, size); + if(self.highlight) { + ctx.globalAlpha = temp + } + }; + + self.doModify = function(flashydude) { + // snap to the nearest candidate, that we've been drawing on + if (flashydude && (flashydude.isCandidate || flashydude.isGaussianVoters) ) { + self.focus = flashydude + self.active = true + self.configure() + if (flashydude.isGaussianVoters) { + model.arena.up = new Up(model,flashydude) // create controls + if (flashydude.crowdShape == "rectangles") { + model.arena.down = new Down(model,flashydude) + } + model.arena.up.init() + if (model.arena.down) { + model.arena.down.init() + } + } + model.arena.right = new Right(model,flashydude) + model.arena.right.init() + model.arena.initARENA() // add the controls to the arena + } else { + self.unInit() + self.init() + model.arena.initARENA() + } + } + + self.unInit = function() { + self.active = false + self.focus = null + model.arena.up = null + model.arena.right = null + model.arena.down = null + } + } + function ViewMan(model) { + var self = this; + Draggable.call(self); + self.isModify = true // might help later + self.isViewMan = true // might help later + self.isArenaObject = true + + // CONFIGURE DEFAULTS + self.size = 20; + self.sizey = 60; + + self.init = function() { + var srcMod = "play/img/viewMan.png" + // if (Loader) { + // if (Loader.assets[srcMod]) { + // self.img = Loader.assets[srcMod] + // } + // } + self.img = new Image(); + self.img.src = srcMod + model.nLoading++ + self.img.onload = onLoadTool + self.configure() + } + self.configure = function() { + if (self.active) { + var f = model.arena.modelToArena(self.focus) + self.x = f.x + self.y = f.y + } else { + self.y = model.size - 20 + var between = 35 + self.x = model.size - between * 7.5 + } + } + self.draw = function(ctx,arena){ + // if it is near a candidate, then draw it on that candidate + // when the mouse is let go, the coordinates will snap to the candidate + + + if (self.active) { + var f = model.arena.modelToArena(self.focus) + self.x = f.x + self.y = f.y + } + + // RETINA + var x = self.x*2; + var y = self.y*2; + var size = self.size*2; + var sizey = self.sizey*2; + + var doDoubleSize = self.highlight || self.active || model.arena.mouse.dragging == self + if(doDoubleSize) { + var temp = ctx.globalAlpha + ctx.globalAlpha = 0.8 + size *= 2 + sizey *= 2 + // y -= size/4 + } + ctx.drawImage(self.img, x-size/2, y-sizey/2, size, sizey); + if(doDoubleSize) { + ctx.globalAlpha = temp + } + }; + self.drag = function(arena) { + + arena.update() // update position of draggable + + // focus on closest voter + var closest = arena.draggableManager.nearestVoterToMouse() + if (closest) { + self.focus = closest + + // check for snap + self.snap(arena) + } + + model.drawArenas() // draw everything inside the arenas + if (closest) model.onDraw() // draw ballot for closest voter + + } + self.snap = function(arena) { + + if (self.focus) { + + // check for snap + var hit = self.objectMouseHitTest(25, self.focus, arena) + if (hit) { + self.active = true + self.configure() + return true + } + // model.arena.initARENA() // add the controls to the arena + } + self.active = false + return false + } + + self.drop = function() { + self.configure() + // model.arena.initARENA() + } + + self.unInit = function() { + self.active = false + self.focus = null + } + } + + function Up(model,o) { // o is the object that is being modified + var self = this; + Draggable.call(self); + self.isUp = true // might help later + self.isArenaObject = true + self.dontchangex = true + self.o = o + self.scale = 4 + self.shapeScale = 5/20 + + // CONFIGURE DEFAULTS + self.size = 20; + + self.init = function() { + var srcMod = "play/img/gear.png" + // if (Loader) { + // if (Loader.assets[srcMod]) { + // self.img = Loader.assets[srcMod] + // } + // } + self.img = new Image(); + self.img.src = srcMod + model.nLoading++ + self.img.onload = onLoadTool + self.configure() + } + self.configure = function() { + var oa = model.arena.modelToArena(o) + var length = self.getLengthFromProp() + self.y = oa.y - length + self.x = oa.x + self.xC = oa.x + self.yC = oa.y + } + self.getLengthFromProp = function() { + if (o.crowdShape == "rectangles" || o.crowdShape == "circles") { + if (typeof o.group_count_h !== "undefined") { + var length = self.propToLength(o.group_count_h) + } else { + var length = 60 + } + } else { + if (typeof o.group_count !== "undefined") { + var length = self.propToLength(o.group_count) + } else { + var length = 60 + } + } + return length + } + self.propToLength = function(p) { + if (o.crowdShape == "rectangles" || o.crowdShape == "circles") { + return Math.round(p / self.shapeScale) + } else { + return Math.round(p / self.scale) + } + + } + self.updatePropFromLength = function(length) { + var prop = self.lengthToProp(length) + if (o.crowdShape == "rectangles" || o.crowdShape == "circles") { + o.group_count_h = prop + } else { + o.group_count = prop + } + + } + self.lengthToProp = function(length) { + if (o.crowdShape == "rectangles" || o.crowdShape == "circles") { + return Math.round(length * self.shapeScale) + } else { + return Math.round(length * self.scale) + } + + } + + self.draw = function(ctx,arena){ + // RETINA + var p = self + var x = p.x*2; + var y = p.y*2; + var size = self.size*2; + + if(self.highlight) { + var temp = ctx.globalAlpha + ctx.globalAlpha = 0.8 + size *= 2 + // y -= size/4 + } + + ctx.drawImage(self.img, x-size/2, y-size/2, size, size); + + var arrow_size = 20 + var offset_y = 0 + ctx.beginPath(); + ctx.moveTo(x,y+offset_y) + ctx.lineTo(self.xC*2,self.yC*2) + ctx.moveTo(x,y+offset_y) + ctx.lineTo(x+arrow_size, y + offset_y + arrow_size) + ctx.moveTo(x,y+offset_y) + ctx.lineTo(x-arrow_size, y + offset_y + arrow_size) + ctx.lineWidth = 10 + ctx.strokeStyle = "#333"; + ctx.stroke(); + + if(self.highlight) { + ctx.globalAlpha = temp + } + }; + + } + + function Right(model,o) { // o is the object that is being modified + var self = this; + Draggable.call(self); + self.isRight = true // might help later + self.isArenaObject = true + self.dontchangey = true + self.o = o + self.scale = 5 + self.sizeScale = 2/3 + + // CONFIGURE DEFAULTS + self.size = 20; + + self.init = function() { + var srcMod = "play/img/gear.png" + // if (Loader) { + // if (Loader.assets[srcMod]) { + // self.img = Loader.assets[srcMod] + // } + // } + self.img = new Image(); + self.img.src = srcMod + model.nLoading++ + self.img.onload = onLoadTool + self.configure() + } + self.configure = function() { + var oa = model.arena.modelToArena(o) + self.y = oa.y + self.yC = oa.y + self.xC = oa.x + if (typeof o.group_spread !== "undefined") { + var length = o.group_spread / self.scale + self.x = oa.x + length + } else if (typeof o.b !== "undefined") { + var length = o.size / self.sizeScale + self.x = oa.x + length + } else { + self.x = oa.x + 60 + } + } + self.draw = function(ctx,arena){ + // RETINA + var p = self + var x = p.x*2; + var y = p.y*2; + var size = self.size*2; + + if(self.highlight) { + var temp = ctx.globalAlpha + ctx.globalAlpha = 0.8 + size *= 2 + // y -= size/4 + } + + ctx.drawImage(self.img, x-size/2, y-size/2, size, size); + + var arrow_size = 20 + var offset_x = 0 + + ctx.beginPath(); + ctx.moveTo(x-offset_x,y) + ctx.lineTo(self.xC*2,self.yC*2) + ctx.moveTo(x-offset_x,y) + ctx.lineTo(x-offset_x-arrow_size, y + arrow_size) + ctx.moveTo(x-offset_x,y) + ctx.lineTo(x-offset_x-arrow_size, y - arrow_size) + ctx.lineWidth = 10 + ctx.strokeStyle = "#333"; + ctx.stroke(); + + if(self.highlight) { + ctx.globalAlpha = temp + } + }; + + } + function Down(model,o) { // o is the object that is being modified + var self = this; + Draggable.call(self); + self.isDown = true // might help later + self.isArenaObject = true + self.dontchangex = true + self.o = o + self.scale = 5/20 + self.sizeScale = 2/3 + + // CONFIGURE DEFAULTS + self.size = 20; + + self.init = function() { + var srcMod = "play/img/gear.png" + // if (Loader) { + // if (Loader.assets[srcMod]) { + // self.img = Loader.assets[srcMod] + // } + // } + self.img = new Image(); + self.img.src = srcMod + model.nLoading++ + self.img.onload = onLoadTool + self.configure() + } + self.configure = function() { + var oa = model.arena.modelToArena(o) + if (typeof o.group_count_vert !== "undefined") { + var length = self.propToLength(o.group_count_vert) + self.y = oa.y + length + } else { + self.y = oa.y + 60 + } + self.x = oa.x + self.xC = oa.x + self.yC = oa.y + } + self.lengthToProp = p => Math.round(p * self.scale) + self.propToLength = p => p / self.scale + self.draw = function(ctx,arena){ + // RETINA + var p = self + var x = p.x*2; + var y = p.y*2; + var size = self.size*2; + + if(self.highlight) { + var temp = ctx.globalAlpha + ctx.globalAlpha = 0.8 + size *= 2 + // y -= size/4 + } + + ctx.drawImage(self.img, x-size/2, y-size/2, size, size); + + var arrow_size = 20 + var offset_y = 0 + + ctx.beginPath(); + ctx.moveTo(x,y-offset_y) + ctx.lineTo(self.xC*2,self.yC*2) + ctx.moveTo(x,y-offset_y) + ctx.lineTo(x+arrow_size, y - offset_y - arrow_size) + ctx.moveTo(x,y-offset_y) + ctx.lineTo(x-arrow_size, y - offset_y - arrow_size) + ctx.lineWidth = 10 + ctx.strokeStyle = "#333"; + ctx.stroke(); + + if(self.highlight) { + ctx.globalAlpha = temp + } + }; + + } + function onLoadTool() { + model.nLoading-- + if (model.nLoading == 0) { + model.draw() + } + } + + + self.bFromY = function (y) { + return (model.size - y)/60 + } + self.yFromB = function (b) { + return model.size - b * 60 + } + + // function Right(model,o) { // o is the object that is being modified + // Up.call(self,model,o) + // } + + + self.initARENA = function() { + + // Draggable candidates and voters + var doControls = false + var doVoters = true + var doCandidates = true + + if (self.id == "arena" && model.showToolbar == "on") { + doControls = true + } + + self.draggables = []; + if (doControls) { + self.draggables.push(self.plusCandidate) + self.draggables.push(self.plusOneVoter) + self.draggables.push(self.plusVoterGroup) + self.draggables.push(self.plusXVoterGroup) + self.draggables.push(self.plusRectangle) + for (var i=0; i<self.trashes.t.length; i++) { + self.draggables.push(self.trashes.t[i]) + } + } + for (var i=0; i<model.candidates.length; i++) { + var c = model.candidates[i] + self.draggables.push(c); + } + for (var i=0; i<model.voterGroups.length; i++) { + var v = model.voterGroups[i] + self.draggables.push(v); + } + if(model.voterCenter && model.voterGroups.length > 1) self.draggables.push(model.voterCenter) + if (doControls) { + self.draggables.push(self.modify) + if (self.modify.active) { + if (self.up) { + self.draggables.push(self.up) + } + if (self.down) { + self.draggables.push(self.down) + } + self.draggables.push(self.right) + } + self.draggables.push(self.viewMan) + } + } + + + self.modelToArena = function(d) { + if (d.isArenaObject) { + return d + } + if (self.id == "tarena") { + if (d.isCandidate) { + if (model.dimensions == "2D") { + // find closest voter's index + var i = _closestVoterIndex(d,model) + var xP = i * (self.canvas.width/2) + return {x:xP,y:50} + } else { + var xP = _xToPercentile(d.x,model) / 100 * (self.canvas.width/2) + // return {x:xP,y:15*d.i+7} // each candidate has his own track + return {x:xP,y:50} + } + } else { + return {x:0,y:-100} // offscreen... move voter offscreen + } + } else { + var x,y + // just a regular arena + x = d.x + if (model.dimensions == "1D+B") { + if (d.isCandidate) { + y = self.yFromB(d.b) + } else if (d.isVoter || d.isVoterCenter || d.isVoterPerson) { + y = self.yDimOne + } else { + y = d.y + } + } else if (model.dimensions == "1D" ) { + if (d.isCandidate) { + y = model.size - self.yDimOne + } else if (d.isVoter || d.isVoterCenter || d.isVoterPerson) { + y = self.yDimOne + } else { + y = d.y + } + } else { + y = d.y + } + return {x:x,y:y} + } + + } + + function _closestVoterIndex(d,model) { // returns where the candidate should be in the sorted list of voters + closest = 0 + min = Infinity + var a = model.voterSet.getVoterArray() + for (var i = 0; i < a.length; i++) { + v = a[model.orderOfVoters[i]] + var dx = d.x - v.x + var dy = d.y - v.y + var d2 = dx*dx + dy*dy + if (d2 < min) { + min = d2 + closest = i + } + } + return closest / a.length + } + + self.arenaToModel = function(p,d) { // p is the new arena coordinate and d is the old model object (usually it is being dragged) + + var x,y,b // return variables + + if (d.isArenaObject) { + return p + } + if (self.id == "tarena") { + // Percentiles for Tetris Ballot Arena + var percentile = p.x/(self.canvas.width/2) * 100 + percentile = Math.min(100,percentile) + percentile = Math.max(0,percentile) // TODO: figure out why I would get a mouse at a negative x + x = _percentileToX(percentile, model) + y = _percentileToY(percentile, model) + if (d.isCandidate) { + b = d.b + } + } else { + // Main Arena + x = p.x + if (model.dimensions == "2D") { + y = p.y + if (d.isCandidate) { + b = d.b + } + } else { + y = d.y + if (d.isCandidate) { + if (model.dimensions == "1D+B") { + var h = (self.yDimOne + self.yDimBuffer) + var limYc = (model.size - h) * .3 + h + if (p.y < limYc) { + b = self.bFromY(limYc) + } else { + b = self.bFromY(p.y) + } + } else { + b = d.b + } + } + } + } + return {x:x,y:y,b:b} // b is sometimes undefined, on purpose + } + + self.update = function(){ + // Move the one that's being dragged, if any + + self.yDimOne = model.size * .6 / 1.6 + self.yDimBuffer = 20 + // self.yDimBuffer = (model.size - self.yDimOne) * .3 + + // if (self.id == "arena") { + if(self.mouse.dragging){ + var d = self.mouse.dragging + if (d.isCandidate || d.isVoter || d.isVoterCenter) { + var p = d.newArenaPosition(self.mouse.x,self.mouse.y); + var n = self.arenaToModel(p,d) + d.x = n.x + d.y = n.y + if (d.isCandidate) { + d.b = n.b + d.update() + model.dm.candidatePicksDistrict(d) + model.dm.districtsListCandidates() + } + if (d.isVoter) { + _pileVoters(model) + model.dm.redistrict() + } + if (d.isVoterCenter) { + d.drag() + model.dm.redistrict() + } + } else { + var p = d.newArenaPosition(self.mouse.x,self.mouse.y); + d.x = p.x + d.y = p.y + } + if (d.isUp) { + d.x = d.xC + d.y = Math.min(d.yC,d.y) + if (d.o.voterGroupType && d.o.voterGroupType=="GaussianVoters") { + var length = -(d.y - d.yC) + d.updatePropFromLength(length) + d.o.init() + _pileVoters(model) + model.dm.redistrict() + model.updateFromModel() + } + } else if (d.isDown) { + d.x = d.xC + d.y = Math.max(d.yC,d.y) + if (d.o.voterGroupType && d.o.voterGroupType=="GaussianVoters" && d.o.crowdShape == "rectangles") { + var length = d.y - d.yC + d.o.group_count_vert = d.lengthToProp(length) + d.o.init() + _pileVoters(model) + model.dm.redistrict() + model.updateFromModel() + } + } else if (d.isRight) { + d.y = d.yC + d.x = Math.max(d.xC,d.x) + var length = d.x - d.xC + if (d.o.voterGroupType && d.o.voterGroupType=="GaussianVoters") { + d.o.group_spread = length * d.scale + d.o.init() + _pileVoters(model) + model.dm.redistrict() + model.updateFromModel() + } else if (d.o.isCandidate) { + d.o.size = length * d.sizeScale + d.o.b = d.o.bFromSize(d.o.size) + } + } else if (d.isModify) { + d.unInit() // dont show the axes when we're dragging the modify gear + } + } + if (self.modify && self.modify.active) { // update the modify value + self.modify.configure() + self.right.configure() // so re-configure the lengths of these controls + if (self.up) { // this value might have changed + self.up.configure() + } + if (self.down) { // this value might have changed + self.down.configure() + } + } + if (self.viewMan && self.viewMan.active) { // update the viewMan value + self.viewMan.configure() + } + } + + self.clear = function(){ + // Clear it all! + self.ctx.clearRect(0,0,self.canvas.width,self.canvas.height); + // not sure why this doesn't work + var darkMode = false + if (darkMode) { + if (model.ballotType == "Ranked" || model.ballotType == "Plurality") { + if (model.system != "Borda") { + if (! (model.yeeon || model.checkDoBeatMap()) ) { + self.ctx.fillStyle = "#222" + self.ctx.beginPath() + self.ctx.fillRect(0,0,self.canvas.width,self.canvas.height); + self.ctx.closePath() + self.ctx.fill() + } + } + } + } + } + + self.drawHorizontal = function(yLine) { + // draw line through middle + var ctx = self.ctx + ctx.beginPath(); + ctx.moveTo(0,yLine*2); + ctx.lineTo(ctx.canvas.width,yLine*2); + ctx.lineWidth = 5; + ctx.strokeStyle = "#888"; + + ctx.setLineDash([5, 15]); + ctx.stroke(); + ctx.setLineDash([]); + } + + self.draw = function(){ + + // DRAW 'EM ALL. + // Draw voters' BG first, then candidates, then voters. + + // set winners + setWinners() + + var noClip = true + //set annotations + resetAnnotations() + if (self.id == "arena") { + setAnnotations() + // if (model.orderOfVoters) drawSortLines() + if (model.dimensions == "2D" || noClip) { + drawVoters0() + drawVoters1() + drawToolbar() + var flashydude = getFlashydude() + drawExtraTrash() + drawVotes() + drawVoters2() + drawVoterCenter() + drawCandidates() + drawAnotherYeeObject() + drawFlashydude() + drawWinners() + } else { + drawToolbar() + var flashydude = getFlashydude() + var flashyFirst = (flashydude && flashydude.isCandidate) + // startClip() + drawCandidates() + if (flashyFirst) drawFlashydude() + drawWinners() + // resetClip() + clipCandidates() + drawVoters0() + drawVoters1() + drawExtraTrash() + drawVotes() + drawVoters2() + if (!flashyFirst) drawFlashydude() + drawVoterCenter() + drawAnotherYeeObject() + } + drawModify() + setBorderColor() + } else { + var flashydude = getFlashydude() + drawCandidates() + var flashyFirst = (flashydude && flashydude.isCandidate) + if (flashyFirst) drawFlashydude() // only if flashydude is a candidate + // flashydude means we are hovering over the object and the object changes color to show we're hovering + drawWinners() + setBorderColor() + } + + // //set annotations + // if (self.id == "arena") { + // setAnnotations() + // // drawSortLines() + // drawToolbar() + // } + // var flashydude = getFlashydude() + // if (model.dimensions != "2D") { + // drawCandidates() + // if (flashydude && flashydude.isCandidate) drawFlashydude() + // if (self.id == "arena") clipCandidates() + // } + // if (self.id == "arena") { + // drawVoters() + // } + // if (model.dimensions == "2D") { + // drawCandidates() + // } + // if (self.id == "arena") { + // drawVoterCenter() + // drawAnotherYeeObject() + // } + // if (!(flashydude && flashydude.isCandidate && model.dimensions != "2D")) drawFlashydude() + + // if (self.id == "arena") { + // drawModify() + // } + // drawWinners() + // setBorderColor() + + function resetAnnotations() { + // reset annotations + for(var i=0; i<self.draggables.length; i++){ + var draggable = self.draggables[i]; + draggable.drawAnnotation = (function(){}); + draggable.drawBackAnnotation = (function(){}); + } + } + function setAnnotations() { + if(model.yeeobject) model.yeeobject.drawBackAnnotation = model.viz.yee.drawYeeGuyBackground + if(model.yeeobject) model.yeeobject.drawAnnotation = model.viz.yee.drawYeeAnnotation + } + + function drawSortLines() { + // draw sort lines + var s = model.getSortedVoters() + for (var i=0; i<s.length-1; i++) { + // draw line from here to next + + var ctx = self.ctx + var va = s[i] + var vb = s[i+1] + ctx.beginPath(); + ctx.moveTo(va.x*2 ,va.y*2 ); + ctx.lineTo(vb.x*2 ,vb.y*2 ); + ctx.lineWidth = 5; + ctx.strokeStyle = "#888"; + ctx.stroke(); + + } + + } + + function drawToolbar() { + if (model.showToolbar == "on") { + self.plusCandidate.draw(self.ctx,self) + self.plusOneVoter.draw(self.ctx,self) + self.plusVoterGroup.draw(self.ctx,self) + self.plusXVoterGroup.draw(self.ctx,self) + self.plusRectangle.draw(self.ctx,self) + self.trashes.t[0].draw(self.ctx,self) + } + } + + + function getFlashydude() { + for(var i=0; i<model.candidates.length; i++){ + var c = model.candidates[i]; + if (c.highlight) { + return c + } + } + for(var i=0; i<model.voterGroups.length; i++){ + var voter = model.voterGroups[i]; + if (voter.highlight) { + return voter + } + } + return null + } + + function startClip() { + // Clip a rectangular area + var dy = self.yDimOne + self.yDimBuffer + var ctx = self.ctx + ctx.rect(0,dy,self.canvas.width,self.canvas.height-dy); + ctx.stroke(); + ctx.clip(); + } + + function resetClip() { + // Clip a rectangular area + var ctx = self.ctx + ctx.rect(0,0,self.canvas.width,self.canvas.height); + ctx.stroke(); + ctx.clip(); + } + + function drawCandidates() { + // There's two ways to draw the candidate. One shows the candidate icon. + // Two shows the vote totals and optionally, the candidate icon. + var go = model.checkDoIRVConcept() && self.id == "arena" && model.opt.doDrawIRVCandidates + if (go) { + for (var k = 0; k < model.district.length; k++) { + result = model.district[k].result + drawIRVCandidates(result) + } + } + var ctx = self.ctx + for(var i=0; i<model.candidates.length; i++){ + var c = model.candidates[i]; + if (c.highlight) { + continue + } + if (go) { // use transparency to draw candidate + temp = ctx.globalAlpha + ctx.globalAlpha = (model.theme == "Letters") ? .8 : .4 + c.draw(ctx,self); + ctx.globalAlpha = temp + } else { + c.draw(ctx,self); + } + } + } + + function drawIRVCandidates(result) { + + if (result === undefined) return + + var coalitions = result.coalitions + + if (coalitions.length == 0) return // edge case: everybody tied + + for (var i = 0; i < coalitions.length; i++) { + var coalition = coalitions[i] + drawCoalition(coalition,result) + } + + } + + function drawCoalition(coalition,result) { + + + var from = model.candidatesById[coalition.id] + var list = coalition.list + + var canIdByDecision = result.canIdByDecision + var nBallots = result.nBallots + var ctx = self.ctx + var norm = 2.5 * nBallots / (135 * model.seats) + var total = 0 + + for (var firstID in list) { + var size = list[firstID] + total = total + size + } + + ctx.lineWidth = 0; + + if (1) { + norm = norm * 0.02 + + sumsize = 0 + var slices = [] + for (var i = 0; i < canIdByDecision.length; i++) { + var firstID = canIdByDecision[i] + if ( list.hasOwnProperty(firstID)) { + var first = model.candidatesById[firstID] + var size = list[firstID] + if (size > 0) { + // add to list + slices.push({num:size, fill:first.fill}) + } + } + } + if (slices.length === 0) return + var x = from.x + var y = from.y + var lastme = slices.pop() + mesize = lastme.num + var totalSlices = total - mesize + var size2 = Math.sqrt(total/norm) + if (slices.length > 0) { + _drawSlices(model, ctx, x, y, size2, slices, totalSlices) + } + + // draw me + r = Math.sqrt(mesize/ norm) + ctx.beginPath(); + ctx.arc(x*2, y*2, r, 0, Math.TAU, false); + ctx.strokeStyle = lastme.fill; + ctx.fillStyle = lastme.fill; + ctx.fill(); + return + } + + + sumsize = 0 + for (var i = 0; i < canIdByDecision.length; i++) { + var firstID = canIdByDecision[i] + if ( list.hasOwnProperty(firstID)) { + var first = model.candidatesById[firstID] + var size = list[firstID] / norm + if (size > 0) { + var x = from.x + var y = from.y + + + r = Math.sqrt((total - sumsize) / norm) * 5 + r = (total - sumsize) / norm * .2 + + ctx.beginPath(); + ctx.arc(x*2, y*2, r*2, 0, Math.TAU, false); + ctx.strokeStyle = first.fill; + ctx.fillStyle = first.fill; + ctx.fill(); + ctx.stroke(); + } + sumsize = sumsize + size + + } + } + } + + + function clipCandidates() { + // draw a white rectangle + var ctx = self.ctx + var temp = ctx.globalAlpha + ctx.globalAlpha = 1 + ctx.fillStyle = "#fff" + ctx.fillRect(0,0,ctx.canvas.width,(self.yDimOne + self.yDimBuffer)*2) // draw a white background + ctx.fill() + ctx.globalAlpha = temp + } + + + function drawExtraTrash() { + if (model.dimensions != "2D" && model.showToolbar == "on") { + self.trashes.t[1].draw(self.ctx,self) + } + } + + function drawVoters0() { + for(var i=0; i<model.voterGroups.length; i++){ + var voter = model.voterGroups[i]; + voter.draw0(self.ctx,self); + } + } + function drawVoters1() { + for(var i=0; i<model.voterGroups.length; i++){ + var voter = model.voterGroups[i]; + voter.draw1(self.ctx,self); + } + } + function drawVotes() { + + if ( ! model.checkDoMultiBallotConcept() ) return + + var go = model.system == "IRV" || model.system == "STV" + if (go && model.dimensions == "2D" && model.result && model.opt.showIRVTransfers) { + ctx = self.ctx + for (var k = 0; k < model.district.length; k++) { + result = model.district[k].result + if (result) drawIRVTransfers(result) + } + } + } + function drawIRVTransfers(result) { + var transfers = result.transfers + var topChoice = result.topChoice + var nBallots = result.nBallots + + if (transfers.length == 0) return // edge case: everybody tied + + for (var i = 0; i < transfers.length; i++) { + for (var k = 0; k < transfers[i].length; k++) { + var transfer = transfers[i][k] + var from = model.candidatesById[transfer.from] + var flows = transfer.flows + for (var toID in flows) { + var f = flows[toID] + var to = model.candidatesById[toID] + drawTransfer(f,from,to,nBallots) + } + } + } + + if (model.opt.IRVShowdown) { + if (0) { + // make a list of voters first choices for the voters in the winning coalition + for (var k = 0; k < result.winners.length; k++) { + winner = result.winners[k] + coalition = {} + for (var i = 0; i < model.candidates.length; i++) { + coalition[model.candidates[i].id] = 0 + } + for (var i = 0; i < topChoice[0].length; i++) { + penultimate = topChoice[topChoice.length-1][i] + first = topChoice[0][i] + if( penultimate == winner) { + coalition[first] ++ + } + } + to = model.candidatesById[winner] + frac = .5 + halfway = {} + halfway.x = from.x * frac + to.x * (1-frac) + halfway.y = from.y * frac + to.y * (1-frac) + drawTransfer(coalition,halfway,to,nBallots) + } + } else { + // make a list of voters first choices for the voters in the winning coalition + for (var m = 0; m < result.lastlosers.length; m++) { + var loser = result.lastlosers[m] + var from = model.candidatesById[loser] + + for (var k = 0; k < result.winners.length; k++) { + winner = result.winners[k] + coalition = {} + for (var i = 0; i < model.candidates.length; i++) { + coalition[model.candidates[i].id] = 0 + } + for (var i = 0; i < topChoice[0].length; i++) { + penultimate = topChoice[topChoice.length-1][i] + first = topChoice[0][i] + if( penultimate == winner) { + coalition[first] ++ + } + } + to = model.candidatesById[winner] + frac = .5 + halfway = {} + halfway.x = from.x * frac + to.x * (1-frac) + halfway.y = from.y * frac + to.y * (1-frac) + drawTransfer(coalition,halfway,to,nBallots) + + } + } + } + } + + + + } + + function drawTransfer(flow,from,to,nBallots) { + var ctx = self.ctx + var norm = 2.5 * nBallots / (135 * model.seats) + norm = norm * 1.5 + var total = 0 + for (var firstID in flow) { + var size = flow[firstID] / norm + total = total + size + } + sumsize = 0 + for (var firstID in flow) { + var size = flow[firstID] / norm + var first = model.candidatesById[firstID] + if (size > 0) { + + var u = _unitVector(to,from) + // amount to move by + move = total/2 - sumsize - size/2 + x1 = from.x + u.y * move + x2 = to.x + u.y * move + y1 = from.y - u.x * move + y2 = to.y - u.x * move + + ctx.beginPath(); + ctx.moveTo(x1*2,y1*2); + ctx.lineTo(x2*2,y2*2); + ctx.lineWidth = size*2; + ctx.strokeStyle = first.fill; + ctx.stroke(); + } + sumsize = sumsize + size + } + + + if (total > 0) { + frac = .5 + halfway = {} + halfway.x = from.x * frac + to.x * (1-frac) + halfway.y = from.y * frac + to.y * (1-frac) + + var u = _unitVector(to,from) + var alen = 5 + a = { + from: { + x: halfway.x - alen * u.x, + y: halfway.y - alen * u.y + }, + to: { + x: halfway.x + alen * u.x, + y: halfway.y + alen * u.y + }, + } + _drawArrow(ctx, a.from.x * 2, a.from.y * 2, a.to.x * 2, a.to.y * 2) + } + + } + + function drawVoters2() { + for(var i=0; i<model.voterGroups.length; i++){ + var voter = model.voterGroups[i]; + voter.draw2(self.ctx,self); + } + } + + function drawVoterCenter() { + var oneVoter = (model.voterGroups.length == 1) + // var oneVoter = (model.voterGroups.length == 1 && model.voterGroups[0].points.length == 1) + var isCenter = (typeof model.voterCenter !== 'undefined') + if (isCenter && ! oneVoter) { + model.voterCenter.draw(self.ctx) + } + } + + function drawAnotherYeeObject() { + var oneVoter = (model.voterGroups.length == 1) + // var oneVoter = (model.voterGroups.length == 1 && model.voterGroups[0].points.length == 1) + var isCenter = (typeof model.voterCenter !== 'undefined') + + if (isCenter && ! oneVoter) { + return + } + + // draw the Yee object last so it is easy to see. + if (model.yeeon && model.yeeobject) { + var yeeCenter = (isCenter) ? (model.yeeobject == model.voterCenter) : false + var yeeOne = model.yeeobject == model.voterGroups[0] + var covering = (oneVoter && (yeeOne || yeeCenter) ) + // unless it covers the one voter + if (! covering ) { + + if (model.yeeobject.isCandidate) { + model.yeeobject.draw(self.ctx,self) + } else { + model.yeeobject.draw2(self.ctx,self) + } + } + } + } + + function drawFlashydude() { + var d = self.mouse.dragging + var dontdo = true // I'm not sure why I did this part and it probably can be removed. + if (!dontdo && d) { // draw the dragging object again (twice but that's okay) + if (d.isVoter) { + d.draw2(self.ctx,self) + } else if (d.isVoterCenter){ + if(! oneVoter) { + d.draw(self.ctx,self) + } + } else { + d.draw(self.ctx,self) + } + } else { + if (flashydude) { + if (flashydude.isVoter) { + flashydude.draw2(self.ctx,self) + } else { + flashydude.draw(self.ctx,self) + } + } + } + } + + function drawModify() { + if (model.showToolbar == "on") { + self.modify.draw(self.ctx,self) + + if (self.modify.active) { + if (self.up) { + self.up.draw(self.ctx,self) + } + if (self.down) { + self.down.draw(self.ctx,self) + } + self.right.draw(self.ctx,self) + } + + self.viewMan.draw(self.ctx,self) + } + + } + + function drawWinners() { + if (!model.dontdrawwinners) { + // draw text next to the winners + + // check how many winners there should be + let winnersAllowed = 1 + if (model.checkMultiWinner(model.system)) { + winnersAllowed = model.seats + } + + for (var k = 0; k < model.district.length; k++) { + var district = model.district[k] + var result = district.result + + + + if(result && result.winners) { + var winners = result.winners + + // draw differently for each round + if (model.roundCurrent !== undefined) { + var round = model.roundCurrent[district.i] + // if (round > model.result.history.rounds.length) round = model.result.history.rounds.length - 1 + if (round !== undefined && (model.system == "STV")) { + if (round >= model.result.won.length) round = model.result.won.length - 1 + winners = model.result.won[round] + // winnerIdxs = model.result.history.rounds[round].winners + // winners = winnerIdxs.map( x => district.candidates[x].id) + } + } + for (let wid of winners) { + let c = model.candidatesById[wid] + if (winners.length > winnersAllowed) { + c.drawTie(self.ctx,self) + } else { + c.drawWin(self.ctx,self) + } + } + } + } + + // if(model.result && model.result.winners) { + // var objWinners = model.result.winners.map(x => model.candidatesById[x]) + // if (objWinners.length > model.seats) { + // for (i in objWinners) { + // objWinners[i].drawTie(self.ctx,self) + // } + // } else { + // for (i in objWinners) { + // objWinners[i].drawWin(self.ctx,self) + // } + // } + // } + + } + } + + function setWinners() { + // might need to change in the future, + // just setting a parameter for each candidate to indicate if they are a winner. + + if (!model.dontdrawwinners) { + // draw text next to the winners + + // check how many winners there should be + let winnersAllowed = 1 + if (model.checkMultiWinner(model.system)) { + winnersAllowed = model.seats + } + + for (var k = 0; k < model.district.length; k++) { + var district = model.district[k] + var result = district.result + + for (let can of district.candidates) { + if (result && result.winners && result.winners.includes(can.id)) { + can.winner = true + } else { + can.winner = false + } + } + + } + + } + } + + function setBorderColor() { + if (model.result && model.useBorderColor) { + self.canvas.style.borderColor = model.result.color; + if (model.yeeon) self.canvas.style.borderColor = "#fff" + } + } + } +} + + +function DistrictManager(model) { + var self = this + + self.redistrict = function() { + // calculate district lines + + // create data structure + model.district = [] + for (var i = 0; i < model.nDistricts; i++) { + model.district[i] = { + voterPeople: [], + candidates: [], + i: i, + parties: [], + stages: {}, + } + if (model.partyRule == 'leftright') { + var numParties = 2 + } else { + // for now, the number of votergroups is the number of parties + var numParties = model.voterGroups.length || 1 // default to 1 + } + for ( var j = 0; j < numParties; j++) { + model.district[i].parties.push({voterPeople:[],candidates:[]}) + } + } + + // new sorted list of all voters + // still refers to original voterPerson objects. + var voterPeopleSorted = model.voterSet.getAllVoters() + voterPeopleSorted.sort(function(a,b){return a.y - b.y}) + + // assign voters equally to districts, and make lists of voters in districts + var factor = model.nDistricts / model.voterSet.totalVoters + var oldDistrict = 0 + var oldy = 0 // boundary starts here + var firsty = [oldy] + var lasty = [] + for (var i = 0; i < voterPeopleSorted.length; i++) { + var voterPerson = voterPeopleSorted[i] + + // assign + var d = Math.floor(i * factor) + voterPerson.iDistrict = d // store district id with voters, + + // not used.. but we could refer to a voter's order of assignment to a district + // voterPerson.iPointWithinDistrict = model.district[d].voterPeople.length + + // fill district[] with references to voters. + model.district[d].voterPeople.push(voterPerson) + + // fill district borders for use with candidates + var y = voterPerson.y + if (oldDistrict != d) { + oldDistrict = d + firsty.push(y) + lasty.push(y) + } + oldy = y + } + lasty.push(oldy) + + // calculate district borders + var borders = [] + for (var i = 0; i < model.nDistricts - 1; i++) { + var middle = ( firsty[i+1] + lasty[i] ) * .5 + borders.push(middle) + } + for (var i = 0; i < model.nDistricts; i++) { + if (i == 0) { + model.district[i].lowerBound = -Infinity + } else { + model.district[i].lowerBound = borders[i-1] + } + if (i == model.nDistricts - 1) { + model.district[i].upperBound = Infinity + } else { + model.district[i].upperBound = borders[i] + } + } + + // put voters into parties + for (var i = 0; i < voterPeopleSorted.length; i++) { + var voterPerson = voterPeopleSorted[i] + if (model.partyRule == 'leftright') { + var iParty = ( voterPerson.x > model.size * .5 ) ? 1 : 0 + } else { // 'crowd' + var iParty = voterPerson.iGroup + } + voterPerson.iParty = iParty // easy, for now + var d = voterPerson.iDistrict + model.district[d].parties[iParty].voterPeople.push(voterPerson) // fill district.parties[i].voters with references to voters + } + + // For each voter, this takes the index over all districts, and it gives the index within a district. + model.districtIndexOfVoter = [] + for (var iDistrict = 0; iDistrict < model.nDistricts; iDistrict++) { + var voterPeople = model.district[iDistrict].voterPeople + for (var i = 0; i < voterPeople.length; i++) { + var iAll = voterPeople[i].iAll + model.districtIndexOfVoter[iAll] = i + } + } + + + self.redistrictCandidates() + } + + self.redistrictCandidates = function() { + // fill candidates with info on district. + for(var i=0; i<model.candidates.length; i++){ + var c = model.candidates[i] + self.candidatePicksDistrict(c) + } + + // fill district[] with info on candidates. + self.districtsListCandidates() + } + + self.candidatePicksDistrict = function(c) { + // put candidate into correct district + for (var i = 0; i < model.nDistricts; i++) { + var uB = model.district[i].upperBound + if (c.y < uB) { + c.iDistrict = i + break + } + } + + // put candidate into correct party + if (model.partyRule == 'leftright') { + c.iParty = ( c.x > model.size * .5 ) ? 1 : 0 + } else if (model.district[c.iDistrict].parties.length == 1){ + c.iParty = 0 + } else { + var min = Infinity + for ( var j = 0; j < model.voterGroups.length; j++){ + var dist2 = distF2(model, model.voterGroups[j], c) + if (min > dist2) { + min = dist2 + c.iParty = j + } + } + } + } + self.districtsListCandidates = function() { + // fill district[] with info on candidates. + // reset + for (var i = 0; i < model.nDistricts; i++) { + var district = model.district[i] + district.candidates = [] + district.candidatesById = {} + district.preFrontrunnerIds = [] + for ( var j = 0; j < district.parties.length; j++) { + district.parties[j].candidates = [] + } + } + + // assign candidates to districts' lists + for(var i=0; i<model.candidates.length; i++){ + var c = model.candidates[i] + model.district[c.iDistrict].candidates.push(c) + model.district[c.iDistrict].candidatesById[c.id] = c + } + + // assign frontrunners to districts' lists + for(var i=0; i<model.preFrontrunnerIds.length; i++){ + var cid = model.preFrontrunnerIds[i] + var c = model.candidatesById[cid] + model.district[c.iDistrict].preFrontrunnerIds.push(cid) + } + + // assign candidates to district party candidates + for(var i=0; i<model.candidates.length; i++){ + var c = model.candidates[i] + model.district[c.iDistrict].parties[c.iParty].candidates.push(c) + } + + } + +} + +// helper +_pileVoters = function(model) { + if (model.dimensions != "2D") { + if (0) { + // list all the voters + // drop the voters + // store the new positions + var forward = true + + var betweenDist = 5 + var stackDist = 5 + var added = [] + var todo = [] + + if (forward) { + for (var m = 0; m < model.voterGroups.length; m++) { + var points = model.voterGroups[m].points + for (var i = 0; i < points.length; i++) { + todo.push([m,i]) + } + } + // for (var i = 0 ; i < self.points.length; i++) { + // todo.push(i) + // } + } else { + // for (var i = self.points.length - 1 ; i >= 0; i--) { + // todo.push(i) + // } + } + var level = 1 + while (todo.length > 0) { + for (var c = 0; c < todo.length; c++) { + var m = todo[c][0] + var i = todo[c][1] + // look for collisions + var collided = false + for (var d = 0; d < added.length; d++) { + var o = added[d][0] + var k = added[d][1] + x1 = model.voterGroups[o].points[k][0] + model.voterGroups[o].x + x2 = model.voterGroups[m].points[i][0] + model.voterGroups[m].x + xDiff = Math.abs(x1 - x2) + if (xDiff < betweenDist) { + collided = true + break + } + } + if (! collided) { + model.voterGroups[m].points[i][2] = (level-1) * -stackDist + added.push([m,i]) + todo.splice(c,1) + c-- + } + } + level++ + var added = [] + } + }else { + // go through each voter and scale it and add to a background + var stdev = [] + var amplitude = [] + var radius = [] + var halfwidth = [] + var halfheight = [] + for (var m = 0; m < model.voterGroups.length; m++) { + var v = model.voterGroups[m] + var points = v.points + amp_factor = 70 + var u = 1.5 + if (!v.isGaussianVoters) { + stdev[m] = 5 + } else if (v.crowdShape == "gaussian sunflower") { + stdev[m] = v.stdev + } else if (v.crowdShape == "circles") { + radius[m] = v.radius + } else if (v.crowdShape == "rectangles") { + halfwidth[m] = v.halfwidth + halfheight[m] = v.halfheight + } else if (v.snowman) { + if (v.disk == 3) { + stdev[m] = u * 42 // 60 + radius[m] = 72 + } else if (v.disk == 2) { + stdev[m] = u * 38 // 55 + radius[m] = 72 + } else if (v.disk == 1) { + stdev[m] = u * 29 // 45 + radius[m] = 48 + } + } else { // oldest method, disks + if (v.disk == 3) { + stdev[m] = 112 + } else if (v.disk == 2) { + stdev[m] = u * 54 // 70 + radius[m] = 96 + } else if (v.disk == 1) { + stdev[m] = u * 41 // 60 + radius[m] = 72 + } + } + stdev[m] = stdev[m] * model.spread_factor_voters * .5 + radius[m] = radius[m] * model.spread_factor_voters * .5 + if (v.crowdShape == "gaussian sunflower") { + amplitude[m] = v.points.length/stdev[m] * amp_factor + } else if (v.crowdShape == "rectangles") { + amplitude[m] = v.points.length/halfwidth[m] * amp_factor * .75 + } else if (v.disk) { + if (!v.snowman && v.disk==3 && v.crowdShape != "circles") { + amplitude[m] = v.points.length/stdev[m] * amp_factor + } else { + amplitude[m] = v.points.length/radius[m] * amp_factor + } + } + sum2 = 0 + max = 0 + for (var i = 0; i < v.points.length; i++) { + var x = v.points[i][0] + if (Math.abs(x) < 300) sum2 = sum2 + x ** 2 + if (max < x) max = x + } + sd = Math.sqrt(sum2/points.length) + // console.log(sd) + // console.log(stdev[m]) + // console.log(max) + for (var i = 0; i < points.length; i++) { + var x = v.points[i][0] + var y = v.points[i][1] + var back = 0 + for (var k = 0; k < m; k++) { + var o = model.voterGroups[k] + // add background + if (o.crowdShape == "gaussian sunflower") { + back -= gaussian( x , o.x-v.x , stdev[k] ) * amplitude[k] + } else if (o.crowdShape == "rectangles") { + back -= step( x , o.x-v.x , halfwidth[k] ) * amplitude[k] + } else if (o.isSingleVoter) { + // skip... ideally, single voters shouldn't have o.disk specified + } else if (o.disk) { + if (!o.snowman && o.disk==3 && o.crowdShape != "circles") { + back -= gaussian( x , o.x-v.x , stdev[k] ) * amplitude[k] + } else { + back -= disk( x , o.x-v.x , radius[k]+5) * .5 * (amplitude[k] + 15) + } + } + } + // add voters + if (v.crowdShape == "gaussian sunflower") { + v.points[i][2] = back + (_erf(y/stdev[m])-1) * .5 * gaussian(x,0,stdev[m]) * amplitude[m] + } else if (v.crowdShape == "rectangles") { + v.points[i][2] = back + (y / halfheight[m] - 1) * .5 * amplitude[k] + } else if (v.disk) { + if (!v.snowman && v.disk==3 && v.crowdShape != "circles") { + v.points[i][2] = back + (_erf(y/stdev[m])-1) * .5 * gaussian(x,0,stdev[m]) * amplitude[m] + } else { + if (1) { + v.points[i][2] = back + ((y/Math.sqrt(radius[m] ** 2 - x ** 2))-1) * .25 * disk(x,0,radius[m] + 0) * amplitude[m] + } else if (0) { + v.points[i][2] = back + (intDisk0(y/(radius[m] + 0))-1) * .5 * disk(x,0,radius[m] + 0) * amplitude[m] + } else { + v.points[i][2] = back + (_erf(y/radius[m])-1) * .5 * disk(x,0,radius[m] + 5) * amplitude[m] + } + } + } // don't need to put single voters there... + } + } + function gaussian(x,mean,stdev) { + return Math.exp(-( (x - mean)**2 / (2 * stdev ** 2))) * 1/Math.sqrt(2*Math.PI) + } + function disk(x,mean,stdev) { + var inside = stdev ** 2 - (x - mean)**2 + return (inside > 0) ? Math.sqrt(inside)/stdev : 0 + } + function intDisk(x,mean,stdev) { + return (stdev ** 2 * (.5 * Math.asin(x/stdev) + .25 * Math.sin(2 * Math.asin(x/stdev)))) / (3.14 * .5) + } + function intDisk0(x) { + if (x < -1) return 0 + if (x > 1) return 1 + return ((.5 * Math.asin(x) + .25 * Math.sin(2 * Math.asin(x)))) / (3.14 * .5) + .5 + } + function step(x,mean,halfwidth) { + var inside = halfwidth - Math.abs(x-mean) + return (inside > 0) ? 1 : 0 + } + + } + } +} + +console.log('ding') diff --git a/play/js/Mouse.js b/play/js/Mouse.js index 95ba49d8..12b8b0fe 100644 --- a/play/js/Mouse.js +++ b/play/js/Mouse.js @@ -1,4 +1,5 @@ -function Mouse(id, target){ +function Mouse(id, target, dragm){ + // mouse events are only triggered when the mouse is used over a target. The only target I've used is a canvas. var self = this; @@ -8,51 +9,68 @@ function Mouse(id, target){ self.x = 0; self.y = 0; self.pressed = false; - + self.ctrlclick = false + self.isTouch = false // Events! var _onmousedown = function(event){ _onmousemove(event); - Mouse.pressed = true; - publish(self.id+"-mousedown"); + self.pressed = true; + if (event.ctrlKey) self.ctrlclick = true + self.isTouch = false + dragm.mousedown(event) }; var _onmousemove = function(event){ - Mouse.x = event.offsetX; - Mouse.y = event.offsetY; - publish(self.id+"-mousemove"); + self.x = event.offsetX; + self.y = event.offsetY; + self.isTouch = false + dragm.mousemove(event) }; - var _onmouseup = function(){ - Mouse.pressed = false; - publish(self.id+"-mouseup"); + var _onmouseup = function(event){ + self.pressed = false; + self.ctrlclick = false + self.isTouch = false + dragm.mouseup(event) }; // Add events! target.onmousedown = _onmousedown; target.onmousemove = _onmousemove; - window.onmouseup = _onmouseup; + var temp = document.onmouseup // this might seem recursive, but it really we're adding another function to call in addition to those already being called. + document.onmouseup = function() { + if(temp) temp() + _onmouseup(); + } // TOUCH. var _onTouchMove; target.addEventListener("touchstart",function(event){ _onTouchMove(event); - Mouse.pressed = true; - publish(self.id+"-mousedown"); + self.pressed = true; + if (event.ctrlKey) self.ctrlclick = true + self.isTouch = true + dragm.mousedown(event) },false); target.addEventListener("touchmove", _onTouchMove=function(event){ - Mouse.x = event.changedTouches[0].clientX - target.offsetLeft; - Mouse.y = event.changedTouches[0].clientY - target.offsetTop; - if(Mouse.x<0) Mouse.x=0; - if(Mouse.y<0) Mouse.y=0; - if(Mouse.x>target.clientWidth) Mouse.x=target.clientWidth; - if(Mouse.y>target.clientHeight) Mouse.y=target.clientHeight; + var rect = target.getBoundingClientRect() + self.x = event.changedTouches[0].clientX - rect.left + self.y = event.changedTouches[0].clientY - rect.top + // self.x = event.changedTouches[0].clientX - target.offsetLeft; + // self.y = event.changedTouches[0].clientY - target.offsetTop; + if(self.x<0) self.x=0; + if(self.y<0) self.y=0; + if(self.x>target.clientWidth) self.x=target.clientWidth; + if(self.y>target.clientHeight) self.y=target.clientHeight; //console.log(target); - publish(self.id+"-mousemove"); - event.preventDefault(); + self.isTouch = true + dragm.mousemove(event) },false); document.body.addEventListener("touchend",function(event){ - Mouse.pressed = false; - publish(self.id+"-mouseup"); + self.pressed = false; + self.ctrlclick = false + self.isTouch = true + dragm.mouseup(event) },false); }; \ No newline at end of file diff --git a/play/js/Presets.js b/play/js/Presets.js index b2c1905c..316cc315 100644 --- a/play/js/Presets.js +++ b/play/js/Presets.js @@ -1,487 +1,1637 @@ -var loadpreset = function(htmlname) { - if (htmlname == "election1.html") { - config = { +// This is only used by original.html and sandbox/original.html +// old way +function loadpreset (ui) { + + // if we don't already have a ui.presetName, generate one + // then look it up - system: "FPTP", + // ui.presetName: for the presets + // ui.idModel : for the divs. This is + + if (ui.presetName == "switch") { + return + } - candidates: 3, - candidatePositions: [[50,125],[250,125],[280,280]], + // clumsy grandfather + if (typeof(ui) == "string") { + ui = { + presetName:ui, + idModel:ui, + } + } - voters: 1, - voterPositions: [[155,125]] + if(ui.quick != undefined) { + ui.presetName = ui.quick + ui.idModel = ui.quick + } + + // default presetName + if (ui.presetName == undefined ) { + ui.presetName = "sandbox" + // then we will end up skipping down to the bottom + } + _lookupPreset(ui) + return ui } - } else if (htmlname == "election2.html") { - config = { + +// new way +function _lookupPreset(ui) { + + // load the preset corresponding to the ui.presetName + + // e.g. loadpreset( {id:"sandbox"} ) + + // input: + // ui.presetName - features:1, - system: "IRV", + // output: additional attributes + // ui.preset.config + // ui.preset.update + // ui.presetName (is generated if not provided) - candidates: 3, - candidatePositions: [[41,271],[257,27],[159,65]], - voters: 1, - voterPositions: [[257,240]] + // defaults + var config = {} + var update = () => null + var uiType = "election" + -} - } else if (htmlname == "election3.html") { - config = { + // helpers + var update_original = function () { + ui.menu.systems.choose.buttons.forEach(x => x.dom.hidden = (["FPTP", "IRV", "Borda", "Condorcet", "Approval", "Score"].includes(x.dom.innerHTML)) ? false : true) + ui.menu.systems.choose.buttons.forEach(x => x.dom.style.marginRight = "4px") + ui.menu.systems.choose.buttons.forEach(x => x.dom.style.width = "106px") + + ui.menu.nVoterGroups.choose.buttons.forEach(x => x.dom.hidden = (["1", "2", "3"].includes(x.dom.innerHTML)) ? false : true) + ui.menu.nCandidates.choose.buttons.forEach(x => x.dom.hidden = (["two", "three", "four", "five"].includes(x.dom.innerHTML)) ? false : true) + } - features:1, + // configurations - system: "Borda", + if (ui.presetName == "sandbox") { + uiType = "sandbox" + if (1) { + config = { + configversion: "2.5", + ballotType: "Plurality", + candidatePositions: [[92,69],[210,70],[245,182],[149,250],[55,180]], + voterPositions: [[150,150]], + dimensions: "2D", + system: "FPTP", + hidegearconfig: false, + secondStrategies: [], + percentSecondStrategy: [0,0,0,0,0,0,0,0,0,0], + voter_group_count: [61,50,50,50,50,50,50,50,50,50], + voter_group_spread: [230,190,190,190,190,190,190,190,190,190], + sandboxsave: true, + featurelist: ["systems","nVoterGroups","nCandidates"], + description: "", + keyyee: "newcan", + snowman: false, + x_voters: false, + oneVoter: false, + rbsystem: "Tideman", + numOfCandidates: 5, + numVoterGroups: 2, + xNumVoterGroups: 4, + nVoterGroupsRealName: "Two Groups", + spread_factor_voters: 2, + arena_size: 300, + median_mean: 1, + theme: "Letters", + utility_shape: "linear", + colorChooser: "pick and generate", + colorSpace: "hsluv with dark", + arena_border: 2, + preFrontrunnerIds: ["square","triangle"], + autoPoll: "Auto", + firstStrategy: "zero strategy. judge on an absolute scale.", + secondStrategy: "normalize frontrunners only", + doTwoStrategies: false, + yeefilter: [true,true,true,true,true], + computeMethod: "ez", + pixelsize: 60, + candidateSerials: [0,1,2,3,4], + voterGroupTypes: ["GaussianVoters"], + voterGroupX: [false], + voterGroupSnowman: [false], + voterGroupDisk: [3], + crowdShape: ["circles"], + seats: 3, + candidateB: [1,1,1,1,1], + nDistricts: 1, + votersAsCandidates: false, + visSingleBallotsOnly: false, + ballotVis: true, + customNames: "No", + namelist: "", + menuVersion: "2", + stepMenu: "vote", + menuLevel: "normal", + doFeatureFilter: false, + yeeon: false, + beatMap: "auto", + kindayee: "newcan", + ballotConcept: "auto", + roundChart: "auto", + sidebarOn: "on", + lastTransfer: "off", + voterIcons: "circle", + candidateIconsSet: ["image","note"], + pairwiseMinimaps: "off", + textBallotInput: "", + doTextBallots: false, + behavior: "stand", + showToolbar: "on", + rankedVizBoundary: "atMidpoint", + doElectabilityPolls: true, + partyRule: "crowd", + doFilterSystems: false, + filterSystems: [], + doFilterStrategy: true, + includeSystems: ["choice","pair","score"], + showPowerChart: true, + putMenuAbove: false, + scoreFirstStrategy: "normalize", + choiceFirstStrategy: "zero strategy. judge on an absolute scale.", + pairFirstStrategy: "zero strategy. judge on an absolute scale.", + scoreSecondStrategy: "normalize frontrunners only", + choiceSecondStrategy: "normalize frontrunners only", + pairSecondStrategy: "zero strategy. judge on an absolute scale.", + useBeatMapForRankedBallotViz: true, + centerPollThreshold: 0.7, + group_count_vert: [null], + group_count_h: [8], + voterCenterIcons: "on", + doMedianDistViz: false, + } + } else { + config = { + description: "", + features: 4, + system: "FPTP", + candidates: 5, + voters: 1, + doFullStrategyConfig: true, + doTwoStrategies: false, + doPercentFirst: false, + arena_size: 300, + arena_border: 2, + spread_factor_voters: 2, + firstStrategy: "normalize", + secondStrategy: "normalize frontrunners only", + autoPoll: "Auto", + visSingleBallotsOnly: false, + theme: "Letters", + menuVersion: "2", + doFeatureFilter: false, + yeeon: false, + rankedVizBoundary: "atMidpoint", + stepMenu: "vote", + includeSystems: ["choice","pair","score"], + // configversion: 2.5, // should stay at latest version + } + } + } else if (ui.presetName == "sandbox_original") { + uiType = "sandbox original" + config = { + description: "[type a description for your model here. for example...]\n\nLook, it's the whole shape gang! Steven Square, Tracy Triangle, Henry Hexagon, Percival Pentagon, and last but not least, Bob.", + features: 4, + system: "FPTP", + candidates: 5, + voters: 1 + } + update = update_original + } else if (ui.presetName == "election1") { + uiType = "election" + update = update_original + config = { - candidates: 3, - candidatePositions: [[173,150],[275,150],[23,150]], - //candidates: 4, - //candidatePositions: [[174,175],[271,266],[23,149],[23,23]], + features: 1, + system: "FPTP", - voters: 1, - voterPositions: [[232,150]] - // voterPositions: [[226,230]] + candidates: 3, + candidatePositions: [ + [50, 125], + [250, 125], + [280, 280] + ], -} - } else if (htmlname == "election4.html") { - config = { - features:2, - system: "Condorcet", - candidates: 3, - voters: 3 -} - } else if (htmlname == "election5.html") { - config = -{ + voters: 1, + voterPositions: [ + [155, 125] + ] + } + } else if (ui.presetName == "election2") { + uiType = "election" + update = update_original + config = { - features:2, - system: "Borda", + features: 1, + system: "IRV", - candidates: 3, - candidatePositions: [[40,115+10],[177,185+10],[224,118+10]], - - voters: 2, - voterPositions: [[75,120+10],[225,120+10]] + candidates: 3, + candidatePositions: [ + [41, 271], + [257, 27], + [159, 65] + ], -} - } else if (htmlname == "election6.html") { - config = { + voters: 1, + voterPositions: [ + [257, 240] + ] - system: "Score", - strategy: "normalize", - candidates: 3, - candidatePositions: [[50,125],[250,125],[280,280]], + } + } else if (ui.presetName == "election3") { + uiType = "election" + update = update_original + config = { - voters: 1, - voterPositions: [[155,125]] + features: 1, -} - } else if (htmlname == "election7.html") { - config = -{ - featurelist: ["percentstrategy"], - voterPercentStrategy: [100,0,0], - - system: "Score", + system: "Borda", - candidates: 3, - candidatePositions: [[150-30,150],[150+130,150],[150+50,150]], - - voters: 1, - voterPositions: [[150,150]], - voterStrategies: ["normalize"] - -} - } else if (htmlname == "election8.html") { - config = -{ + candidates: 3, + candidatePositions: [ + [173, 150], + [275, 150], + [23, 150] + ], + //candidates: 4, + //candidatePositions: [[174,175],[271,266],[23,149],[23,23]], - /* - features:3, - system: "Score", + voters: 1, + voterPositions: [ + [232, 150] + ] + // voterPositions: [[226,230]] - candidates: 3, - candidatePositions: [[100,150],[150,150+100],[300-100,150]], - - voters: 2, - voterPositions: [[100,150],[300-100,150]], - voterStrategies: ["normalize","zero strategy. judge on an absolute scale."], - preFrontrunnerIds: ["square","hexagon"] - */ - -candidatePositions: [[50,150],[250,150]], -voterPositions: [[100,150],[200,150]], -system: "Score", -candidates: 2, -voters: 2, -voterStrategies: ["normalize","normalize","zero strategy. judge on an absolute scale."], -preFrontrunnerIds: ["square","hexagon"], -featurelist: ["percentstrategy"], -sandboxsave: false, -hidegearconfig: false, -description: "", -voterPercentStrategy: ["70","49",0], -snowman: false, -unstrategic: "zero strategy. judge on an absolute scale.", -keyyee: "off", -features: undefined, -doPercentFirst: undefined, -doFullStrategyConfig: undefined, - -} - } else if (htmlname == "election9.html") { - config = -{ + } + } else if (ui.presetName == "election4") { + uiType = "election" + update = update_original + config = { + features: 2, + system: "Condorcet", + candidates: 3, + voters: 3 + } + } else if (ui.presetName == "election5") { + uiType = "election" + update = update_original + config = { - features:3, - doPercentFirst:true, - system: "Score", + features: 2, + system: "Borda", - candidates: 3, - - voters: 2, - voterPositions: [[200,160],[100,160]], - voterStrategies: ["normalize","normalize"], - voterPercentStrategy: [50,50], - doFullStrategyConfig: true - -} - } else if (htmlname == "election10.html") { - config = -{ -/* - features:3, - doPercentFirst:true, - system: "Approval", - - candidates: 3, - candidatePositions: [[150-25,150-20], - [150+20,150-20], - [150,150+75]], - - voters: 3, - voterPositions: [[150,150-70], - [150,150+10], - [150,150+90]], - voterStrategies: ["normalize frontrunners only","normalize frontrunners only","normalize frontrunners only"], - voterPercentStrategy: [100,100,100], - preFrontrunnerIds: ['square','triangle','hexagon'], - doFullStrategyConfig: true - */ - -candidatePositions: [[121,149],[118,170],[194,159]], -voterPositions: [[116,121],[116,184],[195,155]], -system: "Approval", -candidates: 3, -voters: 3, -voterStrategies: ["best frontrunner","best frontrunner","best frontrunner"], -voterPercentStrategy: ["100","100",100], -preFrontrunnerIds: ["square","triangle","hexagon"], -featurelist: ["percentstrategy"], -sandboxsave: false, -hidegearconfig: false, -description: "", -snowman: true, -unstrategic: "normalize", -keyyee: "off", -kindayee: "off", -features: undefined, -doPercentFirst: undefined, -doFullStrategyConfig: undefined -} - } else if (htmlname == "election11.html") { - config = -{ -/* - features:1, - doPercentFirst:true, - system: "Approval", - - candidates: 3, - candidatePositions: [[150-25,150-20], - [150+20,150-20], - [150,150+75]], - - voters: 3, - voterPositions: [[150,150-70], - [150,150+10], - [150,150+90]], - voterStrategies: ["best frontrunner","best frontrunner","best frontrunner"], - voterPercentStrategy: [0,100,100], - preFrontrunnerIds: ['square','triangle','hexagon'], - doFullStrategyConfig: true, - unstrategic: "normalize" - */ - -candidatePositions: [[121,149],[118,170],[194,159]], -voterPositions: [[116,121],[116,184],[195,155]], -system: "Approval", -candidates: 3, -voters: 3, -voterStrategies: ["best frontrunner","best frontrunner","best frontrunner"], -voterPercentStrategy: ["100","100",100], -preFrontrunnerIds: ["square","triangle","hexagon"], -featurelist: ["percentstrategy","systems"], -sandboxsave: false, -hidegearconfig: false, -description: "", -snowman: true, -unstrategic: "normalize", -keyyee: "off", -kindayee: "off", -features: undefined, -doPercentFirst: undefined, -doFullStrategyConfig: undefined -} - } else if (htmlname == "election12.html") { - config = -{ -/* - features:3, - doPercentFirst:true, - system: "IRV", - - candidates: 4, - candidatePositions: [[150-25,150-20], - [150+20,150-20], - [150,150+75], - [150+0,150+10]], - - voters: 3, - voterPositions: [[150,150-70], - [150,150+10], - [150,150+90]], - voterStrategies: ["normalize frontrunners only","normalize frontrunners only","normalize frontrunners only"], - voterPercentStrategy: [100,100,100], - preFrontrunnerIds: ['square','triangle','hexagon'] - */ - -candidatePositions: [[145,155],[184,153],[106,157]], -voterPositions: [[150,150]], -system: "IRV", -candidates: 3, -voters: 1, -voterStrategies: ["zero strategy. judge on an absolute scale.","normalize frontrunners only","normalize frontrunners only"], -voterPercentStrategy: ["100",100,100], -preFrontrunnerIds: ["square","triangle","hexagon"], -featurelist: ["systems"], -sandboxsave: false, -hidegearconfig: false, -description: "", -snowman: false, -unstrategic: "zero strategy. judge on an absolute scale.", -keyyee: "off", -features: undefined, -doPercentFirst: undefined, -doFullStrategyConfig: undefined, -} - } else if (htmlname == "election13.html") { - config = -{ -/* - features:3, - doPercentFirst:true, - system: "STAR", - - candidates: 3, - candidatePositions: [[150-25,150-20], - [150+20,150-20], - [150,150+75]], - - voters: 3, - voterPositions: [[150,150-70], - [150,150+10], - [150,150+90]], - voterStrategies: ["starnormfrontrunners","starnormfrontrunners","starnormfrontrunners"], - voterPercentStrategy: [100,100,100], - preFrontrunnerIds: ['square','triangle','hexagon'] - */ - -candidatePositions: [[121,149],[118,170],[194,159]], -voterPositions: [[116,121],[116,184],[195,155]], -system: "STAR", -candidates: 3, -voters: 3, -voterStrategies: ["best frontrunner","best frontrunner","best frontrunner"], -voterPercentStrategy: ["100","100",100], -preFrontrunnerIds: ["square","triangle","hexagon"], -featurelist: ["percentstrategy"], -sandboxsave: false, -hidegearconfig: false, -description: "", -snowman: true, -unstrategic: "normalize", -keyyee: "off", -kindayee: "off", -features: undefined, -doPercentFirst: undefined, -doFullStrategyConfig: undefined -} - } else if (htmlname == "election14.html") { - config = -{ -/* - features:3, - doPercentFirst:true, - system: "3-2-1", - - candidates: 3, - candidatePositions: [[150-25,150-20], - [150+20,150-20], - [150,150+75]], - - voters: 3, - voterPositions: [[150,150-70], - [150,150+10], - [150,150+90]], - voterStrategies: ["starnormfrontrunners","starnormfrontrunners","starnormfrontrunners"], - voterPercentStrategy: [100,100,100], - preFrontrunnerIds: ['square','triangle','hexagon'] - */ - -candidatePositions: [[121,149],[118,170],[194,159]], -voterPositions: [[116,121],[116,184],[195,155]], -system: "3-2-1", -candidates: 3, -voters: 3, -voterStrategies: ["best frontrunner","best frontrunner","best frontrunner"], -voterPercentStrategy: ["100","100",100], -preFrontrunnerIds: ["square","triangle","hexagon"], -featurelist: ["percentstrategy"], -sandboxsave: false, -hidegearconfig: false, -description: "", -snowman: true, -unstrategic: "normalize", -keyyee: "off", -kindayee: "off", -features: undefined, -doPercentFirst: undefined, -doFullStrategyConfig: undefined -} - } else if (htmlname == "election15.html") { - config = -{ - candidatePositions: [[92,69],[210,70],[245,182],[149,250],[55,180]], -voterPositions: [[150,150]], -description: "", -features: undefined, -system: "FPTP", -candidates: 5, -voters: 1, -doFullStrategyConfig: undefined, -doPercentFirst: undefined, -featurelist: ["systems","voters","candidates","percentstrategy","strategy","percentstrategy","unstrategic","frontrunners","poll","yee"], -sandboxsave: true, -hidegearconfig: false, -preFrontrunnerIds: ["square"], -voterStrategies: ["zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale."], -voterPercentStrategy: [0,0,0], -snowman: false, -unstrategic: "zero strategy. judge on an absolute scale.", -keyyee: "pentagon", -kindayee: "can", -} - } else if (htmlname == "sandbox.html") { - config = -{ - description: "[type a description for your model here. for example...]\n\nLook, it's the whole shape gang! Steven Square, Tracy Triangle, Henry Hexagon, Percival Pentagon, and last but not least, Bob.", - features: 4, - system: "FPTP", - candidates: 5, - voters: 1, - doFullStrategyConfig: true, - doPercentFirst: true -} - } else if (htmlname == "ballot1.html") { - config = -{system: "Plurality"} - - } else if (htmlname == "ballot2.html") { - config = -{system: "Ranked"} - - } else if (htmlname == "ballot3.html") { - config = -{system: "Approval"} - - } else if (htmlname == "ballot4.html") { - config = -{system: "Score"} - - } else if (htmlname == "ballot5.html") { - config = -{ - system: "Score", - strategy: "normalize" -} - } else if (htmlname == "ballot6.html") { - config = -{ - system: "Score", - strategy: "best frontrunner", - preFrontrunnerIds: ["square","triangle"], - showChoiceOfFrontrunners: true, - showChoiceOfStrategy: true -} - } else if (htmlname == "ballot7.html") { - config = -{ - system: "Score", - strategy: "not the worst frontrunner", - showChoiceOfFrontrunners: true -} - } else if (htmlname == "ballot8.html") { - config = -{ - system: "Score", - strategy: "normalize frontrunners only", - preFrontrunnerIds: ["square","triangle"], - showChoiceOfFrontrunners: true, - showChoiceOfStrategy: true -} - } else if (htmlname == "ballot9.html") { - config = -{ - system: "Score", - strategy: "starnormfrontrunners", // for now we are using an "off-menu" option. We should make versions of each of hte strategies for star. - preFrontrunnerIds: ["square","triangle"], - showChoiceOfFrontrunners: true -} - } else if (htmlname == "ballot10.html") { - config = -{ - system: "Three", - strategy: "starnormfrontrunners", - preFrontrunnerIds: ["square","triangle"], - showChoiceOfFrontrunners: true -} - } else if (htmlname == "ballot11.html") { - config = -{ - system: "Score", - strategy: "best frontrunner", - preFrontrunnerIds: ["square","triangle"], - showChoiceOfFrontrunners: true, - showChoiceOfStrategy: true -} - } else if (htmlname == "ballot12.html") { - config = -{ - system: "Score", - strategy: "not the worst frontrunner", - preFrontrunnerIds: ["square","triangle"], - showChoiceOfFrontrunners: true, - showChoiceOfStrategy: true -} - // } else if (htmlname == "election.html") { - // config = + candidates: 3, + candidatePositions: [ + [40, 115 + 10], + [177, 185 + 10], + [224, 118 + 10] + ], + voters: 2, + voterPositions: [ + [75, 120 + 10], + [225, 120 + 10] + ] - } - return config -} + } + } else if (ui.presetName == "election6") { + uiType = "election" + config = { + + system: "Score", + strategy: "normalize", + + candidates: 3, + candidatePositions: [ + [50, 125], + [250, 125], + [280, 280] + ], + + voters: 1, + voterPositions: [ + [155, 125] + ] + + } + } else if (ui.presetName == "election7") { + uiType = "election" + config = { + featurelist: ["percentSecondStrategy"], + percentSecondStrategy: [90, 0, 0], + + system: "Score", + + candidates: 3, + candidatePositions: [ + [150 - 30, 150], + [150 + 130, 150], + [150 + 50, 150] + ], + + voters: 1, + voterPositions: [ + [150, 150] + ], + secondStrategy: "normalize", + firstStrategy: "zero strategy. judge on an absolute scale." + + } + } else if (ui.presetName == "election8") { + uiType = "election" + config = { + + candidatePositions: [ + [100, 150], + [200, 150], + [150,250] + ], + voterPositions: [ + [100, 150], + [200, 150] + ], + system: "Score", + candidates: 2, + voters: 2, + preFrontrunnerIds: ["square", "hexagon"], + featurelist: ["percentSecondStrategy"], + sandboxsave: false, + hidegearconfig: false, + description: "", + percentSecondStrategy: ["70", "49", 0], + snowman: false, + firstStrategy: "zero strategy. judge on an absolute scale.", + secondStrategy: "normalize", + keyyee: "off", + features: undefined, + doPercentFirst: undefined, + doFullStrategyConfig: undefined, + + } + } else if (ui.presetName == "election9") { + uiType = "election" + config = { + + features: 3, + doPercentFirst: true, + system: "Score", + + candidates: 3, + + voters: 2, + voterPositions: [ + [200, 160], + [100, 160] + ], + secondStrategy: "normalize", + percentSecondStrategy: [50, 50], + doFullStrategyConfig: true + + } + } else if (ui.presetName == "election10") { + uiType = "election" + config = { + /* + features:3, + doPercentFirst:true, + system: "Approval", + + candidates: 3, + candidatePositions: [[150-25,150-20], + [150+20,150-20], + [150,150+75]], + + voters: 3, + voterPositions: [[150,150-70], + [150,150+10], + [150,150+90]], + secondStrategies: ["normalize frontrunners only","normalize frontrunners only","normalize frontrunners only"], + percentSecondStrategy: [100,100,100], + preFrontrunnerIds: ['square','triangle','hexagon'], + doFullStrategyConfig: true + */ + + candidatePositions: [ + [121, 149], + [118, 170], + [194, 159] + ], + voterPositions: [ + [116, 121], + [116, 184], + [195, 155] + ], + system: "Approval", + candidates: 3, + voters: 3, + secondStrategy: "best frontrunner", + percentSecondStrategy: [18, 22, 92], + preFrontrunnerIds: ["square", "triangle", "hexagon"], + featurelist: ["percentSecondStrategy"], + sandboxsave: false, + hidegearconfig: false, + description: "", + snowman: true, + firstStrategy: "normalize", + keyyee: "off", + kindayee: "off", + features: undefined, + doPercentFirst: undefined, + doFullStrategyConfig: undefined + } + } else if (ui.presetName == "election11") { + uiType = "election" + config = { + /* + features:1, + doPercentFirst:true, + system: "Approval", + + candidates: 3, + candidatePositions: [[150-25,150-20], + [150+20,150-20], + [150,150+75]], + + voters: 3, + voterPositions: [[150,150-70], + [150,150+10], + [150,150+90]], + secondStrategies: ["best frontrunner","best frontrunner","best frontrunner"], + percentSecondStrategy: [0,100,100], + preFrontrunnerIds: ['square','triangle','hexagon'], + doFullStrategyConfig: true, + firstStrategy: "normalize" + */ + + candidatePositions: [ + [121, 149], + [118, 170], + [194, 159] + ], + voterPositions: [ + [116, 121], + [116, 184], + [195, 155] + ], + system: "Approval", + candidates: 3, + voters: 3, + secondStrategy: "best frontrunner", + percentSecondStrategy: [18, 22, 92], + preFrontrunnerIds: ["square", "triangle", "hexagon"], + featurelist: ["percentSecondStrategy", "systems"], + sandboxsave: false, + hidegearconfig: false, + description: "", + snowman: true, + firstStrategy: "normalize", + keyyee: "off", + kindayee: "off", + features: undefined, + doPercentFirst: undefined, + doFullStrategyConfig: undefined + } + } else if (ui.presetName == "election12") { + uiType = "election" + config = { + /* + features:3, + doPercentFirst:true, + system: "IRV", + + candidates: 4, + candidatePositions: [[150-25,150-20], + [150+20,150-20], + [150,150+75], + [150+0,150+10]], + + voters: 3, + voterPositions: [[150,150-70], + [150,150+10], + [150,150+90]], + secondStrategies: ["normalize frontrunners only","normalize frontrunners only","normalize frontrunners only"], + percentSecondStrategy: [100,100,100], + preFrontrunnerIds: ['square','triangle','hexagon'] + */ + + candidatePositions: [ + [145, 155], + [184, 153], + [106, 157] + ], + voterPositions: [ + [150, 150] + ], + system: "IRV", + candidates: 3, + voters: 1, + preFrontrunnerIds: ["square", "triangle", "hexagon"], + featurelist: ["systems"], + sandboxsave: false, + hidegearconfig: false, + description: "", + snowman: false, + firstStrategy: "zero strategy. judge on an absolute scale.", + keyyee: "off", + features: undefined, + doPercentFirst: undefined, + doFullStrategyConfig: undefined, + } + } else if (ui.presetName == "election13") { + uiType = "election" + config = { + /* + features:3, + doPercentFirst:true, + system: "STAR", + + candidates: 3, + candidatePositions: [[150-25,150-20], + [150+20,150-20], + [150,150+75]], + + voters: 3, + voterPositions: [[150,150-70], + [150,150+10], + [150,150+90]], + secondStrategies: ["starnormfrontrunners","starnormfrontrunners","starnormfrontrunners"], + percentSecondStrategy: [100,100,100], + preFrontrunnerIds: ['square','triangle','hexagon'] + */ + + candidatePositions: [ + [121, 149], + [118, 170], + [194, 159] + ], + voterPositions: [ + [116, 121], + [116, 184], + [195, 155] + ], + system: "STAR", + candidates: 3, + voters: 3, + secondStrategy: "best frontrunner", + percentSecondStrategy: [18, 22, 92], + preFrontrunnerIds: ["square", "triangle", "hexagon"], + featurelist: ["percentSecondStrategy"], + sandboxsave: false, + hidegearconfig: false, + description: "", + snowman: true, + firstStrategy: "normalize", + keyyee: "off", + kindayee: "off", + features: undefined, + doPercentFirst: undefined, + doFullStrategyConfig: undefined + } + } else if (ui.presetName == "election14") { + uiType = "election" + config = { + /* + features:3, + doPercentFirst:true, + system: "3-2-1", + + candidates: 3, + candidatePositions: [[150-25,150-20], + [150+20,150-20], + [150,150+75]], + + voters: 3, + voterPositions: [[150,150-70], + [150,150+10], + [150,150+90]], + secondStrategies: ["starnormfrontrunners","starnormfrontrunners","starnormfrontrunners"], + percentSecondStrategy: [100,100,100], + preFrontrunnerIds: ['square','triangle','hexagon'] + */ + + candidatePositions: [ + [121, 149], + [118, 170], + [194, 159] + ], + voterPositions: [ + [116, 121], + [116, 184], + [195, 155] + ], + system: "3-2-1", + candidates: 3, + voters: 3, + secondStrategy: "best frontrunner", + percentSecondStrategy: [18, 22, 92], + preFrontrunnerIds: ["square", "triangle", "hexagon"], + featurelist: ["percentSecondStrategy"], + sandboxsave: false, + hidegearconfig: false, + description: "", + snowman: true, + firstStrategy: "normalize", + keyyee: "off", + kindayee: "off", + features: undefined, + doPercentFirst: undefined, + doFullStrategyConfig: undefined + } + } else if (ui.presetName == "election15") { + uiType = "election" + config = { + candidatePositions: [ + [92, 69], + [210, 70], + [245, 182], + [149, 250], + [55, 180] + ], + voterPositions: [ + [150, 150] + ], + description: "", + features: undefined, + system: "FPTP", + candidates: 5, + voters: 1, + doFullStrategyConfig: undefined, + doPercentFirst: undefined, + featurelist: ["systems", "voters", "candidates", "percentSecondStrategy", "secondStrategy", "percentSecondStrategy", "firstStrategy", "frontrunners", "poll", "yee"], + sandboxsave: true, + hidegearconfig: false, + preFrontrunnerIds: ["square"], + secondStrategy: "zero strategy. judge on an absolute scale.", + percentSecondStrategy: [0, 0, 0], + snowman: false, + firstStrategy: "zero strategy. judge on an absolute scale.", + keyyee: "pentagon", + kindayee: "can", + } + + + } else if (ui.presetName == "election16") { + uiType = "election" + config = { + candidatePositions: [ + [76, 147], + [151, 144], + [211, 145] + ], + voterPositions: [ + [187, 188] + ], + system: "Approval", + // candidates: 3, + // voters: 1, + // doTwoStrategies: false, + arena_size: 300, + // arena_border: 0, + // spread_factor_voters: 2, + firstStrategy: "normalize", + // secondStrategy: "normalize frontrunners only", + // snowman: false, + // x_voters: false, + // median_mean: 1, + oneVoter: true, + featurelist: [], + // sandboxsave: true, + hidegearconfig: true, + // preFrontrunnerIds: ["square","triangle"], + // secondStrategies: ["zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale."], + // percentSecondStrategy: [0,0,0,0,0,0,0,0,0,0], + // voter_group_count: ["200",50,50,50,50,50,50,50,50,50], + // voter_group_spread: ["237",190,190,190,190,190,190,190,190,190], + // keyyee: "off", + // yeefilter: ["square","triangle","hexagon","pentagon","bob"], + // computeMethod: "ez", + // pixelsize: 12, + // kindayee: "off", + // configversion: 1, + // features: undefined, + // doPercentFirst: undefined, + // doFullStrategyConfig: undefined, + // autoPoll: "Manual", + } + + + } else if (ui.presetName == "election17") { + uiType = "election" + config = { + candidatePositions: [ + [76, 147], + [151, 144], + [211, 145] + ], + voterPositions: [ + [197, 199] + ], + // description: "", + system: "Approval", + // candidates: 3, + // voters: 1, + // doTwoStrategies: false, + arena_size: 300, + // arena_border: 0, + // spread_factor_voters: 2, + firstStrategy: "normalize frontrunners only", + // secondStrategy: "normalize frontrunners only", + // snowman: false, + // x_voters: false, + // median_mean: 1, + oneVoter: true, + featurelist: ["frontrunners"], + putMenuAbove: true, + // sandboxsave: true, + hidegearconfig: true, + preFrontrunnerIds: ["triangle", "hexagon"] + // secondStrategies: ["zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale."], + // percentSecondStrategy: [0,0,0,0,0,0,0,0,0,0], + // voter_group_count: ["200",50,50,50,50,50,50,50,50,50], + // voter_group_spread: ["237",190,190,190,190,190,190,190,190,190], + // keyyee: "off", + // yeefilter: ["square","triangle","hexagon","pentagon","bob"], + // computeMethod: "ez", + // pixelsize: 12, + // filename: "sandbox.html", + // presethtmlname: "sandbox.html", + // kindayee: "off", + // configversion: 1, + // features: undefined, + // doPercentFirst: undefined, + // doFullStrategyConfig: undefined, + // autoPoll: "Manual", + } + + + + } else if (ui.presetName == "election18") { + uiType = "election" + config = { + candidatePositions: [ + [76, 147], + [151, 144], + [211, 145] + ], + voterPositions: [ + [155, 217] + ], + // description: "", + system: "Approval", + // candidates: 3, + // voters: 1, + // doTwoStrategies: false, + arena_size: 300, + // arena_border: 0, + // spread_factor_voters: 2, + firstStrategy: "normalize frontrunners only", + // secondStrategy: "normalize frontrunners only", + // snowman: false, + // x_voters: false, + // median_mean: 1, + // oneVoter: false, + featurelist: [], + // sandboxsave: true, + hidegearconfig: true, + // preFrontrunnerIds: ["square","triangle"], + // secondStrategies: ["zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale."], + // percentSecondStrategy: [0,0,0,0,0,0,0,0,0,0], + // voter_group_count: ["200",50,50,50,50,50,50,50,50,50], + // voter_group_spread: ["237",190,190,190,190,190,190,190,190,190], + // keyyee: "off", + // yeefilter: ["square","triangle","hexagon","pentagon","bob"], + // computeMethod: "ez", + // pixelsize: 12, + // filename: "sandbox.html", + // presethtmlname: "sandbox.html", + // kindayee: "off", + // configversion: 1, + autoPoll: "Auto", + // features: undefined, + // doPercentFirst: undefined, + // doFullStrategyConfig: undefined, + } + + + } else if (ui.presetName == "election19") { + uiType = "election" + config = { + candidatePositions: [ + [76, 147], + [145, 142], + [211, 145] + ], + voterPositions: [ + [176, 193] + ], + system: "FPTP", + // candidates: 3, + // voters: 1, + // doTwoStrategies: false, + arena_size: 300, + // arena_border: 0, + // spread_factor_voters: 2, + firstStrategy: "normalize", + // secondStrategy: "normalize frontrunners only", + // snowman: false, + // x_voters: false, + // median_mean: 1, + oneVoter: true, + featurelist: [], + // sandboxsave: true, + hidegearconfig: true, + // preFrontrunnerIds: ["square","triangle"], + // secondStrategies: ["zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale."], + // percentSecondStrategy: [0,0,0,0,0,0,0,0,0,0], + // voter_group_count: ["200",50,50,50,50,50,50,50,50,50], + // voter_group_spread: ["237",190,190,190,190,190,190,190,190,190], + // keyyee: "off", + // yeefilter: ["square","triangle","hexagon","pentagon","bob"], + // computeMethod: "ez", + // pixelsize: 12, + // kindayee: "off", + // configversion: 1, + // features: undefined, + // doPercentFirst: undefined, + // doFullStrategyConfig: undefined, + // autoPoll: "Manual", + } + + + } else if (ui.presetName == "election20") { + uiType = "election" + config = { + candidatePositions: [ + [76, 147], + [145, 142], + [211, 145] + ], + voterPositions: [ + [176, 193] + ], + system: "FPTP", + // candidates: 3, + // voters: 1, + // doTwoStrategies: false, + arena_size: 300, + // arena_border: 0, + // spread_factor_voters: 2, + firstStrategy: "normalize frontrunners only", + // secondStrategy: "normalize frontrunners only", + // snowman: false, + // x_voters: false, + // median_mean: 1, + oneVoter: true, + featurelist: [], + // sandboxsave: true, + hidegearconfig: true, + preFrontrunnerIds: ["square", "hexagon"], + // secondStrategies: ["zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale."], + // percentSecondStrategy: [0,0,0,0,0,0,0,0,0,0], + // voter_group_count: ["200",50,50,50,50,50,50,50,50,50], + // voter_group_spread: ["237",190,190,190,190,190,190,190,190,190], + // keyyee: "off", + // yeefilter: ["square","triangle","hexagon","pentagon","bob"], + // computeMethod: "ez", + // pixelsize: 12, + // kindayee: "off", + // configversion: 1, + // features: undefined, + // doPercentFirst: undefined, + // doFullStrategyConfig: undefined, + // autoPoll: "Manual", + } + + + } else if (ui.presetName == "election21") { + uiType = "election" + config = { + candidatePositions: [ + [76, 147], + [143, 144], + [211, 145] + ], + voterPositions: [ + [138, 175] + ], + // description: "", + system: "FPTP", + // // candidates: 3, + // voters: 1, + // doTwoStrategies: false, + arena_size: 300, + // arena_border: 0, + spread_factor_voters: 2, + firstStrategy: "normalize frontrunners only", + // secondStrategy: "normalize frontrunners only", + // snowman: false, + // x_voters: false, + // median_mean: 1, + // oneVoter: false, + featurelist: ["frontrunners"], + putMenuAbove: true, + // sandboxsave: true, + hidegearconfig: true, + preFrontrunnerIds: ["square", "hexagon"], + // // secondStrategies: ["zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale."], + // percentSecondStrategy: [0,0,0,0,0,0,0,0,0,0], + // voter_group_count: ["200",50,50,50,50,50,50,50,50,50], + // voter_group_spread: ["237",190,190,190,190,190,190,190,190,190], + // keyyee: "off", + // yeefilter: ["square","triangle","hexagon","pentagon","bob"], + // computeMethod: "ez", + // pixelsize: 12, + // filename: "sandbox.html", + // presethtmlname: "sandbox.html", + // kindayee: "off", + // configversion: 1, + // features: undefined, + // doPercentFirst: undefined, + // doFullStrategyConfig: undefined, + // autoPoll: "Manual", + } + + + } else if (ui.presetName == "election22") { + uiType = "election" + config = { + candidatePositions: [ + [76, 147], + [143, 144], + [211, 145] + ], + voterPositions: [ + [138, 175] + ], + // description: "", + system: "Approval", + // // candidates: 3, + // voters: 1, + // doTwoStrategies: false, + arena_size: 300, + // arena_border: 0, + spread_factor_voters: 2, + firstStrategy: "normalize frontrunners only", + // secondStrategy: "normalize frontrunners only", + // snowman: false, + // x_voters: false, + // median_mean: 1, + // oneVoter: false, + featurelist: ["systems", "voters", "firstStrategy", "autoPoll","frontrunners","poll"], + // sandboxsave: true, + hidegearconfig: false, + preFrontrunnerIds: ["square", "hexagon"], + // // secondStrategies: ["zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale."], + // percentSecondStrategy: [0,0,0,0,0,0,0,0,0,0], + // voter_group_count: ["200",50,50,50,50,50,50,50,50,50], + // voter_group_spread: ["237",190,190,190,190,190,190,190,190,190], + // keyyee: "off", + // yeefilter: ["square","triangle","hexagon","pentagon","bob"], + // computeMethod: "ez", + // pixelsize: 12, + // filename: "sandbox.html", + // presethtmlname: "sandbox.html", + // kindayee: "off", + // configversion: 1, + // features: undefined, + // doPercentFirst: undefined, + // doFullStrategyConfig: undefined, + autoPoll: "Auto", + } + + + } else if (ui.presetName == "election23") { + uiType = "election" + config = { + candidatePositions: [ + [76, 147], + [143, 144], + [211, 145] + ], + voterPositions: [ + [138, 175] + ], + system: "Approval", + arena_size: 300, + spread_factor_voters: 2, + firstStrategy: "normalize frontrunners only", + featurelist: [], + hidegearconfig: true, + preFrontrunnerIds: ["square", "hexagon"], + autoPoll: "Auto", + // configversion: 1, + // candidates: 3, + // voters: 1, + // snowman: false, + // x_voters: false, + // median_mean: 1, + // arena_border: 2, + // oneVoter: false, + // features: undefined, + // sandboxsave: true, + // doPercentFirst: undefined, + // doFullStrategyConfig: undefined, + // secondStrategies: ["zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale."], + // description: "", + // percentSecondStrategy: [0,0,0,0,0,0,0,0,0,0], + // voter_group_count: [50,50,50,50,50,50,50,50,50,50], + // voter_group_spread: [190,190,190,190,190,190,190,190,190,190], + // secondStrategy: "zero strategy. judge on an absolute scale.", + // doTwoStrategies: true, + keyyee: "mean", + // yeefilter: ["square","triangle","hexagon","pentagon","bob"], + // computeMethod: "ez", + // pixelsize: 60, + // filename: "election22.html", + // presethtmlname: "election22.html", + kindayee: "center", + } + + + } else if (ui.presetName == "election24") { + uiType = "election" + config = { + candidatePositions: [ + [78, 187], + [44, 54], + [218, 204] + ], + voterPositions: [ + [150, 150] + ], + // description: "", + system: "Approval", + // candidates: 3, + // voters: 1, + // doTwoStrategies: false, + arena_size: 300, + arena_border: 0, + // spread_factor_voters: 1, + firstStrategy: "normalize frontrunners only", + // secondStrategy: "normalize frontrunners only", + autoPoll: "Auto", + // configversion: 1, + // snowman: false, + // x_voters: false, + // median_mean: 1, + // oneVoter: false, + featurelist: [], + // sandboxsave: true, + hidegearconfig: true, + // preFrontrunnerIds: ["square","triangle"], + // secondStrategies: ["zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale."], + // percentSecondStrategy: [0,0,0,0,0,0,0,0,0,0], + // voter_group_count: [50,50,50,50,50,50,50,50,50,50], + // voter_group_spread: [190,190,190,190,190,190,190,190,190,190], + keyyee: "triangle", + // yeefilter: ["square","triangle","pentagon","bob","hexagon"], + // computeMethod: "ez", + pixelsize: 30, + // filename: "sandbox.html", + // presethtmlname: "sandbox.html", + kindayee: "can", + // features: undefined, + // doPercentFirst: undefined, + // doFullStrategyConfig: undefined, + } + + + } else if (ui.presetName == "election25") { + uiType = "election" + config = { + candidatePositions: [ + [75, 184], + [264, 102], + [219, 180] + ], + voterPositions: [ + [149, 174] + ], + // description: "", + system: "FPTP", + // candidates: 3, + // voters: 1, + doTwoStrategies: true, + // arena_size: 300, + arena_border: 0, + spread_factor_voters: 2, + firstStrategy: "normalize frontrunners only", + secondStrategy: "zero strategy. judge on an absolute scale.", + // snowman: false, + // x_voters: false, + // median_mean: 1, + // oneVoter: false, + featurelist: [], + // sandboxsave: true, + hidegearconfig: true, + preFrontrunnerIds: ["square", "hexagon"], + // secondStrategies: ["zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale."], + percentSecondStrategy: ["27", 0, 0, 0, 0, 0, 0, 0, 0, 0], + // voter_group_count: ["200",50,50,50,50,50,50,50,50,50], + // voter_group_spread: ["237",190,190,190,190,190,190,190,190,190], + keyyee: "triangle", + // yeefilter: ["square","triangle","hexagon","pentagon","bob"], + // computeMethod: "ez", + pixelsize: 12, + // filename: "sandbox.html", + // presethtmlname: "sandbox.html", + kindayee: "can", + // configversion: 1, + // features: undefined, + // doPercentFirst: undefined, + // doFullStrategyConfig: undefined, + // autoPoll: "Manual", + } + + + } else if (ui.presetName == "election26") { + uiType = "election" + config = { + candidatePositions: [ + [40, 132], + [142, 169], + [200, 157], + [101, 180], + [227, 117] + ], + voterPositions: [ + [53, 144], + [231, 152] + ], + // description: "", + system: "+Primary", + candidates: 5, + voters: 2, + // doTwoStrategies: false, + // arena_size: 300, + // arena_border: 0, + spread_factor_voters: 2, + firstStrategy: "normalize", + // secondStrategy: "normalize frontrunners only", + // snowman: false, + // x_voters: false, + // median_mean: 1, + // oneVoter: false, + featurelist: [], + // sandboxsave: true, + hidegearconfig: true, + preFrontrunnerIds: ["square", "hexagon"], + // secondStrategies: ["zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale."], + // percentSecondStrategy: ["53",0,0,0,0,0,0,0,0,0], + // voter_group_count: ["200",50,50,50,50,50,50,50,50,50], + // voter_group_spread: ["237",190,190,190,190,190,190,190,190,190], + // keyyee: "off", + // yeefilter: ["square","triangle","hexagon","pentagon","bob"], + // computeMethod: "ez", + // pixelsize: 12, + // filename: "sandbox.html", + // presethtmlname: "sandbox.html", + // kindayee: "off", + // configversion: 1, + // features: undefined, + // doPercentFirst: undefined, + // doFullStrategyConfig: undefined, + // autoPoll: "Manual", + } + + + } else if (ui.presetName == "election27") { + uiType = "election" + config = { + candidatePositions: [ + [92, 69], + [210, 70], + [245, 182], + [149, 250], + [55, 180] + ], + voterPositions: [ + [101, 189], + [148, 91], + [195, 202] + ], + // description: "[type a description for your model here. for example...]\n\nLook, it's the whole shape gang! Steven Square, Tracy Triangle, Henry Hexagon, Percival Pentagon, and last but not least, Bob.", + // features: undefined, + system: "Minimax", + candidates: 5, + voters: 3, + // doFullStrategyConfig: undefined, + // doTwoStrategies: false, + // doPercentFirst: undefined, + arena_size: 300, + arena_border: 0, + spread_factor_voters: 1, + // firstStrategy: "normalize", + // secondStrategy: "normalize frontrunners only", + // autoPoll: "Auto", + // configversion: 1, + // snowman: false, + // x_voters: false, + // median_mean: 1, + // utility_shape: "linear", + // oneVoter: false, + featurelist: [], + // sandboxsave: true, + hidegearconfig: true + // preFrontrunnerIds: ["square","triangle"], + // secondStrategies: ["zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale."], + // percentSecondStrategy: [0,0,0,0,0,0,0,0,0,0], + // voter_group_count: [50,50,50,50,50,50,50,50,50,50], + // voter_group_spread: [190,190,190,190,190,190,190,190,190,190], + // keyyee: "off", + // yeefilter: ["square","triangle","hexagon","pentagon","bob"], + // computeMethod: "ez", + // pixelsize: 60, + // filename: "sandbox.html", + // presethtmlname: "sandbox.html", + } + + } else if (ui.presetName == "election28") { + uiType = "election" + config = { + candidatePositions: [ + [92, 69], + [210, 70], + [245, 182], + [149, 250], + [55, 180] + ], + voterPositions: [ + [101, 189], + [148, 91], + [195, 202] + ], + // description: "[type a description for your model here. for example...]\n\nLook, it's the whole shape gang! Steven Square, Tracy Triangle, Henry Hexagon, Percival Pentagon, and last but not least, Bob.", + // features: undefined, + system: "RankedPair", + candidates: 5, + voters: 3, + // doFullStrategyConfig: undefined, + // doTwoStrategies: false, + // doPercentFirst: undefined, + arena_size: 300, + arena_border: 0, + spread_factor_voters: 1, + // firstStrategy: "normalize", + // secondStrategy: "normalize frontrunners only", + // autoPoll: "Auto", + // configversion: 1, + // snowman: false, + // x_voters: false, + // median_mean: 1, + // utility_shape: "linear", + // oneVoter: false, + featurelist: [], + // sandboxsave: true, + hidegearconfig: true + // preFrontrunnerIds: ["square","triangle"], + // secondStrategies: ["zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale."], + // percentSecondStrategy: [0,0,0,0,0,0,0,0,0,0], + // voter_group_count: [50,50,50,50,50,50,50,50,50,50], + // voter_group_spread: [190,190,190,190,190,190,190,190,190,190], + // keyyee: "off", + // yeefilter: ["square","triangle","hexagon","pentagon","bob"], + // computeMethod: "ez", + // pixelsize: 60, + // filename: "sandbox.html", + // presethtmlname: "sandbox.html", + } + } else if (ui.presetName == "election29") { + uiType = "election" + config = { + candidatePositions: [ + [92, 69], + [210, 70], + [245, 182], + [149, 250], + [55, 180] + ], + voterPositions: [ + [101, 189], + [148, 91], + [195, 202] + ], + // description: "[type a description for your model here. for example...]\n\nLook, it's the whole shape gang! Steven Square, Tracy Triangle, Henry Hexagon, Percival Pentagon, and last but not least, Bob.", + // features: undefined, + system: "Schulze", + candidates: 5, + voters: 3, + // doFullStrategyConfig: undefined, + // doTwoStrategies: false, + // doPercentFirst: undefined, + arena_size: 300, + arena_border: 0, + spread_factor_voters: 1, + // firstStrategy: "normalize", + // secondStrategy: "normalize frontrunners only", + // autoPoll: "Auto", + // configversion: 1, + // snowman: false, + // x_voters: false, + // median_mean: 1, + // utility_shape: "linear", + // oneVoter: false, + featurelist: [], + // sandboxsave: true, + hidegearconfig: true + // preFrontrunnerIds: ["square","triangle"], + // secondStrategies: ["zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale."], + // percentSecondStrategy: [0,0,0,0,0,0,0,0,0,0], + // voter_group_count: [50,50,50,50,50,50,50,50,50,50], + // voter_group_spread: [190,190,190,190,190,190,190,190,190,190], + // keyyee: "off", + // yeefilter: ["square","triangle","hexagon","pentagon","bob"], + // computeMethod: "ez", + // pixelsize: 60, + // filename: "sandbox.html", + // presethtmlname: "sandbox.html", + } + } else if (ui.presetName == "election30") { + uiType = "election" + config = { + candidatePositions: [ + [92, 69], + [210, 70], + [245, 182], + [149, 250], + [55, 180] + ], + voterPositions: [ + [101, 189], + [148, 91], + [195, 202] + ], + // description: "[type a description for your model here. for example...]\n\nLook, it's the whole shape gang! Steven Square, Tracy Triangle, Henry Hexagon, Percival Pentagon, and last but not least, Bob.", + // features: undefined, + system: "RBVote", + rbsystem: "Schulze", + candidates: 5, + voters: 3, + // doFullStrategyConfig: undefined, + // doTwoStrategies: false, + // doPercentFirst: undefined, + arena_size: 300, + arena_border: 0, + spread_factor_voters: 1, + // firstStrategy: "normalize", + // secondStrategy: "normalize frontrunners only", + // autoPoll: "Auto", + // configversion: 1, + // snowman: false, + // x_voters: false, + // median_mean: 1, + // utility_shape: "linear", + // oneVoter: false, + featurelist: [], + // sandboxsave: true, + hidegearconfig: true + // preFrontrunnerIds: ["square","triangle"], + // secondStrategies: ["zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale.","zero strategy. judge on an absolute scale."], + // percentSecondStrategy: [0,0,0,0,0,0,0,0,0,0], + // voter_group_count: [50,50,50,50,50,50,50,50,50,50], + // voter_group_spread: [190,190,190,190,190,190,190,190,190,190], + // keyyee: "off", + // yeefilter: ["square","triangle","hexagon","pentagon","bob"], + // computeMethod: "ez", + // pixelsize: 60, + // filename: "sandbox.html", + // presethtmlname: "sandbox.html", + } + + } else if (ui.presetName == "election31") { + uiType = "election" + config = { + + features: 1, + system: "FPTP", + + candidates: 3, + candidatePositions: [ + [50, 125], + [250, 125], + [280, 280] + ], + + voters: 1, + voterPositions: [ + [155, 125] + ], + hidegearconfig: true, + theme: "Letters", + + } + update = function () { + ui.menu.systems.choose.buttons.forEach(x => x.dom.hidden = (["FPTP", "Condorcet", "Approval", "Score"].includes(x.dom.innerHTML)) ? false : true) + } + } else if (ui.presetName == "elect_bees") { + uiType = "election" + config = { + candidatePositions: [ + [92, 69], + [210, 70], + [245, 182], + [149, 250], + [55, 180] + ], + voterPositions: [ + [150, 150], + ], + candidates: 5, + theme: "Bees", + // featurelist: ["yee"], + featurelist: [], + hidegearconfig: true, + keyyee: "mean", + pixelsize: 30, + kindayee: "center", + } + + } else if (ui.presetName == "elect_quotaApproval") { + uiType = "election" + config = { + candidatePositions: [ + [92, 69], + [210, 70], + [245, 182], + [149, 250], + [55, 180] + ], + voterPositions: [ + [150, 150], + ], + candidates: 5, + // dimensions:"1D+B", + dimensions:"2D", + system: "QuotaApproval", + hidegearconfig: true, + } + + } else if (ui.presetName == "elect_try") { + uiType = "election" + config = { + candidatePositions: [ + [92, 69], + [210, 70], + [245, 182], + [149, 250], + [55, 180] + ], + voterPositions: [ + [150, 150], + ], + candidates: 5, + // dimensions:"1D", + dimensions:"2D", + nDistricts:5, + system: "FPTP", + hidegearconfig: true, + theme:"Letters", + configversion: 2.3 + } + + } else if (ui.presetName == "ballot1") { + uiType = "ballot" + config = { + ballotType: "Plurality" + } + + } else if (ui.presetName == "ballot2") { + uiType = "ballot" + config = { + ballotType: "Ranked", + system: "IRV" + } + + } else if (ui.presetName == "ballot3") { + uiType = "ballot" + config = { + ballotType: "Approval" + } + + } else if (ui.presetName == "ballot4") { + uiType = "ballot-election" + config = { + ballotType: "Score" + } + + } else if (ui.presetName == "ballot5") { + uiType = "ballot-election" + config = { + ballotType: "Score", + firstStrategy: "normalize" + } + } else if (ui.presetName == "ballot6") { + uiType = "ballot" + config = { + ballotType: "Score", + firstStrategy: "best frontrunner", + preFrontrunnerIds: ["square", "triangle"], + showChoiceOfFrontrunners: true, + showChoiceOfStrategy: true + } + } else if (ui.presetName == "ballot7") { + uiType = "ballot" + config = { + ballotType: "Score", + firstStrategy: "not the worst frontrunner", + showChoiceOfFrontrunners: true + } + } else if (ui.presetName == "ballot8") { + uiType = "ballot-election" + config = { + ballotType: "Score", + firstStrategy: "normalize frontrunners only", + preFrontrunnerIds: ["square", "triangle"], + showChoiceOfFrontrunners: true, + showChoiceOfStrategy: true + } + } else if (ui.presetName == "ballot9") { + uiType = "ballot-election" + config = { + ballotType: "Score", + firstStrategy: "normalize frontrunners only", + preFrontrunnerIds: ["square", "triangle"], + showChoiceOfFrontrunners: true, + doStarStrategy: true + } + } else if (ui.presetName == "ballot10") { + uiType = "ballot-election" + config = { + ballotType: "Three", + firstStrategy: "normalize frontrunners only", + preFrontrunnerIds: ["square", "triangle"], + showChoiceOfFrontrunners: true, + doStarStrategy: true + } + } else if (ui.presetName == "ballot11") { + uiType = "ballot-election" + config = { + ballotType: "Score", + firstStrategy: "best frontrunner", + preFrontrunnerIds: ["square", "triangle"], + showChoiceOfFrontrunners: true, + showChoiceOfStrategy: true + } + } else if (ui.presetName == "ballot12") { + uiType = "ballot-election" + config = { + ballotType: "Score", + firstStrategy: "not the worst frontrunner", + preFrontrunnerIds: ["square", "triangle"], + showChoiceOfFrontrunners: true, + showChoiceOfStrategy: true + } + } else if (ui.presetName == "ballot13") { + uiType = "ballot" + config = { + ballotType: "Plurality", + system: "FPTP", + newWay: true + } + + } else if (ui.presetName == "ballot14") { + uiType = "ballot" + config = { + ballotType: "Ranked", + system: "RankedPair", + newWay: true + } + + } else if (ui.presetName == "ballot15") { + uiType = "ballot" + config = { + ballotType: "Approval", + newWay: true + } + + } else if (ui.presetName == "ballot16") { + uiType = "ballot" + config = { + ballotType: "Score", + newWay: true + } + + } else if (ui.presetName == "ballot17") { + uiType = "ballot" + config = { + ballotType: "Ranked", + system: "IRV", + newWay: true + } + } + ui.preset = { + config: config, + update: update, + } + ui.uiType = uiType -var url = window.location.pathname; -var filename = url.substring(url.lastIndexOf('/')+1); -main(loadpreset(filename)); + return ui +} \ No newline at end of file diff --git a/play/js/Switcher.js b/play/js/Switcher.js new file mode 100644 index 00000000..1cc70fb5 --- /dev/null +++ b/play/js/Switcher.js @@ -0,0 +1,33 @@ +function switcher(ui) { + + // keeps the model but switches the ui + + + if (ui == undefined) ui = {} + + ui.update = function() { + + + + + + ui.switchedUI = false + if (ui.uiType == "ballot") { + main_ballot(ui) + } else { + sandbox(ui) + } + + _insertFunctionAfter(ui.model,"onUpdate", function() { + if (ui.switchedUI) { + // ui.uiType = model.uiType + ui.attach.detach() + // delete div + + ui.update() + } + }) + } + + ui.update() +} diff --git a/play/js/Voters.js b/play/js/Voters.js index 2b01a761..f09020d9 100644 --- a/play/js/Voters.js +++ b/play/js/Voters.js @@ -1,77 +1,542 @@ -// helper function for strategies -function dostrategy(x,y,minscore,maxscore,rangescore,strategy,preFrontrunnerIds,candidates,radiusStep,getScore) { - // no strategy first - var lc = candidates.length - var dottedCircle = false - if (strategy == "zero strategy. judge on an absolute scale.") { - var scores = {}; - var dist2a = []; - for(var i=0; i<lc; i++){ - var c = candidates[i]; - var dx = c.x-x; - var dy = c.y-y; - var dist2 = dx*dx+dy*dy; - dist2a.push(dist2) - scores[c.id] = getScore(dist2); + +///////////////////////////////////// +///////// VOTER MODELS //////////// +///////////////////////////////////// + +// sanity rules: class creation code cannot read attributes from model. + +function VoterModel(model,type) { + // super simple + // assign functions for later use + + var self = this + type = type || 'Plurality' + self.type = type + + GeneralVoterModel(model,self) + InitVoterModel[type](model,self) + + self.castBallot = (voterPerson) => CastBallot[type](model, self, voterPerson) + self.drawBallot = (voterPerson) => DrawBallot[type](model, self, voterPerson) + self.drawTally = (voterPerson) => DrawTally[type](model, self, voterPerson) + self.drawMap = (ctx, voterPerson) => DrawMap[type](ctx, model, self, voterPerson) + self.drawMe = (ctx, voterPerson, scale, opt) => DrawMe[type](ctx, model, self, voterPerson, scale, opt) + + // self.crowd = new VoterCrowd[self.crowdType](model, self) + + // self.setType = function(type) { // don't really need this, just a shortcut + // self.castBallotType = type + // self.drawBallotType = type + // self.drawTallyType = type + // self.drawMeType = type + // self.drawMapType = type + + // InitVoterModel[type](model,self) + + // // self.castBallot = new CastBallot[self.castBallotType](model, self) + // // self.drawBallot = new DrawBallot[self.drawBallotType](model, self) + // // self.drawTally = new DrawTally[self.drawTallyType](model, self) + // // self.drawMap = new DrawMap[self.drawMapType](model, self) + // // self.drawMe = new DrawMe[self.drawMeType](model, self, scale) + + // self.castBallot = (voterPerson) => CastBallot[type](model, self, voterPerson) + // self.drawBallot = (voterPerson) => DrawBallot[self.drawBallotType](model, self, voterPerson) + // self.drawTally = (voterPerson) => DrawTally[self.drawTallyType](model, self, voterPerson) + // self.drawMap = (ctx, voterPerson) => DrawMap[self.drawMapType](ctx, model, self, voterPerson) + // self.drawMe = (ctx, voterPerson) => DrawMe[self.drawMeType](ctx, model, self, voterPerson, scale) + + // // self.getBallot = CastBallot[self.castBallotType](model, self) + // // self.textBallot = DrawBallot[self.drawBallotType](model, self) + // // self.textTally = DrawTally[self.drawTallyType](model, self) + // // self.drawBG = DrawMap[self.drawMapType](model, self) + // // self.drawCircle = DrawMe[self.drawMeType](model, self, scale) + // } + + // self.setType(type) // Default +} + +var InitVoterModel = {} + +InitVoterModel.Score = function (model, voterModel) { + + voterModel.maxscore = 5; + voterModel.minscore = 0; + voterModel.radiusLast = [] + voterModel.radiusFirst = [] + voterModel.defaultMax = model.HACK_BIG_RANGE ? 61 * 4 : 25 * 4; // step: x<25, 25<x<50, 50<x<75, 75<x<100, 100<x + if (model.doOriginal) { + voterModel.filledCircles = false + } else { + voterModel.filledCircles = true // display scores with filled transparent circles rather than unfilled circles. + } +} + +InitVoterModel.Three = function (model,voterModel) { + InitVoterModel.Score(model,voterModel) + voterModel.maxscore = 2 + +} + +InitVoterModel.Approval = function (model,voterModel) { + InitVoterModel.Score(model,voterModel) + voterModel.maxscore = 1 + voterModel.defaultMax = 200 // step: x<25, 25<x<50, 50<x<75, 75<x<100, 100<x + +} + +InitVoterModel.Ranked = function (model,voterModel) { + voterModel.maxscore = 1; // borda may be different +} + +InitVoterModel.Plurality = function (model,voterModel) { + voterModel.maxscore = 1; // just for autopoll + +} + +var CastBallot = {} + +CastBallot.Score = function (model,voterModel,voterPerson) { + var x = voterPerson.x + var y = voterPerson.y + var strategy = voterPerson.strategy + var iDistrict = voterPerson.iDistrict + let district = model.district[iDistrict] + var i = voterPerson.iPoint + + +// return function(x, y, strategy, iDistrict, i){ + var doStar = model.checkDoStarStrategy(strategy) + if (model.autoPoll == "Auto" && district.pollResults) { + tally = district.pollResults + + var factor = voterPerson.poll_threshold_factor + var max1 = 0 + for (var can in tally) { + if (tally[can] > max1) max1 = tally[can] + } + var threshold = max1 * factor + var voterAtStage = voterPerson.stages[model.stage] + voterAtStage.maxPoll = max1 + voterAtStage.threshold = threshold // record keeping for later display + var viable = [] + for (var can in tally) { + if (tally[can] > threshold) viable.push(can) + } + voterAtStage.viable = _jcopy(viable) // record keeping for later display + } else { + viable = model.district[iDistrict].preFrontrunnerIds + } + var cans = model.district[iDistrict].stages[model.stage].candidates + var scoresfirstlast = dostrategy(model,x,y,voterModel.minscore,voterModel.maxscore,strategy,viable,cans,voterModel.defaultMax,doStar,model.utility_shape) + + voterPerson.dottedCircle = scoresfirstlast.dottedCircle + var scores = scoresfirstlast.scores + + // store info on voterPerson + voterPerson.radiusFirst = scoresfirstlast.radiusFirst + voterPerson.radiusLast = scoresfirstlast.radiusLast + voterPerson.maphelp = scoresfirstlast.maphelp + + return {scores: scores} + + +} + +CastBallot.Three = function (model,voterModel,voterPerson) { + return CastBallot.Score(model,voterModel,voterPerson) +} + +CastBallot.Approval = function (model,voterModel,voterPerson) { + return CastBallot.Score(model,voterModel,voterPerson) +} + +CastBallot.Ranked = function (model,voterModel,voterPerson) { + var x = voterPerson.x + var y = voterPerson.y + var strategy = voterPerson.strategy + var iDistrict = voterPerson.iDistrict + let district = model.district[iDistrict] + var i = voterPerson.iPoint + + + // Rank the peeps I'm closest to... + var rank = []; + var cans = model.district[iDistrict].stages[model.stage].candidates + for(var j=0;j<cans.length;j++){ + var c = cans[j]; + rank.push(c.id); + } + rank = rank.sort(function(a,b){ + + var c1 = model.candidatesById[a]; + // var x1 = c1.x-x; + // var y1 = c1.y-y; + // var d1 = x1*x1+y1*y1; + var d1 = distF2(model,{x:x,y:y},c1) + + var c2 = model.candidatesById[b]; + // var x2 = c2.x-x; + // var y2 = c2.y-y; + // var d2 = x2*x2+y2*y2; + var d2 = distF2(model,{x:x,y:y},c2) + + return d1-d2; + + }); + + var considerFrontrunners = (strategy != "normalize" && strategy != "zero strategy. judge on an absolute scale.") + if (considerFrontrunners && model.election == Election.irv && model.autoPoll == "Auto" && district.pollResults) { + // we can do an irv strategy here + + voterPerson.truePreferences = _jcopy(rank) + + // so first figure out if our candidate is winning + // should figure out how close we are to winning + + var tally = district.pollResults + + // who do we have first? + var ourFirst = rank[0] + + + var weLostElectability = ! _electable_all(ourFirst,tally.head2head,voterPerson) + + // are they viable? + var viable = _findViable(model,tally.firstpicks,voterPerson) + var weLostFirstChoices = ! viable.includes(ourFirst) + + // who was first? + var weLostActually = ! tally.winners.includes(ourFirst) + // var weLost = ! model.result.winners.includes(ourFirst) + + // var weLost = weLostElectability || weLostFirstChoices + + var weLost = weLostFirstChoices + + if ( weLost ) { + // find out if our second choice could win head to head + for (var i in rank) { + var ourguy = rank[i] + if (ourguy == winguy) { + break // there is no better candidate, so let's just keep the same strategy + } + var ourguyWins = true + for (var iwinguy in tally.winners) { + var winguy = tally.winners[iwinguy] + var ours = tally.head2head[ourguy][winguy] + var theirs = tally.head2head[winguy][ourguy] + if (theirs > ours) ourguyWins = false + } + if (ourguyWins) { + // okay, we should vote for him first + // probably, this could be improved to vote for more than just this guy first + + // bump ourguy into first on our ballot + for (var j = i; j > 0; j--) { + rank[j] = rank[j-1] + } + rank[0] = ourguy + break + } + } + // done changing rank + } + } + + // Ballot! + return { rank:rank }; + +} + + +function _electable_all(ourCan,hh,voterPerson) { + var cs = Object.keys(hh) + var others = cs.filter(x => x !== ourCan) + + var badness = 1/voterPerson.poll_threshold_factor + + // Which candidates are not defeated badly? + let electable = true + for (let b of others) { + // check how badly we are defeated + let howbad = hh[b][ourCan] / hh[ourCan][b] + + if (howbad > badness) { + // this parameter can be changed. + electable = false + } + } + return electable +} + +CastBallot.Plurality = function (model,voterModel,voterPerson) { + var x = voterPerson.x + var y = voterPerson.y + var strategy = voterPerson.strategy + var iDistrict = voterPerson.iDistrict + let district = model.district[iDistrict] + + // first, make a list of all the candidates + // if we're in a primary then consider electability (if there are polls) + // consider only viable candidates (if there are polls) + // find the closest candidate + // return + // if we're in a general election then consider only viable candidates (if there are polls) + + // make a list of all the candidates in this stage + // if it's a primary, then only consider our party's candidates + var cans = district.stages[model.stage].candidates + + var goodCans = cans + + if (district.primaryPollResults && model.stage == "primary") { + + // if we're in a primary + // then consider electability (if there are polls) + if (model.doElectabilityPolls) { + goodCans = _bestElectable(model, voterPerson) + } else { + // otherwise, only consider our party's candidates + goodCans = district.parties[voterPerson.iParty].candidates + } + } + + // if we have a strategy, then + var checkOnlyFrontrunners = (strategy!="zero strategy. judge on an absolute scale." && strategy!="normalize") + if (checkOnlyFrontrunners) { + + // consider only viable candidates (if there are polls, + if (district.pollResults && model.autoPoll == "Auto") { // Auto is here for safety + var viable = _findViableFromSet(model, goodCans, district, voterPerson) + + } else if (model.autoPoll == "Manual") { // manually set viable candidates, if we want to + var viable = district.preFrontrunnerIds + + } else { // we're the first to take the polls, so any candidate is viable + var viable = goodCans.map(c => c.id) + } + + // but only if there is more than one viable candidate + if (viable.length > 1) { + goodCans = viable.map(cid => model.candidatesById[cid]) } + } + + // Who am I closest to? + var closest = _findClosest(model,goodCans,x,y) + + // Vote for the CLOSEST + return { vote:closest.id }; + +} + +function _bestElectable(model, voterPerson) { + let iDistrict = voterPerson.iDistrict + let district = model.district[iDistrict] + + // check for defeats against other party's candidates + let hh = district.primaryPollResults.head2head // format hh[win][against] = numwins + + // What party do we belong to? + // Check our group id + var iMyParty = voterPerson.iParty + var parties = district.parties + + // Which candidates not defeated badly? + var electset = _electable(model,iMyParty,parties,hh) + + var voterAtStage = voterPerson.stages[model.stage] + voterAtStage.electable = electset + + // if no candidates are electable + if (electset.length == 0) { + // find the most electable candidate + // the one with the best "worst defeat" + var electset = _mostElectable(iMyParty,parties,hh) - var radiusFirst = radiusStep * (minscore + .5) - var radiusLast = radiusStep * (maxscore - .5) - var scoresfirstlast = {scores:scores, radiusFirst:radiusFirst , radiusLast:radiusLast, dottedCircle:dottedCircle} - return scoresfirstlast; + voterAtStage.mostElectable = electset + } + + return electset +} + + +function _electable(model,iMyParty,parties,hh) { + // Which candidates are not defeated badly? + var myParty = parties[iMyParty] + var electset = [] + for (let a of myParty.candidates) { + let electable = true + for (let i = 0; i < parties.length; i++) { + if (iMyParty !== i) { + let party = parties[i] + for (let b of party.candidates) { + // check how badly we are defeated + let howbad = hh[b.id][a.id] / hh[a.id][b.id] + + if (howbad > model.howBadlyDefeatedThreshold) { // 1.1 is default + // this parameter can be changed. + electable = false + } + + } + } + } + if (electable) { + electset.push(a) + } + } + return electset +} + +function _mostElectable(iMyParty,parties,hh) { + // find the most electable candidate + // the one with the best "worst defeat" + var myParty = parties[iMyParty] + + let mostelectable = {id:null}; + let leastbad = Infinity; + for (let a of myParty.candidates) { + let worstdefeat = 0 + for (let i = 0; i < parties.length; i++) { + if (iMyParty !== i) { + let party = parties[i] + for (let b of party.candidates) { + let howbad = hh[b.id][a.id] / hh[a.id][b.id] + // also, find the most electable + if (worstdefeat < howbad) { + worstdefeat = howbad + } + } + } + } + if (leastbad > worstdefeat) { + leastbad = worstdefeat + mostelectable = a + } + } + electset = [mostelectable] + return electset +} + +function _findClosest(model,electset,x,y) { + var closest = {id:null}; + var closestDistance = Infinity; + for(var j=0;j<electset.length;j++){ + let e = electset[j]; + var dist = distF2(model,{x:x,y:y},e) + if(dist<closestDistance){ + closestDistance = dist; + closest = e; + } + } + return closest +} + +function _findViableFromSet(model,cans, district, voterPerson) { + let tally = district.pollResults + tally = _tallyFromSet(cans, tally) + var viable = _findViable(model,tally,voterPerson) + return viable +} + +function _tallyFromSet(electset, tally) { + let oldtally = tally + var tally = {} + for (let e of electset) { + tally[e.id] = oldtally[e.id] + } + return tally +} + + +function _findViable(model,tally,voterPerson) { + var factor = voterPerson.poll_threshold_factor + var max1 = 0 + for (var can in tally) { + if (tally[can] > max1) max1 = tally[can] + } + var threshold = max1 * factor + + var voterAtStage = voterPerson.stages[model.stage] + voterAtStage.maxPoll = max1 + voterAtStage.threshold = threshold // record keeping for later display + var viable = [] + for (var can in tally) { + if (tally[can] > threshold) viable.push(can) } + voterAtStage.viable = _jcopy(viable) // record keeping for later display + return viable +} + +function dostrategy(model,x,y,minscore,maxscore,strategy,preFrontrunnerIds,candidates,defaultMax,doStar,utility_shape) { + + // reference + // {name:"O", realname:"zero strategy. judge on an absolute scale.", margin:4}, + // {name:"N", realname:"normalize", margin:4}, + // {name:"F", realname:"normalize frontrunners only", margin:4}, + // {name:"F+", realname:"best frontrunner", margin:4}, + // {name:"F-", realname:"not the worst frontrunner"} + + var lc = candidates.length + var dottedCircle = false - // find distances and ids + + // find distances and ids // dista = [] canAid = [] for(var i=0; i<lc; i++){ var c = candidates[i]; - var dx = c.x-x; - var dy = c.y-y; - var dist = Math.sqrt(dx*dx+dy*dy); + var dist = distF(model,{x:x,y:y},c) dista.push(dist) canAid.push(c.id) } - // reference - // {name:"O", realname:"zero strategy. judge on an absolute scale.", margin:4}, - // {name:"N", realname:"normalize", margin:4}, - // {name:"F", realname:"normalize frontrunners only", margin:4}, - // {name:"B", realname:"best frontrunner", margin:4}, - // {name:"W", realname:"not the worst frontrunner"} - - var lf = preFrontrunnerIds.length + + // find m and n (max and min) // + + + if (strategy == "zero strategy. judge on an absolute scale.") { + + m = defaultMax + n = 0 - // identify important set - var shortlist = [] - var ls - if (strategy == "normalize" || lf == 0) { // exception for no frontrunners - ls = lc - for (var i = 0; i < lc; i++) { - shortlist.push(i) - } } else { - ls = lf - for (var i = 0; i < ls; i++) { - var index = canAid.indexOf(preFrontrunnerIds[i]) - if (index > -1) {shortlist.push(index)} - } - } - // find min and max of shortlist - var m=-1 - var n=Infinity - var mi=null - var ni=null - for (var i = 0; i < ls; i++) { - var d1 = dista[shortlist[i]] - if (d1 > m) { - m = d1 // max - mi = i + var lf = preFrontrunnerIds.length + + // identify important set + var shortlist = [] + var ls + if (strategy == "normalize" || lf == 0) { // exception for no frontrunners + ls = lc + for (var i = 0; i < lc; i++) { + shortlist.push(i) + } + } else { + ls = lf + for (var i = 0; i < ls; i++) { + var index = canAid.indexOf(preFrontrunnerIds[i]) + if (index > -1) {shortlist.push(index)} + } } - if (d1 < n) { - n = d1 //min - ni = i + + // find min and max of shortlist + var m=-1 + var n=Infinity + var mi=null + var ni=null + for (var i_short = 0; i_short < ls; i_short++) { + var i = shortlist[i_short] + var d1 = dista[i] + if (d1 > m) { + m = d1 // max + mi = i + } + if (d1 < n) { + n = d1 //min + ni = i + } } } @@ -82,8 +547,14 @@ function dostrategy(x,y,minscore,maxscore,rangescore,strategy,preFrontrunnerIds, dottedCircle = true; } - // assign scores + // assign scores // scores = {} + if (utility_shape != "linear") { + var f = utility_function(utility_shape) + var m_inv = 1/m + f_n = f(n*m_inv) + f_m = f(1) + } for(var i=0; i<lc; i++){ var d1 = dista[i] if (d1 < n) { @@ -91,12 +562,34 @@ function dostrategy(x,y,minscore,maxscore,rangescore,strategy,preFrontrunnerIds, } else if (d1 >= m){ // in the case that the voter likes the frontrunner candidates equally, he just votes for everyone better score = minscore } else { // putting this last avoids m==n giving division by 0 - frac = ( d1 - n ) / ( m - n ) + if (utility_shape == "linear") { + frac = ( d1 - n ) / ( m - n ) + } else { + frac = ( f(d1*m_inv) - f_n ) / ( f_m - f_n ) + } score = Math.floor(.5+minscore+(maxscore-minscore)*(1-frac)) } scores[canAid[i]] = score } + + // adjust scores if necessary // + + if (strategy == "zero strategy. judge on an absolute scale.") { + return {scores:scores, radiusFirst:n , radiusLast:m, dottedCircle:false} + } + + + + // star exception + //if (strategy == "starnormfrontrunners") { + if (doStar) { + decision = starStrategy(scores, shortlist, dista, canAid, maxscore, lc, utility_shape, strategy) + scores = decision.scores + var maphelp = decision.maphelp + } + + // boundary condition correction // scores[canAid[mi]] = minscore scores[canAid[ni]] = maxscore @@ -122,641 +615,4552 @@ function dostrategy(x,y,minscore,maxscore,rangescore,strategy,preFrontrunnerIds, } } - - // star exception - if (strategy == "starnormfrontrunners") { - // find best candidate and make sure that only he gets the best score - var n1 = n - var n1i = ni - for (var i = 0; i < lc; i++) { - var d1 = dista[i] - if (d1 < n1) { - n1 = d1 //min - n1i = i - } - } - for (var i = 0; i < lc; i++) { - var c = canAid[i] - if (scores[c]==maxscore && i!=n1i) { - scores[c]=maxscore-1; - }}} - - return {scores:scores, radiusFirst:n , radiusLast:m, dottedCircle:dottedCircle} + return {scores:scores, radiusFirst:n , radiusLast:m, dottedCircle:dottedCircle, maphelp:maphelp} } -///////////////////////////////////// -///////// TYPES OF VOTER //////////// -///////////////////////////////////// - -function ScoreVoter(model){ - - var self = this; - self.model = model; - var maxscore = 5; - var minscore = 0; - var scorearray = []; - for (var i=minscore; i<= maxscore; i++) scorearray.push(i) - self.radiusStep = window.HACK_BIG_RANGE ? 61 : 25; // step: x<25, 25<x<50, 50<x<75, 75<x<100, 100<x - - self.getScore = function(x2){ - var step = self.radiusStep; - if(x2<step*step) return 5; - if(x2<(step*2)*(step*2)) return 4; - if(x2<(step*3)*(step*3)) return 3; - if(x2<(step*4)*(step*4)) return 2; - if(x2<(step*5)*(step*5)) return 1; - return 0; - }; - self.getBallot = function(x, y, strategy){ +function starStrategy(scores, shortlist, dista, canAid, maxscore, lc, utility_shape, strategy) { + // put shortlist in order + sortedShortlist = _jcopy(shortlist).sort( (i,k) => dista[i] - dista[k] ) // shortest distance first + // use the shortlist to make a piece-wise linear function + var ubScore = [] + var lbScore = [] + var tryScore = [] + var ns = sortedShortlist.length + if (ns > maxscore + 1) { + // start with normalized scores - var scoresfirstlast = dostrategy(x,y,minscore,maxscore,scorearray,strategy,self.model.preFrontrunnerIds,self.model.candidates,self.radiusStep,self.getScore) - - self.radiusFirst = scoresfirstlast.radiusFirst - self.radiusLast = scoresfirstlast.radiusLast - self.dottedCircle = scoresfirstlast.dottedCircle - var scores = scoresfirstlast.scores - return scores - - }; + // make groups + var groups = [] // list of list of indexes for candidates + for (var i = 0; i <= maxscore ; i ++) { + groups[i] = [] + } + // add indexes to groups + for ( var i = 0; i < ns ; i ++) { + var score = scores[canAid[shortlist[i]]] + groups[score].push(shortlist[i]) // candidate indices + } + // remove empty groups + for (var i = maxscore; i >= 0 ; i --) { + if (groups[i].length == 0) { + groups.splice(i,1) + } + } - self.drawBG = function(ctx, x, y, ballot){ + // start loop of adding groups + while (groups.length < maxscore + 1) { + var spreadByGroup = [] + // find distance differences within groups + for (var i = 0; i < groups.length ; i ++) { + var gScores = groups[i].map( a => dista[a] ) + var maxa = gScores.reduce( (a, b) => Math.max(a, b) ) + var mina = gScores.reduce( (a, b) => Math.min(a, b) ) + spreadByGroup[i] = maxa - mina + } + // find the group with the biggest spread + var imax = null + var max = -1 + for (var i = 0; i < spreadByGroup.length ; i ++) { + if (max < spreadByGroup[i]) { + max = spreadByGroup[i] + imax = i + } + } + // find the middle of the group + var maxGroup = groups[imax] + var gScores = maxGroup.map( a => dista[a] ) + var maxa = gScores.reduce( (a, b) => Math.max(a, b) ) + var mina = gScores.reduce( (a, b) => Math.min(a, b) ) + var middle = ( maxa + mina ) * .5 + // split the group down the middle + var upgroup = maxGroup.filter( a => dista[a] <= middle) + var downgroup = maxGroup.filter( a => dista[a] > middle) + // insert new groups + var newGroups = [] + for (var i = 0; i < groups.length; i++) { + if (i == imax) { + newGroups.push(downgroup) + newGroups.push(upgroup) + } else { + newGroups.push(groups[i]) + } + } + groups = newGroups + // one cycle complete + // check if there are enough groups + } - // RETINA - x = x*2; - y = y*2; - var scorange = maxscore - minscore - var step = (self.radiusLast - self.radiusFirst)/scorange; - // Draw big ol' circles. - for(var i=0;i<scorange;i++){ - ctx.beginPath(); - ctx.arc(x, y, (step*(i+.5) + self.radiusFirst)*2, 0, Math.TAU, false); - ctx.lineWidth = (5-i)*2; - ctx.strokeStyle = "#888"; - ctx.setLineDash([]); - if (self.dottedCircle) ctx.setLineDash([5, 15]); - ctx.stroke(); - if (self.dottedCircle) ctx.setLineDash([]); + // we're done + + // assign scores to shortlist + var canListTryScore = [] + for ( var i = 0 ; i < groups.length ; i ++) { + var group = groups[i] + for (var k of group) { + canListTryScore[k] = i + } + } + // group is a list of indices of candidates + // tryScore is indexed by indices of the sortedList of candidates + for (var k in sortedShortlist) { + var cani = sortedShortlist[k] // candidate index + tryScore[k] = canListTryScore[cani] } - }; + // summary of above + // see if we can split up some groups + // check the spread of each group + // find the group with the biggest spread of distance + // split the biggest group in two - self.drawCircle = function(ctx, x, y, size, ballot){ + } else { // if (ns <= maxscore + 1) { - // There are #Candidates*5 slices - // Fill 'em in in order -- and the rest is gray. - var totalSlices = self.model.candidates.length*(maxscore-minscore); - var leftover = totalSlices; - var slices = []; - for(var i=0; i<self.model.candidates.length; i++){ - var c = self.model.candidates[i]; - var cID = c.id; - var score = ballot[cID] - minscore; - leftover -= score; - slices.push({ - num: score, - fill: c.fill - }); + // start at top + var k = maxscore + for ( var i = 0 ; i < ns ; i ++) { + ubScore[i] = k + k-- } - // Leftover is gray - slices.push({ - num: leftover, - fill: "#bbb" - }); - // FILL 'EM IN - _drawSlices(ctx, x, y, size, slices, totalSlices); - }; + // start at bottom + var k = 0 + for ( var i = ns - 1 ; i >= 0 ; i --) { + lbScore[i] = k + k++ + } -} + if (strategy == "best frontrunner") { // give lower bound scores + for ( var i = 0 ; i < ns ; i ++) { + tryScore[i] = lbScore[i] + } + } else if (strategy == "not the worst frontrunner") { // give upper bound scores + for ( var i = 0 ; i < ns ; i ++) { + tryScore[i] = ubScore[i] + } + tryScore[ns-1] = 0 // zero score for worst frontrunner + } else { + // try to space candidates + var k = maxscore + for ( var i = 0 ; i < ns ; i ++) { + var desiredScore = scores[canAid[sortedShortlist[i]]] + if (ubScore[i] > desiredScore) { + // we gave too good a score and we can lower the score + k = desiredScore + } + if (lbScore[i] > k) { // did we go too low? + k = lbScore[i] // use lower bound + } + tryScore[i] = k + k-- + } + } -function ThreeVoter(model){ + } - var self = this; - self.model = model; - var maxscore = 2; - var minscore = 0; - var scorearray = []; - for (var i=minscore; i<= maxscore; i++) scorearray.push(i) - self.radiusStep = window.HACK_BIG_RANGE ? 61 : 25; // step: x<25, 25<x<50, 50<x<75, 75<x<100, 100<x - - self.getScore = function(x2){ - var step = self.radiusStep; - if(x2<step*step) return 2; - if(x2<step*3.5*step*3.5) return 1; - return 0; - }; + // assign scores to shortlist + for ( var i = 0 ; i < ns ; i ++) { + scores[canAid[sortedShortlist[i]]] = tryScore[i] + } - self.getBallot = function(x, y, strategy){ + // still need to assign scores to candidates outside the shortlist + // so we've got a list of distances and scores. let's set up linear interpolation intervals. - - var scoresfirstlast = dostrategy(x,y,minscore,maxscore,scorearray,strategy,self.model.preFrontrunnerIds,self.model.candidates,self.radiusStep,self.getScore) - - self.radiusFirst = scoresfirstlast.radiusFirst - self.radiusLast = scoresfirstlast.radiusLast - self.dottedCircle = scoresfirstlast.dottedCircle - var scores = scoresfirstlast.scores - return scores - - }; + // first we set up breaks between scores - self.drawBG = function(ctx, x, y, ballot){ + var breaks = [] - // RETINA - x = x*2; - y = y*2; - var scorange = maxscore - minscore - var step = (self.radiusLast - self.radiusFirst)/scorange; - // Draw big ol' circles. - for(var i=0;i<scorange;i++){ - ctx.beginPath(); - ctx.arc(x, y, (step*(i+.5) + self.radiusFirst)*2, 0, Math.TAU, false); - ctx.lineWidth = (5-i)*2; - ctx.strokeStyle = "#888"; - ctx.setLineDash([]); - if (self.dottedCircle) ctx.setLineDash([5, 15]); - ctx.stroke(); - if (self.dottedCircle) ctx.setLineDash([]); + var iclosest = sortedShortlist[0] + var ifurthest = sortedShortlist[sortedShortlist.length-1] + var dclosest = dista[iclosest] + var dfurthest = dista[ifurthest] + + if (strategy == "best frontrunner") { + var d = dclosest * 1.001 + for (var i = 0; i < maxscore; i++ ) { + breaks.push(d) } + } else if (strategy == "not the worst frontrunner") { + var d = dfurthest * .999 + for (var i = 0; i < maxscore; i++ ) { + breaks.push(d) + } + } else { + for (var i = 0; i < maxscore; i++ ) { + // assign default breaks + var frac = (i+.5) / maxscore + var d = dfurthest + frac * (dclosest - dfurthest) + breaks.push(d) + } + } - }; - - self.drawCircle = function(ctx, x, y, size, ballot){ - - // There are #Candidates*5 slices - // Fill 'em in in order -- and the rest is gray. - var totalSlices = self.model.candidates.length*(maxscore-minscore); - var leftover = totalSlices; - var slices = []; - for(var i=0; i<self.model.candidates.length; i++){ - var c = self.model.candidates[i]; - var cID = c.id; - var score = ballot[cID] - minscore; - leftover -= score; - slices.push({ - num: score, - fill: c.fill - }); + // adjust breaks to match shortlist + for (var i = 0; i < maxscore; i++ ) { + // look at boundary between a score of i and i+1 + var db = breaks[i] + var shi = i+1 // score on high side of break + var slo = i + + // does the interval need to be adjusted? + for ( var k = 0 ; k < ns ; k ++) { // check the shortlist + var d = dista[sortedShortlist[k]] + var s = tryScore[k] + // check for push lower + // if distance is further than break and score is higher than break would suggest, then push break closer + // and opposite, too + var dfurther = d > db + var shigher = s >= shi + var dcloser = d < db + var slower = s <= slo + if ( dfurther && shigher ) { + db = d * 1.001 // put boundary just outside of this candidate + } else if ( dcloser && slower ) { + db = d * .999 // put boundary just in front of this candidate + } } - // Leftover is gray - slices.push({ - num: leftover, - fill: "#bbb" - }); - // FILL 'EM IN - _drawSlices(ctx, x, y, size, slices, totalSlices); + breaks[i] = db // store any adjustments + } - }; + // remake beginning and ending intervals + var intervals = [] + ivScore = [] + intervals.push(0) + ivScore.push(maxscore) + for (var i=breaks.length-1; i >= 0; i--) { + intervals.push(breaks[i]) + ivScore.push(i+.5) + } + iLast = intervals[intervals.length-1] + intervals.push(iLast*2) + ivScore.push(0) + + var fillScore = [] + // var intervals = sortedShortlist.map( i => dista[i] ) // old way, kind of jerky + for(var i=0; i<lc; i++){ + + // first, find the interval this distance fits into + var d1 = dista[i] + var valEnd = intervals.find( x => x >= d1) + var end = intervals.indexOf(valEnd) + + if (end == -1) { + // too big distance, assign min score + fillScore[i] = 0 + } else if (end == 0) { + fillScore[i] = maxscore // easy + } else { + var start = Math.max(0,end-1) + var s = intervals[start] + var e = intervals[end] + if (e === s) { + // avoid dividing by zero + var frac = 0 + } else { + if (utility_shape == "linear") { + frac = ( d1 - s ) / ( e - s ) + } else { + var u = utility_function(utility_shape) + frac = ( u(d1) - u(s) ) / ( u(e) - u(s) ) + } + } + // apply fraction + var ss = ivScore[start] + var es = ivScore[end] + fillScore[i] = Math.round( ss + (es-ss)*frac ) + } + } + + // assign scores to all candidates + for ( var i = 0 ; i < lc ; i ++) { + scores[canAid[i]] = fillScore[i] + } + + + if(0) { // old way + + // find best candidate and make sure that only he gets the best score + var n1 = n + var n1i = ni + for (var i = 0; i < lc; i++) { + var d1 = dista[i] + if (d1 < n1) { + n1 = d1 //min + n1i = i + } + } + for (var i = 0; i < lc; i++) { + var c = canAid[i] + if (scores[c]==maxscore && i!=n1i) { + scores[c]=maxscore-1; + } + } + } + + return {scores:scores, maphelp:{intervals:intervals,scores:ivScore}} +} + + +function utility_function(utility_shape) { + if (utility_shape == "quadratic") { + var f = x => x**2 + } else if (utility_shape == "log") { + var f = x => Math.log(x+.1) + } else { // "linear" + var f = x => x // f is a function defined between 0 and 1 and increasing. + } + return f +} + +function inverse_utility_function(utility_shape) { + if (utility_shape == "quadratic") { + var finv = x => Math.sqrt(x) + } else if (utility_shape == "log") { + var finv = x => Math.exp(x) - .1 + } else { // "linear" + var finv = x => x + } + return finv +} + +function distF(model,v,c) { + return Math.sqrt(distF2(model,v,c)) +} + +function distF2(model,v,c) { // voter and candidate should be in order + if (model.dimensions == "1D+B") { + var dx = v.x - c.x + if (1) { + if (c.b == 0) return 2^30 + var f = 1/c.b * 2 * .5 ** c.b + return dx*dx * f*f + } else { + // these are old + var dy = c.y - (model.yDimOne + model.yDimBuffer) + var a=9 + switch (a) { + case 1: return Math.abs(dx*dy) + case 2: return Math.abs(dx*dy*.1) + case 3: return Math.abs(dx*dy*dy*.001) + case 4: + var adx = Math.abs(dx) + var ady = Math.abs(dy) + if (adx < 10 || ady < 10) return 0 + return (adx + ady)*2 + case 5: + var adx = Math.abs(dx) + var ady = Math.abs(dy) + if (adx < 10 || ady < 10) { + var balance = min(adx,ady) / 10 + return balance * (adx + ady)*2 + } + return (adx + ady)*2 + case 6: + var adx = Math.abs(dx) + var ady = Math.abs(dy) + return (1/(1/adx+1/ady))**2 /// hmm forgot this square while making this function... so things are weird + case 7: + return (1/(1/(dx*dx)+100/(dy*dy))) ** 2 + case 8: + var adx = Math.abs(dx) + var ady = Math.abs(dy) + return (11 / ( 1/adx + 10/ady )) ** 2 + case 9: + var f = .2 * 2 ** (dy/30) + return (Math.abs(dx) * f)**2 + case 10: + var adx = Math.abs(dx) + var ady = Math.abs(dy) + var bInv = ady/30 + return (adx * bInv)**2 + } + } + } else if (model.dimensions == "1D") { + var dx = v.x - c.x + var f = 1/c.b * 2 * .5 ** c.b + return dx*dx * f*f + } else { + var dx = v.x - c.x + var dy = v.y - c.y + var f = 1/c.b * 2 * .5 ** c.b + // f = 1 when c.b = 1 + // var f = .5 * 2 ** (1/c.b) + return (dx*dx + dy*dy) * f*f + } +} + + +var DrawMap = {} + +DrawMap.Score = function (ctx, model,voterModel,voterPerson) { + var x = voterPerson.xArena + var y = voterPerson.yArena + var strategy = voterPerson.strategy + var iDistrict = voterPerson.iDistrict + var ballot = voterPerson.stages[model.stage].ballot + + + if (model.ballotConcept == "off" && ! model.arena.viewMan.active) return + + var drawMapViewMan = (model.arena.viewMan.active && model.arena.viewMan.focus === voterPerson) + // we only want to show the map for the viewMan if he is active. + if (model.onlyVoterMapViewMan) if (model.arena.viewMan.active && model.arena.viewMan.focus !== voterPerson) return + + var scorange = voterModel.maxscore - voterModel.minscore + var step = (voterModel.radiusLast - voterModel.radiusFirst)/scorange; + + var nVoters = model.district[iDistrict].voterPeople.length + if (drawMapViewMan) nVoters = 3 // bolder map, not too bold + if (drawMapViewMan && model.onlyVoterMapViewMan) nVoters = 1 + + // Draw big ol' circles. + var lastDist = Infinity + var f = utility_function(model.utility_shape) + var finv = inverse_utility_function(model.utility_shape) + + + + if (model.doVoterMapGPU) { + voterPerson.rad = [] + voterPerson.idxCan = [] + } else { + var tempComposite = ctx.globalCompositeOperation + // ctx.globalCompositeOperation = "source-over" + // ctx.globalCompositeOperation = "multiply" // kinda cloudy + // ctx.globalCompositeOperation = "screen" + // ctx.globalCompositeOperation = "overlay" + // ctx.globalCompositeOperation = "hue" + ctx.globalCompositeOperation = "lighter" // seems good + // ctx.globalCompositeOperation = "darker" // compatibility issues + // ctx.globalCompositeOperation = "lighten" + // ctx.globalCompositeOperation = "darken" // not uniform 1,2,3 + } + + var doStar = model.checkDoStarStrategy(strategy) + + for(var i=0;i<scorange;i++){ + //var dist = step*(i+.5) + voterModel.radiusFirst + + var frac = (1-(i+.5)/scorange) + var worst = f(1) + var best = f(voterPerson.radiusFirst/voterPerson.radiusLast) + var x1 = finv(frac*(worst-best)+best) + var dist = x1 * voterPerson.radiusLast + + if (doStar) { + // use maphelp + var m = voterPerson.maphelp + var iv = m.intervals + var sc = m.scores + + // we want to use our guiding scores to make maps + var slo = sc.find( x => x <= i) + var ilo = sc.indexOf( slo ) + var ihi = Math.max(ilo - 1,0) + // hi and lo distances + var dhi = iv[ihi] + var dlo = iv[ilo] + // hi and lo scores + var shi = sc[ihi] + var slo = sc[ilo] + // interpolate to find boundary (and handle dividing by zero) + var frac = (slo == shi) ? 0 : ( (i+.5) - slo) / (shi - slo) + if (model.utility_shape == "linear") { + dist = dlo + (dhi - dlo) * frac + } else { + var f = utility_function(model.utility_shape) + var finv = inverse_utility_function(model.utility_shape) + var fdist = f(dlo) + (f(dhi) - f(dlo)) * frac + dist = finv(fdist) + } + + } + + if (model.doVoterMapGPU) { + voterPerson.rad.push(dist) + voterPerson.idxCan.push(0) + continue + } + + ctx.lineWidth = (i+5-scorange)*2 + 2; + ctx.beginPath(); + ctx.arc(x*2, y*2, dist*2, 0, Math.TAU, false); + if (voterModel.filledCircles) { + ctx.fillStyle = '#000' + ctx.strokeStyle = "#000"; + var invert = true // CAN CHANGE + if (invert) { + // draw district rectangle + var donuts = true + if (donuts) { + if (lastDist == Infinity) { + ctx.rect(0,0,ctx.canvas.width,ctx.canvas.height) + } else { + ctx.arc(x*2, y*2, lastDist*2, 0, Math.TAU, false); + } + lastDist = dist // copy + } else { + ctx.rect(0,0,ctx.canvas.width,ctx.canvas.height) + } + } + } else { + ctx.strokeStyle = "#888" + } + ctx.closePath() + ctx.setLineDash([]); + if (voterPerson.dottedCircle) ctx.setLineDash([5, 15]); + if (voterModel.filledCircles) { + var temp = ctx.globalAlpha + // ctx.globalAlpha = .01 + // ctx.stroke(); + // ctx.globalAlpha = .1 + ctx.globalAlpha = 1 / scorange / nVoters + if (invert) { + if (donuts) ctx.globalAlpha = Math.max(.002, 1 / nVoters * (scorange - i) / scorange) // donuts get lighter towards the center + // .002 seems to be a limit + ctx.fill("evenodd") + } else { + ctx.fill() + } + ctx.globalAlpha = temp + } else { + ctx.stroke() + } + if (voterPerson.dottedCircle) ctx.setLineDash([]); + } + ctx.globalCompositeOperation = tempComposite + + +} + +DrawMap.Three = function (ctx, model,voterModel,voterPerson) { + DrawMap.Score(ctx,model,voterModel,voterPerson) + +} + +DrawMap.Approval = function (ctx, model,voterModel,voterPerson) { + DrawMap.Score(ctx,model,voterModel,voterPerson) +} + + +// CalculateMap.Ranked = function (ctx, model,voterModel,voterPerson) { +DrawMap.Ranked = function (ctx, model,voterModel,voterPerson) { + var x = voterPerson.xArena + var y = voterPerson.yArena + var strategy = voterPerson.strategy + var iDistrict = voterPerson.iDistrict + var i = voterPerson.iPoint + var ballot = voterPerson.stages[model.stage].ballot + + + if (model.ballotConcept == "off" && ! model.arena.viewMan.active) return + + var drawMapViewMan = (model.arena.viewMan.active && model.arena.viewMan.focus === voterPerson) + + // we only want to show the map for the viewMan if he is active. + if (model.onlyVoterMapViewMan) if (model.arena.viewMan.active && model.arena.viewMan.focus !== voterPerson) return + + var nVoters = model.district[iDistrict].voterPeople.length + if (drawMapViewMan) nVoters = 3 // bolder map, not too bold + if (drawMapViewMan && model.onlyVoterMapViewMan) nVoters = 1 + + if (model.doOriginal) { + // RETINA + x = x*2; + y = y*2; + // DRAW 'EM LINES + for(var i=0; i<ballot.rank.length; i++){ + + // Line width + var lineWidth = ((ballot.rank.length-i)/ballot.rank.length)*8; + + // To which candidate... + var rank = ballot.rank[i]; + var c = model.candidatesById[rank]; + var cc = model.arena.modelToArena(c) + var cx = cc.x*2; // RETINA + var cy = cc.y*2; // RETINA + + // Draw + ctx.beginPath(); + ctx.moveTo(x,y); + ctx.lineTo(cx,cy); + ctx.lineWidth = lineWidth; + ctx.strokeStyle = "#888"; + ctx.stroke(); + + } + + } else if (model.system == "IRV" || model.system == "STV") { + var bon = model.ballotConcept == "on" || (model.arena.viewMan.active && model.arena.viewMan.focus === voterPerson) + if (bon) { + if (ballot.rank.length == 0) return + var candidate = model.candidatesById[ballot.rank[0]]; + + // RETINA + x = x*2; + y = y*2; + var cc = model.arena.modelToArena(candidate) + var tx = cc.x*2; + var ty = cc.y*2; + + // DRAW - Line + ctx.beginPath(); + ctx.moveTo(x,y); + ctx.lineTo(tx,ty); + ctx.lineWidth = 8; + ctx.strokeStyle = "#888"; + let n = Math.log(nVoters+3) / Math.LN10 + .6 // 1 with 1 voter, 2 with more voters + ctx.globalAlpha = 1/n + ctx.stroke(); + + + } else if (0) { + var old = {} + old.x = x + old.y = y + // DRAW 'EM LINES + for(var i=0; i<ballot.rank.length; i++){ + + // Line width + var lineWidth = ((ballot.rank.length-i)/ballot.rank.length)*8; + + // To which candidate... + var rank = ballot.rank[i]; + var c = model.candidatesById[rank]; + var cc = model.arena.modelToArena(c) + var next = {} + next.x = cc.x; + next.y = cc.y; + + // Draw + ctx.beginPath(); + ctx.moveTo(old.x*2,old.y*2); + ctx.lineTo(next.x*2,next.y*2); + ctx.lineWidth = lineWidth; + ctx.strokeStyle = "#888"; + ctx.stroke(); + old = next + ctx.setLineDash([5, 15]); + } + ctx.setLineDash([]); + } + } else if (model.useBeatMapForRankedBallotViz && model.system != "Borda" && !drawMapViewMan) { + // do nothing .. kind of a temporary bandage while I work out the visualization + } else if (1) { + + if (model.system == "Borda") { + var doColors = false + } else { + var doColors = true + } + + me = {x:x, y:y} + // var meArena = model.arena.modelToArena(me) + + var tempComposite = ctx.globalCompositeOperation + + if (doColors) { + var cmul = 1 + // ctx.globalCompositeOperation = "source-over"; cmul = 1 // maybe + ctx.globalCompositeOperation = "multiply"; cmul = 1 // good , but colors are off + // ctx.globalCompositeOperation = "screen"; cmul = 2 // interesting, bad + // ctx.globalCompositeOperation = "overlay"; cmul = 2 // okay + // ctx.globalCompositeOperation = "hue"; cmul = 1 // great for small numbers (best on color matching) but fails for large numbers + // ctx.globalCompositeOperation = "lighter"; cmul = .4 // 1 // good for small number // bad for large number + // ctx.globalCompositeOperation = "darker"; cmul = .5 // compatibility issues & order matters + // ctx.globalCompositeOperation = "lighten"; cmul = 1 // kinda good for small numbers // bad because background too bright + // ctx.globalCompositeOperation = "darken"; cmul = .3 // not uniform 1,2,3 // not dark enough + } else { + // ctx.globalCompositeOperation = "source-over" + // ctx.globalCompositeOperation = "multiply" // kinda cloudy + // ctx.globalCompositeOperation = "screen" + // ctx.globalCompositeOperation = "overlay" + // ctx.globalCompositeOperation = "hue" + ctx.globalCompositeOperation = "lighter" // seems good + // ctx.globalCompositeOperation = "darker" // compatibility issues + // ctx.globalCompositeOperation = "lighten" + // ctx.globalCompositeOperation = "darken" // not uniform 1,2,3 + } + var temp = ctx.globalAlpha + var lastDist = Infinity + var scorange = ballot.rank.length + var winsAndLosses = false + if (winsAndLosses) { + + var mult = 2 * cmul + ctx.globalAlpha = Math.max(.002, 1 * mult / nVoters / scorange) // donuts get lighter towards the center + + + var nRanks = ballot.rank.length + + // list distances and fill colors + var aDist = [] + var aFill = [] + for (var cid of ballot.rank) { + c = model.candidatesById[cid] + aDist.push(distF(model,me,c)) + aFill.push(c.fill) + } + + // find mindpoints + var aMidpoints = [] + for (var i = 0; i < nRanks - 1; i++) { + aMidpoints.push( .5 * (aDist[i] + aDist[i+1]) ) + } + + // find boundaries of each zone + var aZoneStart = [] + var aZoneEnd = [] + for (var i = 0; i < nRanks; i++) { + if (i==0) { + aZoneStart.push(0) + } else { + aZoneStart.push(aMidpoints[i-1]) + } + if (i == nRanks) { + aZoneEnd.push(Infinity) + } else { + aZoneEnd.push(aMidpoints[i]) + } + } + + for (var i = 0; i < nRanks - 1; i++) { + // notice bottom candidate gets no zones + + // set fill + ctx.fillStyle = aFill[i] + + // make losing zone + ctx.beginPath() + ctx.arc(x*2, y*2, aZoneEnd[i]*2, 0, Math.TAU, false) + ctx.rect(0,0,ctx.canvas.width,ctx.canvas.height) + ctx.closePath() + + // draw other's losses + ctx.fill("evenodd") + var a = 4 + + + // make winning zone + ctx.beginPath() + ctx.arc(x*2, y*2, aZoneStart[i]*2, 0, Math.TAU, false) + ctx.arc(x*2, y*2, aZoneEnd[i]*2, 0, Math.TAU, false) + ctx.closePath() + + // draw wins + var nWins = nRanks - i - 1 + if (0) { + if (nWins > 0) { + ctx.fill("evenodd") + } + } else { + if (1) { + ctx.fillStyle = "#fff" + } + for (var k = 0; k < nWins; k++) { + ctx.fill("evenodd") + } + } + + + } + } else { + if (model.doVoterMapGPU) { + voterPerson.rad = [] + voterPerson.idxCan = [] + } + for(var i=ballot.rank.length-1; i>=0; i--){ + + // reverse order + + + // Line width + var lineWidth = ((ballot.rank.length-i)/ballot.rank.length)*8; + + // To which candidate... + var rank = ballot.rank[i]; + var c = model.candidatesById[rank]; + // var cc = model.arena.modelToArena(c) + + // dist = Math.sqrt((meArena.x - cc.x) ** 2 + (meArena.y - cc.y) ** 2 ) + if (model.rankedVizBoundary === "atMidpoint" || model.rankedVizBoundary === "atLoser") { + if (i <= ballot.rank.length-2) { + var nextRank = ballot.rank[i+1]; + var nextC = model.candidatesById[nextRank]; + var distCurrent = distF(model,me,c) + var distNext = distF(model,me,nextC) + if (model.rankedVizBoundary === "atMidpoint") { + var dist = .5 * (distCurrent + distNext) + } else { + var dist = distNext + } + } else { + continue + } + } else if (model.rankedVizBoundary === "beforeWinner") { + if (i >= 1) { + var previousRank = ballot.rank[i-1]; + var previousC = model.candidatesById[previousRank]; + var distPrevious = distF(model,me,previousC) + } else { + var distPrevious = 0 + } + var distCurrent = distF(model,me,c) + var dist = .5 * (distCurrent + distPrevious) + } else { // atWinner + var dist = distF(model,me,c) + } + + if (model.doVoterMapGPU) { + voterPerson.rad.push(dist) + if (doColors) { + voterPerson.idxCan.push(c.i) + } else { + voterPerson.idxCan.push(0) + } + continue + } + + ctx.beginPath(); + ctx.arc(x*2, y*2, dist*2, 0, Math.TAU, false); + + var invert = true // CAN CHANGE + var donuts = true + if (doColors) { + donuts = false + } + if (invert) { + if (donuts) { + if (lastDist == Infinity) { + ctx.rect(0,0,ctx.canvas.width,ctx.canvas.height) + } else { + ctx.arc(x*2, y*2, lastDist*2, 0, Math.TAU, false); + } + lastDist = dist // copy + } else { + ctx.rect(0,0,ctx.canvas.width,ctx.canvas.height) + } + ctx.closePath() + } + + if (doColors) { + ctx.fillStyle = c.fill + } else { + ctx.fillStyle = '#000' + } + ctx.strokeStyle = "#000"; + + // ctx.setLineDash([]); + + var doLines = false + if (doLines) { + ctx.globalAlpha = .01 + ctx.stroke() + } + if (doColors) { + var mult = 2 * cmul + } else { + var mult = 1 + } + if (donuts) { + ctx.globalAlpha = Math.max(.002, 1 * mult / nVoters * (i+1) / scorange) // donuts get lighter towards the center + } else { + ctx.globalAlpha = Math.max(.002, 1 * mult / nVoters / scorange) // donuts get lighter towards the center + } + if (invert) { + // .002 seems to be a limit + ctx.fill("evenodd") + } else { + ctx.fill() + } + + } + } + if (0) { + // final circle in the middle... hmm doesn't seem to make much difference. + if (doColors) { + ctx.fillStyle = 'white' + ctx.beginPath(); + ctx.arc(x*2, y*2, dist*2, 0, Math.TAU, false); + ctx.closePath() + ctx.fill() + } + } + ctx.globalCompositeOperation = tempComposite + ctx.globalAlpha = temp + } + if (model.pairwiseMinimaps == "auto" && _pickRankedDescription(model).doPairs) { + // customization + var lineWidth = 1 + var connectWidth = 1 + var sizeCircle = 15 + var connectCandidates = false + var minimap = true + var bimetalLine = (true && !minimap) || true + var regularLine = false + + var rankByCandidate = [] + var sortedCans = [] + for(var i=0; i<ballot.rank.length; i++){ + var rank = ballot.rank[i]; + var c = model.candidatesById[rank]; + sortedCans.push(c) + rankByCandidate[c.i] = i + } + for(var i = 0; i < sortedCans.length; i++) { + for (var k = 0; k < i; k++) { + var c1 = model.arena.modelToArena(sortedCans[i]) + c1.fill = sortedCans[i].fill + var c2 = model.arena.modelToArena(sortedCans[k]) + c2.fill = sortedCans[k].fill + var win = rankByCandidate[sortedCans[k].i] > rankByCandidate[sortedCans[i].i] + + if (connectCandidates) { + ctx.setLineDash([5, 45]); + ctx.beginPath(); + ctx.moveTo(c1.x*2,c1.y*2); + ctx.lineTo(c2.x*2,c2.y*2); + ctx.lineWidth = connectWidth; + ctx.strokeStyle = "#888"; + ctx.stroke(); + ctx.setLineDash([]); + } + // draw halfway line + var length = 10 + mid = {} + mid.x = (c1.x + c2.x)*.5 + mid.y = (c1.y + c2.y)*.5 + var mag = Math.sqrt((c1.y-c2.y) **2 + (c1.x-c2.x)**2) + unit = {} + unit.x = (c1.x - c2.x) / mag + unit.y = (c1.y - c2.y) / mag + var start = {} + start.x = mid.x + unit.y * length + start.y = mid.y - unit.x * length + var stop = {} + stop.x = mid.x - unit.y * length + stop.y = mid.y + unit.x * length + if (bimetalLine) { + ctx.beginPath(); + ctx.moveTo((start.x+lineWidth*unit.x)*2,(start.y+lineWidth*unit.y)*2); + ctx.lineTo((stop.x+lineWidth*unit.x)*2,(stop.y+lineWidth*unit.y)*2); + ctx.lineWidth = lineWidth*5; + ctx.strokeStyle = c1.fill; + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo((start.x-lineWidth*unit.x)*2,(start.y-lineWidth*unit.y)*2); + ctx.lineTo((stop.x-lineWidth*unit.x)*2,(stop.y-lineWidth*unit.y)*2); + ctx.lineWidth = lineWidth*5; + ctx.strokeStyle = c2.fill; + ctx.stroke(); + } + if (regularLine) { + ctx.beginPath(); + ctx.moveTo(start.x*2,start.y*2); + ctx.lineTo(stop.x*2,stop.y*2); + ctx.lineWidth = lineWidth; + ctx.strokeStyle = "#888"; + ctx.stroke(); + } + + if (minimap) { + if (win) { + var fill = c1.fill + } else { + var fill = c2.fill + } + var factor = 7 + var miniSize = 8 + var circle = {} + circle.x = (mid.x * factor + x) / (factor + 1) + circle.y = (mid.y * factor + y) / (factor + 1) + + var temp = ctx.globalAlpha + ctx.globalAlpha = temp * .8 + ctx.fillStyle = fill + ctx.moveTo(circle.x*2,circle.y*2); + ctx.beginPath(); + ctx.arc(circle.x*2, circle.y*2, miniSize, 0, Math.TAU, false); + // ctx.lineTo(circle.x*2,circle.y*2); + ctx.closePath(); + ctx.fill(); + ctx.lineWidth = lineWidth; + ctx.strokeStyle = "#888"; + ctx.stroke(); + ctx.globalAlpha = temp + } else { + if (win) { + var offset = 1 + } else { + var offset = -1 + } + var circle = {} + circle.x = mid.x + unit.x * offset * sizeCircle * .5 + circle.y = mid.y + unit.y * offset * sizeCircle * .5 + + var temp = ctx.globalAlpha + ctx.globalAlpha = temp * .3 + ctx.fillStyle = "#000"; + ctx.beginPath(); + ctx.moveTo(circle.x*2,circle.y*2); + ctx.arc(circle.x*2, circle.y*2, sizeCircle, 0, Math.TAU, false); + ctx.lineTo(circle.x*2,circle.y*2); + ctx.closePath(); + ctx.fill(); + ctx.globalAlpha = temp + } + } + } + } + +} + +DrawMap.Plurality = function (ctx, model,voterModel,voterPerson) { + var x = voterPerson.xArena + var y = voterPerson.yArena + var strategy = voterPerson.strategy + var iDistrict = voterPerson.iDistrict + var i = voterPerson.iPoint + var ballot = voterPerson.stages[model.stage].ballot + + + if (model.ballotConcept != "on" && ! model.arena.viewMan.active ) return + + var drawMapViewMan = (model.arena.viewMan.active && model.arena.viewMan.focus === voterPerson) + + // we only want to show the map for the viewMan if he is active. + if (model.onlyVoterMapViewMan) if (model.arena.viewMan.active && model.arena.viewMan.focus !== voterPerson) return + + var candidate = model.candidatesById[ballot.vote]; + + if (candidate === undefined) return // just in case + + // RETINA + x = x*2; + y = y*2; + var cc = model.arena.modelToArena(candidate) + var tx = cc.x*2; + var ty = cc.y*2; + + // DRAW - Line + ctx.beginPath(); + ctx.moveTo(x,y); + ctx.lineTo(tx,ty); + ctx.lineWidth = 8; + ctx.strokeStyle = "#888"; + + var nVoters = model.district[iDistrict].voterPeople.length + if (drawMapViewMan) nVoters = 1 + + // 1 with 1-10 voters + // .5 with 10 - 100 voters + let n = Math.log(nVoters+3) / Math.LN10 + .6 // 1 with 1 voter, 2 with more voters + ctx.globalAlpha = 1/n + ctx.stroke(); + + +} + +var DrawMe = {} + +DrawMe.Score = function (ctx, model,voterModel,voterPerson, scale, opt) { + + if (model.voterIcons == "top") { + _drawTopDefault(model, ctx, voterPerson) + return + } else if (model.voterIcons == "body") { + _drawBodyDefault(model, ctx, voterPerson, scale) + return + } + + var x = voterPerson.xArena + var y = voterPerson.yArena + var size = voterPerson.size + var ballot = voterPerson.stages[model.stage].ballot + var weight = voterPerson.weight + + + // There are #Candidates*5 slices + // Fill 'em in in order -- and the rest is gray. + var totalSlices = model.candidates.length*(voterModel.maxscore-voterModel.minscore); + var leftover = totalSlices; + var slices = []; + totalScore = 0; + + if (opt !== undefined && opt.onlyCandidate !== undefined) { + var c = model.candidates[opt.onlyCandidate]; + var cID = c.id; + var score = ballot.scores[cID] - voterModel.minscore; + leftover -= score; + if (model.allCan || score > 0) { + slices.push({ + num: score, + fill: c.fill + }); + } + totalScore += score + totalSlices = totalScore + } else { + for(var i=0; i<model.candidates.length; i++){ + var c = model.candidates[i]; + var cID = c.id; + if (ballot.scores[cID] == undefined) continue + var score = ballot.scores[cID] - voterModel.minscore; + leftover -= score; + if (model.allCan || score > 0) { + slices.push({ + num: score, + fill: c.fill + }); + } + totalScore += score + } + totalSlices = totalScore + } + // Leftover is gray + // slices.push({ + // num: leftover, + // fill: "#bbb" + // }); + // FILL 'EM IN + + + if (model.drawSliceMethod == "circleBunch") { + _drawCircleCollection(model, ctx, x, y, size, slices, totalSlices,voterModel.maxscore); + } else if (model.drawSliceMethod == "barChart") { + _drawVoterBarChart(model, ctx, x, y, size, slices, totalSlices,voterModel.maxscore); + } else { + if(totalScore==0){ + _drawBlank(model, ctx, x, y, size); + return; + } + _drawSlices(model, ctx, x, y, size, slices, totalSlices); + } + + + +} + +DrawMe.Three = function (ctx, model,voterModel,voterPerson, scale, opt) { + DrawMe.Score(ctx, model,voterModel,voterPerson, scale, opt) + +} + +DrawMe.Approval = function (ctx, model,voterModel,voterPerson, scale, opt) { + + if (model.voterIcons == "top") { + _drawTopDefault(model, ctx, voterPerson) + return + } else if (model.voterIcons == "body") { + _drawBodyDefault(model, ctx, voterPerson, scale) + return + } + + var x = voterPerson.xArena + var y = voterPerson.yArena + var size = voterPerson.size + var ballot = voterPerson.stages[model.stage].ballot + var weight = voterPerson.weight + + + var slices = []; + var numApproved = 0 + + // Draw 'em slices + if (opt !== undefined && opt.onlyCandidate !== undefined) { + var candidate = model.candidates[opt.onlyCandidate] + var approved = ballot.scores[candidate.id] + if (approved) { + slices.push({ num:1, fill:candidate.fill }); + numApproved ++ + } else { + slices.push({ num:0, fill:candidate.fill }); + } + } else if (model.allCan) { + for(var candidate of model.candidates) { + var approved = ballot.scores[candidate.id] + if (approved) { + slices.push({ num:1, fill:candidate.fill }); + numApproved ++ + } else { + slices.push({ num:0, fill:candidate.fill }); + } + } + } else { + for(var candidate of model.candidates) { + var approved = ballot.scores[candidate.id] + if (approved) { + slices.push({ num:1, fill:candidate.fill }); + numApproved ++ + } + } + } + + + if (model.drawSliceMethod == "circleBunch") { + _drawCircleCollection(model, ctx, x, y, size, slices, slices.length,voterModel.maxscore); + } else if (model.drawSliceMethod == "barChart") { + _drawVoterBarChart(model, ctx, x, y, size, slices, slices.length,voterModel.maxscore); + } else { + if(numApproved==0){ + _drawBlank(model, ctx, x, y, size); + return; + } + _drawSlices(model, ctx, x, y, size, slices, numApproved); + } + + + +} + +DrawMe.Ranked = function (ctx, model,voterModel,voterPerson, scale) { + + + var elimSystem = (model.system == "IRV" || model.system == "STV") + if (model.voterIcons == "top" && ! elimSystem) { + _drawTopDefault(model, ctx, voterPerson) + return + } else if (model.voterIcons == "body" && ! elimSystem) { + _drawBodyDefault(model, ctx, voterPerson, scale) + return + } + + var x = voterPerson.xArena + var y = voterPerson.yArena + var size = voterPerson.size + var ballot = voterPerson.stages[model.stage].ballot + var rank = ballot.rank + var weight = voterPerson.weight + var iDistrict = voterPerson.iDistrict + var iAll = voterPerson.iAll + + // change ballot to reflect the round + if (model.roundCurrent !== undefined) { + var round = model.roundCurrent[iDistrict] + if (round !== undefined && elimSystem && round > 0) { + var maxRound = model.result.continuing.length + if (round > maxRound) { + // show the final results + round = maxRound + + var showFinalWeightUsed = false // switch + if (showFinalWeightUsed && model.system == "STV") { + round = round + 1 + // weight in this round + var type1 = _type1Get(model) + if (type1) { + var idx = iAll + } else { + var idx = model.districtIndexOfVoter[iAll] + } + var lastIdx = model.result.history.rounds.length - 1 + var lastround = model.result.history.rounds[lastIdx] + var finalWeightUsed = lastround.beforeWeightUsed[idx] + lastround.weightUsed[idx] + + ctx.globalAlpha = Math.max(.05,finalWeightUsed) + } + + } else { + rank = _jcopy(rank.filter((x) => model.result.continuing[round-1].includes(x))) + + if (model.system == "STV") { + round = round + 1 + // weight in this round + var doAfterFinalRound = (round == -1) || (round == model.result.history.rounds.length + 1) // show the weight after the final round + var type1 = _type1Get(model) + if (type1) { + var idx = iAll + } else { + var idx = model.districtIndexOfVoter[iAll] + } + if (doAfterFinalRound) { + var lastIdx = model.result.history.rounds.length - 1 + var lastround = model.result.history.rounds[lastIdx] + var selectedRoundBeforeWeight = 1 - (lastround.beforeWeightUsed[idx] + lastround.weightUsed[idx]) + } else { + var lastIdx = round - 1 + var lastround = model.result.history.rounds[lastIdx] + var selectedRoundBeforeWeight = 1 - lastround.beforeWeightUsed[idx] + } + + ctx.globalAlpha = selectedRoundBeforeWeight + } + } + } + if (model.voterIcons == "top" || model.voterIcons == "body") { + rank = [rank[0]] + } + } + + if (typeof weight === 'undefined') weight = 1 + var slices = []; + var n = rank.length; + if (n==0) { + var totalSlices = 1 + slices.push({ num:1, fill:"#bbb" }) + } else if(n==2) { + var totalSlices = 1 + var rank0 = rank[0]; + var candidate = model.candidatesById[rank0]; + slices.push({ num:1, fill:candidate.fill }) + } else { + + var totalSlices = (n*(n+1))/2; // num of slices! + + var orderByCandidate = (model.drawSliceMethod == "barChart" && model.system == "Borda") + if (orderByCandidate) var slicesById = {} + + for(var i=0; i<rank.length; i++){ + var rank1 = rank[i]; + var candidate = model.candidatesById[rank1]; + var slice = { num:(n-i), fill:candidate.fill } + slices.push(slice); + if (orderByCandidate) slicesById[candidate.id] = slice + } + + if (orderByCandidate) { + for(var [i,c] of Object.entries(model.district[iDistrict].stages[model.stage].candidates)){ + slices[i] = slicesById[c.id] + } + } + } + if (model.drawSliceMethod == "barChart") { + if (model.system == "Borda") { + _drawVoterBarChart(model, ctx, x, y, size, slices, totalSlices, slices.length); + } else if (model.system == "IRV" || model.system == "STV") { + if (model.voterIcons == "body") { + var colorfill = model.candidatesById[rank[0]].fill + var headColor = voterPerson.skinColor + _drawSpeckMan1(colorfill, headColor, scale, ctx.globalAlpha, x, y, ctx) + + } else { + if (model.squareFirstChoice) { + _drawIRVStack(model, ctx, x, y, size, slices, totalSlices * 1/Math.max(weight,.000001)); + } else { + _drawRankList(model, ctx, x, y, size, slices, totalSlices * 1/Math.max(weight,.000001)); + } + } + } else { + if (model.pairOrderByCandidate) { + if (n==2) { + ballot_sub = {rank: [rank[0]]} + _drawPairTableByCandidate(model, ctx, x, y, size, ballot_sub, weight) + } else { + _drawPairTableByCandidate(model, ctx, x, y, size, ballot, weight) + } + } else { // order by rank + _drawRankList(model, ctx, x, y, size, slices, totalSlices * 1/Math.max(weight,.000001)); + } + } + } else { + if (0) { + _drawSlices(model, ctx, x, y, size * Math.sqrt(weight), slices, totalSlices); + } else { + _drawSlices(model, ctx, x, y, size, slices, totalSlices * 1/Math.max(weight,.000001)); + } + } + + ctx.globalAlpha = 1 + +} + +DrawMe.Plurality = function (ctx, model,voterModel,voterPerson, scale) { + + if (model.voterIcons == "top") { + _drawTopDefault(model, ctx, voterPerson) + return + } else if (model.voterIcons == "body") { + _drawBodyDefault(model, ctx, voterPerson, scale) + return + } + + var x = voterPerson.xArena + var y = voterPerson.yArena + var size = voterPerson.size + var ballot = voterPerson.stages[model.stage].ballot + + + + // RETINA + x = x*2; + y = y*2; + + // What fill? + if (ballot.vote == null) { + var fill = '#bbb' + // return + } else { + var fill = model.candidatesById[ballot.vote].fill; + } + ctx.fillStyle = fill; + ctx.strokeStyle = 'rgb(0,0,0)'; + ctx.lineWidth = 1; // border + + // Just draw a circle. + ctx.beginPath(); + ctx.arc(x, y, size, 0, Math.TAU, true); + ctx.fill(); + if (model.checkDrawCircle()) {ctx.stroke();} + + + ctx.globalAlpha = 1 +} + +function _drawTopDefault(model, ctx, voterPerson) { + var x = voterPerson.xArena + var y = voterPerson.yArena + var circlesize = voterPerson.size + var iDistrict = voterPerson.iDistrict + var c = _findClosestCan(x,y,iDistrict,model) + _drawCircleFill(x,y,circlesize,c.fill,ctx,model) + +} + +function _drawBodyDefault(model, ctx, voterPerson, scale) { + var x = voterPerson.xArena + var y = voterPerson.yArena + var iDistrict = voterPerson.iDistrict + var c = _findClosestCan(x,y,iDistrict,model) + var headColor = voterPerson.skinColor + _drawSpeckMan1(c.fill, headColor, scale, ctx.globalAlpha, x, y, ctx) +} + +function _drawPairTableByCandidate(model, ctx, x, y, size, ballot, weight) { + x = x * 2 + y = y * 2 + size = size * 2 + // background stroke + _centeredRectStroke(ctx,x,y,size,size) + + var districtCandidateIDs = [] + for (var c of model.candidates) { + if (ballot.rank.includes(c.id)) districtCandidateIDs.push(c.id) + } + + var n = ballot.rank.length + pairtable = {} + for (var i=0; i < n; i++) { + var cid = ballot.rank[i] + pairtable[cid] = {} + pairtable[cid][cid] = cid + } + for (var i = 0; i < n; i++) { + var cidWin = ballot.rank[i] + for (var k = i + 1; k < n; k++) { + var cidLose = ballot.rank[k] + pairtable[cidWin][cidLose] = cidWin + pairtable[cidLose][cidWin] = cidWin + } + } + // now draw table + size = size * Math.sqrt(weight) + var sizeSquare = size / n + var yaxis = _lineVertical(n,size) + var xaxis = _lineHorizontal(n,size) + for (var i = 0; i < n; i++) { + for (var k = 0; k < n; k++) { + icid = districtCandidateIDs[i] + kcid = districtCandidateIDs[k] + var cid = pairtable[icid][kcid] + var fill = model.candidatesById[cid].fill + var xp = x + xaxis[i][0] + var yp = y + yaxis[k][1] + _centeredRect(ctx,xp,yp,sizeSquare,sizeSquare,fill) + } + } + + + +} + +function _drawIRVStack(model, ctx, x, y, size, slices, totalSlices) { + + + x = x * 2 + y = y * 2 + size = size * 2 + var maxscore = slices.length + var extraspace = .5 // how much extra space the stack at the bottom should use. - as a fraction. + + let noLastRank = true + if (noLastRank && slices.length !== 1) { + slices.pop() + maxscore -- + } + + // special case looks weird + if (maxscore == 2) { + extraspace = .25 + } + if (maxscore == 1) { + extraspace = 0 + } + + // draw top slice + _centeredRect(ctx,x,y,size,size,slices[0].fill) + slices.shift() // remove top slice + + var yaxis = _lineVertical( slices.length, size * extraspace ) + + var sizeSquare = size * extraspace / (maxscore-1) + for(var i in slices){ + var point = yaxis[i] + var slice = slices[i] + var xp = x + var yp = y + .5 * (1+extraspace) * size + point[1] + _centeredRect(ctx,xp,yp,size,sizeSquare,slice.fill) + } + // _centeredRectStroke(ctx,x,y*1.25,size,size*1.5,'#888') + // _centeredRectStroke(ctx,x,y,size,size,'#888') + + //draw outline + _centeredRectStroke(ctx,x,y+size*(.5*extraspace),size,size*(1+extraspace)) + _centeredRectStroke(ctx,x,y,size,size) + +} + +function _drawRankList(model, ctx, x, y, size, slices, totalSlices) { + x = x * 2 + y = y * 2 + size = size * 2 + var maxscore = model.candidates.length + + if (model.system == "IRV") { // not used anymore + var extra = slices.length / 3 + for (var i = 0; i < extra; i++) slices.unshift(slices[0]) + } + + if (model.allCan) { + var yaxis = _lineVertical(slices.length, size) // points of main spiral + } else { + var yaxis = _lineVertical(model.candidates.length, size) // points of main spiral + } + var sizeSquare = size / maxscore + var subRects = false + _centeredRectStroke(ctx,x,y,size,size,'#888') + for(var i in slices){ + var point = yaxis[i] + var slice = slices[i] + if (subRects) { + // sub collection + var xaxis = _lineHorizontal(slice.num, size) // points of yaxis + for (var subpoint of xaxis) { + var xp = x + point[0] + subpoint[0] + var yp = y + point[1] + subpoint[1] + _centeredRect(ctx, xp, yp, sizeSquare,sizeSquare, slice.fill) + } + } else { + var xp = x + point[0] + var yp = y + point[1] + // _drawRing(ctx,xp/2,yp/2,subsize) + // _centeredRectStroke(ctx,xp,yp,sizeSquare * slice.num,sizeSquare) + if (model.system == "IRV") { // not used anymore + _centeredRect(ctx,xp,yp,size,sizeSquare,slice.fill) + } else { + + xp = x + point[1] + yp = y + .5 * size + var sx = sizeSquare + var sy = sizeSquare * slice.num + + _bottomRect(ctx,xp,yp,sx,sy,slice.fill) + xp = x + .5 * size + yp = y + point[1] + var sx = sizeSquare * slice.num + var sy = sizeSquare + _rightRect(ctx,xp,yp,sx,sy,slice.fill) + } + // _drawSlices(model, ctx, xp/2, yp/2, subsize, [slice], maxscore) + } + } + +} + +function _drawVoterBarChart(model, ctx, x, y, size, slices, totalSlices, maxscore) { + x = x * 2 + y = y * 2 + size = size * 2 + if (model.allCan) { + + var xaxis = _lineHorizontal(slices.length, size) // points of main spiral + } else { + var xaxis = _lineHorizontal(slices.length, size) // points of main spiral + } + var sizex = size / slices.length + var sizey = size / maxscore + var subRects = false + _centeredRectStroke(ctx,x,y,size,size) + for(var i in slices){ + var point = xaxis[i] + var slice = slices[i] + if (subRects) { + // sub collection + var yaxis = _lineVertical(slice.num, size) // points of yaxis + for (var subpoint of yaxis) { + xp = x + point[0] + subpoint[0] + yp = y + point[1] + subpoint[1] + _centeredRect(ctx, xp, yp, sizex,sizey, slice.fill) + } + } else { + xp = x + point[0] + yp = y + point[1] + // _drawRing(ctx,xp/2,yp/2,subsize) + _centeredRectStroke(ctx,xp,yp,sizex,sizey * slice.num) + _centeredRect(ctx,xp,yp,sizex,sizey * slice.num,slice.fill) + // _drawSlices(model, ctx, xp/2, yp/2, subsize, [slice], maxscore) + } + } +} + +function _lineHorizontal(num,size) { + var points = []; + var step = size / num + for (var count = 0; count < num; count++) { + var x = (count+.5) * step - .5 * size + points.push([x,0]) + } + return points +} + +function _lineVertical(num,size) { + var points = []; + var step = size / num + for (var count = 0; count < num; count++) { + var y = (count+.5) * step - .5 * size + points.push([0,y]) + } + return points +} + +function _centeredRect(ctx, x, y, sizex, sizey, fill) { + ctx.fillStyle = fill; + ctx.beginPath() + ctx.rect(x - sizex * .5, y - sizey * .5, sizex, sizey) + ctx.closePath() + ctx.fill(); +} + +function _bottomRect(ctx, x, y, sizex, sizey, fill) { + ctx.fillStyle = fill; + ctx.beginPath() + ctx.rect(x - sizex * .5, y - sizey, sizex, sizey) + ctx.closePath() + ctx.fill(); +} + +function _rightRect(ctx, x, y, sizex, sizey, fill) { + ctx.fillStyle = fill; + ctx.beginPath() + ctx.rect(x - sizex, y - sizey * .5, sizex, sizey) + ctx.closePath() + ctx.fill(); +} + +function _centeredRectStroke(ctx, x, y, sizex, sizey, color) { + color = color || 'black' + ctx.strokeStyle = color; + ctx.lineWidth = 1; + ctx.beginPath() + ctx.rect(x - sizex * .5, y - sizey * .5, sizex, sizey) + ctx.closePath() + ctx.stroke(); +} + +function _drawCircleCollection(model, ctx, x, y, size, slices, totalSlices, maxscore) { + x = x * 2 + y = y * 2 + + if (model.allCan) { + var mainspiral = _spiral(slices.length, size) // points of main spiral + } else { + var mainspiral = _spiral(model.candidates.length, size) // points of main spiral + } + var subsize = size / Math.sqrt(model.candidates.length) // sub spiral is per candidate + var subSpirals = false + if (subSpirals) { + var pointsize = subsize / Math.sqrt(maxscore) // each score gets a point + subsize -= pointsize + } + for(var i in slices){ + var point = mainspiral[i] + var slice = slices[i] + if (subSpirals) { + // sub collection + var subspiral = _spiral(slice.num, subsize) // points of subspiral + for (var subpoint of subspiral) { + xp = x + point[0] + subpoint[0] + yp = y + point[1] + subpoint[1] + _simpleCircle(ctx, xp, yp, pointsize, slice.fill) + } + } else { + xp = x + point[0] + yp = y + point[1] + _drawRing(ctx,xp/2,yp/2,subsize) + _simpleCircle(ctx,xp,yp,subsize,'#ccc') + _drawSlices(model, ctx, xp/2, yp/2, subsize, [slice], maxscore) + } + } +} + +function _simpleCircle(ctx, x, y, size, fill) { + ctx.fillStyle = fill; + ctx.beginPath() + ctx.arc(x, y, size, 0, Math.TAU, false); + ctx.closePath() + ctx.fill(); +} + +function _spiral(num,size) { + var points = []; + var angle = 0; + var _radius = 0; + var _radius_norm = 0; + var _spread_factor = size * 1 + var theta = Math.TAU * .5 * (3 - Math.sqrt(5)) + var center = {x:0,y:0} + for (var count = 0; count < num; count++) { + angle = theta * count + _radius_norm = Math.sqrt((count+.5)/num) + _radius = _radius_norm * _spread_factor + + var x = Math.cos(angle)*_radius + 150 ; + var y = Math.sin(angle)*_radius + 150 ; + points.push([x,y]); + center.x += x + center.y += y + } + center.x /= num + center.y /= num + for (var point of points) { + point[0] -= center.x + point[1] -= center.y + } + return points +} + +function _drawSlices(model, ctx, x, y, size, slices, totalSlices){ + + // RETINA + x = x*2; + y = y*2; + //size = size*2; + + // GO AROUND THE CLOCK... + var startingAngle = -Math.TAU/4; + var endingAngle = 0; + for(var i=0; i<slices.length; i++){ + + slice = slices[i]; + + // Angle! + var sliceAngle = slice.num * (Math.TAU/totalSlices); + endingAngle = startingAngle+sliceAngle; + + // Just draw an arc, clockwise. + ctx.fillStyle = slice.fill; + ctx.beginPath(); + ctx.moveTo(x,y); + ctx.arc(x, y, size, startingAngle, endingAngle, false); + ctx.lineTo(x,y); + ctx.closePath(); + ctx.fill(); + + // For next time... + startingAngle = endingAngle; + + } + + if (model.checkDrawCircle()) { + // Just draw a circle. + _drawRing(ctx,x/2,y/2,size) + } + +}; + +function _drawRing(ctx, x, y, size) { + + // RETINA + x = x*2; + y = y*2; + // Just draw a circle. + ctx.strokeStyle = 'rgb(0,0,0)'; + ctx.lineWidth = 1; // border + ctx.beginPath(); + ctx.arc(x, y, size, 0, Math.TAU, true); + ctx.closePath(); + ctx.stroke(); +} + +function _drawThickRing(ctx, x, y, size) { + + // RETINA + x = x*2; + y = y*2; + // Just draw a circle. + ctx.strokeStyle = 'rgb(150,150,150,.7)'; + ctx.lineWidth = 3; // border + ctx.beginPath(); + ctx.arc(x, y, size, 0, Math.TAU, true); + ctx.moveTo(x+size-8,y) + ctx.arc(x, y, size-8, 0, Math.TAU, true); + // ctx.arc(x, y, size, 0, 0, true); + ctx.closePath(); + ctx.fillStyle = 'rgb(255,255,255,.7)'; + ctx.fill('evenodd') + ctx.stroke(); +} + +function drawArrows(ctx, x, y, size) { + + + ctx.fillStyle = 'rgb(255,255,255,.5)'; + ctx.strokeStyle = 'rgb(0,0,0,.5)'; + ctx.lineWidth = 1 + + size *=4 + + ctx.translate(Math.round(x*2 - size/2), Math.round(y * 2 - size/2)); + var scale = size / 96 + ctx.scale(scale,scale) + + ctx.beginPath(); + ctx.moveTo(73, 48.40); + ctx.lineTo(62.60, 38.80); + ctx.lineTo(62.60, 43.60); + ctx.lineTo(52.40, 43.60); + ctx.lineTo(52.40, 33.40); + ctx.lineTo(57.20, 33.40); + ctx.lineTo(47.60, 23); + ctx.lineTo(38.70, 33.40); + ctx.lineTo(43.50, 33.40); + ctx.lineTo(43.50, 43.60); + ctx.lineTo(33.40, 43.60); + ctx.lineTo(33.40, 38.80); + ctx.lineTo(23, 48.40); + ctx.lineTo(33.40, 57.30); + ctx.lineTo(33.40, 52.50); + ctx.lineTo(43.60, 52.50); + ctx.lineTo(43.60, 62.70); + ctx.lineTo(38.80, 62.70); + ctx.lineTo(47.60, 73); + ctx.lineTo(57.20, 62.60); + ctx.lineTo(52.40, 62.60); + ctx.lineTo(52.40, 52.40); + ctx.lineTo(62.60, 52.40); + ctx.lineTo(62.60, 57.20); + ctx.lineTo(73, 48.40); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + + ctx.setTransform(1, 0, 0, 1, 0, 0); +} + +function _drawBlank(model, ctx, x, y, size){ + var slices = [{ num:1, fill:"#bbb" }]; + _drawSlices(model, ctx, x, y, size, slices, 1); +}; + +var DrawBallot = {} + +DrawBallot.Score = function (model,voterModel,voterPerson) { + var ballot = voterPerson.stages[model.stage].ballot + var district = model.district[voterPerson.iDistrict] + var cans = district.stages[model.stage].candidates + + var text = "" + var scoreByCandidate = [] + for(var i = 0; i < cans.length; i++) { + scoreByCandidate[i] = ballot.scores[cans[i].id] + } + + var rTitle = ` + Give EACH candidate a score<br> + <em><span class="small">from 0 (hate 'em) to 5 (love 'em)</span></em> + ` + text += htmlBallot(model,rTitle,scoreByCandidate,cans) + return text + + +} + +DrawBallot.Three = function (model,voterModel,voterPerson) { + + var text = DrawBallot.Score(model,voterModel,voterPerson) + return text.replace("5 (love 'em)","2 (love 'em)") + +} + +DrawBallot.Approval = function (model,voterModel,voterPerson) { + var ballot = voterPerson.stages[model.stage].ballot + var district = model.district[voterPerson.iDistrict] + var cans = district.stages[model.stage].candidates + + var spotsById = [] + for ( var i = 0; i < cans.length; i++) { + var cid = cans[i].id + spotsById[cid] = i + } + + + var approvedByCandidate = [] + for(var i = 0; i < cans.length; i++) { + approvedByCandidate.push("⠀") + } + for(var candidate of model.candidates) { + var approved = ballot.scores[candidate.id] + if (approved) { + var spot = spotsById[candidate.id] + approvedByCandidate[spot] = "✔" + } + } + + var text = "" + var rTitle = ` + Who do you approve of?<br> + <em><span class="small">(pick as MANY as you like)</span></em> + ` + text += htmlBallot(model,rTitle,approvedByCandidate,cans) + return text + + +} + +DrawBallot.Ranked = function (model,voterModel,voterPerson) { + var ballot = voterPerson.stages[model.stage].ballot + var district = model.district[voterPerson.iDistrict] + var cans = district.stages[model.stage].candidates + + var text = "" + + var spotsById = [] + for ( var i = 0; i < cans.length; i++) { + var cid = cans[i].id + spotsById[cid] = i + } + + var rankByCandidate = [] + for(var i=0; i<ballot.rank.length; i++){ + var rank = ballot.rank[i]; + var spot = spotsById[rank] + rankByCandidate[spot] = i + 1 + } + + var rTitle = ` + Rank in order of your choice:<br> + <em><span class="small">(1=1st choice, 2=2nd choice, etc...)</span></em> + ` + text += htmlBallot(model,rTitle,rankByCandidate,cans) + return text + +} + +DrawBallot.Plurality = function (model,voterModel,voterPerson) { + var ballot = voterPerson.stages[model.stage].ballot + var district = model.district[voterPerson.iDistrict] + var cans = district.stages[model.stage].candidates + + if (model.stage == "primary") { + cans = district.parties[voterPerson.iParty].candidates + } + + var text = "" + var onePickByCandidate = [] + for(var i = 0; i < cans.length; i++) { + var cid = cans[i].id + if (cid == ballot.vote) { + onePickByCandidate.push("✔") + } else { + onePickByCandidate.push("⠀") + } + } + + var rTitle = ` + Who's your favorite candidate?<br> + <em><span class="small">(pick ONLY one)</span></em> + ` + text += htmlBallot(model,rTitle,onePickByCandidate,cans) + return text + +} + +var DrawTally = {} + +DrawTally.Score = function (model,voterModel,voterPerson) { + var ballot = voterPerson.stages[model.stage].ballot + var district = model.district[voterPerson.iDistrict] + var cans = district.stages[model.stage].candidates + + var system = model.system + + // todo: star preferences + var text = "" + + if(cans.length == 0) return text + + if (voterModel.say) text += "<span class='small' style> Vote: </span> <br />" + cIDs = Object.keys(ballot.scores).sort(function(a,b){return -(ballot.scores[a]-ballot.scores[b])}) // sort descending + + if (0){ + for(var i=0; i < cIDs.length; i++){ + cID = cIDs[i] + var score = ballot.scores[cID] + text += model.icon(cID) + ":" + score + text += "<br />" + } + } + if (0){ + for(var i=0; i < cIDs.length; i++){ + cID = cIDs[i] + var score = ballot.scores[cID] + for (var j=0; j < score; j++) { + text += model.icon(cID) + " " + } + text += "<br />" + } + } + if (1) { + var distList = voterPerson.distList + text += tBarChart("score",distList,model,{differentDisplay: true}) + text += `<br>` + + } + if (system == "STAR") { + + text += "<span class='small'>" + text += " Pair Preferences: <br />" + text += "<pre>" + for(var i=1; i<cIDs.length; i++){ + text += "" + for(var j=0; j<i; j++){ + if (j>0) text += " " + text += model.icon(cIDs[j]) + if (ballot.scores[cIDs[j]] > ballot.scores[cIDs[i]]) { + text += ">" + } else text += "=" + text += model.icon(cIDs[i]) + } + // 01 + // 02 12 + // 03 13 23 + text += "</span>" + text += "<br />" + text += "<br />" + } + text += "</pre>" + text += pairChart([ballot], district, model) + text += squarePairChart([ballot], district, model) + } + return text + + +} + +DrawTally.Three = function (model,voterModel,voterPerson) { + var ballot = voterPerson.stages[model.stage].ballot + + var text = "" + cIDs = Object.keys(ballot.scores).sort(function(a,b){return -(ballot.scores[a]-ballot.scores[b])}) // sort descending + if (0){ + if (voterModel.say) text += "<span class='small' style> Vote: </span> <br />" + for(var i in cIDs){ + cID = cIDs[i] + var score = ballot.scores[cID] + text += model.icon(cID) + ":" + score + text += "<br />" + } + } + if (1) { + var distList = voterPerson.distList + text += tBarChart("score",distList,model,{differentDisplay: true}) + text += `<br>` + + } + groups = [[],[],[]] + for (cID in ballot.scores) { + var score = ballot.scores[cID] + groups[score].push(cID) + } + text += "<pre><span class='small' style> Good:</span>" + var good = groups[2] + for (i in good) { + text += model.icon(good[i]) + } + text += "<br />" + text += "<br />" + text += "<span class='small' style>Not Bad:</span>" + for (i in good) { + text += model.icon(good[i]) + } + var okay = groups[1] + for (i in okay) { + text += model.icon(okay[i]) + } + text += "</pre>" + text += "<br>" + if(0) { + text += "<br /> preferences:<br />" + for(var i = 2; i > -1; i--){ + if (i<2) text += ">" + for(j in groups[i]){ + text += model.icon(groups[i][j]) + } + } + } + + text += "<span class='small'>" + text += " Pair Preferences: <br />" + text += "<pre>" + for(var i=1; i<cIDs.length; i++){ + text += "" + for(var j=0; j<i; j++){ + if (j>0) text += " " + text += model.icon(cIDs[j]) + if (ballot.scores[cIDs[j]] > ballot.scores[cIDs[i]]) { + text += ">" + } else text += "=" + text += model.icon(cIDs[i]) + } + // 01 + // 02 12 + // 03 13 23 + text += "</span>" + text += "<br />" + text += "<br />" + } + text += "</pre>" + return text + + +} + +DrawTally.Approval = function (model,voterModel,voterPerson) { + var ballot = voterPerson.stages[model.stage].ballot + + var text = "" + if (voterModel.say) text += "<span class='small' style> Approved </span> <br />" + + if (0) { + for(var candidate of model.candidates) { + var approved = ballot.scores[candidate.id] + if (approved) { + text += model.icon(candidate) + text += "<br />" + } + } + } + if (1) { + var distList = voterPerson.distList + text += tBarChart("score",distList,model,{differentDisplay: true}) + text += `</span><br>` + + } + return text + + +} + +DrawTally.Ranked = function (model,voterModel,voterPerson) { + var voterAtStage = voterPerson.stages[model.stage] + var ballot = voterAtStage.ballot + var district = model.district[voterPerson.iDistrict] + + var system = model.system + var rbsystem = model.rbsystem + // todo: star preferences + var text = "" + var eventsToAssign = [] + + var pick = _pickRankedDescription(model) + + if(system=="RBVote" && rbsystem=="Bucklin") { + // put a 1 in each ranking + text += "<pre>" + // text += " 1 2 3 4 5" + text += " " + for(var i=0; i<ballot.rank.length; i++) { + text += " " + (i+1) + } + text += "<br />" + text += "<br />" + for(var i=0; i<ballot.rank.length; i++) { + text += model.icon(ballot.rank[i]) + text += " " + for(var j=0; j<ballot.rank.length; j++) { + if (j>0) text += " " + if (i==j) { + text += "1" + } else { + text += "0" + } + } + text += "<br />" + } + text += "</pre>" + } + + if (pick.doChain || pick.doPairs) { + text += "<span class='small' style> Preferences: </span> <br />" + for(var i=0; i<ballot.rank.length; i++){ + if (i>0) text += " > " + var candidate = ballot.rank[i]; + text += model.icon(candidate) + } + text += "<br />" + text += "<br />" + } + + if (pick.message != "") { + text += "<span class='small' style>" + text += pick.message + text += "</span>" + text += "<br />" + text += "<br />" + } + + if (pick.doPairs) { + if(0){ + text += "<span class='small'>" + // text += "Pair Preferences: <br />" + for(var i=1; i<ballot.rank.length; i++){ + text += "<span style='float:left'>" + for(var j=0; j<ballot.rank.length-i; j++){ + if (j>0) text += "    " + text += model.icon(ballot.rank[j]) + ">" + text += model.icon(ballot.rank[j+i]) + } + // 01 12 + // 02 13 + text += "</span>" + text += "<br />" + } + text += "</span>" + } + if (0) { + text += "<span class='small'> Pair Preferences: <br /><pre>" + for(var i=1; i<ballot.rank.length; i++){ + text += "<span style='float:left'>" + for(var j=1; j<i; j++){ + text += " " + } + for(var j=0; j<ballot.rank.length-i; j++){ + if (j>0) text += " " + text += model.icon(ballot.rank[j]) + ">" + text += model.icon(ballot.rank[j+i]) + } + // 01 12 + // 02 13 + text += "</span>" + text += "<br />" + text += "<br />" + } + text += "</pre></span>" + } + if(1){ + + text += "<span class='small'>" + // text += " Pair Preferences: <br />" + text += "<pre>" + for(var i=1; i<ballot.rank.length; i++){ + text += "" + let iid = ballot.rank[i] + for(var j=0; j<i; j++){ + let jid = ballot.rank[j] + + let eventID = 'tallypair_' + iid + '_' + jid + '_' + _rand5() + let e = { + eventID: eventID, + f: pairDraw(model,district,jid,iid,false) + } + eventsToAssign.push(e) + + text += '<span id="' + eventID + '">' + + if (j>0) text += " " + text += model.icon(jid) + ">" + text += model.icon(iid) + + text += "</span>" + } + // 01 + // 02 12 + // 03 13 23 + text += "<br />" + text += "<br />" + } + text += "</span>" + text += "</pre>" + } + if(1) { + text += pairChart([ballot], district, model) + text += squarePairChart([ballot], district, model) + } + } + if (pick.doPoints) { + if (voterModel.say) text += "<span class='small' style> Points: </span><br />" + if (1) { + text += tBarChart("score", voterPerson.distList ,model,{differentDisplay:true}) + } else { + var numCandidates = ballot.rank.length + for(var i=0; i<ballot.rank.length; i++){ + var candidate = ballot.rank[i]; + var score = numCandidates - i + for (var j=0; j < score; j++) { + text += model.icon(candidate) + } + text += "<br />" + } + } + } + + model.tallyEventsToAssign = eventsToAssign + return text + } -function ApprovalVoter(model){ +function _pickRankedDescription(model) { + + // var onlyPoints = ["Borda"] + // var onlyPointsRB = ["Baldwin","Borda"] + // var noPreferenceChainRB = ["Black"] + // var onlyPreferenceChain = ["IRV","STV"] + // var onlyPreferenceChainRB = ["Bucklin","Carey","Coombs","Hare"] + // var onlyPairsRB = ["Copeland","Dodgson",] + regular = { + "IRV": {doChain:true , doPairs:false, doPoints:false, message:"Only tally the top choice during elimination rounds."}, + "Borda": {doChain:false, doPairs:false, doPoints:true , message:""}, + "Minimax": {doChain:false, doPairs:true , doPoints:false, message:"These preferences are tallied by pairs."}, + "Schulze": {doChain:false, doPairs:true , doPoints:false, message:"These preferences are tallied by pairs."}, + "RankedPair": {doChain:false, doPairs:true , doPoints:false, message:"These preferences are tallied by pairs."}, + "Condorcet": {doChain:false, doPairs:true , doPoints:false, message:"These preferences are tallied by pairs."}, + "STV": {doChain:true , doPairs:false, doPoints:false, message:"Only tally the top choice during elimination rounds."} + } + + rb = { + "Baldwin": {doChain:true , doPairs:false, doPoints:true , message:"Points are assigned in each elimination round. In round 1, they are as follows:"}, + "Black": {doChain:true , doPairs:true , doPoints:true , message:"These preferences are tallied by pairs. <br /><br /> If there is no Condorcet winner, then borda points are used."}, + "Borda": {doChain:false, doPairs:false, doPoints:true , message:""}, + "Bucklin": {doChain:false, doPairs:false, doPoints:false, message:"Round 1: only count 1's. <br /> Round 2: include 1's and 2's. <br /> Keep including more until approval gets above 50%."}, + "Carey": {doChain:true , doPairs:false, doPoints:false, message:"Only tally the top choice during elimination rounds."}, + "Coombs": {doChain:true , doPairs:false, doPoints:false, message:"Only tally the bottom choice during elimination rounds."}, + "Copeland": {doChain:false, doPairs:true , doPoints:false, message:"These preferences are tallied by pairs."}, + "Dodgson": {doChain:false, doPairs:true , doPoints:false, message:"These preferences are tallied by pairs."}, + "Hare": {doChain:true , doPairs:false, doPoints:false, message:"Only tally the top choice during elimination rounds."}, + "Nanson": {doChain:true , doPairs:false, doPoints:true , message:"Points are assigned in each elimination round. In round 1, they are as follows:"}, + "Raynaud": {doChain:false, doPairs:true , doPoints:false, message:"These preferences are tallied by pairs."}, + "Schulze": {doChain:false, doPairs:true , doPoints:false, message:"These preferences are tallied by pairs."}, + "Simpson": {doChain:false, doPairs:true , doPoints:false, message:"These preferences are tallied by pairs."}, + "Small": {doChain:false, doPairs:true , doPoints:false, message:"These preferences are tallied by pairs."}, + "Tideman": {doChain:false, doPairs:true , doPoints:false, message:"These preferences are tallied by pairs."} + } + if (model.system=="RBVote") { + var pick = rb[model.rbsystem] + } else { + var pick = regular[model.system] + } + if (pick == undefined) { + pick = { + doChain: false, + doPairs: false, + doPoints: false, + message: "Not yet implemented." + } + } + return pick +} - var self = this; - self.model = model; +DrawTally.Plurality = function (model,voterModel,voterPerson) { + var ballot = voterPerson.stages[model.stage].ballot + + var text = "" + if (voterModel.say) text += "<span class='small' style> One vote for </span> " + if (ballot.vote) text += model.icon(ballot.vote) + return text +} + +function GeneralVoterModel(model,voterModel) { + voterModel.say = false + voterModel.toTextV = function(voterPerson) { + if (0) { + return `<div id="paper">` + voterModel.drawBallot(voterPerson) + "</div>" + voterModel.textTally(voterPerson) + } else { + return voterModel.toText(voterPerson,"V") + } + + } + voterModel.toTextH = function(voterPerson) { + return voterModel.toText(voterPerson,"H") + } + voterModel.toText = function(voterPerson,direction) { + + + // setup // + + var makeIcons = x => x ? x.map(a => model.icon(a)) : "" + var makeIconsCan = x => x ? x.map(a => model.icon(a.id)) : "" + + // voters + var voterAtStage = voterPerson.stages[model.stage] + + // candidates + var cans = model.district[voterPerson.iDistrict].stages[model.stage].candidates + if (model.stage == "primary") { + var district = model.district[voterPerson.iDistrict] + cans = district.parties[voterPerson.iParty].candidates + } + + // distances + var distList = makeDistList(model,voterPerson,voterAtStage,cans) + voterPerson.distList = distList // just pass it along.. maybe do this part better + + var tableHead = ` + <table class="main2" border="1"> + <tbody> + <tr> + <td class="tallyText"> + ` + var tableFoot = ` + </td> + </tr> + </tbody> + </table> + ` + // writing // + + var tablewrap = false + var part1 = voterModel.drawBallot(voterPerson) + + + var part2 = ` + <table class="main2" border="1"> + <tbody> + <tr> + <td class="tallyText"> + <span class="small"> + This is how your vote counts: + </span> + <br> <br> + #2 + </td> + </tr> + </tbody> + </table>`.replace("#2",voterModel.drawTally(voterPerson)) + + var text3 = ` + Why did you vote this way? <br> + <br> + ` + + text3 += ` + Your strategy was: <br> + <b>${voterPerson.realNameStrategy}</b> <br> + <br> + ` + + var didStarStrategy = model.system == "STAR" && voterPerson.strategy != "zero strategy. judge on an absolute scale." + var doNormalize = voterPerson.strategy == "normalize" + if (didStarStrategy) { + text3 += ` + And because we're using <b>STAR</b>, you tried to distinguish between ${ (doNormalize) ? "candidates" : "frontrunners" } for the final round ${ (doNormalize) ? "" : "and then fill in everybody else in between those scores" }. <br> + <br> + ` + } + + var consideredElectability = model.stage == "primary" && model.doElectabilityPolls + if (consideredElectability) { + text3 += ` + You also considered <b>electability</b>. <br> + <br> + ` + } + + + + // if (model.ballotType == "Score" || model.ballotType == "Approval") { + // text3 += ` + // You gave the following scores: <br> + // ` + // text3 += tBarChart("score",distList,model,{differentDisplay: true}) + // text3 += `<br>` + // } + + if (model.utility_shape !== "linear") { + text3 += ` + This is your perceived distance from each candidate using a <b>${model.utility_shape}</b> utility function: <span class="percent">(as % of your perceived distance of the arena width)</span><br> + ` + text3 += tBarChart("nUNorm",distList,model,{distLine:true}) + // for (var d of distList) { + // text3 += ` + // ${makeIconsCan([d.c])}: <b>${Math.round(d.uNorm*100)}</b> <br> + // ` + // } + text3 += `<br>` + } + + text3 += ` + This is your perceived utility for each candidate: <span class="percent">(100% minus perceived distance)</span> <br>` + text3 += tBarChart("uNorm",distList,model) + text3 += ` + <br>` + + text3 += ` + This is your distance from each candidate: <span class="percent">(as % of arena width)</span> <br> + ` + text3 += tBarChart("dNorm",distList,model,{distLine:true}) + // for (var d of distList) { + // text3 += ` + // ${makeIconsCan([d.c])}: <b>${Math.round(d.dist/model.size*100)}</b> <br> + // ` + // } + text3 += `<br>` + + var not_f = ["zero strategy. judge on an absolute scale.","normalize"] + var f_strategy = ! not_f.includes(voterPerson.strategy) + var showPollExplanation = f_strategy + + if (consideredElectability) { + if (voterAtStage.electable && voterAtStage.electable.length > 0) { + text3 += ` + In the primary, you picked from the candidates that you considered electable: <br> + ${makeIconsCan(voterAtStage.electable)} <br> + <br> + ` + showPollExplanation = false + if (voterAtStage.electable.length > 2) { + if ( voterAtStage.viable) { + text3 += ` + Also, you considered who among these electable candidates was viable. <br> + <br> + ` + // text3 += ` + // Also, you considered who among these electable candidates was viable: <br> + // ${makeIcons(voterAtStage.viable)} <br> + // <br> + // ` + showPollExplanation = true + } + } + } else { + text3 += ` + In the primary, no candidates seemed electable, so you picked the one that was most electable: <br> + ${makeIconsCan(voterAtStage.mostElectable)} <br> + <br> + ` + showPollExplanation = false + } + text3 += ` + (Candidates were considered electable in head-to-head polls if they won or if the other candidate didn't get + <b>${_textPercent(model.howBadlyDefeatedThreshold - 1)}</b> + more votes.) <br> + <br> + ` + } + + + var district = model.district[voterPerson.iDistrict] + var maxscore = model.voterGroups[0].voterModel.maxscore + + if ( showPollExplanation ) { + if (model.autoPoll == "Manual") { + text3 += ` + and these candidates were manually selected as frontrunners: <br> + ${makeIcons(model.preFrontrunnerIds)} <br> + <br> + ` + } else { + text3 += ` + and you saw these candidates as frontrunners: <br> + ${makeIcons(voterAtStage.viable)} <br> + <br> + ` + if (model.system == "IRV") { + var tp = voterPerson.truePreferences + var rank = voterAtStage.ballot.rank + var didCompromise = rank[0] != tp[0] + if (didCompromise) { + text3 += ` + Your didn't feel your favorite was viable, so you looked at head-to-head polls and picked someone who could beat the winner. Your true preferences were: <br> + ${makeIcons(tp).join(' > ')} <br> + <br> + so you compromised and went with: <br> + ${makeIcons(rank).join(' > ')} <br> + <br> + ` + } + } + text3 += ` + You based your list of viable candidates on your personal feeling that a candidate needed this fraction of the leading frontrunner's votes to be viable: <br> + <b>${_textPercent(voterPerson.poll_threshold_factor)}</b> <br> + <br> + and you saw that the leading frontrunner had <br> + <b>${_percentFormat(district,voterAtStage.maxPoll / maxscore)}</b> <br> + <br> + so, you only saw candidates with votes above this threshold as viable: <br> + <b>${_percentFormat(district,voterAtStage.threshold / maxscore)}</b> <br> + <br> + ` + } + } + + var part3 = ` + <table class="main2" border="1"> + <tbody> + <tr> + <td class="tallyText"> + <span class="small"> + #3 + </span> + </td> + </tr> + </tbody> + </table>`.replace("#3",text3) + + + var part4 = '' + + // alternative form of ballot + + if (model.ballotType == "Ranked" || model.ballotType == "Score" || model.ballotType == "Approval" || model.ballotType == "Three") { + part4 += tableHead + part4 += ` + <span class="small"> + Alternative form of ballot: + </span> + <br> <br> + ` + part4 += tBarChart("score",distList,model,{differentDisplay: true,bubbles:true}) + part4 += `<br>` + part4 += tableFoot + } + + var part5 = '' + if (model.ballotType == "Ranked") { + part5 += tableHead + part5 += ` + <span class="small"> + Visualization of ballot: + </span> + <br> <br> + ` + part5 += tBarChart("score",distList,model,{differentDisplay: true,distLine:true}) + part5 += `<br>` + part5 += tableFoot + } + + + if (tablewrap) { + var text = '' + text += `<table id="paper"> + <tbody> + <tr> + ` + text += `<td valign="top">` + text += part1 + text += `</td>` + if (direction=="V") { + text += '</tr><tr>' + } + text += `<td valign="top">` + text += part2 + text += `</td>` + text += ` + </tr> + </tbody> + </table>` + return text + } else { + if (direction == "H") { + var text = '' + text += ` + <div id="paper"> + <div> + ` + part1 + ` + </div> + <div> + ` + part2 + ` + </div> + <div> + ` + part3 + ` + </div> + <div> + ` + part4 + ` + </div> + <div> + ` + part5 + ` + </div> + </div>` + return text + } else { // direction == "V" + return [part1,part2,part3,part4,part5] + } + } + } +} + + +function makeDistList(model,voterPerson,voterAtStage,cans,opt) { + opt = opt || {} + opt.dontSort = opt.dontSort || false + opt.noBallot = opt.noBallot || false + + var distList = [] + var uf = utility_function(model.utility_shape) + for (var i = 0; i < cans.length; i++) { + var c = cans[i] + var dist = distF(model,{x:voterPerson.x, y:voterPerson.y}, c) + var distSet = { + i:i, + c:c, + dist: dist, + dNorm: dist / model.size, + nUtility: uf(dist), + nUNorm: uf(dist) / uf(model.size), + uNorm: 1-uf(dist) / uf(model.size), + } + if (opt.noBallot) { + + } else if (model.ballotType == "Score" || model.ballotType == "Three") { + var maxscore = model.voterGroups[0].voterModel.maxscore + distSet.maxscore = maxscore + distSet.score = voterAtStage.ballot.scores[c.id] / maxscore + distSet.scoreDisplay = voterAtStage.ballot.scores[c.id] + } else if (model.ballotType == "Approval") { + distSet.maxscore = 1 + distSet.score = voterAtStage.ballot.scores[c.id] + distSet.scoreDisplay = distSet.score + } else if (model.ballotType == "Ranked") { + if (model.system == "Borda") { + var rank = voterAtStage.ballot.rank.indexOf(c.id) + 1 + var maxpoints = voterAtStage.ballot.rank.length + var points = maxpoints - rank + distSet.scoreDisplay = points + distSet.score = points / maxpoints + } else { + distSet.scoreDisplay = (voterAtStage.ballot.rank.indexOf(c.id) + 1) + distSet.score = distSet.scoreDisplay / voterAtStage.ballot.rank.length + } + } + distList.push(distSet) + + } + + var doSort = ! opt.dontSort + if (doSort) { + distList.sort(function(a,b) {return a.dist - b.dist}) + for (var i = 0; i < distList.length; i++) { + distList[i].iSort = i // we might want to show these by the sorted order + } + } + + return distList +} + +function makeDistListFromTally(tally, cans, maxscore, nballots,opt) { + opt = opt || {} + opt.dontSort = opt.dontSort || false + + var distList = [] + + var k = 0 + for (var i = 0; i < cans.length; i++) { + var c = cans[i] + if (tally[c.id] !== undefined) { + var distSet = { + i:k, + c:c, + cid:c.id, + score: tally[c.id] / maxscore / nballots, + scoreDisplay: tally[c.id] / maxscore, + maxscore: maxscore, + } + distList.push(distSet) + k++ + } + } + + var doSort = ! opt.dontSort + if (doSort) { + distList.sort(function(a,b) {return a.score - b.score}) + for (var i = 0; i < distList.length; i++) { + distList[i].iSort = i // we might want to show these by the sorted order + } + } + + return distList +} + +function tBarChart(measure,distList,model,opt) { + opt = opt || {} + opt.differentDisplay = opt.differentDisplay || false + opt.sortOrder = opt.sortOrder || false + opt.distLine = opt.distLine || false + opt.bubbles = opt.bubbles || false + opt.percent = opt.percent || false + + var text = "" + + // helper + var makeIconsCan = x => x ? x.map(a => model.icon(a.id)) : "" + + // sortOrder = true + if (opt.differentDisplay) { + var mult = 1 + var display = measure + "Display" + } else { + var mult = 100 + var display = measure // default display to measurement + } + + // option for vertical dimenison.. 0 to turn off. + vertdim = 1; + + // dot plot from 0 to 150 + // border at 100 * 220 / 141 = 156 + // also the .5 em margins and padding help center the icons. + var w1 = 156 + var w2 = 200 + var ncans = distList.length + text += `<div style=' position: relative; width: ${ (1) ? w2 : w2-4}px; height: ${Math.max( 1 , vertdim * ncans )}em; border: ${ (1) ? 0 : 2}px solid #ccc; padding: .25em 0;'>` + text += `<div style=' position: relative; width: ${ (1) ? w1 : w1-4}px; height: ${Math.max( 1 , vertdim * ncans )}em; border: 0px solid #ccc; border-right: ${(opt.bubbles) ? 0 : 1}px dashed #ccc; padding: 0 ${ (1) ? 0 : .5}em;'>` + distList.reverse() + for (var d of distList) { + var iV = (opt.sortOrder) ? d.iSort : d.i + var y = iV*vertdim + var x = Math.round(d[measure]*w1) + if (opt.distLine) { + text += ` + <div style=' position: absolute; top: ${y + .5}em; width: ${x}px; background-color: #ccc; height: 2px; opacity: .8; '> + </div> + <img src="play/img/voter.png" style=' position: absolute; top: ${y}em; left:0; '/> + ` + } else if (opt.bubbles) { + text += ` + <div style=' position: absolute; top: ${y}em; left: ${0+2}px; white-space: nowrap;'> + ${makeIconsCan([d.c])} <br> + </div> + ` + var nbubbles = (model.ballotType == "Ranked") ? ncans : d.maxscore+1 + for (var k=0; k < nbubbles; k++) { + var km = k + if (model.ballotType == "Ranked") { + km ++ + } + var kx = (k+1) * Math.min( (w2-4-13)/ncans, 20 ) - 5 + var back = (d[display] == km) ? "#555" : "white" + text += `<div class="circle" style=' position: absolute; top: ${y}em; left: ${kx}px; background-color:${back};'> + </div> + <div style=' position: absolute; top: ${y-.05}em; left: ${kx+3}px; color: #ccc; '> + <span style=' font-size:${(km > 9 ) ? 50 : 80}%; vertical-align: middle;' > + ${km} + </span> + </div> + ` + } + continue + } else { + text += ` + <div style=' position: absolute; top: ${y}em; width: ${x}px; left: 0; background-color: ${d.c.fill}; height: 1em; opacity: .8;'> + </div> + ` + } + var f = x => x + if (opt.percent) f = x => _textPercent(x/100) + text += ` + <div style=' position: absolute; top: ${y}em; left: ${x+2}px; white-space: nowrap;'> + ${makeIconsCan([d.c])}: <b>${f(Math.round(d[display] * mult))}</b> <br> + </div> + ` + } + distList.reverse() + text += ` + </div> + </div> + ` + return text +} + +function dLineChart(measure,dls,model,opt) { + opt = opt || {} + + var text = "" + + // helper + var makeIconsCan = x => x ? x.map(a => model.icon(a.id)) : "" + + // dot plot from 0 to 150 + // border at 100 * 220 / 141 = 156 + // also the .5 em margins and padding help center the icons. + var w1 = 156 + var w2 = 200 + var npolls = dls.length + var yscale = 20 + var h1 = Math.max( 1 , npolls-1 ) * yscale // pixels + var ncans = dls[0].length + text += `<div style=' position: relative; width: ${w1}px; height: ${h1}px; padding: .25em 0;'>` + text += `<div style=' position: relative; width: ${w1}px; height: ${h1}px; border: 1px dashed #ccc; border-left: 0px dashed #ccc; padding: 0 0em;'>` + text += `<svg id="pollChart" viewBox="0 0 ${w1} ${h1}" xmlns="http://www.w3.org/2000/svg">` + for (var k = 0; k < ncans; k++) { + text += `<path d="` + for (var i = 0; i < dls.length; i++) { + var dl = dls[i] + var d = dl[k] + var color = d.c.fill + var y2 = i * yscale + var x2 = d[measure]*w1 + if (i == 0) { + text += `M ${x2} ${y2}` + } else { + text += ` L ${x2} ${y2}` + } + } + text += `" fill="transparent" stroke="${color}" stroke-width="5" opacity=".8" />` + } + text += `</svg>` + text += ` + </div> + </div> + ` + return text +} + + +function htmlBallot(model,rTitle,textByCandidate,cans) { + var text = "" + var tTitle = ` + <table class="main" border="1"> + <tbody> + <tr> + <th class="main"> + #title + </th> + </tr> + <tr> + <td class="main"> + <table class="canList" border="0"> + <tbody> + ` + text += tTitle.replace("#title",rTitle) + + tRow = ` + <tr> + <td> + <table class="num"> + <tbody> + <tr> + <td class="num">#num</td> + </tr> + </tbody> + </table> + </td> + <td class="nameLabel">#name</td> + </tr> + ` + for(var i=0; i < cans.length; i++) { + var c = cans[i] + var num = textByCandidate[i] + if (model.theme == "Letters") { + // icon is same as name + var name = model.icon(c.id) //+ " <b style='color:"+c.fill+"'>" + model.nameUpper(c.id) + "</b>" + } else { + var name = model.icon(c.id) + " <span class='nameLabelName' style='color:"+c.fill+"'>" + model.nameUpper(c.id) + "</span>" + } + text += tRow.replace("#num",num).replace("#name",name) + } + text += ` + </tbody> + </table> + </td> + </tr> + </tbody> + </table> + ` + return text +} - self.radiusStep = 200; - self.approvalRadius = 100; // whatever. - self.drawApprovalRadius = 100; // whatever. - self.getScore = function(x2){ - return (x2<self.approvalRadius**2) ? 1 : 0 +///////////////////////////////////////// +///////// Voter Objects //////////// +///////////////////////////////////////// + +// sanity rules: class creation code cannot read attributes from model. + +function VoterPerson(model,voterModel) { + // all the data on a person you'd like to know + + var self = this + _addAttributes(self,{ + isVoterPerson: true, + x: undefined, + y: undefined, + xArena: undefined, + yArena: undefined, + strategy: undefined, + iPoint: undefined, + iGroup: undefined, + iAll: undefined, + iDistrict: undefined, + iParty: undefined, + weight: undefined, + // ballotType: voterModel.type, + // voterModel: voterModel, + stages: {}, + }) +} + +function VoterSet(model) { + var self = this + self.allVoters = undefined + self.totalVoters = undefined + // self.crowds = [] // not ready yet + // self.districts = [] + self.newCrowd = function() { + var voterPeople = [] + // self.crowds.push({voterPeople:voterPeople}) + return voterPeople + } + self.init = function() { + // list voters + // Information about where to find the voters in the groups AKA model.voterGroups[] + self.allVoters = [] + var j = 0 + for (var i = 0; i < model.voterGroups.length; i++) { + for (var k = 0; k < model.voterGroups[i].voterPeople.length; k++) { + var voterPerson = model.voterGroups[i].voterPeople[k] + voterPerson.iGroup = i + voterPerson.iAll = j + // voterPerson.iPont = k + self.allVoters.push(voterPerson) + j++ + } + } + self.totalVoters = self.allVoters.length + } + + self.getAllVoters = function() { + // a shallow copy of self.allVoters + var copyAll = [] + for (var voterPerson of self.allVoters) { + copyAll.push(voterPerson) + } + return copyAll + + } + self.getBallotsDistrict = function(district){ + var ballots = []; + for(var i=0; i<district.voterPeople.length; i++){ + var v = district.voterPeople[i] + var b = model.voterGroups[v.iGroup].voterPeople[v.iPoint].stages[model.stage].ballot + ballots = ballots.concat(b); + } + return ballots; }; + self.getBallotsCrowdAndDistrict = function(iCrowd,district) { + var ballots = []; + for(var i=0; i<district.voterPeople.length; i++){ + var v = district.voterPeople[i] + if (v.iGroup == iCrowd) { + // var b = self.crowds[iCrowd].voterPeople[v.iPoint].stages[model.stage].ballot + var b = model.voterGroups[iCrowd].voterPeople[v.iPoint].stages[model.stage].ballot + ballots.push(b) + } + } + return ballots; + } + self.getBallotsPartyAndDistrict = function(iParty,district) { + var ballots = []; + var voterPeople = district.parties[iParty].voterPeople + for(var i=0; i<voterPeople.length; i++){ + var voterPerson = voterPeople[i] + var b = voterPerson.stages[model.stage].ballot + ballots.push(b) + } + return ballots + } + self.getBallotsCrowd = function(iCrowd) { + var voterPeople = model.voterGroups[iCrowd].voterPeople + var ballots = [] + for (var voterPerson of voterPeople) { + ballots.push(voterPerson.stages[model.stage].ballot) + } + return ballots + } + + self.getVoterArrayXY = function() { + // returns an array of all the voters and their distinguishing info + + var vs = [] + for (var i = 0; i < model.voterGroups.length; i++) { + var voterGroup = model.voterGroups[i] + var points = voterGroup.points + var xGroup = voterGroup.x + var yGroup = voterGroup.y + for (var k = 0; k < points.length; k++) { + var v = { + x: points[k][0] + xGroup, + y: points[k][1] + yGroup + } + vs.push(v) + } + } + return vs + } + + self.getDistrictVoterArray = function(district) { + // only for voters of a district + + // returns an array of all the voters and their distinguishing info + + var vs = [] + for (var i = 0; i < model.voterGroups.length; i++) { + var voterGroup = model.voterGroups[i] + var points = voterGroup.points + var xGroup = voterGroup.x + var yGroup = voterGroup.y + var ballots = model.voterSet.getBallotsCrowd(i) + for (var k = 0; k < points.length; k++) { + var v = { + x: points[k][0] + xGroup, + y: points[k][1] + yGroup, + b: [], + iDistrict: voterGroup.voterPeople[k].iDistrict, + } + for (var m = 0; m < model.candidates.length; m++) { + v.b[m] = 0 // zero out all the counts + } + + if (v.iDistrict !== district.i) { // the only difference from the regular function + // vs.push(v) + continue + } + + // var optByDist = model.byDist + var optByDist = false + if (optByDist) { + + var uf = utility_function(model.utility_shape) + for (var n = 0; n < model.candidates.length; n++) { + var c = model.candidates[n] + var dist = distF(model,{x:v.x, y:v.y}, c) + // dNorm: dist / model.size, + // nUtility: uf(dist), + // nUNorm: uf(dist) / uf(model.size), + var uNorm = 1-uf(dist) / uf(model.size) + v.b[n] = uNorm + } + vs.push(v) + + } else if (model.ballotType == "Approval") { // not yet fully functional TODO + var ballot = ballots[k] + for (var n = 0; n < model.candidates.length; n++) { + var id = model.candidates[n].id + v.b[n] = ballot.scores[id] || 0 + } + vs.push(v) + } else if (model.ballotType == "Score") { + var ballot = ballots[k] + for (var n = 0; n < model.candidates.length; n++) { + var id = model.candidates[n].id + v.b[n] = ballot.scores[id] || 0 + } + vs.push(v) + } else if (model.ballotType == "Ranked") { + var ballot = ballots[k] + for (var n=0; n<ballot.rank.length; n++) { + var cid = ballot.rank[n] + var ci = model.candidatesById[cid].i + // v.b[ci] = n+1 + v.b[n] = ci + } + vs.push(v) + + } else { + vs.push(v) + } + } + } + return vs + } + + self.getVoterArray = function() { + // returns an array of all the voters and their distinguishing info + + var all = [] + for (var district of model.district) { + var some = self.getDistrictVoterArray(district) + all = all.concat(some) + } + return all + } + + self.getArrayAttr = function(a) { + // returns an array of all the voters and their distinguishing info + var s = [] + for (var voterGroup of model.voterGroups) { + for (var voterPerson of voterGroup.voterPeople) { + s.push( voterPerson[a] ) + } + } + return s + } + + self.updateBallots = function() { + for (var voterGroup of model.voterGroups) { + self.updateCrowdBallots(voterGroup) + } + } + self.updateCrowdBallots = function(crowd) { + for(var voterPerson of crowd.voterPeople){ + self.updatePersonBallot(voterPerson) + } + } + self.updateCrowdDistrictBallots = function(crowd,district) { + for(var voterPerson of crowd.voterPeople){ + if (voterPerson.iDistrict == district.i) { + self.updatePersonBallot(voterPerson) + } + } + } + self.updateDistrictBallots = function(district) { + for(var voterPerson of district.voterPeople){ + self.updatePersonBallot(voterPerson) + } + } + self.updatePersonBallot = function(voterPerson) { + var voterModel = model.voterGroups[voterPerson.iGroup].voterModel + voterPerson.stages[model.stage] = {} + var ballot = voterModel.castBallot(voterPerson) + // store ballot for current stage + self.loadPersonBallot(voterPerson, ballot) + } + self.loadDistrictBallotsFromStage = function(district,stage) { + for(var voterPerson of district.voterPeople){ + var ballot = voterPerson.stages[stage].ballot + self.loadPersonBallot(voterPerson, ballot) + } + } + self.copyDistrictBallotsToStage = function(district,stage) { + for (let voterPerson of district.voterPeople) { + voterPerson.stages[stage] = {ballot: _jcopy(voterPerson.stages[model.stage].ballot)} + } + } + self.loadPersonBallot = function(voterPerson, ballot) { + + if (voterPerson.stages[model.stage] == undefined) { + var stageInfo = {} + stageInfo[model.stage] = {ballot:ballot} + _addAttributes(voterPerson.stages, stageInfo) + } else { + _addAttributes(voterPerson.stages[model.stage], {ballot:ballot}) + } + } + +} + +function VoterCrowd(model) { + var self = this; + Draggable.call(self); + + _fillVoterDefaults(self) + self.voterGroupType = undefined + self.size = undefined + + self.typeVoterModel = 'Plurality' + self.voterModel = undefined + + self.voterPeople = model.voterSet.newCrowd() // voterPeople will reference the data in voterSet + self.points = [[0,0]]; + + self.img = new Image(); // use the face + self.img.src = "play/img/voter_face.png"; + + var n = 1000 + self.randomSeed = Math.round(model.random() * n) % n + + self.drawBackAnnotation = function(x,y,ctx) {} + self.drawAnnotation = function(x,y,ctx) {}; // TO IMPLEMENT - self.getBallot = function(x, y, strategy){ + self.initVoterModel = function() { + + self.voterModel = new VoterModel(model,self.typeVoterModel) + + } + self.initVoterSet = function() { + + // make a voterPerson for each point + self.voterPeople.length = 0 + for(var i=0; i<self.points.length; i++){ + var voterPerson = new VoterPerson(model,self.voterModel) + voterPerson.iPoint = i + voterPerson.weight = 1 + voterPerson.skinColor = skinColor(i + self.randomSeed) + self.voterPeople.push(voterPerson) + } + model.voterSet.init() + self.updateVoterSet() + } + self.initVoterName = function(i) { + if (model.voterGroupCustomNames == "Yes" && i < model.voterGroupNameList.length && model.voterGroupNameList[i] != "") { + self.name = model.voterGroupNameList[i] + } else { + self.name = i + 1 + } + } + self.updateVoterSet = function() { + + for(var i=0; i<self.points.length; i++){ + var p = self.points[i]; + var x = self.x + p[0]; + var y = self.y + p[1]; + var voterPerson = self.voterPeople[i] + voterPerson.x = x + voterPerson.y = y + } + } + self.updateBallots = function() { + model.voterSet.updateCrowdBallots(self) + } + self.updateDistrictBallots = function(district) { + model.voterSet.updateCrowdDistrictBallots(self,district) + } +} + +function _fillVoterDefaults(self) { + // a helper for configuring + + _fillInDefaults(self,{ + // FIRST group in expVoterPositionsAndDistributions + vid: 0, + snowman: false, + x_voters: false, + crowdShape: "Nicky circles", + // SECOND group in "exp_addVoters" + // same for all voter groups in model + preFrontrunnerIds:["square","triangle"], + doTwoStrategies: false, + spread_factor_voters: 1, + // could vary between voters + secondStrategy: "zero strategy. judge on an absolute scale.", + percentSecondStrategy: 0, + group_count: 50, + group_spread: 190, + isVoter: true + }) +} + +function GaussianVoters(model){ // this config comes from addVoters in main_sandbox + + var self = this; + VoterCrowd.call(self,model) + self.isGaussianVoters = true + self.voterGroupType = "GaussianVoters" + self.disk = 3 + self.size = 30 + + self.init = function() { + self.initVoterModel() + self.initPoints() + self.initVoterSet() + } + + self.updatePeople = function() { + + self.updateVoterSet() + + self.strategyPick() + + } + + self.initPoints = function () { + // puts the voters into position + + // HACK: larger grab area + // self.radius = 50; + if (self.crowdShape == "Nicky circles") { + // SPACINGS, dependent on NUM + var spacings = [0, 12, 12, 12, 12, 20, 30, 50, 100]; + if (self.snowman) { + if (self.vid == 0) { + spacings.splice(3) + } else if (self.vid == 1) { + spacings = [0,12,12,12] + } else if (self.vid == 2) { + spacings.splice(4) + } + //spacings.splice(2+self.vid) + } else if(self.disk==1){ + spacings.splice(4); + } else if(self.disk==2){ + spacings.splice(5); + } else if (self.disk==3){ + spacings = [0, 10, 11, 12, 15, 20, 30, 50, 100]; + } + + // Create 100+ points, in a Gaussian-ish distribution! + var points = [[0,0]]; + self.points = points; + var _radius = 0, + _RINGS = spacings.length; + for(var i=1; i<_RINGS; i++){ + + var spacing = spacings[i]; + _radius += spacing; + + var circum = Math.TAU*_radius; + var num = Math.floor(circum/(spacing-1)); + if (self.snowman && self.vid == 1 && i==3){ + num = 10 + } + + // HACK TO MAKE IT PRIME - 137 VOTERS + //if(i==_RINGS-1) num += 3; + + var err = 0.01; // yeah whatever + for(var angle=0; angle<Math.TAU-err; angle+=Math.TAU/num){ + var x = Math.cos(angle)*_radius * self.spread_factor_voters; + var y = Math.sin(angle)*_radius * self.spread_factor_voters; + points.push([x,y]); + } + + } + } else if (self.crowdShape == "circles") { + + var _spread_factor = 2 * Math.exp(.01*self.group_spread) / 20 + var space = 12 * self.spread_factor_voters * _spread_factor + + self.group_count_h = self.group_count_h || 5 + var numRings = self.group_count_h / 2 + var odd = self.group_count_h % 2 + + var points = [[0,0]] + if (odd) points = [] + + for(var i=(odd)?0:1; i<=numRings; i++){ + + var radius = i * space + if (odd) radius += .5 * space + + var symmetry = true + if (symmetry) { + var num = (i + odd*.5) * 6 + var dAngle = Math.TAU/num + + for(var k = 0; k < num; k++){ + var angle = k * dAngle + var x = Math.sin(angle)*radius + var y = -Math.cos(angle)*radius + points.push([x,y]) + } + } else { // the old way + var circum = Math.TAU*radius + var num = Math.floor(circum/(space-1)) + + var dAngle = Math.TAU/num + + for(var k = 0; k < num; k++){ + var angle = k * dAngle + var x = Math.cos(angle)*radius + var y = Math.sin(angle)*radius + points.push([x,y]) + } + } + } + self.group_count = points.length + self.radius = radius // last radius + self.points = points + + } else if (self.crowdShape == "rectangles") { + + var _spread_factor = 2 * Math.exp(.01*self.group_spread) / 20 + var space = 12 * self.spread_factor_voters * _spread_factor + + self.group_count_vert = self.group_count_vert || 5 + self.group_count_h = self.group_count_h || 5 + + var numRings = self.group_count_h / 2 + var vNumRings = self.group_count_vert / 2 + var points = [] + + + for (var i=-numRings+.5; i<numRings; i++) { + for (var k=-vNumRings+.5; k<vNumRings; k++) { + var x = space * i + var y = space * k + points.push([x,y]) + } + } + self.group_count = points.length + self.halfwidth = (numRings+.5) * space + self.halfheight = (vNumRings+.5) * space + self.points = points + + } else if (self.crowdShape == "gaussian sunflower" ) { + var points = []; + self.points = points; + var angle = 0; + var _radius = 0; + var _radius_norm = 0; + var _spread_factor = 2 * Math.exp(.01*self.group_spread) * Math.sqrt(self.group_count/20) // so the slider is exponential + self.stdev = _spread_factor * 1.38 + var theta = Math.TAU * .5 * (3 - Math.sqrt(5)) + for (var count = 0; count < self.group_count; count++) { + angle = theta * count + if (0) { + _radius_norm = Math.sqrt(1-(count+.5)/self.group_count) + _radius = _erfinv(_radius_norm) * _spread_factor + self.stdev = _spread_factor + } else { + _radius_norm = 1-(count+.5)/self.group_count + _radius = Math.sqrt(-2*Math.log(1-_radius_norm)) * self.stdev * .482 + // _radius = Math.sqrt(_radius_norm) * self.stdev * .482 + } + var x = Math.cos(angle)*_radius * self.spread_factor_voters; + var y = Math.sin(angle)*_radius * self.spread_factor_voters; + points.push([x,y]); + } + self.points = points + } + if (0 && (model.dimensions == "1D+B" || model.dimensions == "1D")) { + + var build1 = false + var forward = true + if (build1) { // cool method doesn't work + for (var i = 0; i < self.points.length; i++) { + // for (var i = self.points.length - 1; i >= 0; i--) { + points[i][0] = self.points[i][0] + points[i][1] = 0 + var diameter2 = 30 + var yNewUp, yNewDown + var yMax = 0 + var yMin = 10000 + var noneighbors = true + // for (var k = self.points.length - 1; k > i; k--) { + for (var k = 0; k < i; k++) { + xDiff2 = (points[k][0] - points[i][0])**2 + if (xDiff2 < diameter2) { + noneighbors = false + yDiff = Math.sqrt(diameter2-xDiff2) + yNewUp = yDiff + points[k][1] + yNewDown = - yDiff + points[k][1] + if (yNewUp > yMax) { + yMax = yNewUp + } + if (yNewDown < yMin) { + yMin = yNewDown + } + } + } + if (noneighbors) { + yChoose = 0 + } else if (yMin > 0) { + var yChoose = yMin + } else { + var yChoose = yMax + } + points[i][1] = yChoose + } + + for (var i = 0; i < points.length; i++) { + points[i][1] = -points[i][1] + } + self.points = points + } else { + var betweenDist = 5 + var stackDist = 5 + var added = [] + var todo = [] + if (forward) { + for (var i = 0 ; i < self.points.length; i++) { + todo.push(i) + } + } else { + for (var i = self.points.length - 1 ; i >= 0; i--) { + todo.push(i) + } + } + var level = 1 + while (todo.length > 0) { + for (var c = 0; c < todo.length; c++) { + var i = todo[c] + // look for collisions + var collided = false + for (var d = 0; d < added.length; d++) { + var k = added[d] + xDiff = Math.abs(self.points[k][0] - self.points[i][0]) + if (xDiff < betweenDist) { + collided = true + break + } + } + if (! collided) { + self.points[i][2] = (level-1) * -stackDist + added.push(i) + todo.splice(c,1) + c-- + } + } + level++ + var added = [] + } + + } + } - var scoresfirstlast = dostrategy(x,y,0,1,[0,1],strategy,self.model.preFrontrunnerIds,self.model.candidates,self.radiusStep,self.getScore) - var scores = scoresfirstlast.scores - self.drawApprovalRadius = (scoresfirstlast.radiusFirst + scoresfirstlast.radiusLast) * .5 - self.dottedCircle = scoresfirstlast.dottedCircle + } - // Anyone close enough. If anyone. - var approved = []; - for(var i=0; i<self.model.candidates.length; i++){ - var c = self.model.candidates[i]; - if(scores[c.id] == 1){ - approved.push(c.id); + self.strategyPick = function() { + + //randomly assign voter strategy based on percentages, but using the same seed each time + // from http://davidbau.com/encode/seedrandom.js + Math.seedrandom('hi'); + + for(var i=0; i<self.points.length; i++){ + if (0) { // two ways to choose which voters use secondStrategy + var r1 = Math.random() * 99.8 + .1; + } else { + if (!self.x_voters) { + var r1 = (1861*i) % 100 + .5; + } else { + var r1 = (34*i) % 100 + .5; + } + } + if (r1 < self.percentSecondStrategy && self.doTwoStrategies) { + var strategy = model.secondStrategy // yes + var realNameStrategy = model.realNameSecondStrategy + } else { + var strategy = model.firstStrategy; // no e.g. + var realNameStrategy = model.realNameFirstStrategy } - } - - // Vote for the CLOSEST - return { approved: approved }; + + // choose the threshold of voters for polls + var r_11 = Math.random() * 2 - 1 + + var voterPerson = self.voterPeople[i] + var gauss = _erfinv(r_11) * .2 + model.centerPollThreshold // was .5 + voterPerson.poll_threshold_factor = Math.min(1, gauss) - }; + voterPerson.strategy = strategy + voterPerson.realNameStrategy = realNameStrategy + } + } - self.drawBG = function(ctx, x, y, ballot){ + + // DRAW! + self.draw0 = function(ctx){ + if (model.showVoters) { + setPositionAndSizeGaussian() - // RETINA - x = x*2; - y = y*2; + if (model.ballotVis && ! model.visSingleBallotsOnly) { - // Draw a big ol' circle - ctx.beginPath(); - ctx.arc(x, y, self.drawApprovalRadius*2, 0, Math.TAU, false); - ctx.lineWidth = 8; - ctx.strokeStyle = "#888"; - ctx.setLineDash([]); - if (self.dottedCircle) ctx.setLineDash([5, 15]); - ctx.stroke(); - if (self.dottedCircle) ctx.setLineDash([]); + _drawMap(ctx) + } + } + } + self.draw1 = function(ctx){ - }; + if (model.voterIcons == "off") return + if (model.showVoters) { - self.drawCircle = function(ctx, x, y, size, ballot){ + setPositionAndSizeGaussian() - // If none, whatever. - var slices = []; - if(ballot.approved.length==0){ - _drawBlank(ctx, x, y, size); - return; + _drawMe(ctx) } - - // Draw 'em slices - for(var i=0; i<ballot.approved.length; i++){ - var candidate = self.model.candidatesById[ballot.approved[i]]; - slices.push({ num:1, fill:candidate.fill }); + } + self.draw2 = function(ctx){ + if ( model.voterCenterIcons == "on") { + _drawCenter(ctx) } - _drawSlices(ctx, x, y, size, slices, ballot.approved.length); + } - }; + function _drawMap(ctx) { + ctx.save() + temp = ctx.globalAlpha + ctx.globalAlpha = .2 + for(var voterPerson of self.voterPeople){ + self.voterModel.drawMap(ctx, voterPerson) + } + ctx.globalAlpha = temp + ctx.restore() + } -} + function _drawMe(ctx){ + for(var voterPerson of self.voterPeople){ + if (model.voterIcons == "dots") { + var x = voterPerson.xArena + var y = voterPerson.yArena + _drawDot( 2, x, y, ctx) + } else { + self.voterModel.drawMe(ctx, voterPerson, 1) + } + } + } -function RankedVoter(model){ + function _drawCenter(ctx) { - var self = this; - self.model = model; + // Don't draw a individual group under a votercenter, which looks weird. + // if(model.voterCenter && model.voterGroups.length == 1) return + // I guess this fixed something.. at some point.. but not anymore - self.getBallot = function(x, y, strategy){ + // Circle! + var size = self.size; + + var s = model.arena.modelToArena(self) + var x = s.x + var y = s.y + + //self.voterModel.drawCircle(ctx, self.x, self.y, size, ballot); + if(self.highlight) var temp = ctx.globalAlpha + if(self.highlight) ctx.globalAlpha = 0.8 + self.drawBackAnnotation(x*2,y*2,ctx) + + if (model.voterIcons != "off") { + if (model.voterIcons == "dots") { + _drawDot(4, x, y, ctx) + } else { - // Rank the peeps I'm closest to... - var rank = []; - for(var i=0;i<self.model.candidates.length;i++){ - rank.push(self.model.candidates[i].id); + // _drawBlank(model, ctx, x, y, size) + // _drawRing(ctx,x,y,self.size) + // _drawThickRing(ctx,x,y,size) + drawArrows(ctx,x,y,size) + + // Face! + // ctx.drawImage(self.img, x*2-size, y*2-size, size*2, size*2); + _drawName(model,ctx,x,y,self.name) + } + } - rank = rank.sort(function(a,b){ - - var c1 = self.model.candidatesById[a]; - var x1 = c1.x-x; - var y1 = c1.y-y; - var d1 = x1*x1+y1*y1; - - var c2 = self.model.candidatesById[b]; - var x2 = c2.x-x; - var y2 = c2.y-y; - var d2 = x2*x2+y2*y2; - - return d1-d2; + self.drawAnnotation(x*2,y*2,ctx) + if(self.highlight) ctx.globalAlpha = temp + }; + self.draw = function(ctx){ + self.draw1(ctx) + self.draw2(ctx) + }; + + function setPositionAndSizeGaussian() { + var circlesize = 10 + for(var i=0; i<self.points.length; i++){ + var p = self.points[i]; + var s = model.arena.modelToArena(self) + var x = s.x + p[0]; + if (model.dimensions == "2D") { + var y = s.y + p[1]; + } else { + var y = s.y + p[2]; + circlesize = 6 + } - }); + var voterPerson = self.voterPeople[i] + _addAttributes( voterPerson, { + xArena:x, + yArena:y, + size:circlesize, + }) + } + } - // Ballot! - return { rank:rank }; +} - }; +function _erfinv(x){ // from https://stackoverflow.com/a/12556710 + var z; + var a = 0.147; + var the_sign_of_x; + if(0==x) { + the_sign_of_x = 0; + } else if(x>0){ + the_sign_of_x = 1; + } else { + the_sign_of_x = -1; + } - self.drawBG = function(ctx, x, y, ballot){ + if(0 != x) { + var ln_1minus_x_sqrd = Math.log(1-x*x); + var ln_1minusxx_by_a = ln_1minus_x_sqrd / a; + var ln_1minusxx_by_2 = ln_1minus_x_sqrd / 2; + var ln_etc_by2_plus2 = ln_1minusxx_by_2 + (2/(Math.PI * a)); + var first_sqrt = Math.sqrt((ln_etc_by2_plus2*ln_etc_by2_plus2)-ln_1minusxx_by_a); + var second_sqrt = Math.sqrt(first_sqrt - ln_etc_by2_plus2); + z = second_sqrt * the_sign_of_x; + } else { // x is zero + z = 0; + } + return z; +} - // RETINA - x = x*2; - y = y*2; +function _erf(x) { + var z; + const ERF_A = 0.147; + var the_sign_of_x; + if(0==x) { + the_sign_of_x = 0; + return 0; + } else if(x>0){ + the_sign_of_x = 1; + } else { + the_sign_of_x = -1; + } - // DRAW 'EM LINES - for(var i=0; i<ballot.rank.length; i++){ + var one_plus_axsqrd = 1 + ERF_A * x * x; + var four_ovr_pi_etc = 4/Math.PI + ERF_A * x * x; + var ratio = four_ovr_pi_etc / one_plus_axsqrd; + ratio *= x * -x; + var expofun = Math.exp(ratio); + var radical = Math.sqrt(1-expofun); + z = radical * the_sign_of_x; + return z; +} - // Line width - var lineWidth = ((ballot.rank.length-i)/ballot.rank.length)*8; +function _drawDot(diameter,x,y,ctx) { + ctx.fillStyle = '#333' + // ctx.strokeStyle = '#333' + ctx.lineWidth = 1 - // To which candidate... - var rank = ballot.rank[i]; - var c = self.model.candidatesById[rank]; - var cx = c.x*2; // RETINA - var cy = c.y*2; // RETINA + // Just draw a circle. + ctx.beginPath() + ctx.arc(x*2,y*2, diameter, 0, Math.TAU, false) + ctx.fill() + // ctx.stroke() +} - // Draw - ctx.beginPath(); - ctx.moveTo(x,y); - ctx.lineTo(cx,cy); - ctx.lineWidth = lineWidth; - ctx.strokeStyle = "#888"; - ctx.stroke(); +function _drawSpeckMan1(fill,headColor,scale,alpha,x,y,ctx) { + + ctx.save() - } + scale *= 4 + sizex = 5 * scale + sizey = 7 * scale + offx = (-34/64*50 + .5 * 50) * .1 + offy = (-59/88*70 + .5 * 70) * .1 + + ctx.translate(Math.round(x*2 - sizex/2), Math.round(y * 2 - sizey/2)); + ctx.scale(scale,scale) + ctx.translate(offx,offy) - }; + + ctx.fillStyle = fill; + ctx.strokeStyle = "black"; + ctx.globalAlpha = alpha; + ctx.lineWidth = ".2"; + ctx.lineCap = "butt"; + ctx.lineJoin = "round"; + ctx.mitterLimit = "1"; + // ctx.font = "normal normal 12 Courier"; + + ctx.shadowColor = "rgba(0,0,0,0.3)"; + ctx.shadowBlur = 4; + ctx.shadowOffsetX = 2; + ctx.shadowOffsetY = 2; + + // head + // ctx.fillStyle = headColor; + // ctx.roundRect(5.25, 0, 8.5, 8, 2); + // ctx.globalAlpha = alpha * 0.3; + // ctx.stroke() + // ctx.globalAlpha = alpha; + // ctx.fill() + + // ctx.fillRect(2, 0, 7, 8); + // ctx.globalAlpha = alpha * 0.3; + // ctx.strokeRect(2, 0, 7, 8); + // ctx.globalAlpha = alpha; + + ctx.translate(0,2.167984-1.631359) + + ctx.beginPath(); + ctx.fillStyle = headColor; + ctx.moveTo(2.001401, 1.631359); + ctx.lineTo(2.001401, 2.569706); + ctx.quadraticCurveTo(2.001401, 2.898966, 2.330661, 2.898966); + ctx.lineTo(2.961005, 2.898966); + ctx.quadraticCurveTo(3.290265, 2.898966, 3.290265, 2.569706); + ctx.lineTo(3.290265, 1.631359); + ctx.quadraticCurveTo(3.290265, 1.302099, 2.961005, 1.302099); + ctx.lineTo(2.330661, 1.302099); + ctx.quadraticCurveTo(2.001401, 1.302099, 2.001401, 1.631359); + + ctx.globalAlpha = alpha * 0.3; + ctx.stroke() + ctx.globalAlpha = alpha; + ctx.fill() - self.drawCircle = function(ctx, x, y, size, ballot){ + // body + + ctx.fillStyle = fill; + ctx.scale(0.264583,0.264583) + ctx.beginPath(); + ctx.moveTo(6.474609, 11.205078); + ctx.bezierCurveTo(5.595976, 11.205078, 4.888672, 11.912383, 4.888672, 12.791016); + ctx.lineTo(4.888672, 17.923828); + ctx.bezierCurveTo(4.888672, 18.802461, 5.595976, 19.509766, 6.474609, 19.509766); + ctx.lineTo(6.843750, 19.509766); + ctx.lineTo(6.843750, 23.679688); + ctx.bezierCurveTo(6.843750, 24.558320, 7.551054, 25.265625, 8.429688, 25.265625); + ctx.lineTo(11.570312, 25.265625); + ctx.bezierCurveTo(12.448945, 25.265625, 13.156250, 24.558320, 13.156250, 23.679688); + ctx.lineTo(13.156250, 19.509766); + ctx.lineTo(13.525391, 19.509766); + ctx.bezierCurveTo(14.404023, 19.509766, 15.111328, 18.802461, 15.111328, 17.923828); + ctx.lineTo(15.111328, 12.791016); + ctx.bezierCurveTo(15.111328, 11.912383, 14.404023, 11.205078, 13.525391, 11.205078); + ctx.lineTo(6.474609, 11.205078); + ctx.fill(); + + ctx.globalAlpha = alpha * 0.3; + ctx.stroke() + ctx.globalAlpha = alpha; + ctx.fill() + + ctx.shadowBlur = 0; + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 0; + + + // eyes + // ctx.fillStyle = "white"; + // ctx.strokeStyle = "white"; + // ctx.fillRect(10, 2, 1, 2); + // ctx.strokeRect(10, 2, 1, 2); + // ctx.fillRect(7, 2, 1, 2); + // ctx.strokeRect(7, 2, 1, 2); + + + ctx.setTransform(1, 0, 0, 1, 0, 0); + + ctx.restore() +} - var slices = []; - var n = ballot.rank.length; - var totalSlices = (n*(n+1))/2; // num of slices! +function _drawSpeckMan2(fill,headColor,scale,alpha,x,y,ctx) { - for(var i=0; i<ballot.rank.length; i++){ - var rank = ballot.rank[i]; - var candidate = self.model.candidatesById[rank]; - slices.push({ num:(n-i), fill:candidate.fill }); - } + ctx.save() - _drawSlices(ctx, x, y, size, slices, totalSlices); + scale *= 4 + sizex = 5 * scale + sizey = 7 * scale + offx = (-34/64*50 + .5 * 50) * .1 + offy = (-59/88*70 + .5 * 70) * .1 + + ctx.translate(Math.round(x*2 - sizex/2), Math.round(y * 2 - sizey/2)); + ctx.scale(scale,scale) + ctx.translate(offx,offy) - }; + + ctx.fillStyle = fill; + ctx.strokeStyle = "black"; + ctx.globalAlpha = alpha; + ctx.lineWidth = ".2"; + ctx.lineCap = "butt"; + ctx.lineJoin = "round"; + ctx.mitterLimit = "1"; + // ctx.font = "normal normal 12 Courier"; + + ctx.shadowColor = "rgba(0,0,0,0.3)"; + ctx.shadowBlur = 4; + ctx.shadowOffsetX = 2; + ctx.shadowOffsetY = 2; + + // head + // ctx.fillStyle = headColor; + // ctx.roundRect(5.25, 0, 8.5, 8, 2); + // ctx.globalAlpha = alpha * 0.3; + // ctx.stroke() + // ctx.globalAlpha = alpha; + // ctx.fill() + + // ctx.fillRect(2, 0, 7, 8); + // ctx.globalAlpha = alpha * 0.3; + // ctx.strokeRect(2, 0, 7, 8); + // ctx.globalAlpha = alpha; + + ctx.beginPath(); + ctx.lineCap = "butt"; + ctx.lineJoin = "round"; + // ctx.mitterLimit = "1"; + ctx.fillStyle = headColor; + ctx.mitterLimit = "4"; + + ctx.moveTo(2.001395, 2.167984); + ctx.lineTo(2.001395, 3.106331); + ctx.quadraticCurveTo(2.001395, 3.435591, 2.330655, 3.435591); + ctx.lineTo(2.960999, 3.435591); + ctx.quadraticCurveTo(3.290259, 3.435591, 3.290259, 3.106331); + ctx.lineTo(3.290259, 2.167984); + ctx.quadraticCurveTo(3.290259, 1.838724, 2.960999, 1.838724); + ctx.lineTo(2.330655, 1.838724); + ctx.quadraticCurveTo(2.001395, 1.838724, 2.001395, 2.167984); + ctx.globalAlpha = alpha * 0.3; + ctx.stroke() + ctx.globalAlpha = alpha; + ctx.fill() + + // body + + ctx.fillStyle = fill; + ctx.beginPath(); + ctx.moveTo(0.859890, 2.077099); + ctx.bezierCurveTo(0.825575, 2.082039, 0.792366, 2.092621, 0.761705, 2.110172); + ctx.lineTo(0.581871, 2.213008); + ctx.bezierCurveTo(0.459225, 2.283213, 0.429463, 2.430372, 0.515208, 2.542704); + ctx.lineTo(1.810736, 4.240275); + ctx.lineTo(1.810736, 6.801876); + ctx.bezierCurveTo(1.810736, 7.034347, 1.997877, 7.221488, 2.230348, 7.221488); + ctx.lineTo(3.061305, 7.221488); + ctx.bezierCurveTo(3.293777, 7.221488, 3.480918, 7.034347, 3.480918, 6.801876); + ctx.lineTo(3.480918, 4.248543); + ctx.lineTo(4.773346, 2.555623); + ctx.bezierCurveTo(4.864501, 2.436203, 4.832933, 2.279891, 4.702549, 2.205257); + ctx.lineTo(4.550620, 2.118441); + ctx.bezierCurveTo(4.420236, 2.043806, 4.242316, 2.079637, 4.151161, 2.199056); + ctx.lineTo(3.126418, 3.541092); + ctx.bezierCurveTo(3.105058, 3.537762, 3.083627, 3.534892, 3.061306, 3.534892); + ctx.lineTo(2.230348, 3.534892); + ctx.bezierCurveTo(2.210150, 3.534892, 2.190857, 3.537842, 2.171437, 3.540582); + ctx.lineTo(1.137392, 2.186653); + ctx.bezierCurveTo(1.073083, 2.102405, 0.962834, 2.062286, 0.859890, 2.077099); + ctx.fill(); + + ctx.globalAlpha = alpha * 0.3; + ctx.stroke() + ctx.globalAlpha = alpha; + ctx.fill() + + // crown + ctx.beginPath(); + ctx.strokeStyle = "#000000"; + ctx.lineJoin = "miter"; + ctx.fillStyle = "#eed508"; + ctx.moveTo(1.975740, 1.732761); + ctx.lineTo(1.643042, 0.864743); + ctx.lineTo(2.332061, 1.334261); + ctx.lineTo(2.641136, 0.864743); + ctx.lineTo(2.997457, 1.334261); + ctx.lineTo(3.639229, 0.864743); + ctx.lineTo(3.306531, 1.732761); + + ctx.globalAlpha = alpha * 0.3; + ctx.stroke() + ctx.globalAlpha = alpha; + ctx.fill() + + ctx.shadowBlur = 0; + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 0; + + // eyes + // ctx.fillStyle = "white"; + // ctx.strokeStyle = "white"; + // ctx.fillRect(10, 2, 1, 2); + // ctx.strokeRect(10, 2, 1, 2); + // ctx.fillRect(7, 2, 1, 2); + // ctx.strokeRect(7, 2, 1, 2); + + + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.restore() +} +CanvasRenderingContext2D.prototype.roundRect = function (x, y, w, h, r) { + if (w < 2 * r) r = w / 2; + if (h < 2 * r) r = h / 2; + this.beginPath(); + this.moveTo(x+r, y); + this.arcTo(x+w, y, x+w, y+h, r); + this.arcTo(x+w, y+h, x, y+h, r); + this.arcTo(x, y+h, x, y, r); + this.arcTo(x, y, x+w, y, r); + this.closePath(); + } + +function skinColor(i) { + + // var colors = [ + // "#3b2219", + // "#a16e4b", + // "#d4aa78", + // "#e6bc98", + // "#ffe7d1", + // "#80654B", + // "#271914", + // "#EEC1A0", + // ] + var colors = [ + "#382922", + "#3F2B2A", + "#512B1E", + "#522E20", + "#6A3928", + "#8F4C31", + "#9A5E42", + "#9A5E42", + "#AD6D4A", + "#AF613D", + "#AF775E", + "#AF775E", + "#BB7752", + "#BC7750", + "#BE8866", + "#BE8866", + "#CAA088", + "#CE8248", + "#D1957D", + "#D1957D", + "#D39475", + "#D3A67C", + "#D3AF97", + "#D6BAA5", + "#DCA788", + "#DCA788", + "#DCAE8D", + "#DCAE8D", + "#DDA479", + "#E19F7D", + "#E6B7B1", + "#E8BD9B", + "#E9BFA7", + "#E9CBC3", + "#EBD1B8", + "#F3C2B3", + ] + // var x = i + Math.seedrandom(i * 100); + var x = Math.round(Math.random() * 100) + return colors[x % colors.length] } -function PluralityVoter(model){ +function SingleVoter(model){ var self = this; - self.model = model; + VoterCrowd.call(self,model) + self.isSingleVoter = true + self.voterGroupType = "SingleVoter" + self.size = 20 - self.getBallot = function(x, y, strategy){ + self.init = function () { - // Who am I closest to? Use their fill - var checkOnlyFrontrunners = (strategy!="zero strategy. judge on an absolute scale." && model.preFrontrunnerIds.length > 1) - var closest = null; - var closestDistance = Infinity; - for(var j=0;j<self.model.candidates.length;j++){ - var c = self.model.candidates[j]; - if(checkOnlyFrontrunners && ! model.preFrontrunnerIds.includes(c.id) ) { - continue // skip this candidate because he isn't one of the 2 or more frontrunners, so we can't vote for him - } - var dx = c.x-x; - var dy = c.y-y; - var dist = dx*dx+dy*dy; - if(dist<closestDistance){ - closestDistance = dist; - closest = c; - } - } + self.initVoterModel() + self.initVoterSet() - // Vote for the CLOSEST - return { vote:closest.id }; + self.voterPerson = self.voterPeople[0] // shorthand + + } - }; + self.updatePeople = function() { - self.drawBG = function(ctx, x, y, ballot){ + self.updateVoterSet() - var candidate = model.candidatesById[ballot.vote]; + var voterPerson = self.voterPeople[0] - // RETINA - x = x*2; - y = y*2; - var tx = candidate.x*2; - var ty = candidate.y*2; + voterPerson.poll_threshold_factor = .6 - // DRAW - Line - ctx.beginPath(); - ctx.moveTo(x,y); - ctx.lineTo(tx,ty); - ctx.lineWidth = 8; - ctx.strokeStyle = "#888"; - ctx.stroke(); + voterPerson.strategy = model.firstStrategy + voterPerson.realNameStrategy = model.realNameFirstStrategy - }; + } - self.drawCircle = function(ctx, x, y, size, ballot){ + // DRAW! + self.draw = function(ctx){ + self.draw1(ctx) + self.draw2(ctx) + }; + self.draw0 = function(ctx) { + setPositionAndSizeSingle() - // RETINA - x = x*2; - y = y*2; + _drawMap(ctx) - // What fill? - var fill = Candidate.graphics[ballot.vote].fill; - ctx.fillStyle = fill; - ctx.strokeStyle = 'rgb(0,0,0)'; - ctx.lineWidth = 1; // border + } + self.draw1 = function(ctx) { + setPositionAndSizeSingle() + + _drawBack(ctx) - // Just draw a circle. - ctx.beginPath(); - ctx.arc(x, y, size, 0, Math.TAU, true); - ctx.fill(); - if (self.model.yeeon) {ctx.stroke();} + } + self.draw2 = function(ctx){ + setPositionAndSizeSingle() - }; + _drawMe(ctx) -} + if (model.drawNameSingleVoter || model.voterGroupCustomNames == "Yes") { + _drawName(model,ctx,self.voterPerson.xArena,self.voterPerson.yArena,self.name) + } + } -// helper method... -var _drawSlices = function(ctx, x, y, size, slices, totalSlices){ + function setPositionAndSizeSingle() { + var s = model.arena.modelToArena(self) + _addAttributes(self.voterPerson, { + xArena:s.x, + yArena:s.y, + size:self.size, + }) + } + function _drawMap (ctx) { + if (model.ballotVis) self.voterModel.drawMap(ctx, self.voterPerson); + } + function _drawBack(ctx) { + var x = self.voterPerson.xArena + var y = self.voterPerson.yArena - // RETINA - x = x*2; - y = y*2; - //size = size*2; + if(self.highlight) var temp = ctx.globalAlpha + if(self.highlight) ctx.globalAlpha = 0.8 + // Background, for showing HOW the decision works... + self.drawBackAnnotation(x*2,y*2,ctx) + if(self.highlight) ctx.globalAlpha = temp + } + function _drawMe(ctx){ + var x = self.voterPerson.xArena + var y = self.voterPerson.yArena + + if (model.arena.mouse.pressed && model.arena.mouse.dragging == self && ! model.doOriginal){ + self.voterPerson.size *= 2 + } + var size = self.voterPerson.size - // GO AROUND THE CLOCK... - var startingAngle = -Math.TAU/4; - var endingAngle = 0; - for(var i=0; i<slices.length; i++){ + if(self.highlight) var temp = ctx.globalAlpha + if(self.highlight) ctx.globalAlpha = 0.8 + // Circle! + if (model.voterIcons == "circle") { + - slice = slices[i]; - // Angle! - var sliceAngle = slice.num * (Math.TAU/totalSlices); - endingAngle = startingAngle+sliceAngle; + // Face! + var scoreTypeMethod = (model.ballotType == "Score" || model.ballotType == "Approval" || model.ballotType == "Three") + if (scoreTypeMethod && model.drawSliceMethod == "circleBunch") { - // Just draw an arc, clockwise. - ctx.fillStyle = slice.fill; - ctx.beginPath(); - ctx.moveTo(x,y); - ctx.arc(x, y, size, startingAngle, endingAngle, false); - ctx.lineTo(x,y); - ctx.closePath(); - ctx.fill(); + _drawRing(ctx,x,y,size) + _simpleCircle(ctx,x*2,y*2,size,'#ccc') + self.voterModel.drawMe(ctx, self.voterPerson, 2) + + } else if (scoreTypeMethod && model.drawSliceMethod == "barChart") { + self.voterModel.drawMe(ctx, self.voterPerson, 2) + } else if (model.ballotType == "Ranked" && model.drawSliceMethod == "barChart") { + self.voterModel.drawMe(ctx, self.voterPerson, 2) + if (model.system != "Borda") { + size = size*2; + ctx.drawImage(self.img, x*2 -size/2, y * 2 -size/2, size, size); + } + } else { + _drawRing(ctx,x,y,size) + self.voterModel.drawMe(ctx, self.voterPerson, 2) + size = size*2; + ctx.drawImage(self.img, x*2 -size/2, y*2-size/2, size, size); + } - // For next time... - startingAngle = endingAngle; + } else if (model.voterIcons == "dots") { + _drawDot(2, x, y, ctx) + } else { + self.voterModel.drawMe(ctx, self.voterPerson, 2) + } + + self.drawAnnotation(x*2,y*2,ctx) + if(self.highlight) ctx.globalAlpha = temp + } + +} - } +function _drawName(model,ctx,x,y,name) { + // Number ID + var textsize = 20 + ctx.textAlign = "center"; - if (self.model.yeeon) { - // Just draw a circle. - ctx.strokeStyle = 'rgb(0,0,0)'; - ctx.lineWidth = 1; // border - ctx.beginPath(); - ctx.arc(x, y, size, 0, Math.TAU, true); - ctx.closePath(); - ctx.stroke(); + if(model.voterGroupCustomNames == "Yes" || model.voterGroups.length != 1) { + _drawStroked(name,x*2+0*textsize,y*2+0*textsize,textsize,ctx); } +} -}; -var _drawBlank = function(ctx, x, y, size){ - var slices = [{ num:1, fill:"#bbb" }]; - _drawSlices(ctx, x, y, size, slices, 1); -}; - +function _findClosestCan(x,y,iDistrict,model) { + + var closest = {id:null}; + var closestDistance = Infinity; + var cans = model.district[iDistrict].stages[model.stage].candidates + for(var c of cans){ + var dist = distF2(model,{x:x,y:y},c) + if(dist<closestDistance){ + closestDistance = dist; + closest = c; + } + } + return closest +} -///////////////////////////////////////// -///////// SINGLE OR GAUSSIAN //////////// -///////////////////////////////////////// +function _drawCircleFill(x,y,size,fill,ctx,model) { + x = x*2; + y = y*2; + ctx.fillStyle = fill; + ctx.strokeStyle = 'rgb(0,0,0)'; + ctx.lineWidth = 1 + + ctx.beginPath() + ctx.arc(x, y, size, 0, Math.TAU, true) + ctx.fill() + if (model.checkDrawCircle()) ctx.stroke() +} -function GaussianVoters(config){ // this config comes from addVoters in main_sandbox +function VoterCenter(model){ var self = this; - Draggable.call(self, config); - - // NUM - self.num = config.num || 3; - self.vid = config.vid || 0; - self.snowman = config.snowman || false; - - // WHAT TYPE? - self.type = new config.type(self.model); - self.setType = function(newType){ - self.type = new newType(self.model); - }; + Draggable.call(self); - self.percentStrategy = config.percentStrategy - self.strategy = config.strategy - self.unstrategic = config.unstrategic - self.preFrontrunnerIds = config.preFrontrunnerIds + // LOAD + self.id = "voterCenter"; + self.isVoterCenter = true; + self.size = 30; + self.points = [[0,0]]; + self.img = new Image(); // use the face + var oldway + if (oldway) { + self.img.src = "play/img/voter_face.png"; + } else { + self.img.src = "play/img/center.png"; + } + self.findVoterCenter = function(){ // calculate the center of the voter groups + // UPDATE + var mean = findMean() + + var method = model.median_mean // 1,2,3 + // 1 is the mean + // 2 is the geometric median + // 3 is a 1-d median along 4 projections + // 4 is a 1-d median along 2 projections + // 5 is 2 medians using the usual median method + + if (method == 1) { + return mean + } else if (method == 5) { + var median = findMedianOrthogonal() + return median + } else { // try to find geometric median ... still thinking about whether this is a good idea. + + if (method == 2) { + var distancemeasure = function(xd,yd) { + return Math.sqrt(xd*xd+yd*yd) + } + } else if (method == 3) { + var distancemeasure = function(xd,yd) { + return Math.abs(xd) + Math.abs(yd) + Math.abs(xd+yd) + Math.abs(xd-yd) + } + } else if (method == 4) { + var distancemeasure = function(xd,yd) { + return Math.abs(xd) + Math.abs(yd)// + Math.abs(xd+yd) + Math.abs(xd-yd) + } + } - // HACK: larger grab area - self.radius = 50; + var median = geometricMedian(mean,distancemeasure) + return median - // SPACINGS, dependent on NUM - var spacings = [0, 12, 12, 12, 12, 20, 30, 50, 100]; - if (self.snowman) { - if (self.vid == 0) { - spacings.splice(3) - } else if (self.vid == 1) { - spacings = [0,12,12,12] - } else if (self.vid == 2) { - spacings.splice(4) + } + } + function findMean() { + var x = 0 + var y = 0 + var totalnumbervoters = 0 + for(var i=0; i<model.voterGroups.length; i++){ + var voterGroup = model.voterGroups[i] + var numbervoters = voterGroup.points.length + x += voterGroup.x * numbervoters + y += voterGroup.y * numbervoters + totalnumbervoters += numbervoters } - //spacings.splice(2+self.vid) - } else if(self.num==1){ - spacings.splice(4); - } else if(self.num==2){ - spacings.splice(5); - } else if (self.num==3){ - spacings = [0, 10, 11, 12, 15, 20, 30, 50, 100]; + x/=totalnumbervoters + y/=totalnumbervoters + return {x:x, y:y} } - // Create 100+ points, in a Gaussian-ish distribution! - var points = [[0,0]]; - self.points = points; - var _radius = 0, - _RINGS = spacings.length; - for(var i=1; i<_RINGS; i++){ - - var spacing = spacings[i]; - _radius += spacing; - - var circum = Math.TAU*_radius; - var num = Math.floor(circum/(spacing-1)); - if (self.snowman && self.vid == 1 && i==3){ - num = 10 + function findMedianOrthogonal() { + xvals = [] + yvals = [] + for(i=0; i<model.voterGroups.length; i++){ + voterGroup = model.voterGroups[i] + for(m=0; m<voterGroup.points.length; m++) { + point = voterGroup.points[m] + xvals.push(point[0]+voterGroup.x) + yvals.push(point[1]+voterGroup.y) + } } + x = median(xvals) + y = median(yvals) + return {x:x, y:y} + } - // HACK TO MAKE IT PRIME - 137 VOTERS - //if(i==_RINGS-1) num += 3; + var median = function(values) { - var err = 0.01; // yeah whatever - for(var angle=0; angle<Math.TAU-err; angle+=Math.TAU/num){ - var x = Math.cos(angle)*_radius; - var y = Math.sin(angle)*_radius; - points.push([x,y]); - } + values.sort( function(a,b) {return a - b;} ); + var half = Math.floor(values.length/2); + + if(values.length % 2) + return values[half]; + else + return (values[half-1] + values[half]) / 2.0; } - // UPDATE! Get all ballots. - self.ballots = []; - self.update = function(){ - self.ballots = []; - - //randomly assign voter strategy based on percentages, but using the same seed each time - // from http://davidbau.com/encode/seedrandom.js - Math.seedrandom('hi'); + function geometricMedian(mean, distancemeasure) { + + var x = mean.x + var y = mean.y + + // first for centers + var d, voterGroup, yv,xv,xd,yd,itnv,moved,xt,yt,j,i,dg,m,point - for(var i=0; i<points.length; i++){ - var p = points[i]; - var x = self.x + p[0]; - var y = self.y + p[1]; - - var r1 = Math.random() * 100; - if (r1 < self.percentStrategy) { - var strategy = self.strategy // yes - } else { - var strategy = self.unstrategic; // no e.g. "zero strategy. judge on an absolute scale." + d = 0 + for(i=0; i<model.voterGroups.length; i++){ + voterGroup = model.voterGroups[i] + xv = voterGroup.x + yv = voterGroup.y + xd = xv - x + yd = yv - y + d += distancemeasure(xd,yd) * voterGroup.voterPeople.length // d is total distance, not average + } + if (0) { + for (var a = 200; a > .1; ) { + xt = [x-a,x+a,x-a,x+a] // try these points + yt = [y-a,y-a,y+a,y+a] + moved = false + for (j in xt) { + xg = xt[j] // the guess + yg = yt[j] + // calculate distance + dg=0 + for(i=0; i<model.voterGroups.length; i++){ + voterGroup = model.voterGroups[i] + xv = voterGroup.x + yv = voterGroup.y + xd = xv - xg + yd = yv - yg + dg += distancemeasure(xd,yd) * voterGroup.voterPeople.length + } + if (dg < d) { // we found a better point + d=dg * 1 + x=xg * 1 + y=yg * 1 + moved = true + } + } + if(!moved) a*=.5 } - - var ballot = self.type.getBallot(x, y, strategy); - self.ballots.push(ballot); } - }; + // now we do it again for all the individual points within the voter group + + var voterPeople = model.voterSet.allVoters + var guess = {x:x, y:y} + return numericApproxGeometricMedian(voterPeople, guess, distancemeasure) + } - // DRAW! - self.draw = function(ctx){ - // DRAW ALL THE POINTS - for(var i=0; i<points.length; i++){ - var p = points[i]; - var x = self.x + p[0]; - var y = self.y + p[1]; - var ballot = self.ballots[i]; - self.type.drawCircle(ctx, x, y, 10, ballot); + self.update = function() {// do the center voter thing + // UPDATE + var recenter = self.findVoterCenter() + self.x = recenter.x + self.y = recenter.y + } + self.drag = function() { + var oldcenter = self.findVoterCenter() + var changecenter = {x:self.x - oldcenter.x, y:self.y - oldcenter.y} + for(var i=0; i<model.voterGroups.length; i++){ + model.voterGroups[i].x += changecenter.x + model.voterGroups[i].y += changecenter.y } + } - }; - -} - -function SingleVoter(config){ - - var self = this; - Draggable.call(self, config); - - - // not sure if we need all these, but just in case - self.num = 1; - self.vid = 0; - self.snowman = false; - self.percentStrategy = config.percentStrategy - self.strategy = config.strategy - self.unstrategic = config.unstrategic - self.preFrontrunnerIds = config.preFrontrunnerIds - - - // WHAT TYPE? - self.type = new config.type(self.model); - self.setType = function(newType){ - self.type = new newType(self.model); - }; + // DRAW! + self.drawBackAnnotation = function(x,y,ctx) {} + self.drawAnnotation = function(x,y,ctx) {}; // TO IMPLEMENT + self.draw = function(ctx){ + // UPDATE + var s = model.arena.modelToArena(self) + var x = s.x*2; + var y = s.y*2; + size = self.size + if(self.highlight) var temp = ctx.globalAlpha + if(self.highlight) ctx.globalAlpha = 0.8 + + if (model.voterIcons != "off") { + + self.drawBackAnnotation(x,y,ctx) + if (model.voterIcons == "dots") { + _drawDot(5, s.x, s.y, ctx) + } else { + if (oldway) { + _drawBlank(model, ctx, s.x, s.y, size); + _drawRing(ctx,s.x,s.y,self.size) + + // Face! + size = size*2; + ctx.drawImage(self.img, x-size/2, y-size/2, size, size); + + } else { + size = size*2; - // Image! - self.img = new Image(); - self.img.src = "img/voter_face.png"; + // drawArrows(ctx,x/2,y/2,size/2) + // _drawThickRing(ctx,x/2,y/2,size/2 * .5) + _drawThickRing(ctx,x/2,y/2,size/2 * .7) + // ctx.drawImage(self.img, x-size/2, y-size/2, size, size); - self.points = [[0,0]]; + } + } + self.drawAnnotation(x,y,ctx) + } - // UPDATE! - self.ballot = null; - self.ballots = []; - self.update = function(){ - self.ballot = self.type.getBallot(self.x, self.y, self.strategy); - self.ballots = [self.ballot] + if(self.highlight) ctx.globalAlpha = temp }; - // DRAW! - self.draw = function(ctx){ - - // Background, for showing HOW the decision works... - self.type.drawBG(ctx, self.x, self.y, self.ballot); +} - // Circle! - var size = 20; - self.type.drawCircle(ctx, self.x, self.y, size, self.ballot); +function VoterManager(model) { + var self = this + self.initVoters = function() { + for (let i = 0; i < model.voterGroups.length; i++) { + const voterGroup = model.voterGroups[i]; + voterGroup.init() + voterGroup.initVoterName(i) + } + } + self.initNames = function() { + for (let i = 0; i < model.voterGroups.length; i++) { + const voterGroup = model.voterGroups[i]; + voterGroup.initVoterName(i) + } + } + self.onDeleteVoterGroup = () => null // hook for plug in +} - // Face! - size = size*2; - var x = self.x*2; - var y = self.y*2; - ctx.drawImage(self.img, x-size/2, y-size/2, size, size); +function numericApproxGeometricMedian(voterPeople, guess, distancemeasure) { + var x = guess.x + var y = guess.y - }; + d=0 + for (var voterPerson of voterPeople) { + xv = voterPerson.x + yv = voterPerson.y + xd = xv - x + yd = yv - y + d += distancemeasure(xd,yd) + } + for (var a = 200; a > .1; ) { + xt = [x-a,x+a,x-a,x+a] // try these points + yt = [y-a,y-a,y+a,y+a] + moved = false + for (j in xt) { + xg = xt[j] // the guess + yg = yt[j] + // calculate distance + dg=0 + for (var voterPerson of voterPeople) { + xv = voterPerson.x + yv = voterPerson.y + xd = xv - xg + yd = yv - yg + dg += distancemeasure(xd,yd) + } + if (dg < d) { // we found a better point + d=dg * 1 + x=xg * 1 + y=yg * 1 + moved = true + } + } + if(!moved) a*=.5 + } + return {x:x, y:y} } diff --git a/play/js/Yee.js b/play/js/Yee.js new file mode 100644 index 00000000..fc5cdf9a --- /dev/null +++ b/play/js/Yee.js @@ -0,0 +1,1267 @@ + +function Viz(model) { + // There are four places where drawings go. + // Viz + // Arena + // Candidates + // Voters + + + var self = this + + var yee = new Yee(model) + var beatMap = new BeatMap(model) + var voterMapGPU = new VoterMapGPU(model) + voterMapGPU.init() + self.yee = yee + self.beatMap = beatMap + self.medianDistViz = new MedianDistViz(model) + self.lpAssignmentsViz = new LpAssignmentsViz(model) + + self.calculateBeforeElection = function() { + + // calculate yee if its turned on and we haven't already calculated it ( we aren't dragging the yee object) + if (model.yeeon) { + var draggingYeeObject = model.yeeobject != undefined && (model.arena.mouse.dragging === model.yeeobject || model.tarena.mouse.dragging === model.yeeobject) + var voterCenterIsYeeObject = model.yeeobject != undefined && model.voterCenter === model.yeeobject + var onlyOneVoterGroup = model.voterGroups.length == 1 + var draggingVoterGroup = (model.arena.mouse.dragging && model.arena.mouse.dragging.isVoter) || (model.tarena.mouse.dragging && model.tarena.mouse.dragging.isVoter) + if (draggingYeeObject || (voterCenterIsYeeObject && onlyOneVoterGroup && draggingVoterGroup)) { + // dragging the yee object, so no need to recalculate, we can save time... + // unless we wanted to calculate one of these: + if (model.kindayee == 'newcan') { + yee.calculateYee() + } + } else { + // something caused an update and we aren't dragging the yee object + yee.calculateYee() + } + } + + } + + self.calculateAfterElection = function() { + + if (model.checkDoBeatMap()) { + beatMap.calculateBeatMap() + } + + if (model.doVoterMapGPU) { + voterMapGPU.calculateVoterMapGPU() + } + + } + + self.drawBackground = function() { + + if (model.yeeon) { + yee.drawBackgroundYee() + } + + if (model.checkDoBeatMap()) { + beatMap.drawBackgroundBeatMap() + } + + if (model.doVoterMapGPU) { + voterMapGPU.drawVoterMapGPU() + } + + if (model.doMedianDistViz) { + self.medianDistViz.drawMedianDistViz() + } + + model.doLpAssignmentsViz = model.system == "PhragmenMax" || model.system == "equalFacilityLocation" || model.system == "PAV" + if (model.doLpAssignmentsViz) { + self.lpAssignmentsViz.drawLpAssignmentsViz() + } + } + +} + +function Yee(model) { + var self = this + + + var colorNewCan = 'hsl(0,100%,100%)' + var colorNewCan = 'hsl(0,0%,0%)' + var colorNewCan = '#ccc' + + self.calculateYee = function(){ + var ctx = model.arena.ctx + // model.pixelsize= 30.0; + var pixelsize = model.pixelsize; + var WIDTH = ctx.canvas.width; + var HEIGHT = ctx.canvas.height; + var doArrayWay = model.computeMethod != "ez" + var doB = (model.dimensions == "1D+B" && (model.kindayee == "newcan" || (model.yeeobject && model.yeeobject.isCandidate))) + var winners + + // if we are considering a potential candidate, then add it + var doNewCan = (model.kindayee == "newcan") + if (doNewCan) { + var newway = true + if (newway) { + + model.yeeon = false + var doDummy = true + model.arena.plusCandidate.doPlus(doDummy) + model.yeeon = true + + model.initMODEL() + model.yeeobject = model.candidates[model.candidates.length - 1] + model.yeeobject.fill = colorNewCan + + model.dm.redistrict() + } else { + var nc = new Candidate(model) + model.candidates.push(nc) + // CONFIGURE + // Object.assign( nc,{x:153, y: 95} ) + // , icon:"newdude" + // INIT + nc.init() + nc.icon = "newdude" + model.initMODEL() + model.dm.redistrict() + // UPDATE + nc.fill = 'hsl(0,100%,100%)' + model.yeeobject = nc + + } + } + + + + if (doArrayWay) { // note that voterCenter is not yet implemented in the array way. Only if "ez" is selected will the yee diagram work + // Also note that the broadness representation hasn't been added to this method yet. + + // put candidate information into arrays + var canAid = [], xc = [], yc = [], fillc = [] //, canA = [], revCan = {} // candidates + var f=[] // , fA = [], fAid = [], xf = [], yf = [], fillf = [] // frontrunners + var movethisidx, whichtypetomove + var i = 0 + for (var can in model.candidatesById) { + var c = model.candidatesById[can] + canAid.push(can) + // canA.push(c) + // revCan[c] = i + xc.push(c.x*2) // remember the 2 + yc.push(c.y*2) + fillc.push(c.fill) + if (model.preFrontrunnerIds.includes(c.id)) { + // fAid.push(can) + // fA.push(c) + f.push(i) + // xf.push(c.x*2) + // yf.push(c.y*2) + // fillf.push(c.fill) // maybe don't need + } + if (model.yeeobject == c){ + movethisidx = i + whichtypetomove = "candidate" + } + i++ + } + // now we have xc,yc,fillc,xf,yf + // maybe we don't need fillf, fA, canA, canAid, fAid, but they might help + + // put voter information into arrays + var av = [], xv = [], yv = [] , vg = [] , xvcenter = [] , yvcenter = []// candidates + var movethisidx, whichtypetomove + var i = 0 + for (var vidx in model.voterGroups) { + var v = model.voterGroups[vidx] + av.push(v) + xvcenter.push(v.x*2) + yvcenter.push(v.y*2) + if (model.yeeobject == v){ + movethisidx = i + whichtypetomove = "voter" + } + for (var j in v.points) { + var p = v.points[j] + xv.push((p[0] + v.x)*2) + yv.push((p[1] + v.y)*2) + vg.push(i) + } + i++ + } + + if (model.yeeobject == model.voterCenter) { // this is just a workaround until I get around to implementing this in the gpu and js methods of computing the yee diagram + movethisidx = 0 + whichtypetomove = "voter" + } + + // now we have xv,yv, + // we might not need av + + // need to compile yee and decide when to recompile + // basically the only reason to recompile is when the number of voters or candidates changes + + var lv = xv.length + var lc = xc.length + model.fastyeesettings = [lc,lv,WIDTH,HEIGHT,pixelsize] + function arraysEqual(arr1, arr2) { + arr1 = arr1 || [0] + arr2 = arr2 || [0] + if(arr1.length !== arr2.length) + return false; + for(var i = arr1.length; i--;) { + if(arr1[i] !== arr2[i]) + return false; + } + + return true; + } + var recompileyee = !arraysEqual(model.fastyeesettings,model.oldfastyeesettings) + //(model.fastyeesettings || 0) != (model.oldfastyeesettings || 0)) + model.oldfastyeesettings = model.fastyeesettings + if (recompileyee) { + fastyee = createKernelYee(lc,lv,WIDTH,HEIGHT,pixelsize) + } + //method = "gpu" + //method = "js" + method = model.computeMethod + winners = fastyee(xc,yc,f,xv,yv,vg,xvcenter,yvcenter,movethisidx,whichtypetomove,method) + + } + model.gridx = []; + model.gridy = []; + model.gridl = []; + model.gridb = []; + saveo = {} + + + saveo.x = model.yeeobject.x; + if (doB) { + saveo.b = model.yeeobject.b; + } else { + saveo.y = model.yeeobject.y; + } + if (model.yeeobject == model.voterCenter) { + var voterso = [] + for(var i=0; i<model.voterGroups.length; i++){ + voterso[i] = {} + voterso[i].x = model.voterGroups[i].x + voterso[i].y = model.voterGroups[i].y + } + } + + // model.hexgrid + // make grid + + if (model.theme == "Bees") { + model.grid = "hexagon" + } else if (model.dimensions == "1D") { + model.grid = "linear" + } else { + model.grid = "square" + } + if (model.grid == "square") { // square grid + for(var x=.5*pixelsize ; x<=WIDTH; x+= pixelsize) { + for(var y=.5*pixelsize; y<=HEIGHT; y+= pixelsize) { + model.gridx.push(x); + model.gridy.push(y); + } + } + } else if (model.grid == "linear") { + var y=.5*pixelsize + for(var x=.5*pixelsize ; x<=WIDTH; x+= pixelsize) { + model.gridx.push(x); + model.gridy.push(y); + } + } else { // hex grid + model.gridType = "horizontal hexagon" + if (model.gridType == "horizontal") { + var grids = hexgrid(pixelsize, HEIGHT, WIDTH) + model.gridx = grids.w + model.gridy = grids.h + } + else if (model.gridType == "vertical") { + var grids = hexgrid(pixelsize, WIDTH, HEIGHT) + model.gridx = grids.h + model.gridy = grids.w + } else if (model.gridType == "horizontal hexagon") { + var grids = hexagon_hexgrid(pixelsize, HEIGHT, WIDTH) + model.gridx = grids.w + model.gridy = grids.h + } + else if (model.gridType == "vertical hexagon") { + var grids = hexagon_hexgrid(pixelsize, WIDTH, HEIGHT) + model.gridx = grids.h + model.gridy = grids.w + } + function hexgrid(pixelsize, HEIGHT, WIDTH) { + var w = [] + var h = [] + var hexHeight = pixelsize + var hexWidth = pixelsize * 2/Math.sqrt(3) + var hexSide = pixelsize * .5 + var row = 0 + for( var y = hexHeight * .5; y + hexHeight <= HEIGHT; y += hexHeight / 2, row++) { + if (row % 2 == 1) { + var offset = (hexWidth - hexSide)/2 + hexSide + var col = 1 + } else { + var offset = 0.0 + var col = 0 + } + for ( var x=offset + hexWidth*.5; x+hexWidth <= WIDTH; x += hexWidth + hexSide, col += 2) { + w.push(x); + h.push(y); + } + } + return {w:w, h:h} + } + function hexagon_hexgrid(pixelsize, HEIGHT, WIDTH) { + var w = [] + var h = [] + var hexHeight = pixelsize + var hexWidth = pixelsize * 2/Math.sqrt(3) + var hexSide = pixelsize * .5 + + // default is horizontal flat-top Hexagons + + // how many rings of hexagons fit into the total width? + var numWidth = Math.floor( (WIDTH - hexWidth) / (hexWidth + hexSide) ) + // how many rings of hexagons fit into the total height? + var numHeight = Math.floor( (HEIGHT - hexHeight) / (2*hexHeight) ) + + // how many rings should we make? + var n = Math.min(numWidth,numHeight) + + var centerX = Math.floor(WIDTH/2) + var centerY = Math.floor(HEIGHT/2) + for (var i = -n; i <= n; i++) { + var iPos = Math.abs(i) + for (var j = -(2*n-iPos); j <= (2*n-iPos); j+=2) { + var x = j * hexHeight/2 + centerX + var y = i * (hexWidth+hexSide) / 2 + centerY + w.push(x); + h.push(y); + } + } + return {w:w, h:h} + } + } + + + // get winners + for (var i=0; i < model.gridx.length; i++) { + var x = model.gridx[i] + var y = model.gridy[i] + if (doArrayWay) { + var winner = Math.round(winners[i]) + if (winner > lc) { // we have a set of winners to decode + //winner = 3 + lc* (2+lc*(4)) + //var decode = function (winner) { + wl = [] + for (var s = 0; s < lc; s++) { + if (winner <= lc) {break} + wl.push(winner % lc) + winner = Math.floor(winner / lc) + } + wl.push(winner) + // return wl + //} + colorlist = [] + for (w in wl) {colorlist.push(model.candidatesById[canAid[wl[w]] || "square"].fill)} + model.gridb[i] = colorlist + var a = "#ccc" // grey is actually a code for "look for more colors" + } else { + var a = model.candidatesById[canAid[winner] || "square"].fill + } + // if (a == "#ccc") {a = "#ddd"} // hack for now, but will deal with ties later + model.gridl.push(a); + continue; + } + model.yeeobject.x = x * .5; + if (doB) { + model.yeeobject.b = model.arena.bFromY(y * .5) + } else { + model.yeeobject.y = y * .5; + } + // update positions of all the voters if the voterCenter is the yee object + if (model.yeeobject == model.voterCenter) { + var changecenter = { + x:model.yeeobject.x - saveo.x, + y:model.yeeobject.y - saveo.y + } + for(var j=0; j<model.voterGroups.length; j++){ + model.voterGroups[j].x = voterso[j].x + changecenter.x + model.voterGroups[j].y = voterso[j].y + changecenter.y + } + } + if (model.yeeobject.isVoter) { + model.dm.redistrict() + } + if (model.yeeobject.isCandidate) { + model.dm.redistrictCandidates() + } + + for(var j=0; j<model.voterGroups.length; j++){ + model.voterGroups[j].updatePeople(); + } + if (model.nDistricts > 1) { + + + // put all the results together + result = { + colors: [] + } + for (var k = 0; k < model.nDistricts; k++) { + result_k = model.election( model.district[k], model, model.optionsForElection ); + if (result) { + result.colors = [].concat(result.colors , result_k.colors) + } + + } + if (result.colors.length > 1) { + result.color = "#ccc" + } else { + result.color = result.colors[0] + } + } else { + var result = model.election(model.district[0], model, {sidebar:false, yeefast:true}); + } + + + model.gridl.push(result.color); + model.gridb.push(result.colors) + // model.caption.innerHTML = "Calculating " + Math.round(x/WIDTH*100) + "%"; // doesn't work yet + } + model.yeeobject.x = saveo.x; + if (doB) { + model.yeeobject.b = saveo.b; + } else { + model.yeeobject.y = saveo.y; + } + if (model.yeeobject == model.voterCenter) { + for(var i=0; i<model.voterGroups.length; i++){ + model.voterGroups[i].x = voterso[i].x + model.voterGroups[i].y = voterso[i].y + } + } + + + if (doNewCan) { + model.candidates.pop() + if (newway) { + + model.initMODEL() + + // update the GUI + model.onAddCandidate() + + model.dm.redistrict() + model.yeeobject = undefined + + } else { + + model.dm.redistrict() + // UPDATE + // model.update() + + } + } + for(var voterGroup of model.voterGroups){ + voterGroup.updatePeople() + } + } + + self.winSeek = function(can) { + + // this function will find the closest point where a candidate wins. + + var temp = {o: model.yeeobject, + b:model.gridb, + x:model.gridx, + y:model.gridy, + l:model.gridl + } + var xMe = can.x + var yMe = can.y + + // do calculations + model.yeeobject = can + self.calculateYee() + + // find closest winning point + var colorMe = can.fill + var dist + var minDist = Infinity + var kGoal + for(var k=0;k<model.gridl.length;k++) { + var ca = model.gridl[k] + var check = false + if (ca=="#ccc") { // make stripes instead of gray + var cb = model.gridb[k] + if (cb.includes(colorMe)) { + check = true + } + } else { + if (colorMe == ca) + check = true + } + if (check) { + dist = d2(model.gridx[k]-xMe, model.gridy[k]-yMe) + if (dist < minDist) { + minDist = dist + kGoal = k + } + } + } + function d2(x,y) { + return x**2 + y**2 + } + + if(kGoal != undefined) { + var goal = { + x:model.gridx[kGoal], + y:model.gridy[kGoal] + } + } else { + var goal = undefined + } + + + model.yeeobject = temp.o + model.gridb=temp.b + model.gridx=temp.x + model.gridy=temp.y + model.gridl=temp.l + + return goal + } + + self.drawBackgroundYee = function() { + var arena = model.arena + var ctx = arena.ctx + + if(model.yeeon){ + var temp = ctx.globalAlpha + ctx.globalAlpha = 1 + ctx.fillStyle = "#fff" + // ctx.fillStyle = "#ccc" + // ctx.fillStyle = "#333" + ctx.fillRect(0,0,ctx.canvas.width,ctx.canvas.height) // draw a white background + ctx.fill() + ctx.globalAlpha = .9 + var pixelsize = model.pixelsize; + + var can_filter_yee = [] + for(var id in model.yeefilter) { + if (model.yeefilter[id]) { + can_filter_yee.push(id) + } + } + var method_1 = (Election.stv == model.election) || (Election.rrv == model.election) // two methods for filtering colors in the yee diagram + if (method_1) { + color_filter_yee = can_filter_yee.map(x => model.candidatesById[x].fill) + if (model.kindayee=='newcan') color_filter_yee.push(colorNewCan) + } else { + translate = {} + for(can in model.candidatesById) { + var colorcan = model.candidatesById[can].fill + translate[colorcan] = can_filter_yee.includes(can) ? colorcan : 'white' + } + if (model.kindayee=='newcan') translate[colorNewCan] = colorNewCan + } + var HEIGHT = ctx.canvas.height + + for(var k=0;k<model.gridx.length;k++) { + var ca = model.gridl[k] + + if (ca=="#ccc" && model.computeMethod != "gpu" && model.computeMethod != "js") { // make stripes instead of gray // I'm not sure why this part didn't work for the other compute methods + var cb = model.gridb[k] + if (method_1) { + cb = cb.filter(function(x) {return color_filter_yee.includes(x)} )// filter the colors so that only the selected colors are displayed + if (cb.length == 0) cb = ['white'] + } else { + cb = cb.map(x => translate[x]) + } + var xb = model.gridx[k]-pixelsize*.5 + var yb = model.gridy[k]-pixelsize*.5 + var wb = pixelsize + var hb = pixelsize + var hh = pixelsize / 6; // height of stripe // used to be 5 + var numstripes = pixelsize/hh + if (model.grid == "linear") numstripes = HEIGHT / hh + for (var j=0; j< numstripes; j++) { + ctx.fillStyle = cb[j % cb.length] + ctx.fillRect(xb,yb+j*hh,wb,hh); + } + } else { + if (method_1) { + if (color_filter_yee.includes(ca)) { + ctx.fillStyle = ca; + } else { + ctx.fillStyle = 'white'; + } + } else { + ctx.fillStyle = translate[ca] + } + if (model.grid == "square") { + ctx.fillRect(model.gridx[k]-pixelsize*.5, model.gridy[k]-pixelsize*.5, pixelsize, pixelsize); + } else if (model.grid == "linear") { + ctx.fillRect(model.gridx[k]-pixelsize*.5, 0, pixelsize, HEIGHT); + } else { + // var size = pixelsize / Math.sqrt(3) + var size = pixelsize / 2 + var x = model.gridx[k] + var y = model.gridy[k] + self.drawHexagon(x,y,size,ctx) + } + } + } + ctx.globalAlpha = temp + // Draw axes + //var background = new Image(); + //background.src = "../play/img/axis.png"; + // ctx.drawImage(background,0,0); // eh, I don't like the axis. + } + + } + + self.drawHexagon = function(x,y,size,ctx) { + ctx.beginPath(); + ctx.moveTo(x + size * Math.cos(0), y + size * Math.sin(0)); + + for (var side = 0.5; side < 7.5; side++) { + ctx.lineTo(x + size * Math.cos(side * 2 * Math.PI / 6), y + size * Math.sin(side * 2 * Math.PI / 6)); + } + // ctx.fillStyle = fill; + ctx.fill(); + } + + self.drawYeeGuyBackground = function(x,y,ctx){ + + // put circle behind yee candidate + if(model.yeeon){ + ctx.beginPath(); + ctx.arc(x, y, 60, 0, Math.TAU, true); + ctx.strokeStyle = "white"; + ctx.lineWidth = 8; + ctx.fillStyle = 'white'; + var temp = ctx.globalAlpha + ctx.globalAlpha = 0.3 + ctx.fill(); + ctx.stroke(); + ctx.globalAlpha = temp + } + } + + self.drawYeeAnnotation = function(x,y,ctx) { + + // make the candidate that is moving say "yee-yee!" + if(model.yeeon){ + ctx.textAlign = "center"; + var temp = ctx.globalAlpha + ctx.globalAlpha = 0.9 + _drawStroked("yee-yee!",x,y+50,40,ctx); + var dot = 3 + ctx.fillStyle = "#000" + ctx.fillRect(x-dot-1,y-dot-1,dot*2+2,dot*2+2); + ctx.fillStyle = "#fff" + ctx.fillRect(x-dot,y-dot,dot*2,dot*2); + ctx.globalAlpha = temp + } + } + +} + + + +function BeatMap(model) { + var self = this + + // calculate Condorcet decision boundaries + + // calculate medians in all directions + // detail: even number + // detail: counterclockwise + + // calculate winner circle against each candidate + + // The data is a list of points that connect to form a circle. This is calcualted per candidate. + + + self.calculateBeatMap = function() { + // calculate medians + + var median = function(values) { + + values.sort( function(a,b) {return a - b;} ); + + var half = Math.floor(values.length/2); + + if(values.length % 2) + return values[half]; + else + return (values[half-1] + values[half]) / 2.0; + } + xvals = [] + yvals = [] + for(i=0; i<model.voterGroups.length; i++){ + voter = model.voterGroups[i] + for(m=0; m<voter.points.length; m++) { + point = voter.points[m] + xvals.push(point[0]+voter.x) + yvals.push(point[1]+voter.y) + } + } + var xCenter = model.voterCenter.x + var yCenter = model.voterCenter.y + var medians = [] + var angles = [] + var xMedians = [] + var yMedians = [] + var xCenteredMedians = [] + var yCenteredMedians = [] + + beatCircle = {} + beatCircle.x = {} + beatCircle.y = {} + for( var j = 0; j < model.candidates.length; j++) { + var can = model.candidates[j] + beatCircle.x[can.id] = [] + beatCircle.y[can.id] = [] + } + + for (var angle = 0; angle <= 180; angle+= .5) { + angles.push(angle) + var angleR = angle * Math.PI / 180 + var s = Math.sin(angleR) + var c = Math.cos(angleR) + var dot = [] + for( var j = 0; j < xvals.length; j++) { + x = xvals[j] + y = yvals[j] + dot.push( c * x + s * y) + } + var m = median(dot) + medians.push(m) + + // for (var i = 0; i < angles.length; i++) { + // calculate medians diagram + // median point + var xMedian = m * c + var yMedian = m * s + // perpendicular is (s, -c) + // project center along perpendicular (perp dot center) + var proj = s * xCenter - c * yCenter + // calculate change vector + var xChange = proj * s + var yChange = proj * -c + // add projected length to the median point + var xCenteredMedian = xMedian + xChange + var yCenteredMedian = yMedian + yChange + xMedians.push(xMedian) + yMedians.push(yMedian) + xCenteredMedians.push(xCenteredMedian) + yCenteredMedians.push(yCenteredMedian) + + // calculate beat circles + for( var j = 0; j < model.candidates.length; j++) { + var can = model.candidates[j] + if (0) { + var xMove = (m * c - can.x) * 2 + var yMove = (m * c - can.y) * 2 + } else { + var canProj = c * can.x + s * can.y + var xMove = (m - canProj) * c * 2 + var yMove = (m - canProj) * s * 2 + } + beatCircle.x[can.id].push(can.x + xMove) + beatCircle.y[can.id].push(can.y + yMove) + } + } + model.beatCircle = beatCircle + return + } + + self.drawBackgroundBeatMap = function () { + var arena = model.arena + var ctx = arena.ctx + var temp = ctx.globalAlpha + var tempComposite = ctx.globalCompositeOperation + var tempLinewidth = ctx.lineWidth + + // ctx.globalAlpha = 1 + // ctx.fillStyle = "#fff" + // ctx.fillRect(0,0,ctx.canvas.width,ctx.canvas.height) // draw a white background + // ctx.fill() + + var can_filter_yee = [] + for(var id in model.yeefilter) { + if (model.yeefilter[id]) { + can_filter_yee.push(id) + } + } + + + + ctx.globalAlpha = .3 + + + // draw the beatcircles + ctx.globalCompositeOperation = "multiply" + // ctx.globalCompositeOperation = "screen" + // ctx.globalCompositeOperation = "overlay" + // ctx.globalCompositeOperation = "hue" + // ctx.globalCompositeOperation = "lighter" + // ctx.globalCompositeOperation = "darker" // compatibility issues + // ctx.globalCompositeOperation = "lighten" + // ctx.globalCompositeOperation = "darken" + + ctx.lineWidth = 1 + + for (var i = 0; i < can_filter_yee.length; i++) { + var cid = can_filter_yee[i] + x = model.beatCircle.x[cid] + y = model.beatCircle.y[cid] + var bc = new Path2D() + + bc.moveTo(x[0]*2,y[0]*2) + for (var j=1; j < x.length; j++) { + bc.lineTo(x[j]*2,y[j]*2) + } + var fillCircle = true + if (fillCircle) { + bc.rect(0,0,ctx.canvas.width,ctx.canvas.height) + ctx.fillStyle = model.candidatesById[cid].fill; + ctx.closePath() + ctx.fill(bc,"evenodd") + if (1) { + ctx.strokeStyle = model.candidatesById[cid].fill; + ctx.stroke(bc) + } + } else { + // ctx.lineWidth = 8; + ctx.strokeStyle = model.candidatesById[cid].fill; + ctx.stroke(bc) + + } + } + ctx.globalCompositeOperation = tempComposite + ctx.globalAlpha = temp + } +} + +VoterMapGPU = function(model) { + var self = this + var backup = false + self.init = function(){ + + self.canvasGPU = document.createElement('canvas') + if (backup) return + + var ProcessorType = "gpu" + self.gpu = new GPU({ + processor: ProcessorType, + canvas: self.canvasGPU, + }); + + self.oldNumValues = undefined + + // document.body.appendChild(self.canvasGPU); + // var width = model.size + // var height = model.size + // canvasGPU.width = width*2; + // canvasGPU.height = height*2; + // canvasGPU.style.width = width+"px"; + // canvasGPU.style.height = height+"px"; + + } + self.calculateVoterMapGPU = function() { + // self.flag = true // flag to do calculation + // self.renderGPU() + // } + // self.renderGPU = function() { + // if (self.flag = false) return + // self.flag = false + + if (model.ballotType == "Plurality" || model.system == "IRV" || model.system == "STV") return + if (model.voterSet.totalVoters == 0) return + if (model.voterSet.allVoters[0].stages[model.stage].ballot == undefined) return + + // need some calculations + for(var voterGroup of model.voterGroups) { + for(var voterPerson of voterGroup.voterPeople){ + voterGroup.voterModel.drawMap(model.arena.ctx, voterPerson) + } + } + + // required to be constant to compile kernel, I think + var numVoters = model.voterSet.totalVoters + + var width = model.size * 2 + var height = model.size * 2 + + var xpos1 = model.voterSet.getArrayAttr('x') + var ypos1 = model.voterSet.getArrayAttr('y') + var rad1 = model.voterSet.getArrayAttr('rad') + var idxCan1 = model.voterSet.getArrayAttr('idxCan') + + // for multiple radii per voter + var xpos = [] + var ypos = [] + var rad = [] + var idxCan = [] + for (var i = 0; i < numVoters; i++) { + for (var k = 0; k < rad1[i].length; k++) { + xpos.push(xpos1[i]) + ypos.push(ypos1[i]) + rad.push(rad1[i][k]) + idxCan.push(idxCan1[i][k]) + } + } + + var numCircles = rad1[0].length + var numValues = numVoters * numCircles + var numValues = xpos.length + + + // candidate color list + var colors = [] + for (var i = 0; i < model.candidates.length; i++) { + colors.push(model.candidates[i].fill) + } + if (model.ballotType == "Score" || model.ballotType == "Approval" || model.ballotType == "Three" || model.system == "Borda") { + colors[0] = "#000" + } + colorData = getColorScale(colors) // just a list of colors in an array + + if (backup) { + doBackup() + return + } + + var changedVotingModel = false + var changedNumValues = (numValues != self.oldNumValues) + + if (changedVotingModel || changedNumValues) { + Acompile() + self.oldNumValues = numValues + } + self.render( + xpos, + ypos, + idxCan, + colorData, + rad, + ); + + + function Acompile() { + var theKernel = function ( + xpos, + ypos, + idxCan, + colorData, + rad, + ) { + var dist = 0; + var r = 0.0; + var g = 0.0; + var b = 0.0; + + for (var i = 0; i < this.constants.numPoints; i++) { + var x = this.thread.x - xpos[i] * 2, + y = this.thread.y - ( 600 - ypos[i] * 2); + + dist = Math.sqrt(x * x + y * y); + + if (dist > rad[i] * 2) { + + var c = idxCan[i] + + r = r + (colorData[c * 4] / 255) **2 / this.constants.numPoints; + g = g + (colorData[1 + c * 4] / 255) **2 / this.constants.numPoints; + b = b + (colorData[2 + c * 4] / 255) **2 / this.constants.numPoints; + } else { // white + r = r + 1 / this.constants.numPoints; + g = g + 1 / this.constants.numPoints; + b = b + 1 / this.constants.numPoints; + } + this.color(Math.sqrt(r),Math.sqrt(g),Math.sqrt(b),1) + + } + } + + self.render = self.gpu.createKernel(theKernel) + .setConstants({ + numPoints: numValues + }) + .setOutput([width, height]) + .setGraphical(true); + } + function Bcompile() { + var c1 = 1/numValues + var c2 = 1/255**2 /numValues + var theKernel = function ( + xpos, + ypos, + idxCan, + colorData, + rad, + ) { + // var sum = this.vec4(0,0,0,1) // maybe + var r = 0.0; + var g = 0.0; + var b = 0.0; + + for (var i = 0; i < this.constants.numPoints; i++) { + var x = this.thread.x - xpos[i] * 2, + y = this.thread.y - ( 600 - ypos[i] * 2); + + if (x * x + y * y > (rad[i] * 2)**2 ) { + + var c = idxCan[i] + + r += this.constants.c2 * colorData[c * 4] ** 2 + g += this.constants.c2 * colorData[1 + c * 4] ** 2 + b += this.constants.c2 * colorData[2 + c * 4] ** 2 + } else { // white + r += this.constants.c1 + g += this.constants.c1 + b += this.constants.c1 + } + + } + this.color(Math.sqrt(r),Math.sqrt(g),Math.sqrt(b)) + + } + self.render = self.gpu.createKernel(theKernel) + .setConstants({ + numPoints: numValues, + c1: c1, + c2: c2, + }) + .setOutput([width, height]) + .setGraphical(true); + + } + function doBackup () { + // doesn't work because rgb values are too small + // transparency values are too small too I guess + + self.canvasGPU.width = width + self.canvasGPU.height = height + var ctx = self.canvasGPU.getContext('2d') + ctx.save() + ctx.fillStyle = "black" + ctx.fillRect(0,0,width,height) + ctx.globalCompositeOperation = "source-over" + var c1 = 1/numValues + // var c2 = 1/255**2 /numValues + var c2 = 1/255 /numValues + ctx.globalAlpha = c1 + var insideColor = 'white' + // var insideColor = `rgba(${c1},${c1},${c1},1)` + for (var i = 0; i < numValues; i++) { + + // outside color + var c = idxCan[i] + ctx.fillStyle = colors[c] + // r2 = c2 * colorData[c * 4] ** 2 + // g2 = c2 * colorData[1 + c * 4] ** 2 + // b2 = c2 * colorData[2 + c * 4] ** 2 + // r2 = c2 * colorData[c * 4] + // g2 = c2 * colorData[1 + c * 4] + // b2 = c2 * colorData[2 + c * 4] + // ctx.fillStyle = `rgba(${r2},${g2},${b2},1)` + ctx.beginPath() + ctx.arc(xpos[i]*2, ypos[i]*2, rad[i]*2, 0, Math.TAU, false) + ctx.rect(0,0,width,height) + ctx.closePath() + ctx.fill('evenodd') + // inside color + ctx.fillStyle = insideColor + ctx.beginPath() + ctx.arc(xpos[i]*2, ypos[i]*2, rad[i]*2, 0, Math.TAU, false) + ctx.closePath() + ctx.fill() + + } + ctx.restore() + } + } + + self.drawVoterMapGPU = function () { + // self.renderGPU() // only runs if update was called (flag) + + var arena = model.arena + var ctx = arena.ctx + ctx.save() + + ctx.globalAlpha = .8 + ctx.drawImage(self.canvasGPU, 0, 0); + + ctx.restore() + + } + + var getColorScale = function (colors) { + var canvasColorScale = document.createElement("canvas"); + canvasColorScale.width = 256; + canvasColorScale.height = 1; + canvasColorScale.style.display = "none"; + var contextColorScale = canvasColorScale.getContext("2d"); + for (var i = 0; i < colors.length; ++i) { + contextColorScale.fillStyle = colors[i]; + contextColorScale.fillRect(i, 0, 256, 1); + } + return contextColorScale.getImageData(0, 0, 255, 1).data; + // return contextColorScale.getImageData(0, 0, colors.length-1, 1).data; + } + +} + +function MedianDistViz(model) { + var self = this + self.drawMedianDistViz = function() { + + var median = function(values) { + + values.sort( function(a,b) {return a - b;} ); + + var half = Math.floor(values.length/2); + + if(values.length % 2) + return values[half]; + else + return (values[half-1] + values[half]) / 2.0; + } + + for (var district of model.district) { + + let cans = district.stages[model.stage].candidates + if (model.stage == "primary") { + cans = district.parties[voterPerson.iParty].candidates + } + + if ( model.dimensions == "2D") { + var voterPeople = district.voterPeople + var distancemeasure = (xd,yd) => Math.sqrt(xd*xd+yd*yd) + var guess = meanPeople(voterPeople) + var median = numericApproxGeometricMedian(voterPeople, guess, distancemeasure) + + var ctx = model.arena.ctx + ctx.save() + ctx.globalAlpha = .5 + ctx.globalCompositeOperation = "multiply" + for (var c of cans) { + doDraw2(c) + } + ctx.globalAlpha = 1 + median.fill = "#ccc" + doDraw2(median) + ctx.restore() + + function doDraw2(c) { + for (var voterPerson of voterPeople) { + ctx.beginPath(); + ctx.moveTo(voterPerson.x*2,voterPerson.y*2) + ctx.lineTo(c.x*2,c.y*2) + ctx.lineWidth = 10 + ctx.strokeStyle = c.fill; + ctx.stroke(); + } + } + + + } else if( model.dimensions == "1D") { + xvals = [] + for (var voterPerson of district.voterPeople) { + + xvals.push(voterPerson.x) + } + var xmed = median(xvals) + var scaley = 100 / xvals.length + var widthy = 100 / xvals.length + var ctx = model.arena.ctx + ctx.save() + ctx.globalAlpha = .5 + ctx.globalCompositeOperation = "multiply" + for (var c of cans) { + doDraw(c) + } + ctx.globalAlpha = 1 + doDraw({x:xmed,fill:"#ccc"}) + ctx.restore(); + + function doDraw(c) { + for (var i = 0; i < xvals.length; i++) { + var x = xvals[i] + var y = i * scaley + 150 + ctx.beginPath(); + ctx.moveTo(x*2,y*2) + ctx.lineTo(c.x*2,y*2) + ctx.lineWidth = widthy + ctx.strokeStyle = c.fill; + ctx.stroke(); + } + } + + } + } + } +} + + +function meanPeople(voterPeople) { + var x = 0 + var y = 0 + for(var voterPerson of voterPeople){ + x += voterPerson.x + y += voterPerson.y + } + var totalnumbervoters = voterPeople.length + x/=totalnumbervoters + y/=totalnumbervoters + return {x:x, y:y} +} + +function LpAssignmentsViz(model) { + var self = this + self.drawLpAssignmentsViz = function() { + for (var district of model.district) { + + var a = district.stages[model.stage].assignments + + drawA(a,district) + + } + } + + function drawA(a,district) { + var ctx = model.arena.ctx + ctx.save() + var cans = district.stages[model.stage].candidates + for (var i = 0; i < a.length; i++) { + var x = district.voterPeople[i].x + var y = district.voterPeople[i].y + for (var k = 0; k < a[0].length; k++) { + var assign = a[i][k] + + if (assign) { + // if we draw it + ctx.globalAlpha = assign + + var c = cans[k] + + ctx.beginPath(); + ctx.moveTo(x*2,y*2) + ctx.lineTo(c.x*2,c.y*2) + ctx.lineWidth = 4 + ctx.strokeStyle = c.fill; + ctx.stroke(); + } + } + + } + ctx.restore() + } + +} \ No newline at end of file diff --git a/play/js/classDescriptions.txt b/play/js/classDescriptions.txt new file mode 100644 index 00000000..677cd087 --- /dev/null +++ b/play/js/classDescriptions.txt @@ -0,0 +1,33 @@ +Classes + +voter classes by functionality +VoterSet + has functions to get sets of ballots, positions, etc + very simple + in future, will be used by a district + in future, could replace model.voters +VoterCrowd + allows the mouse to move a group of voters + generates the group positions + should be renamed to voterGroup + a voterCrowd will update the x,y positions of it's voters when it moves. +VoterPerson + super simple + ballot, x, y +VoterModel + holds all the Type information + castBallotType + drawMeType + drawMapType +InitVoterModel + sets some voterModel parameters to share between functions (as if all were part of one class) +DrawBallot + the sidebar ballot +DrawTally + sidebar tally +DrawMap + the background map +DrawMe + the mini ballot visualization +CastBallot + strategies diff --git a/play/js/codemirror/addon/autorefresh.js b/play/js/codemirror/addon/autorefresh.js new file mode 100644 index 00000000..37014dc3 --- /dev/null +++ b/play/js/codemirror/addon/autorefresh.js @@ -0,0 +1,47 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")) + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod) + else // Plain browser env + mod(CodeMirror) +})(function(CodeMirror) { + "use strict" + + CodeMirror.defineOption("autoRefresh", false, function(cm, val) { + if (cm.state.autoRefresh) { + stopListening(cm, cm.state.autoRefresh) + cm.state.autoRefresh = null + } + if (val && cm.display.wrapper.offsetHeight == 0) + startListening(cm, cm.state.autoRefresh = {delay: val.delay || 250}) + }) + + function startListening(cm, state) { + function check() { + if (cm.display.wrapper.offsetHeight) { + stopListening(cm, state) + if (cm.display.lastWrapHeight != cm.display.wrapper.clientHeight) + cm.refresh() + } else { + state.timeout = setTimeout(check, state.delay) + } + } + state.timeout = setTimeout(check, state.delay) + state.hurry = function() { + clearTimeout(state.timeout) + state.timeout = setTimeout(check, 50) + } + CodeMirror.on(window, "mouseup", state.hurry) + CodeMirror.on(window, "keyup", state.hurry) + } + + function stopListening(_cm, state) { + clearTimeout(state.timeout) + CodeMirror.off(window, "mouseup", state.hurry) + CodeMirror.off(window, "keyup", state.hurry) + } +}); diff --git a/play/js/codemirror/codemirror.js b/play/js/codemirror/codemirror.js new file mode 100644 index 00000000..88a93243 --- /dev/null +++ b/play/js/codemirror/codemirror.js @@ -0,0 +1,9788 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +// This is CodeMirror (https://codemirror.net), a code editor +// implemented in JavaScript on top of the browser's DOM. +// +// You can find some technical background for some of the code below +// at http://marijnhaverbeke.nl/blog/#cm-internals . + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, global.CodeMirror = factory()); +}(this, (function () { 'use strict'; + + // Kludges for bugs and behavior differences that can't be feature + // detected are enabled based on userAgent etc sniffing. + var userAgent = navigator.userAgent; + var platform = navigator.platform; + + var gecko = /gecko\/\d/i.test(userAgent); + var ie_upto10 = /MSIE \d/.test(userAgent); + var ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(userAgent); + var edge = /Edge\/(\d+)/.exec(userAgent); + var ie = ie_upto10 || ie_11up || edge; + var ie_version = ie && (ie_upto10 ? document.documentMode || 6 : +(edge || ie_11up)[1]); + var webkit = !edge && /WebKit\//.test(userAgent); + var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(userAgent); + var chrome = !edge && /Chrome\//.test(userAgent); + var presto = /Opera\//.test(userAgent); + var safari = /Apple Computer/.test(navigator.vendor); + var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(userAgent); + var phantom = /PhantomJS/.test(userAgent); + + var ios = !edge && /AppleWebKit/.test(userAgent) && /Mobile\/\w+/.test(userAgent); + var android = /Android/.test(userAgent); + // This is woefully incomplete. Suggestions for alternative methods welcome. + var mobile = ios || android || /webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(userAgent); + var mac = ios || /Mac/.test(platform); + var chromeOS = /\bCrOS\b/.test(userAgent); + var windows = /win/i.test(platform); + + var presto_version = presto && userAgent.match(/Version\/(\d*\.\d*)/); + if (presto_version) { presto_version = Number(presto_version[1]); } + if (presto_version && presto_version >= 15) { presto = false; webkit = true; } + // Some browsers use the wrong event properties to signal cmd/ctrl on OS X + var flipCtrlCmd = mac && (qtwebkit || presto && (presto_version == null || presto_version < 12.11)); + var captureRightClick = gecko || (ie && ie_version >= 9); + + function classTest(cls) { return new RegExp("(^|\\s)" + cls + "(?:$|\\s)\\s*") } + + var rmClass = function(node, cls) { + var current = node.className; + var match = classTest(cls).exec(current); + if (match) { + var after = current.slice(match.index + match[0].length); + node.className = current.slice(0, match.index) + (after ? match[1] + after : ""); + } + }; + + function removeChildren(e) { + for (var count = e.childNodes.length; count > 0; --count) + { e.removeChild(e.firstChild); } + return e + } + + function removeChildrenAndAdd(parent, e) { + return removeChildren(parent).appendChild(e) + } + + function elt(tag, content, className, style) { + var e = document.createElement(tag); + if (className) { e.className = className; } + if (style) { e.style.cssText = style; } + if (typeof content == "string") { e.appendChild(document.createTextNode(content)); } + else if (content) { for (var i = 0; i < content.length; ++i) { e.appendChild(content[i]); } } + return e + } + // wrapper for elt, which removes the elt from the accessibility tree + function eltP(tag, content, className, style) { + var e = elt(tag, content, className, style); + e.setAttribute("role", "presentation"); + return e + } + + var range; + if (document.createRange) { range = function(node, start, end, endNode) { + var r = document.createRange(); + r.setEnd(endNode || node, end); + r.setStart(node, start); + return r + }; } + else { range = function(node, start, end) { + var r = document.body.createTextRange(); + try { r.moveToElementText(node.parentNode); } + catch(e) { return r } + r.collapse(true); + r.moveEnd("character", end); + r.moveStart("character", start); + return r + }; } + + function contains(parent, child) { + if (child.nodeType == 3) // Android browser always returns false when child is a textnode + { child = child.parentNode; } + if (parent.contains) + { return parent.contains(child) } + do { + if (child.nodeType == 11) { child = child.host; } + if (child == parent) { return true } + } while (child = child.parentNode) + } + + function activeElt() { + // IE and Edge may throw an "Unspecified Error" when accessing document.activeElement. + // IE < 10 will throw when accessed while the page is loading or in an iframe. + // IE > 9 and Edge will throw when accessed in an iframe if document.body is unavailable. + var activeElement; + try { + activeElement = document.activeElement; + } catch(e) { + activeElement = document.body || null; + } + while (activeElement && activeElement.shadowRoot && activeElement.shadowRoot.activeElement) + { activeElement = activeElement.shadowRoot.activeElement; } + return activeElement + } + + function addClass(node, cls) { + var current = node.className; + if (!classTest(cls).test(current)) { node.className += (current ? " " : "") + cls; } + } + function joinClasses(a, b) { + var as = a.split(" "); + for (var i = 0; i < as.length; i++) + { if (as[i] && !classTest(as[i]).test(b)) { b += " " + as[i]; } } + return b + } + + var selectInput = function(node) { node.select(); }; + if (ios) // Mobile Safari apparently has a bug where select() is broken. + { selectInput = function(node) { node.selectionStart = 0; node.selectionEnd = node.value.length; }; } + else if (ie) // Suppress mysterious IE10 errors + { selectInput = function(node) { try { node.select(); } catch(_e) {} }; } + + function bind(f) { + var args = Array.prototype.slice.call(arguments, 1); + return function(){return f.apply(null, args)} + } + + function copyObj(obj, target, overwrite) { + if (!target) { target = {}; } + for (var prop in obj) + { if (obj.hasOwnProperty(prop) && (overwrite !== false || !target.hasOwnProperty(prop))) + { target[prop] = obj[prop]; } } + return target + } + + // Counts the column offset in a string, taking tabs into account. + // Used mostly to find indentation. + function countColumn(string, end, tabSize, startIndex, startValue) { + if (end == null) { + end = string.search(/[^\s\u00a0]/); + if (end == -1) { end = string.length; } + } + for (var i = startIndex || 0, n = startValue || 0;;) { + var nextTab = string.indexOf("\t", i); + if (nextTab < 0 || nextTab >= end) + { return n + (end - i) } + n += nextTab - i; + n += tabSize - (n % tabSize); + i = nextTab + 1; + } + } + + var Delayed = function() { + this.id = null; + this.f = null; + this.time = 0; + this.handler = bind(this.onTimeout, this); + }; + Delayed.prototype.onTimeout = function (self) { + self.id = 0; + if (self.time <= +new Date) { + self.f(); + } else { + setTimeout(self.handler, self.time - +new Date); + } + }; + Delayed.prototype.set = function (ms, f) { + this.f = f; + var time = +new Date + ms; + if (!this.id || time < this.time) { + clearTimeout(this.id); + this.id = setTimeout(this.handler, ms); + this.time = time; + } + }; + + function indexOf(array, elt) { + for (var i = 0; i < array.length; ++i) + { if (array[i] == elt) { return i } } + return -1 + } + + // Number of pixels added to scroller and sizer to hide scrollbar + var scrollerGap = 50; + + // Returned or thrown by various protocols to signal 'I'm not + // handling this'. + var Pass = {toString: function(){return "CodeMirror.Pass"}}; + + // Reused option objects for setSelection & friends + var sel_dontScroll = {scroll: false}, sel_mouse = {origin: "*mouse"}, sel_move = {origin: "+move"}; + + // The inverse of countColumn -- find the offset that corresponds to + // a particular column. + function findColumn(string, goal, tabSize) { + for (var pos = 0, col = 0;;) { + var nextTab = string.indexOf("\t", pos); + if (nextTab == -1) { nextTab = string.length; } + var skipped = nextTab - pos; + if (nextTab == string.length || col + skipped >= goal) + { return pos + Math.min(skipped, goal - col) } + col += nextTab - pos; + col += tabSize - (col % tabSize); + pos = nextTab + 1; + if (col >= goal) { return pos } + } + } + + var spaceStrs = [""]; + function spaceStr(n) { + while (spaceStrs.length <= n) + { spaceStrs.push(lst(spaceStrs) + " "); } + return spaceStrs[n] + } + + function lst(arr) { return arr[arr.length-1] } + + function map(array, f) { + var out = []; + for (var i = 0; i < array.length; i++) { out[i] = f(array[i], i); } + return out + } + + function insertSorted(array, value, score) { + var pos = 0, priority = score(value); + while (pos < array.length && score(array[pos]) <= priority) { pos++; } + array.splice(pos, 0, value); + } + + function nothing() {} + + function createObj(base, props) { + var inst; + if (Object.create) { + inst = Object.create(base); + } else { + nothing.prototype = base; + inst = new nothing(); + } + if (props) { copyObj(props, inst); } + return inst + } + + var nonASCIISingleCaseWordChar = /[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/; + function isWordCharBasic(ch) { + return /\w/.test(ch) || ch > "\x80" && + (ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch)) + } + function isWordChar(ch, helper) { + if (!helper) { return isWordCharBasic(ch) } + if (helper.source.indexOf("\\w") > -1 && isWordCharBasic(ch)) { return true } + return helper.test(ch) + } + + function isEmpty(obj) { + for (var n in obj) { if (obj.hasOwnProperty(n) && obj[n]) { return false } } + return true + } + + // Extending unicode characters. A series of a non-extending char + + // any number of extending chars is treated as a single unit as far + // as editing and measuring is concerned. This is not fully correct, + // since some scripts/fonts/browsers also treat other configurations + // of code points as a group. + var extendingChars = /[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/; + function isExtendingChar(ch) { return ch.charCodeAt(0) >= 768 && extendingChars.test(ch) } + + // Returns a number from the range [`0`; `str.length`] unless `pos` is outside that range. + function skipExtendingChars(str, pos, dir) { + while ((dir < 0 ? pos > 0 : pos < str.length) && isExtendingChar(str.charAt(pos))) { pos += dir; } + return pos + } + + // Returns the value from the range [`from`; `to`] that satisfies + // `pred` and is closest to `from`. Assumes that at least `to` + // satisfies `pred`. Supports `from` being greater than `to`. + function findFirst(pred, from, to) { + // At any point we are certain `to` satisfies `pred`, don't know + // whether `from` does. + var dir = from > to ? -1 : 1; + for (;;) { + if (from == to) { return from } + var midF = (from + to) / 2, mid = dir < 0 ? Math.ceil(midF) : Math.floor(midF); + if (mid == from) { return pred(mid) ? from : to } + if (pred(mid)) { to = mid; } + else { from = mid + dir; } + } + } + + // BIDI HELPERS + + function iterateBidiSections(order, from, to, f) { + if (!order) { return f(from, to, "ltr", 0) } + var found = false; + for (var i = 0; i < order.length; ++i) { + var part = order[i]; + if (part.from < to && part.to > from || from == to && part.to == from) { + f(Math.max(part.from, from), Math.min(part.to, to), part.level == 1 ? "rtl" : "ltr", i); + found = true; + } + } + if (!found) { f(from, to, "ltr"); } + } + + var bidiOther = null; + function getBidiPartAt(order, ch, sticky) { + var found; + bidiOther = null; + for (var i = 0; i < order.length; ++i) { + var cur = order[i]; + if (cur.from < ch && cur.to > ch) { return i } + if (cur.to == ch) { + if (cur.from != cur.to && sticky == "before") { found = i; } + else { bidiOther = i; } + } + if (cur.from == ch) { + if (cur.from != cur.to && sticky != "before") { found = i; } + else { bidiOther = i; } + } + } + return found != null ? found : bidiOther + } + + // Bidirectional ordering algorithm + // See http://unicode.org/reports/tr9/tr9-13.html for the algorithm + // that this (partially) implements. + + // One-char codes used for character types: + // L (L): Left-to-Right + // R (R): Right-to-Left + // r (AL): Right-to-Left Arabic + // 1 (EN): European Number + // + (ES): European Number Separator + // % (ET): European Number Terminator + // n (AN): Arabic Number + // , (CS): Common Number Separator + // m (NSM): Non-Spacing Mark + // b (BN): Boundary Neutral + // s (B): Paragraph Separator + // t (S): Segment Separator + // w (WS): Whitespace + // N (ON): Other Neutrals + + // Returns null if characters are ordered as they appear + // (left-to-right), or an array of sections ({from, to, level} + // objects) in the order in which they occur visually. + var bidiOrdering = (function() { + // Character types for codepoints 0 to 0xff + var lowTypes = "bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN"; + // Character types for codepoints 0x600 to 0x6f9 + var arabicTypes = "nnnnnnNNr%%r,rNNmmmmmmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmmmnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmnNmmmmmmrrmmNmmmmrr1111111111"; + function charType(code) { + if (code <= 0xf7) { return lowTypes.charAt(code) } + else if (0x590 <= code && code <= 0x5f4) { return "R" } + else if (0x600 <= code && code <= 0x6f9) { return arabicTypes.charAt(code - 0x600) } + else if (0x6ee <= code && code <= 0x8ac) { return "r" } + else if (0x2000 <= code && code <= 0x200b) { return "w" } + else if (code == 0x200c) { return "b" } + else { return "L" } + } + + var bidiRE = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/; + var isNeutral = /[stwN]/, isStrong = /[LRr]/, countsAsLeft = /[Lb1n]/, countsAsNum = /[1n]/; + + function BidiSpan(level, from, to) { + this.level = level; + this.from = from; this.to = to; + } + + return function(str, direction) { + var outerType = direction == "ltr" ? "L" : "R"; + + if (str.length == 0 || direction == "ltr" && !bidiRE.test(str)) { return false } + var len = str.length, types = []; + for (var i = 0; i < len; ++i) + { types.push(charType(str.charCodeAt(i))); } + + // W1. Examine each non-spacing mark (NSM) in the level run, and + // change the type of the NSM to the type of the previous + // character. If the NSM is at the start of the level run, it will + // get the type of sor. + for (var i$1 = 0, prev = outerType; i$1 < len; ++i$1) { + var type = types[i$1]; + if (type == "m") { types[i$1] = prev; } + else { prev = type; } + } + + // W2. Search backwards from each instance of a European number + // until the first strong type (R, L, AL, or sor) is found. If an + // AL is found, change the type of the European number to Arabic + // number. + // W3. Change all ALs to R. + for (var i$2 = 0, cur = outerType; i$2 < len; ++i$2) { + var type$1 = types[i$2]; + if (type$1 == "1" && cur == "r") { types[i$2] = "n"; } + else if (isStrong.test(type$1)) { cur = type$1; if (type$1 == "r") { types[i$2] = "R"; } } + } + + // W4. A single European separator between two European numbers + // changes to a European number. A single common separator between + // two numbers of the same type changes to that type. + for (var i$3 = 1, prev$1 = types[0]; i$3 < len - 1; ++i$3) { + var type$2 = types[i$3]; + if (type$2 == "+" && prev$1 == "1" && types[i$3+1] == "1") { types[i$3] = "1"; } + else if (type$2 == "," && prev$1 == types[i$3+1] && + (prev$1 == "1" || prev$1 == "n")) { types[i$3] = prev$1; } + prev$1 = type$2; + } + + // W5. A sequence of European terminators adjacent to European + // numbers changes to all European numbers. + // W6. Otherwise, separators and terminators change to Other + // Neutral. + for (var i$4 = 0; i$4 < len; ++i$4) { + var type$3 = types[i$4]; + if (type$3 == ",") { types[i$4] = "N"; } + else if (type$3 == "%") { + var end = (void 0); + for (end = i$4 + 1; end < len && types[end] == "%"; ++end) {} + var replace = (i$4 && types[i$4-1] == "!") || (end < len && types[end] == "1") ? "1" : "N"; + for (var j = i$4; j < end; ++j) { types[j] = replace; } + i$4 = end - 1; + } + } + + // W7. Search backwards from each instance of a European number + // until the first strong type (R, L, or sor) is found. If an L is + // found, then change the type of the European number to L. + for (var i$5 = 0, cur$1 = outerType; i$5 < len; ++i$5) { + var type$4 = types[i$5]; + if (cur$1 == "L" && type$4 == "1") { types[i$5] = "L"; } + else if (isStrong.test(type$4)) { cur$1 = type$4; } + } + + // N1. A sequence of neutrals takes the direction of the + // surrounding strong text if the text on both sides has the same + // direction. European and Arabic numbers act as if they were R in + // terms of their influence on neutrals. Start-of-level-run (sor) + // and end-of-level-run (eor) are used at level run boundaries. + // N2. Any remaining neutrals take the embedding direction. + for (var i$6 = 0; i$6 < len; ++i$6) { + if (isNeutral.test(types[i$6])) { + var end$1 = (void 0); + for (end$1 = i$6 + 1; end$1 < len && isNeutral.test(types[end$1]); ++end$1) {} + var before = (i$6 ? types[i$6-1] : outerType) == "L"; + var after = (end$1 < len ? types[end$1] : outerType) == "L"; + var replace$1 = before == after ? (before ? "L" : "R") : outerType; + for (var j$1 = i$6; j$1 < end$1; ++j$1) { types[j$1] = replace$1; } + i$6 = end$1 - 1; + } + } + + // Here we depart from the documented algorithm, in order to avoid + // building up an actual levels array. Since there are only three + // levels (0, 1, 2) in an implementation that doesn't take + // explicit embedding into account, we can build up the order on + // the fly, without following the level-based algorithm. + var order = [], m; + for (var i$7 = 0; i$7 < len;) { + if (countsAsLeft.test(types[i$7])) { + var start = i$7; + for (++i$7; i$7 < len && countsAsLeft.test(types[i$7]); ++i$7) {} + order.push(new BidiSpan(0, start, i$7)); + } else { + var pos = i$7, at = order.length, isRTL = direction == "rtl" ? 1 : 0; + for (++i$7; i$7 < len && types[i$7] != "L"; ++i$7) {} + for (var j$2 = pos; j$2 < i$7;) { + if (countsAsNum.test(types[j$2])) { + if (pos < j$2) { order.splice(at, 0, new BidiSpan(1, pos, j$2)); at += isRTL; } + var nstart = j$2; + for (++j$2; j$2 < i$7 && countsAsNum.test(types[j$2]); ++j$2) {} + order.splice(at, 0, new BidiSpan(2, nstart, j$2)); + at += isRTL; + pos = j$2; + } else { ++j$2; } + } + if (pos < i$7) { order.splice(at, 0, new BidiSpan(1, pos, i$7)); } + } + } + if (direction == "ltr") { + if (order[0].level == 1 && (m = str.match(/^\s+/))) { + order[0].from = m[0].length; + order.unshift(new BidiSpan(0, 0, m[0].length)); + } + if (lst(order).level == 1 && (m = str.match(/\s+$/))) { + lst(order).to -= m[0].length; + order.push(new BidiSpan(0, len - m[0].length, len)); + } + } + + return direction == "rtl" ? order.reverse() : order + } + })(); + + // Get the bidi ordering for the given line (and cache it). Returns + // false for lines that are fully left-to-right, and an array of + // BidiSpan objects otherwise. + function getOrder(line, direction) { + var order = line.order; + if (order == null) { order = line.order = bidiOrdering(line.text, direction); } + return order + } + + // EVENT HANDLING + + // Lightweight event framework. on/off also work on DOM nodes, + // registering native DOM handlers. + + var noHandlers = []; + + var on = function(emitter, type, f) { + if (emitter.addEventListener) { + emitter.addEventListener(type, f, false); + } else if (emitter.attachEvent) { + emitter.attachEvent("on" + type, f); + } else { + var map = emitter._handlers || (emitter._handlers = {}); + map[type] = (map[type] || noHandlers).concat(f); + } + }; + + function getHandlers(emitter, type) { + return emitter._handlers && emitter._handlers[type] || noHandlers + } + + function off(emitter, type, f) { + if (emitter.removeEventListener) { + emitter.removeEventListener(type, f, false); + } else if (emitter.detachEvent) { + emitter.detachEvent("on" + type, f); + } else { + var map = emitter._handlers, arr = map && map[type]; + if (arr) { + var index = indexOf(arr, f); + if (index > -1) + { map[type] = arr.slice(0, index).concat(arr.slice(index + 1)); } + } + } + } + + function signal(emitter, type /*, values...*/) { + var handlers = getHandlers(emitter, type); + if (!handlers.length) { return } + var args = Array.prototype.slice.call(arguments, 2); + for (var i = 0; i < handlers.length; ++i) { handlers[i].apply(null, args); } + } + + // The DOM events that CodeMirror handles can be overridden by + // registering a (non-DOM) handler on the editor for the event name, + // and preventDefault-ing the event in that handler. + function signalDOMEvent(cm, e, override) { + if (typeof e == "string") + { e = {type: e, preventDefault: function() { this.defaultPrevented = true; }}; } + signal(cm, override || e.type, cm, e); + return e_defaultPrevented(e) || e.codemirrorIgnore + } + + function signalCursorActivity(cm) { + var arr = cm._handlers && cm._handlers.cursorActivity; + if (!arr) { return } + var set = cm.curOp.cursorActivityHandlers || (cm.curOp.cursorActivityHandlers = []); + for (var i = 0; i < arr.length; ++i) { if (indexOf(set, arr[i]) == -1) + { set.push(arr[i]); } } + } + + function hasHandler(emitter, type) { + return getHandlers(emitter, type).length > 0 + } + + // Add on and off methods to a constructor's prototype, to make + // registering events on such objects more convenient. + function eventMixin(ctor) { + ctor.prototype.on = function(type, f) {on(this, type, f);}; + ctor.prototype.off = function(type, f) {off(this, type, f);}; + } + + // Due to the fact that we still support jurassic IE versions, some + // compatibility wrappers are needed. + + function e_preventDefault(e) { + if (e.preventDefault) { e.preventDefault(); } + else { e.returnValue = false; } + } + function e_stopPropagation(e) { + if (e.stopPropagation) { e.stopPropagation(); } + else { e.cancelBubble = true; } + } + function e_defaultPrevented(e) { + return e.defaultPrevented != null ? e.defaultPrevented : e.returnValue == false + } + function e_stop(e) {e_preventDefault(e); e_stopPropagation(e);} + + function e_target(e) {return e.target || e.srcElement} + function e_button(e) { + var b = e.which; + if (b == null) { + if (e.button & 1) { b = 1; } + else if (e.button & 2) { b = 3; } + else if (e.button & 4) { b = 2; } + } + if (mac && e.ctrlKey && b == 1) { b = 3; } + return b + } + + // Detect drag-and-drop + var dragAndDrop = function() { + // There is *some* kind of drag-and-drop support in IE6-8, but I + // couldn't get it to work yet. + if (ie && ie_version < 9) { return false } + var div = elt('div'); + return "draggable" in div || "dragDrop" in div + }(); + + var zwspSupported; + function zeroWidthElement(measure) { + if (zwspSupported == null) { + var test = elt("span", "\u200b"); + removeChildrenAndAdd(measure, elt("span", [test, document.createTextNode("x")])); + if (measure.firstChild.offsetHeight != 0) + { zwspSupported = test.offsetWidth <= 1 && test.offsetHeight > 2 && !(ie && ie_version < 8); } + } + var node = zwspSupported ? elt("span", "\u200b") : + elt("span", "\u00a0", null, "display: inline-block; width: 1px; margin-right: -1px"); + node.setAttribute("cm-text", ""); + return node + } + + // Feature-detect IE's crummy client rect reporting for bidi text + var badBidiRects; + function hasBadBidiRects(measure) { + if (badBidiRects != null) { return badBidiRects } + var txt = removeChildrenAndAdd(measure, document.createTextNode("A\u062eA")); + var r0 = range(txt, 0, 1).getBoundingClientRect(); + var r1 = range(txt, 1, 2).getBoundingClientRect(); + removeChildren(measure); + if (!r0 || r0.left == r0.right) { return false } // Safari returns null in some cases (#2780) + return badBidiRects = (r1.right - r0.right < 3) + } + + // See if "".split is the broken IE version, if so, provide an + // alternative way to split lines. + var splitLinesAuto = "\n\nb".split(/\n/).length != 3 ? function (string) { + var pos = 0, result = [], l = string.length; + while (pos <= l) { + var nl = string.indexOf("\n", pos); + if (nl == -1) { nl = string.length; } + var line = string.slice(pos, string.charAt(nl - 1) == "\r" ? nl - 1 : nl); + var rt = line.indexOf("\r"); + if (rt != -1) { + result.push(line.slice(0, rt)); + pos += rt + 1; + } else { + result.push(line); + pos = nl + 1; + } + } + return result + } : function (string) { return string.split(/\r\n?|\n/); }; + + var hasSelection = window.getSelection ? function (te) { + try { return te.selectionStart != te.selectionEnd } + catch(e) { return false } + } : function (te) { + var range; + try {range = te.ownerDocument.selection.createRange();} + catch(e) {} + if (!range || range.parentElement() != te) { return false } + return range.compareEndPoints("StartToEnd", range) != 0 + }; + + var hasCopyEvent = (function () { + var e = elt("div"); + if ("oncopy" in e) { return true } + e.setAttribute("oncopy", "return;"); + return typeof e.oncopy == "function" + })(); + + var badZoomedRects = null; + function hasBadZoomedRects(measure) { + if (badZoomedRects != null) { return badZoomedRects } + var node = removeChildrenAndAdd(measure, elt("span", "x")); + var normal = node.getBoundingClientRect(); + var fromRange = range(node, 0, 1).getBoundingClientRect(); + return badZoomedRects = Math.abs(normal.left - fromRange.left) > 1 + } + + // Known modes, by name and by MIME + var modes = {}, mimeModes = {}; + + // Extra arguments are stored as the mode's dependencies, which is + // used by (legacy) mechanisms like loadmode.js to automatically + // load a mode. (Preferred mechanism is the require/define calls.) + function defineMode(name, mode) { + if (arguments.length > 2) + { mode.dependencies = Array.prototype.slice.call(arguments, 2); } + modes[name] = mode; + } + + function defineMIME(mime, spec) { + mimeModes[mime] = spec; + } + + // Given a MIME type, a {name, ...options} config object, or a name + // string, return a mode config object. + function resolveMode(spec) { + if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) { + spec = mimeModes[spec]; + } else if (spec && typeof spec.name == "string" && mimeModes.hasOwnProperty(spec.name)) { + var found = mimeModes[spec.name]; + if (typeof found == "string") { found = {name: found}; } + spec = createObj(found, spec); + spec.name = found.name; + } else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+xml$/.test(spec)) { + return resolveMode("application/xml") + } else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+json$/.test(spec)) { + return resolveMode("application/json") + } + if (typeof spec == "string") { return {name: spec} } + else { return spec || {name: "null"} } + } + + // Given a mode spec (anything that resolveMode accepts), find and + // initialize an actual mode object. + function getMode(options, spec) { + spec = resolveMode(spec); + var mfactory = modes[spec.name]; + if (!mfactory) { return getMode(options, "text/plain") } + var modeObj = mfactory(options, spec); + if (modeExtensions.hasOwnProperty(spec.name)) { + var exts = modeExtensions[spec.name]; + for (var prop in exts) { + if (!exts.hasOwnProperty(prop)) { continue } + if (modeObj.hasOwnProperty(prop)) { modeObj["_" + prop] = modeObj[prop]; } + modeObj[prop] = exts[prop]; + } + } + modeObj.name = spec.name; + if (spec.helperType) { modeObj.helperType = spec.helperType; } + if (spec.modeProps) { for (var prop$1 in spec.modeProps) + { modeObj[prop$1] = spec.modeProps[prop$1]; } } + + return modeObj + } + + // This can be used to attach properties to mode objects from + // outside the actual mode definition. + var modeExtensions = {}; + function extendMode(mode, properties) { + var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {}); + copyObj(properties, exts); + } + + function copyState(mode, state) { + if (state === true) { return state } + if (mode.copyState) { return mode.copyState(state) } + var nstate = {}; + for (var n in state) { + var val = state[n]; + if (val instanceof Array) { val = val.concat([]); } + nstate[n] = val; + } + return nstate + } + + // Given a mode and a state (for that mode), find the inner mode and + // state at the position that the state refers to. + function innerMode(mode, state) { + var info; + while (mode.innerMode) { + info = mode.innerMode(state); + if (!info || info.mode == mode) { break } + state = info.state; + mode = info.mode; + } + return info || {mode: mode, state: state} + } + + function startState(mode, a1, a2) { + return mode.startState ? mode.startState(a1, a2) : true + } + + // STRING STREAM + + // Fed to the mode parsers, provides helper functions to make + // parsers more succinct. + + var StringStream = function(string, tabSize, lineOracle) { + this.pos = this.start = 0; + this.string = string; + this.tabSize = tabSize || 8; + this.lastColumnPos = this.lastColumnValue = 0; + this.lineStart = 0; + this.lineOracle = lineOracle; + }; + + StringStream.prototype.eol = function () {return this.pos >= this.string.length}; + StringStream.prototype.sol = function () {return this.pos == this.lineStart}; + StringStream.prototype.peek = function () {return this.string.charAt(this.pos) || undefined}; + StringStream.prototype.next = function () { + if (this.pos < this.string.length) + { return this.string.charAt(this.pos++) } + }; + StringStream.prototype.eat = function (match) { + var ch = this.string.charAt(this.pos); + var ok; + if (typeof match == "string") { ok = ch == match; } + else { ok = ch && (match.test ? match.test(ch) : match(ch)); } + if (ok) {++this.pos; return ch} + }; + StringStream.prototype.eatWhile = function (match) { + var start = this.pos; + while (this.eat(match)){} + return this.pos > start + }; + StringStream.prototype.eatSpace = function () { + var start = this.pos; + while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) { ++this.pos; } + return this.pos > start + }; + StringStream.prototype.skipToEnd = function () {this.pos = this.string.length;}; + StringStream.prototype.skipTo = function (ch) { + var found = this.string.indexOf(ch, this.pos); + if (found > -1) {this.pos = found; return true} + }; + StringStream.prototype.backUp = function (n) {this.pos -= n;}; + StringStream.prototype.column = function () { + if (this.lastColumnPos < this.start) { + this.lastColumnValue = countColumn(this.string, this.start, this.tabSize, this.lastColumnPos, this.lastColumnValue); + this.lastColumnPos = this.start; + } + return this.lastColumnValue - (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0) + }; + StringStream.prototype.indentation = function () { + return countColumn(this.string, null, this.tabSize) - + (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0) + }; + StringStream.prototype.match = function (pattern, consume, caseInsensitive) { + if (typeof pattern == "string") { + var cased = function (str) { return caseInsensitive ? str.toLowerCase() : str; }; + var substr = this.string.substr(this.pos, pattern.length); + if (cased(substr) == cased(pattern)) { + if (consume !== false) { this.pos += pattern.length; } + return true + } + } else { + var match = this.string.slice(this.pos).match(pattern); + if (match && match.index > 0) { return null } + if (match && consume !== false) { this.pos += match[0].length; } + return match + } + }; + StringStream.prototype.current = function (){return this.string.slice(this.start, this.pos)}; + StringStream.prototype.hideFirstChars = function (n, inner) { + this.lineStart += n; + try { return inner() } + finally { this.lineStart -= n; } + }; + StringStream.prototype.lookAhead = function (n) { + var oracle = this.lineOracle; + return oracle && oracle.lookAhead(n) + }; + StringStream.prototype.baseToken = function () { + var oracle = this.lineOracle; + return oracle && oracle.baseToken(this.pos) + }; + + // Find the line object corresponding to the given line number. + function getLine(doc, n) { + n -= doc.first; + if (n < 0 || n >= doc.size) { throw new Error("There is no line " + (n + doc.first) + " in the document.") } + var chunk = doc; + while (!chunk.lines) { + for (var i = 0;; ++i) { + var child = chunk.children[i], sz = child.chunkSize(); + if (n < sz) { chunk = child; break } + n -= sz; + } + } + return chunk.lines[n] + } + + // Get the part of a document between two positions, as an array of + // strings. + function getBetween(doc, start, end) { + var out = [], n = start.line; + doc.iter(start.line, end.line + 1, function (line) { + var text = line.text; + if (n == end.line) { text = text.slice(0, end.ch); } + if (n == start.line) { text = text.slice(start.ch); } + out.push(text); + ++n; + }); + return out + } + // Get the lines between from and to, as array of strings. + function getLines(doc, from, to) { + var out = []; + doc.iter(from, to, function (line) { out.push(line.text); }); // iter aborts when callback returns truthy value + return out + } + + // Update the height of a line, propagating the height change + // upwards to parent nodes. + function updateLineHeight(line, height) { + var diff = height - line.height; + if (diff) { for (var n = line; n; n = n.parent) { n.height += diff; } } + } + + // Given a line object, find its line number by walking up through + // its parent links. + function lineNo(line) { + if (line.parent == null) { return null } + var cur = line.parent, no = indexOf(cur.lines, line); + for (var chunk = cur.parent; chunk; cur = chunk, chunk = chunk.parent) { + for (var i = 0;; ++i) { + if (chunk.children[i] == cur) { break } + no += chunk.children[i].chunkSize(); + } + } + return no + cur.first + } + + // Find the line at the given vertical position, using the height + // information in the document tree. + function lineAtHeight(chunk, h) { + var n = chunk.first; + outer: do { + for (var i$1 = 0; i$1 < chunk.children.length; ++i$1) { + var child = chunk.children[i$1], ch = child.height; + if (h < ch) { chunk = child; continue outer } + h -= ch; + n += child.chunkSize(); + } + return n + } while (!chunk.lines) + var i = 0; + for (; i < chunk.lines.length; ++i) { + var line = chunk.lines[i], lh = line.height; + if (h < lh) { break } + h -= lh; + } + return n + i + } + + function isLine(doc, l) {return l >= doc.first && l < doc.first + doc.size} + + function lineNumberFor(options, i) { + return String(options.lineNumberFormatter(i + options.firstLineNumber)) + } + + // A Pos instance represents a position within the text. + function Pos(line, ch, sticky) { + if ( sticky === void 0 ) sticky = null; + + if (!(this instanceof Pos)) { return new Pos(line, ch, sticky) } + this.line = line; + this.ch = ch; + this.sticky = sticky; + } + + // Compare two positions, return 0 if they are the same, a negative + // number when a is less, and a positive number otherwise. + function cmp(a, b) { return a.line - b.line || a.ch - b.ch } + + function equalCursorPos(a, b) { return a.sticky == b.sticky && cmp(a, b) == 0 } + + function copyPos(x) {return Pos(x.line, x.ch)} + function maxPos(a, b) { return cmp(a, b) < 0 ? b : a } + function minPos(a, b) { return cmp(a, b) < 0 ? a : b } + + // Most of the external API clips given positions to make sure they + // actually exist within the document. + function clipLine(doc, n) {return Math.max(doc.first, Math.min(n, doc.first + doc.size - 1))} + function clipPos(doc, pos) { + if (pos.line < doc.first) { return Pos(doc.first, 0) } + var last = doc.first + doc.size - 1; + if (pos.line > last) { return Pos(last, getLine(doc, last).text.length) } + return clipToLen(pos, getLine(doc, pos.line).text.length) + } + function clipToLen(pos, linelen) { + var ch = pos.ch; + if (ch == null || ch > linelen) { return Pos(pos.line, linelen) } + else if (ch < 0) { return Pos(pos.line, 0) } + else { return pos } + } + function clipPosArray(doc, array) { + var out = []; + for (var i = 0; i < array.length; i++) { out[i] = clipPos(doc, array[i]); } + return out + } + + var SavedContext = function(state, lookAhead) { + this.state = state; + this.lookAhead = lookAhead; + }; + + var Context = function(doc, state, line, lookAhead) { + this.state = state; + this.doc = doc; + this.line = line; + this.maxLookAhead = lookAhead || 0; + this.baseTokens = null; + this.baseTokenPos = 1; + }; + + Context.prototype.lookAhead = function (n) { + var line = this.doc.getLine(this.line + n); + if (line != null && n > this.maxLookAhead) { this.maxLookAhead = n; } + return line + }; + + Context.prototype.baseToken = function (n) { + if (!this.baseTokens) { return null } + while (this.baseTokens[this.baseTokenPos] <= n) + { this.baseTokenPos += 2; } + var type = this.baseTokens[this.baseTokenPos + 1]; + return {type: type && type.replace(/( |^)overlay .*/, ""), + size: this.baseTokens[this.baseTokenPos] - n} + }; + + Context.prototype.nextLine = function () { + this.line++; + if (this.maxLookAhead > 0) { this.maxLookAhead--; } + }; + + Context.fromSaved = function (doc, saved, line) { + if (saved instanceof SavedContext) + { return new Context(doc, copyState(doc.mode, saved.state), line, saved.lookAhead) } + else + { return new Context(doc, copyState(doc.mode, saved), line) } + }; + + Context.prototype.save = function (copy) { + var state = copy !== false ? copyState(this.doc.mode, this.state) : this.state; + return this.maxLookAhead > 0 ? new SavedContext(state, this.maxLookAhead) : state + }; + + + // Compute a style array (an array starting with a mode generation + // -- for invalidation -- followed by pairs of end positions and + // style strings), which is used to highlight the tokens on the + // line. + function highlightLine(cm, line, context, forceToEnd) { + // A styles array always starts with a number identifying the + // mode/overlays that it is based on (for easy invalidation). + var st = [cm.state.modeGen], lineClasses = {}; + // Compute the base array of styles + runMode(cm, line.text, cm.doc.mode, context, function (end, style) { return st.push(end, style); }, + lineClasses, forceToEnd); + var state = context.state; + + // Run overlays, adjust style array. + var loop = function ( o ) { + context.baseTokens = st; + var overlay = cm.state.overlays[o], i = 1, at = 0; + context.state = true; + runMode(cm, line.text, overlay.mode, context, function (end, style) { + var start = i; + // Ensure there's a token end at the current position, and that i points at it + while (at < end) { + var i_end = st[i]; + if (i_end > end) + { st.splice(i, 1, end, st[i+1], i_end); } + i += 2; + at = Math.min(end, i_end); + } + if (!style) { return } + if (overlay.opaque) { + st.splice(start, i - start, end, "overlay " + style); + i = start + 2; + } else { + for (; start < i; start += 2) { + var cur = st[start+1]; + st[start+1] = (cur ? cur + " " : "") + "overlay " + style; + } + } + }, lineClasses); + context.state = state; + context.baseTokens = null; + context.baseTokenPos = 1; + }; + + for (var o = 0; o < cm.state.overlays.length; ++o) loop( o ); + + return {styles: st, classes: lineClasses.bgClass || lineClasses.textClass ? lineClasses : null} + } + + function getLineStyles(cm, line, updateFrontier) { + if (!line.styles || line.styles[0] != cm.state.modeGen) { + var context = getContextBefore(cm, lineNo(line)); + var resetState = line.text.length > cm.options.maxHighlightLength && copyState(cm.doc.mode, context.state); + var result = highlightLine(cm, line, context); + if (resetState) { context.state = resetState; } + line.stateAfter = context.save(!resetState); + line.styles = result.styles; + if (result.classes) { line.styleClasses = result.classes; } + else if (line.styleClasses) { line.styleClasses = null; } + if (updateFrontier === cm.doc.highlightFrontier) + { cm.doc.modeFrontier = Math.max(cm.doc.modeFrontier, ++cm.doc.highlightFrontier); } + } + return line.styles + } + + function getContextBefore(cm, n, precise) { + var doc = cm.doc, display = cm.display; + if (!doc.mode.startState) { return new Context(doc, true, n) } + var start = findStartLine(cm, n, precise); + var saved = start > doc.first && getLine(doc, start - 1).stateAfter; + var context = saved ? Context.fromSaved(doc, saved, start) : new Context(doc, startState(doc.mode), start); + + doc.iter(start, n, function (line) { + processLine(cm, line.text, context); + var pos = context.line; + line.stateAfter = pos == n - 1 || pos % 5 == 0 || pos >= display.viewFrom && pos < display.viewTo ? context.save() : null; + context.nextLine(); + }); + if (precise) { doc.modeFrontier = context.line; } + return context + } + + // Lightweight form of highlight -- proceed over this line and + // update state, but don't save a style array. Used for lines that + // aren't currently visible. + function processLine(cm, text, context, startAt) { + var mode = cm.doc.mode; + var stream = new StringStream(text, cm.options.tabSize, context); + stream.start = stream.pos = startAt || 0; + if (text == "") { callBlankLine(mode, context.state); } + while (!stream.eol()) { + readToken(mode, stream, context.state); + stream.start = stream.pos; + } + } + + function callBlankLine(mode, state) { + if (mode.blankLine) { return mode.blankLine(state) } + if (!mode.innerMode) { return } + var inner = innerMode(mode, state); + if (inner.mode.blankLine) { return inner.mode.blankLine(inner.state) } + } + + function readToken(mode, stream, state, inner) { + for (var i = 0; i < 10; i++) { + if (inner) { inner[0] = innerMode(mode, state).mode; } + var style = mode.token(stream, state); + if (stream.pos > stream.start) { return style } + } + throw new Error("Mode " + mode.name + " failed to advance stream.") + } + + var Token = function(stream, type, state) { + this.start = stream.start; this.end = stream.pos; + this.string = stream.current(); + this.type = type || null; + this.state = state; + }; + + // Utility for getTokenAt and getLineTokens + function takeToken(cm, pos, precise, asArray) { + var doc = cm.doc, mode = doc.mode, style; + pos = clipPos(doc, pos); + var line = getLine(doc, pos.line), context = getContextBefore(cm, pos.line, precise); + var stream = new StringStream(line.text, cm.options.tabSize, context), tokens; + if (asArray) { tokens = []; } + while ((asArray || stream.pos < pos.ch) && !stream.eol()) { + stream.start = stream.pos; + style = readToken(mode, stream, context.state); + if (asArray) { tokens.push(new Token(stream, style, copyState(doc.mode, context.state))); } + } + return asArray ? tokens : new Token(stream, style, context.state) + } + + function extractLineClasses(type, output) { + if (type) { for (;;) { + var lineClass = type.match(/(?:^|\s+)line-(background-)?(\S+)/); + if (!lineClass) { break } + type = type.slice(0, lineClass.index) + type.slice(lineClass.index + lineClass[0].length); + var prop = lineClass[1] ? "bgClass" : "textClass"; + if (output[prop] == null) + { output[prop] = lineClass[2]; } + else if (!(new RegExp("(?:^|\\s)" + lineClass[2] + "(?:$|\\s)")).test(output[prop])) + { output[prop] += " " + lineClass[2]; } + } } + return type + } + + // Run the given mode's parser over a line, calling f for each token. + function runMode(cm, text, mode, context, f, lineClasses, forceToEnd) { + var flattenSpans = mode.flattenSpans; + if (flattenSpans == null) { flattenSpans = cm.options.flattenSpans; } + var curStart = 0, curStyle = null; + var stream = new StringStream(text, cm.options.tabSize, context), style; + var inner = cm.options.addModeClass && [null]; + if (text == "") { extractLineClasses(callBlankLine(mode, context.state), lineClasses); } + while (!stream.eol()) { + if (stream.pos > cm.options.maxHighlightLength) { + flattenSpans = false; + if (forceToEnd) { processLine(cm, text, context, stream.pos); } + stream.pos = text.length; + style = null; + } else { + style = extractLineClasses(readToken(mode, stream, context.state, inner), lineClasses); + } + if (inner) { + var mName = inner[0].name; + if (mName) { style = "m-" + (style ? mName + " " + style : mName); } + } + if (!flattenSpans || curStyle != style) { + while (curStart < stream.start) { + curStart = Math.min(stream.start, curStart + 5000); + f(curStart, curStyle); + } + curStyle = style; + } + stream.start = stream.pos; + } + while (curStart < stream.pos) { + // Webkit seems to refuse to render text nodes longer than 57444 + // characters, and returns inaccurate measurements in nodes + // starting around 5000 chars. + var pos = Math.min(stream.pos, curStart + 5000); + f(pos, curStyle); + curStart = pos; + } + } + + // Finds the line to start with when starting a parse. Tries to + // find a line with a stateAfter, so that it can start with a + // valid state. If that fails, it returns the line with the + // smallest indentation, which tends to need the least context to + // parse correctly. + function findStartLine(cm, n, precise) { + var minindent, minline, doc = cm.doc; + var lim = precise ? -1 : n - (cm.doc.mode.innerMode ? 1000 : 100); + for (var search = n; search > lim; --search) { + if (search <= doc.first) { return doc.first } + var line = getLine(doc, search - 1), after = line.stateAfter; + if (after && (!precise || search + (after instanceof SavedContext ? after.lookAhead : 0) <= doc.modeFrontier)) + { return search } + var indented = countColumn(line.text, null, cm.options.tabSize); + if (minline == null || minindent > indented) { + minline = search - 1; + minindent = indented; + } + } + return minline + } + + function retreatFrontier(doc, n) { + doc.modeFrontier = Math.min(doc.modeFrontier, n); + if (doc.highlightFrontier < n - 10) { return } + var start = doc.first; + for (var line = n - 1; line > start; line--) { + var saved = getLine(doc, line).stateAfter; + // change is on 3 + // state on line 1 looked ahead 2 -- so saw 3 + // test 1 + 2 < 3 should cover this + if (saved && (!(saved instanceof SavedContext) || line + saved.lookAhead < n)) { + start = line + 1; + break + } + } + doc.highlightFrontier = Math.min(doc.highlightFrontier, start); + } + + // Optimize some code when these features are not used. + var sawReadOnlySpans = false, sawCollapsedSpans = false; + + function seeReadOnlySpans() { + sawReadOnlySpans = true; + } + + function seeCollapsedSpans() { + sawCollapsedSpans = true; + } + + // TEXTMARKER SPANS + + function MarkedSpan(marker, from, to) { + this.marker = marker; + this.from = from; this.to = to; + } + + // Search an array of spans for a span matching the given marker. + function getMarkedSpanFor(spans, marker) { + if (spans) { for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if (span.marker == marker) { return span } + } } + } + // Remove a span from an array, returning undefined if no spans are + // left (we don't store arrays for lines without spans). + function removeMarkedSpan(spans, span) { + var r; + for (var i = 0; i < spans.length; ++i) + { if (spans[i] != span) { (r || (r = [])).push(spans[i]); } } + return r + } + // Add a span to a line. + function addMarkedSpan(line, span) { + line.markedSpans = line.markedSpans ? line.markedSpans.concat([span]) : [span]; + span.marker.attachLine(line); + } + + // Used for the algorithm that adjusts markers for a change in the + // document. These functions cut an array of spans at a given + // character position, returning an array of remaining chunks (or + // undefined if nothing remains). + function markedSpansBefore(old, startCh, isInsert) { + var nw; + if (old) { for (var i = 0; i < old.length; ++i) { + var span = old[i], marker = span.marker; + var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh); + if (startsBefore || span.from == startCh && marker.type == "bookmark" && (!isInsert || !span.marker.insertLeft)) { + var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh) + ;(nw || (nw = [])).push(new MarkedSpan(marker, span.from, endsAfter ? null : span.to)); + } + } } + return nw + } + function markedSpansAfter(old, endCh, isInsert) { + var nw; + if (old) { for (var i = 0; i < old.length; ++i) { + var span = old[i], marker = span.marker; + var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh); + if (endsAfter || span.from == endCh && marker.type == "bookmark" && (!isInsert || span.marker.insertLeft)) { + var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh) + ;(nw || (nw = [])).push(new MarkedSpan(marker, startsBefore ? null : span.from - endCh, + span.to == null ? null : span.to - endCh)); + } + } } + return nw + } + + // Given a change object, compute the new set of marker spans that + // cover the line in which the change took place. Removes spans + // entirely within the change, reconnects spans belonging to the + // same marker that appear on both sides of the change, and cuts off + // spans partially within the change. Returns an array of span + // arrays with one element for each line in (after) the change. + function stretchSpansOverChange(doc, change) { + if (change.full) { return null } + var oldFirst = isLine(doc, change.from.line) && getLine(doc, change.from.line).markedSpans; + var oldLast = isLine(doc, change.to.line) && getLine(doc, change.to.line).markedSpans; + if (!oldFirst && !oldLast) { return null } + + var startCh = change.from.ch, endCh = change.to.ch, isInsert = cmp(change.from, change.to) == 0; + // Get the spans that 'stick out' on both sides + var first = markedSpansBefore(oldFirst, startCh, isInsert); + var last = markedSpansAfter(oldLast, endCh, isInsert); + + // Next, merge those two ends + var sameLine = change.text.length == 1, offset = lst(change.text).length + (sameLine ? startCh : 0); + if (first) { + // Fix up .to properties of first + for (var i = 0; i < first.length; ++i) { + var span = first[i]; + if (span.to == null) { + var found = getMarkedSpanFor(last, span.marker); + if (!found) { span.to = startCh; } + else if (sameLine) { span.to = found.to == null ? null : found.to + offset; } + } + } + } + if (last) { + // Fix up .from in last (or move them into first in case of sameLine) + for (var i$1 = 0; i$1 < last.length; ++i$1) { + var span$1 = last[i$1]; + if (span$1.to != null) { span$1.to += offset; } + if (span$1.from == null) { + var found$1 = getMarkedSpanFor(first, span$1.marker); + if (!found$1) { + span$1.from = offset; + if (sameLine) { (first || (first = [])).push(span$1); } + } + } else { + span$1.from += offset; + if (sameLine) { (first || (first = [])).push(span$1); } + } + } + } + // Make sure we didn't create any zero-length spans + if (first) { first = clearEmptySpans(first); } + if (last && last != first) { last = clearEmptySpans(last); } + + var newMarkers = [first]; + if (!sameLine) { + // Fill gap with whole-line-spans + var gap = change.text.length - 2, gapMarkers; + if (gap > 0 && first) + { for (var i$2 = 0; i$2 < first.length; ++i$2) + { if (first[i$2].to == null) + { (gapMarkers || (gapMarkers = [])).push(new MarkedSpan(first[i$2].marker, null, null)); } } } + for (var i$3 = 0; i$3 < gap; ++i$3) + { newMarkers.push(gapMarkers); } + newMarkers.push(last); + } + return newMarkers + } + + // Remove spans that are empty and don't have a clearWhenEmpty + // option of false. + function clearEmptySpans(spans) { + for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if (span.from != null && span.from == span.to && span.marker.clearWhenEmpty !== false) + { spans.splice(i--, 1); } + } + if (!spans.length) { return null } + return spans + } + + // Used to 'clip' out readOnly ranges when making a change. + function removeReadOnlyRanges(doc, from, to) { + var markers = null; + doc.iter(from.line, to.line + 1, function (line) { + if (line.markedSpans) { for (var i = 0; i < line.markedSpans.length; ++i) { + var mark = line.markedSpans[i].marker; + if (mark.readOnly && (!markers || indexOf(markers, mark) == -1)) + { (markers || (markers = [])).push(mark); } + } } + }); + if (!markers) { return null } + var parts = [{from: from, to: to}]; + for (var i = 0; i < markers.length; ++i) { + var mk = markers[i], m = mk.find(0); + for (var j = 0; j < parts.length; ++j) { + var p = parts[j]; + if (cmp(p.to, m.from) < 0 || cmp(p.from, m.to) > 0) { continue } + var newParts = [j, 1], dfrom = cmp(p.from, m.from), dto = cmp(p.to, m.to); + if (dfrom < 0 || !mk.inclusiveLeft && !dfrom) + { newParts.push({from: p.from, to: m.from}); } + if (dto > 0 || !mk.inclusiveRight && !dto) + { newParts.push({from: m.to, to: p.to}); } + parts.splice.apply(parts, newParts); + j += newParts.length - 3; + } + } + return parts + } + + // Connect or disconnect spans from a line. + function detachMarkedSpans(line) { + var spans = line.markedSpans; + if (!spans) { return } + for (var i = 0; i < spans.length; ++i) + { spans[i].marker.detachLine(line); } + line.markedSpans = null; + } + function attachMarkedSpans(line, spans) { + if (!spans) { return } + for (var i = 0; i < spans.length; ++i) + { spans[i].marker.attachLine(line); } + line.markedSpans = spans; + } + + // Helpers used when computing which overlapping collapsed span + // counts as the larger one. + function extraLeft(marker) { return marker.inclusiveLeft ? -1 : 0 } + function extraRight(marker) { return marker.inclusiveRight ? 1 : 0 } + + // Returns a number indicating which of two overlapping collapsed + // spans is larger (and thus includes the other). Falls back to + // comparing ids when the spans cover exactly the same range. + function compareCollapsedMarkers(a, b) { + var lenDiff = a.lines.length - b.lines.length; + if (lenDiff != 0) { return lenDiff } + var aPos = a.find(), bPos = b.find(); + var fromCmp = cmp(aPos.from, bPos.from) || extraLeft(a) - extraLeft(b); + if (fromCmp) { return -fromCmp } + var toCmp = cmp(aPos.to, bPos.to) || extraRight(a) - extraRight(b); + if (toCmp) { return toCmp } + return b.id - a.id + } + + // Find out whether a line ends or starts in a collapsed span. If + // so, return the marker for that span. + function collapsedSpanAtSide(line, start) { + var sps = sawCollapsedSpans && line.markedSpans, found; + if (sps) { for (var sp = (void 0), i = 0; i < sps.length; ++i) { + sp = sps[i]; + if (sp.marker.collapsed && (start ? sp.from : sp.to) == null && + (!found || compareCollapsedMarkers(found, sp.marker) < 0)) + { found = sp.marker; } + } } + return found + } + function collapsedSpanAtStart(line) { return collapsedSpanAtSide(line, true) } + function collapsedSpanAtEnd(line) { return collapsedSpanAtSide(line, false) } + + function collapsedSpanAround(line, ch) { + var sps = sawCollapsedSpans && line.markedSpans, found; + if (sps) { for (var i = 0; i < sps.length; ++i) { + var sp = sps[i]; + if (sp.marker.collapsed && (sp.from == null || sp.from < ch) && (sp.to == null || sp.to > ch) && + (!found || compareCollapsedMarkers(found, sp.marker) < 0)) { found = sp.marker; } + } } + return found + } + + // Test whether there exists a collapsed span that partially + // overlaps (covers the start or end, but not both) of a new span. + // Such overlap is not allowed. + function conflictingCollapsedRange(doc, lineNo, from, to, marker) { + var line = getLine(doc, lineNo); + var sps = sawCollapsedSpans && line.markedSpans; + if (sps) { for (var i = 0; i < sps.length; ++i) { + var sp = sps[i]; + if (!sp.marker.collapsed) { continue } + var found = sp.marker.find(0); + var fromCmp = cmp(found.from, from) || extraLeft(sp.marker) - extraLeft(marker); + var toCmp = cmp(found.to, to) || extraRight(sp.marker) - extraRight(marker); + if (fromCmp >= 0 && toCmp <= 0 || fromCmp <= 0 && toCmp >= 0) { continue } + if (fromCmp <= 0 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.to, from) >= 0 : cmp(found.to, from) > 0) || + fromCmp >= 0 && (sp.marker.inclusiveRight && marker.inclusiveLeft ? cmp(found.from, to) <= 0 : cmp(found.from, to) < 0)) + { return true } + } } + } + + // A visual line is a line as drawn on the screen. Folding, for + // example, can cause multiple logical lines to appear on the same + // visual line. This finds the start of the visual line that the + // given line is part of (usually that is the line itself). + function visualLine(line) { + var merged; + while (merged = collapsedSpanAtStart(line)) + { line = merged.find(-1, true).line; } + return line + } + + function visualLineEnd(line) { + var merged; + while (merged = collapsedSpanAtEnd(line)) + { line = merged.find(1, true).line; } + return line + } + + // Returns an array of logical lines that continue the visual line + // started by the argument, or undefined if there are no such lines. + function visualLineContinued(line) { + var merged, lines; + while (merged = collapsedSpanAtEnd(line)) { + line = merged.find(1, true).line + ;(lines || (lines = [])).push(line); + } + return lines + } + + // Get the line number of the start of the visual line that the + // given line number is part of. + function visualLineNo(doc, lineN) { + var line = getLine(doc, lineN), vis = visualLine(line); + if (line == vis) { return lineN } + return lineNo(vis) + } + + // Get the line number of the start of the next visual line after + // the given line. + function visualLineEndNo(doc, lineN) { + if (lineN > doc.lastLine()) { return lineN } + var line = getLine(doc, lineN), merged; + if (!lineIsHidden(doc, line)) { return lineN } + while (merged = collapsedSpanAtEnd(line)) + { line = merged.find(1, true).line; } + return lineNo(line) + 1 + } + + // Compute whether a line is hidden. Lines count as hidden when they + // are part of a visual line that starts with another line, or when + // they are entirely covered by collapsed, non-widget span. + function lineIsHidden(doc, line) { + var sps = sawCollapsedSpans && line.markedSpans; + if (sps) { for (var sp = (void 0), i = 0; i < sps.length; ++i) { + sp = sps[i]; + if (!sp.marker.collapsed) { continue } + if (sp.from == null) { return true } + if (sp.marker.widgetNode) { continue } + if (sp.from == 0 && sp.marker.inclusiveLeft && lineIsHiddenInner(doc, line, sp)) + { return true } + } } + } + function lineIsHiddenInner(doc, line, span) { + if (span.to == null) { + var end = span.marker.find(1, true); + return lineIsHiddenInner(doc, end.line, getMarkedSpanFor(end.line.markedSpans, span.marker)) + } + if (span.marker.inclusiveRight && span.to == line.text.length) + { return true } + for (var sp = (void 0), i = 0; i < line.markedSpans.length; ++i) { + sp = line.markedSpans[i]; + if (sp.marker.collapsed && !sp.marker.widgetNode && sp.from == span.to && + (sp.to == null || sp.to != span.from) && + (sp.marker.inclusiveLeft || span.marker.inclusiveRight) && + lineIsHiddenInner(doc, line, sp)) { return true } + } + } + + // Find the height above the given line. + function heightAtLine(lineObj) { + lineObj = visualLine(lineObj); + + var h = 0, chunk = lineObj.parent; + for (var i = 0; i < chunk.lines.length; ++i) { + var line = chunk.lines[i]; + if (line == lineObj) { break } + else { h += line.height; } + } + for (var p = chunk.parent; p; chunk = p, p = chunk.parent) { + for (var i$1 = 0; i$1 < p.children.length; ++i$1) { + var cur = p.children[i$1]; + if (cur == chunk) { break } + else { h += cur.height; } + } + } + return h + } + + // Compute the character length of a line, taking into account + // collapsed ranges (see markText) that might hide parts, and join + // other lines onto it. + function lineLength(line) { + if (line.height == 0) { return 0 } + var len = line.text.length, merged, cur = line; + while (merged = collapsedSpanAtStart(cur)) { + var found = merged.find(0, true); + cur = found.from.line; + len += found.from.ch - found.to.ch; + } + cur = line; + while (merged = collapsedSpanAtEnd(cur)) { + var found$1 = merged.find(0, true); + len -= cur.text.length - found$1.from.ch; + cur = found$1.to.line; + len += cur.text.length - found$1.to.ch; + } + return len + } + + // Find the longest line in the document. + function findMaxLine(cm) { + var d = cm.display, doc = cm.doc; + d.maxLine = getLine(doc, doc.first); + d.maxLineLength = lineLength(d.maxLine); + d.maxLineChanged = true; + doc.iter(function (line) { + var len = lineLength(line); + if (len > d.maxLineLength) { + d.maxLineLength = len; + d.maxLine = line; + } + }); + } + + // LINE DATA STRUCTURE + + // Line objects. These hold state related to a line, including + // highlighting info (the styles array). + var Line = function(text, markedSpans, estimateHeight) { + this.text = text; + attachMarkedSpans(this, markedSpans); + this.height = estimateHeight ? estimateHeight(this) : 1; + }; + + Line.prototype.lineNo = function () { return lineNo(this) }; + eventMixin(Line); + + // Change the content (text, markers) of a line. Automatically + // invalidates cached information and tries to re-estimate the + // line's height. + function updateLine(line, text, markedSpans, estimateHeight) { + line.text = text; + if (line.stateAfter) { line.stateAfter = null; } + if (line.styles) { line.styles = null; } + if (line.order != null) { line.order = null; } + detachMarkedSpans(line); + attachMarkedSpans(line, markedSpans); + var estHeight = estimateHeight ? estimateHeight(line) : 1; + if (estHeight != line.height) { updateLineHeight(line, estHeight); } + } + + // Detach a line from the document tree and its markers. + function cleanUpLine(line) { + line.parent = null; + detachMarkedSpans(line); + } + + // Convert a style as returned by a mode (either null, or a string + // containing one or more styles) to a CSS style. This is cached, + // and also looks for line-wide styles. + var styleToClassCache = {}, styleToClassCacheWithMode = {}; + function interpretTokenStyle(style, options) { + if (!style || /^\s*$/.test(style)) { return null } + var cache = options.addModeClass ? styleToClassCacheWithMode : styleToClassCache; + return cache[style] || + (cache[style] = style.replace(/\S+/g, "cm-$&")) + } + + // Render the DOM representation of the text of a line. Also builds + // up a 'line map', which points at the DOM nodes that represent + // specific stretches of text, and is used by the measuring code. + // The returned object contains the DOM node, this map, and + // information about line-wide styles that were set by the mode. + function buildLineContent(cm, lineView) { + // The padding-right forces the element to have a 'border', which + // is needed on Webkit to be able to get line-level bounding + // rectangles for it (in measureChar). + var content = eltP("span", null, null, webkit ? "padding-right: .1px" : null); + var builder = {pre: eltP("pre", [content], "CodeMirror-line"), content: content, + col: 0, pos: 0, cm: cm, + trailingSpace: false, + splitSpaces: cm.getOption("lineWrapping")}; + lineView.measure = {}; + + // Iterate over the logical lines that make up this visual line. + for (var i = 0; i <= (lineView.rest ? lineView.rest.length : 0); i++) { + var line = i ? lineView.rest[i - 1] : lineView.line, order = (void 0); + builder.pos = 0; + builder.addToken = buildToken; + // Optionally wire in some hacks into the token-rendering + // algorithm, to deal with browser quirks. + if (hasBadBidiRects(cm.display.measure) && (order = getOrder(line, cm.doc.direction))) + { builder.addToken = buildTokenBadBidi(builder.addToken, order); } + builder.map = []; + var allowFrontierUpdate = lineView != cm.display.externalMeasured && lineNo(line); + insertLineContent(line, builder, getLineStyles(cm, line, allowFrontierUpdate)); + if (line.styleClasses) { + if (line.styleClasses.bgClass) + { builder.bgClass = joinClasses(line.styleClasses.bgClass, builder.bgClass || ""); } + if (line.styleClasses.textClass) + { builder.textClass = joinClasses(line.styleClasses.textClass, builder.textClass || ""); } + } + + // Ensure at least a single node is present, for measuring. + if (builder.map.length == 0) + { builder.map.push(0, 0, builder.content.appendChild(zeroWidthElement(cm.display.measure))); } + + // Store the map and a cache object for the current logical line + if (i == 0) { + lineView.measure.map = builder.map; + lineView.measure.cache = {}; + } else { + (lineView.measure.maps || (lineView.measure.maps = [])).push(builder.map) + ;(lineView.measure.caches || (lineView.measure.caches = [])).push({}); + } + } + + // See issue #2901 + if (webkit) { + var last = builder.content.lastChild; + if (/\bcm-tab\b/.test(last.className) || (last.querySelector && last.querySelector(".cm-tab"))) + { builder.content.className = "cm-tab-wrap-hack"; } + } + + signal(cm, "renderLine", cm, lineView.line, builder.pre); + if (builder.pre.className) + { builder.textClass = joinClasses(builder.pre.className, builder.textClass || ""); } + + return builder + } + + function defaultSpecialCharPlaceholder(ch) { + var token = elt("span", "\u2022", "cm-invalidchar"); + token.title = "\\u" + ch.charCodeAt(0).toString(16); + token.setAttribute("aria-label", token.title); + return token + } + + // Build up the DOM representation for a single token, and add it to + // the line map. Takes care to render special characters separately. + function buildToken(builder, text, style, startStyle, endStyle, css, attributes) { + if (!text) { return } + var displayText = builder.splitSpaces ? splitSpaces(text, builder.trailingSpace) : text; + var special = builder.cm.state.specialChars, mustWrap = false; + var content; + if (!special.test(text)) { + builder.col += text.length; + content = document.createTextNode(displayText); + builder.map.push(builder.pos, builder.pos + text.length, content); + if (ie && ie_version < 9) { mustWrap = true; } + builder.pos += text.length; + } else { + content = document.createDocumentFragment(); + var pos = 0; + while (true) { + special.lastIndex = pos; + var m = special.exec(text); + var skipped = m ? m.index - pos : text.length - pos; + if (skipped) { + var txt = document.createTextNode(displayText.slice(pos, pos + skipped)); + if (ie && ie_version < 9) { content.appendChild(elt("span", [txt])); } + else { content.appendChild(txt); } + builder.map.push(builder.pos, builder.pos + skipped, txt); + builder.col += skipped; + builder.pos += skipped; + } + if (!m) { break } + pos += skipped + 1; + var txt$1 = (void 0); + if (m[0] == "\t") { + var tabSize = builder.cm.options.tabSize, tabWidth = tabSize - builder.col % tabSize; + txt$1 = content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab")); + txt$1.setAttribute("role", "presentation"); + txt$1.setAttribute("cm-text", "\t"); + builder.col += tabWidth; + } else if (m[0] == "\r" || m[0] == "\n") { + txt$1 = content.appendChild(elt("span", m[0] == "\r" ? "\u240d" : "\u2424", "cm-invalidchar")); + txt$1.setAttribute("cm-text", m[0]); + builder.col += 1; + } else { + txt$1 = builder.cm.options.specialCharPlaceholder(m[0]); + txt$1.setAttribute("cm-text", m[0]); + if (ie && ie_version < 9) { content.appendChild(elt("span", [txt$1])); } + else { content.appendChild(txt$1); } + builder.col += 1; + } + builder.map.push(builder.pos, builder.pos + 1, txt$1); + builder.pos++; + } + } + builder.trailingSpace = displayText.charCodeAt(text.length - 1) == 32; + if (style || startStyle || endStyle || mustWrap || css || attributes) { + var fullStyle = style || ""; + if (startStyle) { fullStyle += startStyle; } + if (endStyle) { fullStyle += endStyle; } + var token = elt("span", [content], fullStyle, css); + if (attributes) { + for (var attr in attributes) { if (attributes.hasOwnProperty(attr) && attr != "style" && attr != "class") + { token.setAttribute(attr, attributes[attr]); } } + } + return builder.content.appendChild(token) + } + builder.content.appendChild(content); + } + + // Change some spaces to NBSP to prevent the browser from collapsing + // trailing spaces at the end of a line when rendering text (issue #1362). + function splitSpaces(text, trailingBefore) { + if (text.length > 1 && !/ /.test(text)) { return text } + var spaceBefore = trailingBefore, result = ""; + for (var i = 0; i < text.length; i++) { + var ch = text.charAt(i); + if (ch == " " && spaceBefore && (i == text.length - 1 || text.charCodeAt(i + 1) == 32)) + { ch = "\u00a0"; } + result += ch; + spaceBefore = ch == " "; + } + return result + } + + // Work around nonsense dimensions being reported for stretches of + // right-to-left text. + function buildTokenBadBidi(inner, order) { + return function (builder, text, style, startStyle, endStyle, css, attributes) { + style = style ? style + " cm-force-border" : "cm-force-border"; + var start = builder.pos, end = start + text.length; + for (;;) { + // Find the part that overlaps with the start of this text + var part = (void 0); + for (var i = 0; i < order.length; i++) { + part = order[i]; + if (part.to > start && part.from <= start) { break } + } + if (part.to >= end) { return inner(builder, text, style, startStyle, endStyle, css, attributes) } + inner(builder, text.slice(0, part.to - start), style, startStyle, null, css, attributes); + startStyle = null; + text = text.slice(part.to - start); + start = part.to; + } + } + } + + function buildCollapsedSpan(builder, size, marker, ignoreWidget) { + var widget = !ignoreWidget && marker.widgetNode; + if (widget) { builder.map.push(builder.pos, builder.pos + size, widget); } + if (!ignoreWidget && builder.cm.display.input.needsContentAttribute) { + if (!widget) + { widget = builder.content.appendChild(document.createElement("span")); } + widget.setAttribute("cm-marker", marker.id); + } + if (widget) { + builder.cm.display.input.setUneditable(widget); + builder.content.appendChild(widget); + } + builder.pos += size; + builder.trailingSpace = false; + } + + // Outputs a number of spans to make up a line, taking highlighting + // and marked text into account. + function insertLineContent(line, builder, styles) { + var spans = line.markedSpans, allText = line.text, at = 0; + if (!spans) { + for (var i$1 = 1; i$1 < styles.length; i$1+=2) + { builder.addToken(builder, allText.slice(at, at = styles[i$1]), interpretTokenStyle(styles[i$1+1], builder.cm.options)); } + return + } + + var len = allText.length, pos = 0, i = 1, text = "", style, css; + var nextChange = 0, spanStyle, spanEndStyle, spanStartStyle, collapsed, attributes; + for (;;) { + if (nextChange == pos) { // Update current marker set + spanStyle = spanEndStyle = spanStartStyle = css = ""; + attributes = null; + collapsed = null; nextChange = Infinity; + var foundBookmarks = [], endStyles = (void 0); + for (var j = 0; j < spans.length; ++j) { + var sp = spans[j], m = sp.marker; + if (m.type == "bookmark" && sp.from == pos && m.widgetNode) { + foundBookmarks.push(m); + } else if (sp.from <= pos && (sp.to == null || sp.to > pos || m.collapsed && sp.to == pos && sp.from == pos)) { + if (sp.to != null && sp.to != pos && nextChange > sp.to) { + nextChange = sp.to; + spanEndStyle = ""; + } + if (m.className) { spanStyle += " " + m.className; } + if (m.css) { css = (css ? css + ";" : "") + m.css; } + if (m.startStyle && sp.from == pos) { spanStartStyle += " " + m.startStyle; } + if (m.endStyle && sp.to == nextChange) { (endStyles || (endStyles = [])).push(m.endStyle, sp.to); } + // support for the old title property + // https://github.com/codemirror/CodeMirror/pull/5673 + if (m.title) { (attributes || (attributes = {})).title = m.title; } + if (m.attributes) { + for (var attr in m.attributes) + { (attributes || (attributes = {}))[attr] = m.attributes[attr]; } + } + if (m.collapsed && (!collapsed || compareCollapsedMarkers(collapsed.marker, m) < 0)) + { collapsed = sp; } + } else if (sp.from > pos && nextChange > sp.from) { + nextChange = sp.from; + } + } + if (endStyles) { for (var j$1 = 0; j$1 < endStyles.length; j$1 += 2) + { if (endStyles[j$1 + 1] == nextChange) { spanEndStyle += " " + endStyles[j$1]; } } } + + if (!collapsed || collapsed.from == pos) { for (var j$2 = 0; j$2 < foundBookmarks.length; ++j$2) + { buildCollapsedSpan(builder, 0, foundBookmarks[j$2]); } } + if (collapsed && (collapsed.from || 0) == pos) { + buildCollapsedSpan(builder, (collapsed.to == null ? len + 1 : collapsed.to) - pos, + collapsed.marker, collapsed.from == null); + if (collapsed.to == null) { return } + if (collapsed.to == pos) { collapsed = false; } + } + } + if (pos >= len) { break } + + var upto = Math.min(len, nextChange); + while (true) { + if (text) { + var end = pos + text.length; + if (!collapsed) { + var tokenText = end > upto ? text.slice(0, upto - pos) : text; + builder.addToken(builder, tokenText, style ? style + spanStyle : spanStyle, + spanStartStyle, pos + tokenText.length == nextChange ? spanEndStyle : "", css, attributes); + } + if (end >= upto) {text = text.slice(upto - pos); pos = upto; break} + pos = end; + spanStartStyle = ""; + } + text = allText.slice(at, at = styles[i++]); + style = interpretTokenStyle(styles[i++], builder.cm.options); + } + } + } + + + // These objects are used to represent the visible (currently drawn) + // part of the document. A LineView may correspond to multiple + // logical lines, if those are connected by collapsed ranges. + function LineView(doc, line, lineN) { + // The starting line + this.line = line; + // Continuing lines, if any + this.rest = visualLineContinued(line); + // Number of logical lines in this visual line + this.size = this.rest ? lineNo(lst(this.rest)) - lineN + 1 : 1; + this.node = this.text = null; + this.hidden = lineIsHidden(doc, line); + } + + // Create a range of LineView objects for the given lines. + function buildViewArray(cm, from, to) { + var array = [], nextPos; + for (var pos = from; pos < to; pos = nextPos) { + var view = new LineView(cm.doc, getLine(cm.doc, pos), pos); + nextPos = pos + view.size; + array.push(view); + } + return array + } + + var operationGroup = null; + + function pushOperation(op) { + if (operationGroup) { + operationGroup.ops.push(op); + } else { + op.ownsGroup = operationGroup = { + ops: [op], + delayedCallbacks: [] + }; + } + } + + function fireCallbacksForOps(group) { + // Calls delayed callbacks and cursorActivity handlers until no + // new ones appear + var callbacks = group.delayedCallbacks, i = 0; + do { + for (; i < callbacks.length; i++) + { callbacks[i].call(null); } + for (var j = 0; j < group.ops.length; j++) { + var op = group.ops[j]; + if (op.cursorActivityHandlers) + { while (op.cursorActivityCalled < op.cursorActivityHandlers.length) + { op.cursorActivityHandlers[op.cursorActivityCalled++].call(null, op.cm); } } + } + } while (i < callbacks.length) + } + + function finishOperation(op, endCb) { + var group = op.ownsGroup; + if (!group) { return } + + try { fireCallbacksForOps(group); } + finally { + operationGroup = null; + endCb(group); + } + } + + var orphanDelayedCallbacks = null; + + // Often, we want to signal events at a point where we are in the + // middle of some work, but don't want the handler to start calling + // other methods on the editor, which might be in an inconsistent + // state or simply not expect any other events to happen. + // signalLater looks whether there are any handlers, and schedules + // them to be executed when the last operation ends, or, if no + // operation is active, when a timeout fires. + function signalLater(emitter, type /*, values...*/) { + var arr = getHandlers(emitter, type); + if (!arr.length) { return } + var args = Array.prototype.slice.call(arguments, 2), list; + if (operationGroup) { + list = operationGroup.delayedCallbacks; + } else if (orphanDelayedCallbacks) { + list = orphanDelayedCallbacks; + } else { + list = orphanDelayedCallbacks = []; + setTimeout(fireOrphanDelayed, 0); + } + var loop = function ( i ) { + list.push(function () { return arr[i].apply(null, args); }); + }; + + for (var i = 0; i < arr.length; ++i) + loop( i ); + } + + function fireOrphanDelayed() { + var delayed = orphanDelayedCallbacks; + orphanDelayedCallbacks = null; + for (var i = 0; i < delayed.length; ++i) { delayed[i](); } + } + + // When an aspect of a line changes, a string is added to + // lineView.changes. This updates the relevant part of the line's + // DOM structure. + function updateLineForChanges(cm, lineView, lineN, dims) { + for (var j = 0; j < lineView.changes.length; j++) { + var type = lineView.changes[j]; + if (type == "text") { updateLineText(cm, lineView); } + else if (type == "gutter") { updateLineGutter(cm, lineView, lineN, dims); } + else if (type == "class") { updateLineClasses(cm, lineView); } + else if (type == "widget") { updateLineWidgets(cm, lineView, dims); } + } + lineView.changes = null; + } + + // Lines with gutter elements, widgets or a background class need to + // be wrapped, and have the extra elements added to the wrapper div + function ensureLineWrapped(lineView) { + if (lineView.node == lineView.text) { + lineView.node = elt("div", null, null, "position: relative"); + if (lineView.text.parentNode) + { lineView.text.parentNode.replaceChild(lineView.node, lineView.text); } + lineView.node.appendChild(lineView.text); + if (ie && ie_version < 8) { lineView.node.style.zIndex = 2; } + } + return lineView.node + } + + function updateLineBackground(cm, lineView) { + var cls = lineView.bgClass ? lineView.bgClass + " " + (lineView.line.bgClass || "") : lineView.line.bgClass; + if (cls) { cls += " CodeMirror-linebackground"; } + if (lineView.background) { + if (cls) { lineView.background.className = cls; } + else { lineView.background.parentNode.removeChild(lineView.background); lineView.background = null; } + } else if (cls) { + var wrap = ensureLineWrapped(lineView); + lineView.background = wrap.insertBefore(elt("div", null, cls), wrap.firstChild); + cm.display.input.setUneditable(lineView.background); + } + } + + // Wrapper around buildLineContent which will reuse the structure + // in display.externalMeasured when possible. + function getLineContent(cm, lineView) { + var ext = cm.display.externalMeasured; + if (ext && ext.line == lineView.line) { + cm.display.externalMeasured = null; + lineView.measure = ext.measure; + return ext.built + } + return buildLineContent(cm, lineView) + } + + // Redraw the line's text. Interacts with the background and text + // classes because the mode may output tokens that influence these + // classes. + function updateLineText(cm, lineView) { + var cls = lineView.text.className; + var built = getLineContent(cm, lineView); + if (lineView.text == lineView.node) { lineView.node = built.pre; } + lineView.text.parentNode.replaceChild(built.pre, lineView.text); + lineView.text = built.pre; + if (built.bgClass != lineView.bgClass || built.textClass != lineView.textClass) { + lineView.bgClass = built.bgClass; + lineView.textClass = built.textClass; + updateLineClasses(cm, lineView); + } else if (cls) { + lineView.text.className = cls; + } + } + + function updateLineClasses(cm, lineView) { + updateLineBackground(cm, lineView); + if (lineView.line.wrapClass) + { ensureLineWrapped(lineView).className = lineView.line.wrapClass; } + else if (lineView.node != lineView.text) + { lineView.node.className = ""; } + var textClass = lineView.textClass ? lineView.textClass + " " + (lineView.line.textClass || "") : lineView.line.textClass; + lineView.text.className = textClass || ""; + } + + function updateLineGutter(cm, lineView, lineN, dims) { + if (lineView.gutter) { + lineView.node.removeChild(lineView.gutter); + lineView.gutter = null; + } + if (lineView.gutterBackground) { + lineView.node.removeChild(lineView.gutterBackground); + lineView.gutterBackground = null; + } + if (lineView.line.gutterClass) { + var wrap = ensureLineWrapped(lineView); + lineView.gutterBackground = elt("div", null, "CodeMirror-gutter-background " + lineView.line.gutterClass, + ("left: " + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px; width: " + (dims.gutterTotalWidth) + "px")); + cm.display.input.setUneditable(lineView.gutterBackground); + wrap.insertBefore(lineView.gutterBackground, lineView.text); + } + var markers = lineView.line.gutterMarkers; + if (cm.options.lineNumbers || markers) { + var wrap$1 = ensureLineWrapped(lineView); + var gutterWrap = lineView.gutter = elt("div", null, "CodeMirror-gutter-wrapper", ("left: " + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px")); + cm.display.input.setUneditable(gutterWrap); + wrap$1.insertBefore(gutterWrap, lineView.text); + if (lineView.line.gutterClass) + { gutterWrap.className += " " + lineView.line.gutterClass; } + if (cm.options.lineNumbers && (!markers || !markers["CodeMirror-linenumbers"])) + { lineView.lineNumber = gutterWrap.appendChild( + elt("div", lineNumberFor(cm.options, lineN), + "CodeMirror-linenumber CodeMirror-gutter-elt", + ("left: " + (dims.gutterLeft["CodeMirror-linenumbers"]) + "px; width: " + (cm.display.lineNumInnerWidth) + "px"))); } + if (markers) { for (var k = 0; k < cm.display.gutterSpecs.length; ++k) { + var id = cm.display.gutterSpecs[k].className, found = markers.hasOwnProperty(id) && markers[id]; + if (found) + { gutterWrap.appendChild(elt("div", [found], "CodeMirror-gutter-elt", + ("left: " + (dims.gutterLeft[id]) + "px; width: " + (dims.gutterWidth[id]) + "px"))); } + } } + } + } + + function updateLineWidgets(cm, lineView, dims) { + if (lineView.alignable) { lineView.alignable = null; } + var isWidget = classTest("CodeMirror-linewidget"); + for (var node = lineView.node.firstChild, next = (void 0); node; node = next) { + next = node.nextSibling; + if (isWidget.test(node.className)) { lineView.node.removeChild(node); } + } + insertLineWidgets(cm, lineView, dims); + } + + // Build a line's DOM representation from scratch + function buildLineElement(cm, lineView, lineN, dims) { + var built = getLineContent(cm, lineView); + lineView.text = lineView.node = built.pre; + if (built.bgClass) { lineView.bgClass = built.bgClass; } + if (built.textClass) { lineView.textClass = built.textClass; } + + updateLineClasses(cm, lineView); + updateLineGutter(cm, lineView, lineN, dims); + insertLineWidgets(cm, lineView, dims); + return lineView.node + } + + // A lineView may contain multiple logical lines (when merged by + // collapsed spans). The widgets for all of them need to be drawn. + function insertLineWidgets(cm, lineView, dims) { + insertLineWidgetsFor(cm, lineView.line, lineView, dims, true); + if (lineView.rest) { for (var i = 0; i < lineView.rest.length; i++) + { insertLineWidgetsFor(cm, lineView.rest[i], lineView, dims, false); } } + } + + function insertLineWidgetsFor(cm, line, lineView, dims, allowAbove) { + if (!line.widgets) { return } + var wrap = ensureLineWrapped(lineView); + for (var i = 0, ws = line.widgets; i < ws.length; ++i) { + var widget = ws[i], node = elt("div", [widget.node], "CodeMirror-linewidget" + (widget.className ? " " + widget.className : "")); + if (!widget.handleMouseEvents) { node.setAttribute("cm-ignore-events", "true"); } + positionLineWidget(widget, node, lineView, dims); + cm.display.input.setUneditable(node); + if (allowAbove && widget.above) + { wrap.insertBefore(node, lineView.gutter || lineView.text); } + else + { wrap.appendChild(node); } + signalLater(widget, "redraw"); + } + } + + function positionLineWidget(widget, node, lineView, dims) { + if (widget.noHScroll) { + (lineView.alignable || (lineView.alignable = [])).push(node); + var width = dims.wrapperWidth; + node.style.left = dims.fixedPos + "px"; + if (!widget.coverGutter) { + width -= dims.gutterTotalWidth; + node.style.paddingLeft = dims.gutterTotalWidth + "px"; + } + node.style.width = width + "px"; + } + if (widget.coverGutter) { + node.style.zIndex = 5; + node.style.position = "relative"; + if (!widget.noHScroll) { node.style.marginLeft = -dims.gutterTotalWidth + "px"; } + } + } + + function widgetHeight(widget) { + if (widget.height != null) { return widget.height } + var cm = widget.doc.cm; + if (!cm) { return 0 } + if (!contains(document.body, widget.node)) { + var parentStyle = "position: relative;"; + if (widget.coverGutter) + { parentStyle += "margin-left: -" + cm.display.gutters.offsetWidth + "px;"; } + if (widget.noHScroll) + { parentStyle += "width: " + cm.display.wrapper.clientWidth + "px;"; } + removeChildrenAndAdd(cm.display.measure, elt("div", [widget.node], null, parentStyle)); + } + return widget.height = widget.node.parentNode.offsetHeight + } + + // Return true when the given mouse event happened in a widget + function eventInWidget(display, e) { + for (var n = e_target(e); n != display.wrapper; n = n.parentNode) { + if (!n || (n.nodeType == 1 && n.getAttribute("cm-ignore-events") == "true") || + (n.parentNode == display.sizer && n != display.mover)) + { return true } + } + } + + // POSITION MEASUREMENT + + function paddingTop(display) {return display.lineSpace.offsetTop} + function paddingVert(display) {return display.mover.offsetHeight - display.lineSpace.offsetHeight} + function paddingH(display) { + if (display.cachedPaddingH) { return display.cachedPaddingH } + var e = removeChildrenAndAdd(display.measure, elt("pre", "x", "CodeMirror-line-like")); + var style = window.getComputedStyle ? window.getComputedStyle(e) : e.currentStyle; + var data = {left: parseInt(style.paddingLeft), right: parseInt(style.paddingRight)}; + if (!isNaN(data.left) && !isNaN(data.right)) { display.cachedPaddingH = data; } + return data + } + + function scrollGap(cm) { return scrollerGap - cm.display.nativeBarWidth } + function displayWidth(cm) { + return cm.display.scroller.clientWidth - scrollGap(cm) - cm.display.barWidth + } + function displayHeight(cm) { + return cm.display.scroller.clientHeight - scrollGap(cm) - cm.display.barHeight + } + + // Ensure the lineView.wrapping.heights array is populated. This is + // an array of bottom offsets for the lines that make up a drawn + // line. When lineWrapping is on, there might be more than one + // height. + function ensureLineHeights(cm, lineView, rect) { + var wrapping = cm.options.lineWrapping; + var curWidth = wrapping && displayWidth(cm); + if (!lineView.measure.heights || wrapping && lineView.measure.width != curWidth) { + var heights = lineView.measure.heights = []; + if (wrapping) { + lineView.measure.width = curWidth; + var rects = lineView.text.firstChild.getClientRects(); + for (var i = 0; i < rects.length - 1; i++) { + var cur = rects[i], next = rects[i + 1]; + if (Math.abs(cur.bottom - next.bottom) > 2) + { heights.push((cur.bottom + next.top) / 2 - rect.top); } + } + } + heights.push(rect.bottom - rect.top); + } + } + + // Find a line map (mapping character offsets to text nodes) and a + // measurement cache for the given line number. (A line view might + // contain multiple lines when collapsed ranges are present.) + function mapFromLineView(lineView, line, lineN) { + if (lineView.line == line) + { return {map: lineView.measure.map, cache: lineView.measure.cache} } + for (var i = 0; i < lineView.rest.length; i++) + { if (lineView.rest[i] == line) + { return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i]} } } + for (var i$1 = 0; i$1 < lineView.rest.length; i$1++) + { if (lineNo(lineView.rest[i$1]) > lineN) + { return {map: lineView.measure.maps[i$1], cache: lineView.measure.caches[i$1], before: true} } } + } + + // Render a line into the hidden node display.externalMeasured. Used + // when measurement is needed for a line that's not in the viewport. + function updateExternalMeasurement(cm, line) { + line = visualLine(line); + var lineN = lineNo(line); + var view = cm.display.externalMeasured = new LineView(cm.doc, line, lineN); + view.lineN = lineN; + var built = view.built = buildLineContent(cm, view); + view.text = built.pre; + removeChildrenAndAdd(cm.display.lineMeasure, built.pre); + return view + } + + // Get a {top, bottom, left, right} box (in line-local coordinates) + // for a given character. + function measureChar(cm, line, ch, bias) { + return measureCharPrepared(cm, prepareMeasureForLine(cm, line), ch, bias) + } + + // Find a line view that corresponds to the given line number. + function findViewForLine(cm, lineN) { + if (lineN >= cm.display.viewFrom && lineN < cm.display.viewTo) + { return cm.display.view[findViewIndex(cm, lineN)] } + var ext = cm.display.externalMeasured; + if (ext && lineN >= ext.lineN && lineN < ext.lineN + ext.size) + { return ext } + } + + // Measurement can be split in two steps, the set-up work that + // applies to the whole line, and the measurement of the actual + // character. Functions like coordsChar, that need to do a lot of + // measurements in a row, can thus ensure that the set-up work is + // only done once. + function prepareMeasureForLine(cm, line) { + var lineN = lineNo(line); + var view = findViewForLine(cm, lineN); + if (view && !view.text) { + view = null; + } else if (view && view.changes) { + updateLineForChanges(cm, view, lineN, getDimensions(cm)); + cm.curOp.forceUpdate = true; + } + if (!view) + { view = updateExternalMeasurement(cm, line); } + + var info = mapFromLineView(view, line, lineN); + return { + line: line, view: view, rect: null, + map: info.map, cache: info.cache, before: info.before, + hasHeights: false + } + } + + // Given a prepared measurement object, measures the position of an + // actual character (or fetches it from the cache). + function measureCharPrepared(cm, prepared, ch, bias, varHeight) { + if (prepared.before) { ch = -1; } + var key = ch + (bias || ""), found; + if (prepared.cache.hasOwnProperty(key)) { + found = prepared.cache[key]; + } else { + if (!prepared.rect) + { prepared.rect = prepared.view.text.getBoundingClientRect(); } + if (!prepared.hasHeights) { + ensureLineHeights(cm, prepared.view, prepared.rect); + prepared.hasHeights = true; + } + found = measureCharInner(cm, prepared, ch, bias); + if (!found.bogus) { prepared.cache[key] = found; } + } + return {left: found.left, right: found.right, + top: varHeight ? found.rtop : found.top, + bottom: varHeight ? found.rbottom : found.bottom} + } + + var nullRect = {left: 0, right: 0, top: 0, bottom: 0}; + + function nodeAndOffsetInLineMap(map, ch, bias) { + var node, start, end, collapse, mStart, mEnd; + // First, search the line map for the text node corresponding to, + // or closest to, the target character. + for (var i = 0; i < map.length; i += 3) { + mStart = map[i]; + mEnd = map[i + 1]; + if (ch < mStart) { + start = 0; end = 1; + collapse = "left"; + } else if (ch < mEnd) { + start = ch - mStart; + end = start + 1; + } else if (i == map.length - 3 || ch == mEnd && map[i + 3] > ch) { + end = mEnd - mStart; + start = end - 1; + if (ch >= mEnd) { collapse = "right"; } + } + if (start != null) { + node = map[i + 2]; + if (mStart == mEnd && bias == (node.insertLeft ? "left" : "right")) + { collapse = bias; } + if (bias == "left" && start == 0) + { while (i && map[i - 2] == map[i - 3] && map[i - 1].insertLeft) { + node = map[(i -= 3) + 2]; + collapse = "left"; + } } + if (bias == "right" && start == mEnd - mStart) + { while (i < map.length - 3 && map[i + 3] == map[i + 4] && !map[i + 5].insertLeft) { + node = map[(i += 3) + 2]; + collapse = "right"; + } } + break + } + } + return {node: node, start: start, end: end, collapse: collapse, coverStart: mStart, coverEnd: mEnd} + } + + function getUsefulRect(rects, bias) { + var rect = nullRect; + if (bias == "left") { for (var i = 0; i < rects.length; i++) { + if ((rect = rects[i]).left != rect.right) { break } + } } else { for (var i$1 = rects.length - 1; i$1 >= 0; i$1--) { + if ((rect = rects[i$1]).left != rect.right) { break } + } } + return rect + } + + function measureCharInner(cm, prepared, ch, bias) { + var place = nodeAndOffsetInLineMap(prepared.map, ch, bias); + var node = place.node, start = place.start, end = place.end, collapse = place.collapse; + + var rect; + if (node.nodeType == 3) { // If it is a text node, use a range to retrieve the coordinates. + for (var i$1 = 0; i$1 < 4; i$1++) { // Retry a maximum of 4 times when nonsense rectangles are returned + while (start && isExtendingChar(prepared.line.text.charAt(place.coverStart + start))) { --start; } + while (place.coverStart + end < place.coverEnd && isExtendingChar(prepared.line.text.charAt(place.coverStart + end))) { ++end; } + if (ie && ie_version < 9 && start == 0 && end == place.coverEnd - place.coverStart) + { rect = node.parentNode.getBoundingClientRect(); } + else + { rect = getUsefulRect(range(node, start, end).getClientRects(), bias); } + if (rect.left || rect.right || start == 0) { break } + end = start; + start = start - 1; + collapse = "right"; + } + if (ie && ie_version < 11) { rect = maybeUpdateRectForZooming(cm.display.measure, rect); } + } else { // If it is a widget, simply get the box for the whole widget. + if (start > 0) { collapse = bias = "right"; } + var rects; + if (cm.options.lineWrapping && (rects = node.getClientRects()).length > 1) + { rect = rects[bias == "right" ? rects.length - 1 : 0]; } + else + { rect = node.getBoundingClientRect(); } + } + if (ie && ie_version < 9 && !start && (!rect || !rect.left && !rect.right)) { + var rSpan = node.parentNode.getClientRects()[0]; + if (rSpan) + { rect = {left: rSpan.left, right: rSpan.left + charWidth(cm.display), top: rSpan.top, bottom: rSpan.bottom}; } + else + { rect = nullRect; } + } + + var rtop = rect.top - prepared.rect.top, rbot = rect.bottom - prepared.rect.top; + var mid = (rtop + rbot) / 2; + var heights = prepared.view.measure.heights; + var i = 0; + for (; i < heights.length - 1; i++) + { if (mid < heights[i]) { break } } + var top = i ? heights[i - 1] : 0, bot = heights[i]; + var result = {left: (collapse == "right" ? rect.right : rect.left) - prepared.rect.left, + right: (collapse == "left" ? rect.left : rect.right) - prepared.rect.left, + top: top, bottom: bot}; + if (!rect.left && !rect.right) { result.bogus = true; } + if (!cm.options.singleCursorHeightPerLine) { result.rtop = rtop; result.rbottom = rbot; } + + return result + } + + // Work around problem with bounding client rects on ranges being + // returned incorrectly when zoomed on IE10 and below. + function maybeUpdateRectForZooming(measure, rect) { + if (!window.screen || screen.logicalXDPI == null || + screen.logicalXDPI == screen.deviceXDPI || !hasBadZoomedRects(measure)) + { return rect } + var scaleX = screen.logicalXDPI / screen.deviceXDPI; + var scaleY = screen.logicalYDPI / screen.deviceYDPI; + return {left: rect.left * scaleX, right: rect.right * scaleX, + top: rect.top * scaleY, bottom: rect.bottom * scaleY} + } + + function clearLineMeasurementCacheFor(lineView) { + if (lineView.measure) { + lineView.measure.cache = {}; + lineView.measure.heights = null; + if (lineView.rest) { for (var i = 0; i < lineView.rest.length; i++) + { lineView.measure.caches[i] = {}; } } + } + } + + function clearLineMeasurementCache(cm) { + cm.display.externalMeasure = null; + removeChildren(cm.display.lineMeasure); + for (var i = 0; i < cm.display.view.length; i++) + { clearLineMeasurementCacheFor(cm.display.view[i]); } + } + + function clearCaches(cm) { + clearLineMeasurementCache(cm); + cm.display.cachedCharWidth = cm.display.cachedTextHeight = cm.display.cachedPaddingH = null; + if (!cm.options.lineWrapping) { cm.display.maxLineChanged = true; } + cm.display.lineNumChars = null; + } + + function pageScrollX() { + // Work around https://bugs.chromium.org/p/chromium/issues/detail?id=489206 + // which causes page_Offset and bounding client rects to use + // different reference viewports and invalidate our calculations. + if (chrome && android) { return -(document.body.getBoundingClientRect().left - parseInt(getComputedStyle(document.body).marginLeft)) } + return window.pageXOffset || (document.documentElement || document.body).scrollLeft + } + function pageScrollY() { + if (chrome && android) { return -(document.body.getBoundingClientRect().top - parseInt(getComputedStyle(document.body).marginTop)) } + return window.pageYOffset || (document.documentElement || document.body).scrollTop + } + + function widgetTopHeight(lineObj) { + var height = 0; + if (lineObj.widgets) { for (var i = 0; i < lineObj.widgets.length; ++i) { if (lineObj.widgets[i].above) + { height += widgetHeight(lineObj.widgets[i]); } } } + return height + } + + // Converts a {top, bottom, left, right} box from line-local + // coordinates into another coordinate system. Context may be one of + // "line", "div" (display.lineDiv), "local"./null (editor), "window", + // or "page". + function intoCoordSystem(cm, lineObj, rect, context, includeWidgets) { + if (!includeWidgets) { + var height = widgetTopHeight(lineObj); + rect.top += height; rect.bottom += height; + } + if (context == "line") { return rect } + if (!context) { context = "local"; } + var yOff = heightAtLine(lineObj); + if (context == "local") { yOff += paddingTop(cm.display); } + else { yOff -= cm.display.viewOffset; } + if (context == "page" || context == "window") { + var lOff = cm.display.lineSpace.getBoundingClientRect(); + yOff += lOff.top + (context == "window" ? 0 : pageScrollY()); + var xOff = lOff.left + (context == "window" ? 0 : pageScrollX()); + rect.left += xOff; rect.right += xOff; + } + rect.top += yOff; rect.bottom += yOff; + return rect + } + + // Coverts a box from "div" coords to another coordinate system. + // Context may be "window", "page", "div", or "local"./null. + function fromCoordSystem(cm, coords, context) { + if (context == "div") { return coords } + var left = coords.left, top = coords.top; + // First move into "page" coordinate system + if (context == "page") { + left -= pageScrollX(); + top -= pageScrollY(); + } else if (context == "local" || !context) { + var localBox = cm.display.sizer.getBoundingClientRect(); + left += localBox.left; + top += localBox.top; + } + + var lineSpaceBox = cm.display.lineSpace.getBoundingClientRect(); + return {left: left - lineSpaceBox.left, top: top - lineSpaceBox.top} + } + + function charCoords(cm, pos, context, lineObj, bias) { + if (!lineObj) { lineObj = getLine(cm.doc, pos.line); } + return intoCoordSystem(cm, lineObj, measureChar(cm, lineObj, pos.ch, bias), context) + } + + // Returns a box for a given cursor position, which may have an + // 'other' property containing the position of the secondary cursor + // on a bidi boundary. + // A cursor Pos(line, char, "before") is on the same visual line as `char - 1` + // and after `char - 1` in writing order of `char - 1` + // A cursor Pos(line, char, "after") is on the same visual line as `char` + // and before `char` in writing order of `char` + // Examples (upper-case letters are RTL, lower-case are LTR): + // Pos(0, 1, ...) + // before after + // ab a|b a|b + // aB a|B aB| + // Ab |Ab A|b + // AB B|A B|A + // Every position after the last character on a line is considered to stick + // to the last character on the line. + function cursorCoords(cm, pos, context, lineObj, preparedMeasure, varHeight) { + lineObj = lineObj || getLine(cm.doc, pos.line); + if (!preparedMeasure) { preparedMeasure = prepareMeasureForLine(cm, lineObj); } + function get(ch, right) { + var m = measureCharPrepared(cm, preparedMeasure, ch, right ? "right" : "left", varHeight); + if (right) { m.left = m.right; } else { m.right = m.left; } + return intoCoordSystem(cm, lineObj, m, context) + } + var order = getOrder(lineObj, cm.doc.direction), ch = pos.ch, sticky = pos.sticky; + if (ch >= lineObj.text.length) { + ch = lineObj.text.length; + sticky = "before"; + } else if (ch <= 0) { + ch = 0; + sticky = "after"; + } + if (!order) { return get(sticky == "before" ? ch - 1 : ch, sticky == "before") } + + function getBidi(ch, partPos, invert) { + var part = order[partPos], right = part.level == 1; + return get(invert ? ch - 1 : ch, right != invert) + } + var partPos = getBidiPartAt(order, ch, sticky); + var other = bidiOther; + var val = getBidi(ch, partPos, sticky == "before"); + if (other != null) { val.other = getBidi(ch, other, sticky != "before"); } + return val + } + + // Used to cheaply estimate the coordinates for a position. Used for + // intermediate scroll updates. + function estimateCoords(cm, pos) { + var left = 0; + pos = clipPos(cm.doc, pos); + if (!cm.options.lineWrapping) { left = charWidth(cm.display) * pos.ch; } + var lineObj = getLine(cm.doc, pos.line); + var top = heightAtLine(lineObj) + paddingTop(cm.display); + return {left: left, right: left, top: top, bottom: top + lineObj.height} + } + + // Positions returned by coordsChar contain some extra information. + // xRel is the relative x position of the input coordinates compared + // to the found position (so xRel > 0 means the coordinates are to + // the right of the character position, for example). When outside + // is true, that means the coordinates lie outside the line's + // vertical range. + function PosWithInfo(line, ch, sticky, outside, xRel) { + var pos = Pos(line, ch, sticky); + pos.xRel = xRel; + if (outside) { pos.outside = outside; } + return pos + } + + // Compute the character position closest to the given coordinates. + // Input must be lineSpace-local ("div" coordinate system). + function coordsChar(cm, x, y) { + var doc = cm.doc; + y += cm.display.viewOffset; + if (y < 0) { return PosWithInfo(doc.first, 0, null, -1, -1) } + var lineN = lineAtHeight(doc, y), last = doc.first + doc.size - 1; + if (lineN > last) + { return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, null, 1, 1) } + if (x < 0) { x = 0; } + + var lineObj = getLine(doc, lineN); + for (;;) { + var found = coordsCharInner(cm, lineObj, lineN, x, y); + var collapsed = collapsedSpanAround(lineObj, found.ch + (found.xRel > 0 || found.outside > 0 ? 1 : 0)); + if (!collapsed) { return found } + var rangeEnd = collapsed.find(1); + if (rangeEnd.line == lineN) { return rangeEnd } + lineObj = getLine(doc, lineN = rangeEnd.line); + } + } + + function wrappedLineExtent(cm, lineObj, preparedMeasure, y) { + y -= widgetTopHeight(lineObj); + var end = lineObj.text.length; + var begin = findFirst(function (ch) { return measureCharPrepared(cm, preparedMeasure, ch - 1).bottom <= y; }, end, 0); + end = findFirst(function (ch) { return measureCharPrepared(cm, preparedMeasure, ch).top > y; }, begin, end); + return {begin: begin, end: end} + } + + function wrappedLineExtentChar(cm, lineObj, preparedMeasure, target) { + if (!preparedMeasure) { preparedMeasure = prepareMeasureForLine(cm, lineObj); } + var targetTop = intoCoordSystem(cm, lineObj, measureCharPrepared(cm, preparedMeasure, target), "line").top; + return wrappedLineExtent(cm, lineObj, preparedMeasure, targetTop) + } + + // Returns true if the given side of a box is after the given + // coordinates, in top-to-bottom, left-to-right order. + function boxIsAfter(box, x, y, left) { + return box.bottom <= y ? false : box.top > y ? true : (left ? box.left : box.right) > x + } + + function coordsCharInner(cm, lineObj, lineNo, x, y) { + // Move y into line-local coordinate space + y -= heightAtLine(lineObj); + var preparedMeasure = prepareMeasureForLine(cm, lineObj); + // When directly calling `measureCharPrepared`, we have to adjust + // for the widgets at this line. + var widgetHeight = widgetTopHeight(lineObj); + var begin = 0, end = lineObj.text.length, ltr = true; + + var order = getOrder(lineObj, cm.doc.direction); + // If the line isn't plain left-to-right text, first figure out + // which bidi section the coordinates fall into. + if (order) { + var part = (cm.options.lineWrapping ? coordsBidiPartWrapped : coordsBidiPart) + (cm, lineObj, lineNo, preparedMeasure, order, x, y); + ltr = part.level != 1; + // The awkward -1 offsets are needed because findFirst (called + // on these below) will treat its first bound as inclusive, + // second as exclusive, but we want to actually address the + // characters in the part's range + begin = ltr ? part.from : part.to - 1; + end = ltr ? part.to : part.from - 1; + } + + // A binary search to find the first character whose bounding box + // starts after the coordinates. If we run across any whose box wrap + // the coordinates, store that. + var chAround = null, boxAround = null; + var ch = findFirst(function (ch) { + var box = measureCharPrepared(cm, preparedMeasure, ch); + box.top += widgetHeight; box.bottom += widgetHeight; + if (!boxIsAfter(box, x, y, false)) { return false } + if (box.top <= y && box.left <= x) { + chAround = ch; + boxAround = box; + } + return true + }, begin, end); + + var baseX, sticky, outside = false; + // If a box around the coordinates was found, use that + if (boxAround) { + // Distinguish coordinates nearer to the left or right side of the box + var atLeft = x - boxAround.left < boxAround.right - x, atStart = atLeft == ltr; + ch = chAround + (atStart ? 0 : 1); + sticky = atStart ? "after" : "before"; + baseX = atLeft ? boxAround.left : boxAround.right; + } else { + // (Adjust for extended bound, if necessary.) + if (!ltr && (ch == end || ch == begin)) { ch++; } + // To determine which side to associate with, get the box to the + // left of the character and compare it's vertical position to the + // coordinates + sticky = ch == 0 ? "after" : ch == lineObj.text.length ? "before" : + (measureCharPrepared(cm, preparedMeasure, ch - (ltr ? 1 : 0)).bottom + widgetHeight <= y) == ltr ? + "after" : "before"; + // Now get accurate coordinates for this place, in order to get a + // base X position + var coords = cursorCoords(cm, Pos(lineNo, ch, sticky), "line", lineObj, preparedMeasure); + baseX = coords.left; + outside = y < coords.top ? -1 : y >= coords.bottom ? 1 : 0; + } + + ch = skipExtendingChars(lineObj.text, ch, 1); + return PosWithInfo(lineNo, ch, sticky, outside, x - baseX) + } + + function coordsBidiPart(cm, lineObj, lineNo, preparedMeasure, order, x, y) { + // Bidi parts are sorted left-to-right, and in a non-line-wrapping + // situation, we can take this ordering to correspond to the visual + // ordering. This finds the first part whose end is after the given + // coordinates. + var index = findFirst(function (i) { + var part = order[i], ltr = part.level != 1; + return boxIsAfter(cursorCoords(cm, Pos(lineNo, ltr ? part.to : part.from, ltr ? "before" : "after"), + "line", lineObj, preparedMeasure), x, y, true) + }, 0, order.length - 1); + var part = order[index]; + // If this isn't the first part, the part's start is also after + // the coordinates, and the coordinates aren't on the same line as + // that start, move one part back. + if (index > 0) { + var ltr = part.level != 1; + var start = cursorCoords(cm, Pos(lineNo, ltr ? part.from : part.to, ltr ? "after" : "before"), + "line", lineObj, preparedMeasure); + if (boxIsAfter(start, x, y, true) && start.top > y) + { part = order[index - 1]; } + } + return part + } + + function coordsBidiPartWrapped(cm, lineObj, _lineNo, preparedMeasure, order, x, y) { + // In a wrapped line, rtl text on wrapping boundaries can do things + // that don't correspond to the ordering in our `order` array at + // all, so a binary search doesn't work, and we want to return a + // part that only spans one line so that the binary search in + // coordsCharInner is safe. As such, we first find the extent of the + // wrapped line, and then do a flat search in which we discard any + // spans that aren't on the line. + var ref = wrappedLineExtent(cm, lineObj, preparedMeasure, y); + var begin = ref.begin; + var end = ref.end; + if (/\s/.test(lineObj.text.charAt(end - 1))) { end--; } + var part = null, closestDist = null; + for (var i = 0; i < order.length; i++) { + var p = order[i]; + if (p.from >= end || p.to <= begin) { continue } + var ltr = p.level != 1; + var endX = measureCharPrepared(cm, preparedMeasure, ltr ? Math.min(end, p.to) - 1 : Math.max(begin, p.from)).right; + // Weigh against spans ending before this, so that they are only + // picked if nothing ends after + var dist = endX < x ? x - endX + 1e9 : endX - x; + if (!part || closestDist > dist) { + part = p; + closestDist = dist; + } + } + if (!part) { part = order[order.length - 1]; } + // Clip the part to the wrapped line. + if (part.from < begin) { part = {from: begin, to: part.to, level: part.level}; } + if (part.to > end) { part = {from: part.from, to: end, level: part.level}; } + return part + } + + var measureText; + // Compute the default text height. + function textHeight(display) { + if (display.cachedTextHeight != null) { return display.cachedTextHeight } + if (measureText == null) { + measureText = elt("pre", null, "CodeMirror-line-like"); + // Measure a bunch of lines, for browsers that compute + // fractional heights. + for (var i = 0; i < 49; ++i) { + measureText.appendChild(document.createTextNode("x")); + measureText.appendChild(elt("br")); + } + measureText.appendChild(document.createTextNode("x")); + } + removeChildrenAndAdd(display.measure, measureText); + var height = measureText.offsetHeight / 50; + if (height > 3) { display.cachedTextHeight = height; } + removeChildren(display.measure); + return height || 1 + } + + // Compute the default character width. + function charWidth(display) { + if (display.cachedCharWidth != null) { return display.cachedCharWidth } + var anchor = elt("span", "xxxxxxxxxx"); + var pre = elt("pre", [anchor], "CodeMirror-line-like"); + removeChildrenAndAdd(display.measure, pre); + var rect = anchor.getBoundingClientRect(), width = (rect.right - rect.left) / 10; + if (width > 2) { display.cachedCharWidth = width; } + return width || 10 + } + + // Do a bulk-read of the DOM positions and sizes needed to draw the + // view, so that we don't interleave reading and writing to the DOM. + function getDimensions(cm) { + var d = cm.display, left = {}, width = {}; + var gutterLeft = d.gutters.clientLeft; + for (var n = d.gutters.firstChild, i = 0; n; n = n.nextSibling, ++i) { + var id = cm.display.gutterSpecs[i].className; + left[id] = n.offsetLeft + n.clientLeft + gutterLeft; + width[id] = n.clientWidth; + } + return {fixedPos: compensateForHScroll(d), + gutterTotalWidth: d.gutters.offsetWidth, + gutterLeft: left, + gutterWidth: width, + wrapperWidth: d.wrapper.clientWidth} + } + + // Computes display.scroller.scrollLeft + display.gutters.offsetWidth, + // but using getBoundingClientRect to get a sub-pixel-accurate + // result. + function compensateForHScroll(display) { + return display.scroller.getBoundingClientRect().left - display.sizer.getBoundingClientRect().left + } + + // Returns a function that estimates the height of a line, to use as + // first approximation until the line becomes visible (and is thus + // properly measurable). + function estimateHeight(cm) { + var th = textHeight(cm.display), wrapping = cm.options.lineWrapping; + var perLine = wrapping && Math.max(5, cm.display.scroller.clientWidth / charWidth(cm.display) - 3); + return function (line) { + if (lineIsHidden(cm.doc, line)) { return 0 } + + var widgetsHeight = 0; + if (line.widgets) { for (var i = 0; i < line.widgets.length; i++) { + if (line.widgets[i].height) { widgetsHeight += line.widgets[i].height; } + } } + + if (wrapping) + { return widgetsHeight + (Math.ceil(line.text.length / perLine) || 1) * th } + else + { return widgetsHeight + th } + } + } + + function estimateLineHeights(cm) { + var doc = cm.doc, est = estimateHeight(cm); + doc.iter(function (line) { + var estHeight = est(line); + if (estHeight != line.height) { updateLineHeight(line, estHeight); } + }); + } + + // Given a mouse event, find the corresponding position. If liberal + // is false, it checks whether a gutter or scrollbar was clicked, + // and returns null if it was. forRect is used by rectangular + // selections, and tries to estimate a character position even for + // coordinates beyond the right of the text. + function posFromMouse(cm, e, liberal, forRect) { + var display = cm.display; + if (!liberal && e_target(e).getAttribute("cm-not-content") == "true") { return null } + + var x, y, space = display.lineSpace.getBoundingClientRect(); + // Fails unpredictably on IE[67] when mouse is dragged around quickly. + try { x = e.clientX - space.left; y = e.clientY - space.top; } + catch (e$1) { return null } + var coords = coordsChar(cm, x, y), line; + if (forRect && coords.xRel > 0 && (line = getLine(cm.doc, coords.line).text).length == coords.ch) { + var colDiff = countColumn(line, line.length, cm.options.tabSize) - line.length; + coords = Pos(coords.line, Math.max(0, Math.round((x - paddingH(cm.display).left) / charWidth(cm.display)) - colDiff)); + } + return coords + } + + // Find the view element corresponding to a given line. Return null + // when the line isn't visible. + function findViewIndex(cm, n) { + if (n >= cm.display.viewTo) { return null } + n -= cm.display.viewFrom; + if (n < 0) { return null } + var view = cm.display.view; + for (var i = 0; i < view.length; i++) { + n -= view[i].size; + if (n < 0) { return i } + } + } + + // Updates the display.view data structure for a given change to the + // document. From and to are in pre-change coordinates. Lendiff is + // the amount of lines added or subtracted by the change. This is + // used for changes that span multiple lines, or change the way + // lines are divided into visual lines. regLineChange (below) + // registers single-line changes. + function regChange(cm, from, to, lendiff) { + if (from == null) { from = cm.doc.first; } + if (to == null) { to = cm.doc.first + cm.doc.size; } + if (!lendiff) { lendiff = 0; } + + var display = cm.display; + if (lendiff && to < display.viewTo && + (display.updateLineNumbers == null || display.updateLineNumbers > from)) + { display.updateLineNumbers = from; } + + cm.curOp.viewChanged = true; + + if (from >= display.viewTo) { // Change after + if (sawCollapsedSpans && visualLineNo(cm.doc, from) < display.viewTo) + { resetView(cm); } + } else if (to <= display.viewFrom) { // Change before + if (sawCollapsedSpans && visualLineEndNo(cm.doc, to + lendiff) > display.viewFrom) { + resetView(cm); + } else { + display.viewFrom += lendiff; + display.viewTo += lendiff; + } + } else if (from <= display.viewFrom && to >= display.viewTo) { // Full overlap + resetView(cm); + } else if (from <= display.viewFrom) { // Top overlap + var cut = viewCuttingPoint(cm, to, to + lendiff, 1); + if (cut) { + display.view = display.view.slice(cut.index); + display.viewFrom = cut.lineN; + display.viewTo += lendiff; + } else { + resetView(cm); + } + } else if (to >= display.viewTo) { // Bottom overlap + var cut$1 = viewCuttingPoint(cm, from, from, -1); + if (cut$1) { + display.view = display.view.slice(0, cut$1.index); + display.viewTo = cut$1.lineN; + } else { + resetView(cm); + } + } else { // Gap in the middle + var cutTop = viewCuttingPoint(cm, from, from, -1); + var cutBot = viewCuttingPoint(cm, to, to + lendiff, 1); + if (cutTop && cutBot) { + display.view = display.view.slice(0, cutTop.index) + .concat(buildViewArray(cm, cutTop.lineN, cutBot.lineN)) + .concat(display.view.slice(cutBot.index)); + display.viewTo += lendiff; + } else { + resetView(cm); + } + } + + var ext = display.externalMeasured; + if (ext) { + if (to < ext.lineN) + { ext.lineN += lendiff; } + else if (from < ext.lineN + ext.size) + { display.externalMeasured = null; } + } + } + + // Register a change to a single line. Type must be one of "text", + // "gutter", "class", "widget" + function regLineChange(cm, line, type) { + cm.curOp.viewChanged = true; + var display = cm.display, ext = cm.display.externalMeasured; + if (ext && line >= ext.lineN && line < ext.lineN + ext.size) + { display.externalMeasured = null; } + + if (line < display.viewFrom || line >= display.viewTo) { return } + var lineView = display.view[findViewIndex(cm, line)]; + if (lineView.node == null) { return } + var arr = lineView.changes || (lineView.changes = []); + if (indexOf(arr, type) == -1) { arr.push(type); } + } + + // Clear the view. + function resetView(cm) { + cm.display.viewFrom = cm.display.viewTo = cm.doc.first; + cm.display.view = []; + cm.display.viewOffset = 0; + } + + function viewCuttingPoint(cm, oldN, newN, dir) { + var index = findViewIndex(cm, oldN), diff, view = cm.display.view; + if (!sawCollapsedSpans || newN == cm.doc.first + cm.doc.size) + { return {index: index, lineN: newN} } + var n = cm.display.viewFrom; + for (var i = 0; i < index; i++) + { n += view[i].size; } + if (n != oldN) { + if (dir > 0) { + if (index == view.length - 1) { return null } + diff = (n + view[index].size) - oldN; + index++; + } else { + diff = n - oldN; + } + oldN += diff; newN += diff; + } + while (visualLineNo(cm.doc, newN) != newN) { + if (index == (dir < 0 ? 0 : view.length - 1)) { return null } + newN += dir * view[index - (dir < 0 ? 1 : 0)].size; + index += dir; + } + return {index: index, lineN: newN} + } + + // Force the view to cover a given range, adding empty view element + // or clipping off existing ones as needed. + function adjustView(cm, from, to) { + var display = cm.display, view = display.view; + if (view.length == 0 || from >= display.viewTo || to <= display.viewFrom) { + display.view = buildViewArray(cm, from, to); + display.viewFrom = from; + } else { + if (display.viewFrom > from) + { display.view = buildViewArray(cm, from, display.viewFrom).concat(display.view); } + else if (display.viewFrom < from) + { display.view = display.view.slice(findViewIndex(cm, from)); } + display.viewFrom = from; + if (display.viewTo < to) + { display.view = display.view.concat(buildViewArray(cm, display.viewTo, to)); } + else if (display.viewTo > to) + { display.view = display.view.slice(0, findViewIndex(cm, to)); } + } + display.viewTo = to; + } + + // Count the number of lines in the view whose DOM representation is + // out of date (or nonexistent). + function countDirtyView(cm) { + var view = cm.display.view, dirty = 0; + for (var i = 0; i < view.length; i++) { + var lineView = view[i]; + if (!lineView.hidden && (!lineView.node || lineView.changes)) { ++dirty; } + } + return dirty + } + + function updateSelection(cm) { + cm.display.input.showSelection(cm.display.input.prepareSelection()); + } + + function prepareSelection(cm, primary) { + if ( primary === void 0 ) primary = true; + + var doc = cm.doc, result = {}; + var curFragment = result.cursors = document.createDocumentFragment(); + var selFragment = result.selection = document.createDocumentFragment(); + + for (var i = 0; i < doc.sel.ranges.length; i++) { + if (!primary && i == doc.sel.primIndex) { continue } + var range = doc.sel.ranges[i]; + if (range.from().line >= cm.display.viewTo || range.to().line < cm.display.viewFrom) { continue } + var collapsed = range.empty(); + if (collapsed || cm.options.showCursorWhenSelecting) + { drawSelectionCursor(cm, range.head, curFragment); } + if (!collapsed) + { drawSelectionRange(cm, range, selFragment); } + } + return result + } + + // Draws a cursor for the given range + function drawSelectionCursor(cm, head, output) { + var pos = cursorCoords(cm, head, "div", null, null, !cm.options.singleCursorHeightPerLine); + + var cursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor")); + cursor.style.left = pos.left + "px"; + cursor.style.top = pos.top + "px"; + cursor.style.height = Math.max(0, pos.bottom - pos.top) * cm.options.cursorHeight + "px"; + + if (pos.other) { + // Secondary cursor, shown when on a 'jump' in bi-directional text + var otherCursor = output.appendChild(elt("div", "\u00a0", "CodeMirror-cursor CodeMirror-secondarycursor")); + otherCursor.style.display = ""; + otherCursor.style.left = pos.other.left + "px"; + otherCursor.style.top = pos.other.top + "px"; + otherCursor.style.height = (pos.other.bottom - pos.other.top) * .85 + "px"; + } + } + + function cmpCoords(a, b) { return a.top - b.top || a.left - b.left } + + // Draws the given range as a highlighted selection + function drawSelectionRange(cm, range, output) { + var display = cm.display, doc = cm.doc; + var fragment = document.createDocumentFragment(); + var padding = paddingH(cm.display), leftSide = padding.left; + var rightSide = Math.max(display.sizerWidth, displayWidth(cm) - display.sizer.offsetLeft) - padding.right; + var docLTR = doc.direction == "ltr"; + + function add(left, top, width, bottom) { + if (top < 0) { top = 0; } + top = Math.round(top); + bottom = Math.round(bottom); + fragment.appendChild(elt("div", null, "CodeMirror-selected", ("position: absolute; left: " + left + "px;\n top: " + top + "px; width: " + (width == null ? rightSide - left : width) + "px;\n height: " + (bottom - top) + "px"))); + } + + function drawForLine(line, fromArg, toArg) { + var lineObj = getLine(doc, line); + var lineLen = lineObj.text.length; + var start, end; + function coords(ch, bias) { + return charCoords(cm, Pos(line, ch), "div", lineObj, bias) + } + + function wrapX(pos, dir, side) { + var extent = wrappedLineExtentChar(cm, lineObj, null, pos); + var prop = (dir == "ltr") == (side == "after") ? "left" : "right"; + var ch = side == "after" ? extent.begin : extent.end - (/\s/.test(lineObj.text.charAt(extent.end - 1)) ? 2 : 1); + return coords(ch, prop)[prop] + } + + var order = getOrder(lineObj, doc.direction); + iterateBidiSections(order, fromArg || 0, toArg == null ? lineLen : toArg, function (from, to, dir, i) { + var ltr = dir == "ltr"; + var fromPos = coords(from, ltr ? "left" : "right"); + var toPos = coords(to - 1, ltr ? "right" : "left"); + + var openStart = fromArg == null && from == 0, openEnd = toArg == null && to == lineLen; + var first = i == 0, last = !order || i == order.length - 1; + if (toPos.top - fromPos.top <= 3) { // Single line + var openLeft = (docLTR ? openStart : openEnd) && first; + var openRight = (docLTR ? openEnd : openStart) && last; + var left = openLeft ? leftSide : (ltr ? fromPos : toPos).left; + var right = openRight ? rightSide : (ltr ? toPos : fromPos).right; + add(left, fromPos.top, right - left, fromPos.bottom); + } else { // Multiple lines + var topLeft, topRight, botLeft, botRight; + if (ltr) { + topLeft = docLTR && openStart && first ? leftSide : fromPos.left; + topRight = docLTR ? rightSide : wrapX(from, dir, "before"); + botLeft = docLTR ? leftSide : wrapX(to, dir, "after"); + botRight = docLTR && openEnd && last ? rightSide : toPos.right; + } else { + topLeft = !docLTR ? leftSide : wrapX(from, dir, "before"); + topRight = !docLTR && openStart && first ? rightSide : fromPos.right; + botLeft = !docLTR && openEnd && last ? leftSide : toPos.left; + botRight = !docLTR ? rightSide : wrapX(to, dir, "after"); + } + add(topLeft, fromPos.top, topRight - topLeft, fromPos.bottom); + if (fromPos.bottom < toPos.top) { add(leftSide, fromPos.bottom, null, toPos.top); } + add(botLeft, toPos.top, botRight - botLeft, toPos.bottom); + } + + if (!start || cmpCoords(fromPos, start) < 0) { start = fromPos; } + if (cmpCoords(toPos, start) < 0) { start = toPos; } + if (!end || cmpCoords(fromPos, end) < 0) { end = fromPos; } + if (cmpCoords(toPos, end) < 0) { end = toPos; } + }); + return {start: start, end: end} + } + + var sFrom = range.from(), sTo = range.to(); + if (sFrom.line == sTo.line) { + drawForLine(sFrom.line, sFrom.ch, sTo.ch); + } else { + var fromLine = getLine(doc, sFrom.line), toLine = getLine(doc, sTo.line); + var singleVLine = visualLine(fromLine) == visualLine(toLine); + var leftEnd = drawForLine(sFrom.line, sFrom.ch, singleVLine ? fromLine.text.length + 1 : null).end; + var rightStart = drawForLine(sTo.line, singleVLine ? 0 : null, sTo.ch).start; + if (singleVLine) { + if (leftEnd.top < rightStart.top - 2) { + add(leftEnd.right, leftEnd.top, null, leftEnd.bottom); + add(leftSide, rightStart.top, rightStart.left, rightStart.bottom); + } else { + add(leftEnd.right, leftEnd.top, rightStart.left - leftEnd.right, leftEnd.bottom); + } + } + if (leftEnd.bottom < rightStart.top) + { add(leftSide, leftEnd.bottom, null, rightStart.top); } + } + + output.appendChild(fragment); + } + + // Cursor-blinking + function restartBlink(cm) { + if (!cm.state.focused) { return } + var display = cm.display; + clearInterval(display.blinker); + var on = true; + display.cursorDiv.style.visibility = ""; + if (cm.options.cursorBlinkRate > 0) + { display.blinker = setInterval(function () { + if (!cm.hasFocus()) { onBlur(cm); } + display.cursorDiv.style.visibility = (on = !on) ? "" : "hidden"; + }, cm.options.cursorBlinkRate); } + else if (cm.options.cursorBlinkRate < 0) + { display.cursorDiv.style.visibility = "hidden"; } + } + + function ensureFocus(cm) { + if (!cm.state.focused) { cm.display.input.focus(); onFocus(cm); } + } + + function delayBlurEvent(cm) { + cm.state.delayingBlurEvent = true; + setTimeout(function () { if (cm.state.delayingBlurEvent) { + cm.state.delayingBlurEvent = false; + onBlur(cm); + } }, 100); + } + + function onFocus(cm, e) { + if (cm.state.delayingBlurEvent) { cm.state.delayingBlurEvent = false; } + + if (cm.options.readOnly == "nocursor") { return } + if (!cm.state.focused) { + signal(cm, "focus", cm, e); + cm.state.focused = true; + addClass(cm.display.wrapper, "CodeMirror-focused"); + // This test prevents this from firing when a context + // menu is closed (since the input reset would kill the + // select-all detection hack) + if (!cm.curOp && cm.display.selForContextMenu != cm.doc.sel) { + cm.display.input.reset(); + if (webkit) { setTimeout(function () { return cm.display.input.reset(true); }, 20); } // Issue #1730 + } + cm.display.input.receivedFocus(); + } + restartBlink(cm); + } + function onBlur(cm, e) { + if (cm.state.delayingBlurEvent) { return } + + if (cm.state.focused) { + signal(cm, "blur", cm, e); + cm.state.focused = false; + rmClass(cm.display.wrapper, "CodeMirror-focused"); + } + clearInterval(cm.display.blinker); + setTimeout(function () { if (!cm.state.focused) { cm.display.shift = false; } }, 150); + } + + // Read the actual heights of the rendered lines, and update their + // stored heights to match. + function updateHeightsInViewport(cm) { + var display = cm.display; + var prevBottom = display.lineDiv.offsetTop; + for (var i = 0; i < display.view.length; i++) { + var cur = display.view[i], wrapping = cm.options.lineWrapping; + var height = (void 0), width = 0; + if (cur.hidden) { continue } + if (ie && ie_version < 8) { + var bot = cur.node.offsetTop + cur.node.offsetHeight; + height = bot - prevBottom; + prevBottom = bot; + } else { + var box = cur.node.getBoundingClientRect(); + height = box.bottom - box.top; + // Check that lines don't extend past the right of the current + // editor width + if (!wrapping && cur.text.firstChild) + { width = cur.text.firstChild.getBoundingClientRect().right - box.left - 1; } + } + var diff = cur.line.height - height; + if (diff > .005 || diff < -.005) { + updateLineHeight(cur.line, height); + updateWidgetHeight(cur.line); + if (cur.rest) { for (var j = 0; j < cur.rest.length; j++) + { updateWidgetHeight(cur.rest[j]); } } + } + if (width > cm.display.sizerWidth) { + var chWidth = Math.ceil(width / charWidth(cm.display)); + if (chWidth > cm.display.maxLineLength) { + cm.display.maxLineLength = chWidth; + cm.display.maxLine = cur.line; + cm.display.maxLineChanged = true; + } + } + } + } + + // Read and store the height of line widgets associated with the + // given line. + function updateWidgetHeight(line) { + if (line.widgets) { for (var i = 0; i < line.widgets.length; ++i) { + var w = line.widgets[i], parent = w.node.parentNode; + if (parent) { w.height = parent.offsetHeight; } + } } + } + + // Compute the lines that are visible in a given viewport (defaults + // the the current scroll position). viewport may contain top, + // height, and ensure (see op.scrollToPos) properties. + function visibleLines(display, doc, viewport) { + var top = viewport && viewport.top != null ? Math.max(0, viewport.top) : display.scroller.scrollTop; + top = Math.floor(top - paddingTop(display)); + var bottom = viewport && viewport.bottom != null ? viewport.bottom : top + display.wrapper.clientHeight; + + var from = lineAtHeight(doc, top), to = lineAtHeight(doc, bottom); + // Ensure is a {from: {line, ch}, to: {line, ch}} object, and + // forces those lines into the viewport (if possible). + if (viewport && viewport.ensure) { + var ensureFrom = viewport.ensure.from.line, ensureTo = viewport.ensure.to.line; + if (ensureFrom < from) { + from = ensureFrom; + to = lineAtHeight(doc, heightAtLine(getLine(doc, ensureFrom)) + display.wrapper.clientHeight); + } else if (Math.min(ensureTo, doc.lastLine()) >= to) { + from = lineAtHeight(doc, heightAtLine(getLine(doc, ensureTo)) - display.wrapper.clientHeight); + to = ensureTo; + } + } + return {from: from, to: Math.max(to, from + 1)} + } + + // SCROLLING THINGS INTO VIEW + + // If an editor sits on the top or bottom of the window, partially + // scrolled out of view, this ensures that the cursor is visible. + function maybeScrollWindow(cm, rect) { + if (signalDOMEvent(cm, "scrollCursorIntoView")) { return } + + var display = cm.display, box = display.sizer.getBoundingClientRect(), doScroll = null; + if (rect.top + box.top < 0) { doScroll = true; } + else if (rect.bottom + box.top > (window.innerHeight || document.documentElement.clientHeight)) { doScroll = false; } + if (doScroll != null && !phantom) { + var scrollNode = elt("div", "\u200b", null, ("position: absolute;\n top: " + (rect.top - display.viewOffset - paddingTop(cm.display)) + "px;\n height: " + (rect.bottom - rect.top + scrollGap(cm) + display.barHeight) + "px;\n left: " + (rect.left) + "px; width: " + (Math.max(2, rect.right - rect.left)) + "px;")); + cm.display.lineSpace.appendChild(scrollNode); + scrollNode.scrollIntoView(doScroll); + cm.display.lineSpace.removeChild(scrollNode); + } + } + + // Scroll a given position into view (immediately), verifying that + // it actually became visible (as line heights are accurately + // measured, the position of something may 'drift' during drawing). + function scrollPosIntoView(cm, pos, end, margin) { + if (margin == null) { margin = 0; } + var rect; + if (!cm.options.lineWrapping && pos == end) { + // Set pos and end to the cursor positions around the character pos sticks to + // If pos.sticky == "before", that is around pos.ch - 1, otherwise around pos.ch + // If pos == Pos(_, 0, "before"), pos and end are unchanged + pos = pos.ch ? Pos(pos.line, pos.sticky == "before" ? pos.ch - 1 : pos.ch, "after") : pos; + end = pos.sticky == "before" ? Pos(pos.line, pos.ch + 1, "before") : pos; + } + for (var limit = 0; limit < 5; limit++) { + var changed = false; + var coords = cursorCoords(cm, pos); + var endCoords = !end || end == pos ? coords : cursorCoords(cm, end); + rect = {left: Math.min(coords.left, endCoords.left), + top: Math.min(coords.top, endCoords.top) - margin, + right: Math.max(coords.left, endCoords.left), + bottom: Math.max(coords.bottom, endCoords.bottom) + margin}; + var scrollPos = calculateScrollPos(cm, rect); + var startTop = cm.doc.scrollTop, startLeft = cm.doc.scrollLeft; + if (scrollPos.scrollTop != null) { + updateScrollTop(cm, scrollPos.scrollTop); + if (Math.abs(cm.doc.scrollTop - startTop) > 1) { changed = true; } + } + if (scrollPos.scrollLeft != null) { + setScrollLeft(cm, scrollPos.scrollLeft); + if (Math.abs(cm.doc.scrollLeft - startLeft) > 1) { changed = true; } + } + if (!changed) { break } + } + return rect + } + + // Scroll a given set of coordinates into view (immediately). + function scrollIntoView(cm, rect) { + var scrollPos = calculateScrollPos(cm, rect); + if (scrollPos.scrollTop != null) { updateScrollTop(cm, scrollPos.scrollTop); } + if (scrollPos.scrollLeft != null) { setScrollLeft(cm, scrollPos.scrollLeft); } + } + + // Calculate a new scroll position needed to scroll the given + // rectangle into view. Returns an object with scrollTop and + // scrollLeft properties. When these are undefined, the + // vertical/horizontal position does not need to be adjusted. + function calculateScrollPos(cm, rect) { + var display = cm.display, snapMargin = textHeight(cm.display); + if (rect.top < 0) { rect.top = 0; } + var screentop = cm.curOp && cm.curOp.scrollTop != null ? cm.curOp.scrollTop : display.scroller.scrollTop; + var screen = displayHeight(cm), result = {}; + if (rect.bottom - rect.top > screen) { rect.bottom = rect.top + screen; } + var docBottom = cm.doc.height + paddingVert(display); + var atTop = rect.top < snapMargin, atBottom = rect.bottom > docBottom - snapMargin; + if (rect.top < screentop) { + result.scrollTop = atTop ? 0 : rect.top; + } else if (rect.bottom > screentop + screen) { + var newTop = Math.min(rect.top, (atBottom ? docBottom : rect.bottom) - screen); + if (newTop != screentop) { result.scrollTop = newTop; } + } + + var screenleft = cm.curOp && cm.curOp.scrollLeft != null ? cm.curOp.scrollLeft : display.scroller.scrollLeft; + var screenw = displayWidth(cm) - (cm.options.fixedGutter ? display.gutters.offsetWidth : 0); + var tooWide = rect.right - rect.left > screenw; + if (tooWide) { rect.right = rect.left + screenw; } + if (rect.left < 10) + { result.scrollLeft = 0; } + else if (rect.left < screenleft) + { result.scrollLeft = Math.max(0, rect.left - (tooWide ? 0 : 10)); } + else if (rect.right > screenw + screenleft - 3) + { result.scrollLeft = rect.right + (tooWide ? 0 : 10) - screenw; } + return result + } + + // Store a relative adjustment to the scroll position in the current + // operation (to be applied when the operation finishes). + function addToScrollTop(cm, top) { + if (top == null) { return } + resolveScrollToPos(cm); + cm.curOp.scrollTop = (cm.curOp.scrollTop == null ? cm.doc.scrollTop : cm.curOp.scrollTop) + top; + } + + // Make sure that at the end of the operation the current cursor is + // shown. + function ensureCursorVisible(cm) { + resolveScrollToPos(cm); + var cur = cm.getCursor(); + cm.curOp.scrollToPos = {from: cur, to: cur, margin: cm.options.cursorScrollMargin}; + } + + function scrollToCoords(cm, x, y) { + if (x != null || y != null) { resolveScrollToPos(cm); } + if (x != null) { cm.curOp.scrollLeft = x; } + if (y != null) { cm.curOp.scrollTop = y; } + } + + function scrollToRange(cm, range) { + resolveScrollToPos(cm); + cm.curOp.scrollToPos = range; + } + + // When an operation has its scrollToPos property set, and another + // scroll action is applied before the end of the operation, this + // 'simulates' scrolling that position into view in a cheap way, so + // that the effect of intermediate scroll commands is not ignored. + function resolveScrollToPos(cm) { + var range = cm.curOp.scrollToPos; + if (range) { + cm.curOp.scrollToPos = null; + var from = estimateCoords(cm, range.from), to = estimateCoords(cm, range.to); + scrollToCoordsRange(cm, from, to, range.margin); + } + } + + function scrollToCoordsRange(cm, from, to, margin) { + var sPos = calculateScrollPos(cm, { + left: Math.min(from.left, to.left), + top: Math.min(from.top, to.top) - margin, + right: Math.max(from.right, to.right), + bottom: Math.max(from.bottom, to.bottom) + margin + }); + scrollToCoords(cm, sPos.scrollLeft, sPos.scrollTop); + } + + // Sync the scrollable area and scrollbars, ensure the viewport + // covers the visible area. + function updateScrollTop(cm, val) { + if (Math.abs(cm.doc.scrollTop - val) < 2) { return } + if (!gecko) { updateDisplaySimple(cm, {top: val}); } + setScrollTop(cm, val, true); + if (gecko) { updateDisplaySimple(cm); } + startWorker(cm, 100); + } + + function setScrollTop(cm, val, forceScroll) { + val = Math.max(0, Math.min(cm.display.scroller.scrollHeight - cm.display.scroller.clientHeight, val)); + if (cm.display.scroller.scrollTop == val && !forceScroll) { return } + cm.doc.scrollTop = val; + cm.display.scrollbars.setScrollTop(val); + if (cm.display.scroller.scrollTop != val) { cm.display.scroller.scrollTop = val; } + } + + // Sync scroller and scrollbar, ensure the gutter elements are + // aligned. + function setScrollLeft(cm, val, isScroller, forceScroll) { + val = Math.max(0, Math.min(val, cm.display.scroller.scrollWidth - cm.display.scroller.clientWidth)); + if ((isScroller ? val == cm.doc.scrollLeft : Math.abs(cm.doc.scrollLeft - val) < 2) && !forceScroll) { return } + cm.doc.scrollLeft = val; + alignHorizontally(cm); + if (cm.display.scroller.scrollLeft != val) { cm.display.scroller.scrollLeft = val; } + cm.display.scrollbars.setScrollLeft(val); + } + + // SCROLLBARS + + // Prepare DOM reads needed to update the scrollbars. Done in one + // shot to minimize update/measure roundtrips. + function measureForScrollbars(cm) { + var d = cm.display, gutterW = d.gutters.offsetWidth; + var docH = Math.round(cm.doc.height + paddingVert(cm.display)); + return { + clientHeight: d.scroller.clientHeight, + viewHeight: d.wrapper.clientHeight, + scrollWidth: d.scroller.scrollWidth, clientWidth: d.scroller.clientWidth, + viewWidth: d.wrapper.clientWidth, + barLeft: cm.options.fixedGutter ? gutterW : 0, + docHeight: docH, + scrollHeight: docH + scrollGap(cm) + d.barHeight, + nativeBarWidth: d.nativeBarWidth, + gutterWidth: gutterW + } + } + + var NativeScrollbars = function(place, scroll, cm) { + this.cm = cm; + var vert = this.vert = elt("div", [elt("div", null, null, "min-width: 1px")], "CodeMirror-vscrollbar"); + var horiz = this.horiz = elt("div", [elt("div", null, null, "height: 100%; min-height: 1px")], "CodeMirror-hscrollbar"); + vert.tabIndex = horiz.tabIndex = -1; + place(vert); place(horiz); + + on(vert, "scroll", function () { + if (vert.clientHeight) { scroll(vert.scrollTop, "vertical"); } + }); + on(horiz, "scroll", function () { + if (horiz.clientWidth) { scroll(horiz.scrollLeft, "horizontal"); } + }); + + this.checkedZeroWidth = false; + // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8). + if (ie && ie_version < 8) { this.horiz.style.minHeight = this.vert.style.minWidth = "18px"; } + }; + + NativeScrollbars.prototype.update = function (measure) { + var needsH = measure.scrollWidth > measure.clientWidth + 1; + var needsV = measure.scrollHeight > measure.clientHeight + 1; + var sWidth = measure.nativeBarWidth; + + if (needsV) { + this.vert.style.display = "block"; + this.vert.style.bottom = needsH ? sWidth + "px" : "0"; + var totalHeight = measure.viewHeight - (needsH ? sWidth : 0); + // A bug in IE8 can cause this value to be negative, so guard it. + this.vert.firstChild.style.height = + Math.max(0, measure.scrollHeight - measure.clientHeight + totalHeight) + "px"; + } else { + this.vert.style.display = ""; + this.vert.firstChild.style.height = "0"; + } + + if (needsH) { + this.horiz.style.display = "block"; + this.horiz.style.right = needsV ? sWidth + "px" : "0"; + this.horiz.style.left = measure.barLeft + "px"; + var totalWidth = measure.viewWidth - measure.barLeft - (needsV ? sWidth : 0); + this.horiz.firstChild.style.width = + Math.max(0, measure.scrollWidth - measure.clientWidth + totalWidth) + "px"; + } else { + this.horiz.style.display = ""; + this.horiz.firstChild.style.width = "0"; + } + + if (!this.checkedZeroWidth && measure.clientHeight > 0) { + if (sWidth == 0) { this.zeroWidthHack(); } + this.checkedZeroWidth = true; + } + + return {right: needsV ? sWidth : 0, bottom: needsH ? sWidth : 0} + }; + + NativeScrollbars.prototype.setScrollLeft = function (pos) { + if (this.horiz.scrollLeft != pos) { this.horiz.scrollLeft = pos; } + if (this.disableHoriz) { this.enableZeroWidthBar(this.horiz, this.disableHoriz, "horiz"); } + }; + + NativeScrollbars.prototype.setScrollTop = function (pos) { + if (this.vert.scrollTop != pos) { this.vert.scrollTop = pos; } + if (this.disableVert) { this.enableZeroWidthBar(this.vert, this.disableVert, "vert"); } + }; + + NativeScrollbars.prototype.zeroWidthHack = function () { + var w = mac && !mac_geMountainLion ? "12px" : "18px"; + this.horiz.style.height = this.vert.style.width = w; + this.horiz.style.pointerEvents = this.vert.style.pointerEvents = "none"; + this.disableHoriz = new Delayed; + this.disableVert = new Delayed; + }; + + NativeScrollbars.prototype.enableZeroWidthBar = function (bar, delay, type) { + bar.style.pointerEvents = "auto"; + function maybeDisable() { + // To find out whether the scrollbar is still visible, we + // check whether the element under the pixel in the bottom + // right corner of the scrollbar box is the scrollbar box + // itself (when the bar is still visible) or its filler child + // (when the bar is hidden). If it is still visible, we keep + // it enabled, if it's hidden, we disable pointer events. + var box = bar.getBoundingClientRect(); + var elt = type == "vert" ? document.elementFromPoint(box.right - 1, (box.top + box.bottom) / 2) + : document.elementFromPoint((box.right + box.left) / 2, box.bottom - 1); + if (elt != bar) { bar.style.pointerEvents = "none"; } + else { delay.set(1000, maybeDisable); } + } + delay.set(1000, maybeDisable); + }; + + NativeScrollbars.prototype.clear = function () { + var parent = this.horiz.parentNode; + parent.removeChild(this.horiz); + parent.removeChild(this.vert); + }; + + var NullScrollbars = function () {}; + + NullScrollbars.prototype.update = function () { return {bottom: 0, right: 0} }; + NullScrollbars.prototype.setScrollLeft = function () {}; + NullScrollbars.prototype.setScrollTop = function () {}; + NullScrollbars.prototype.clear = function () {}; + + function updateScrollbars(cm, measure) { + if (!measure) { measure = measureForScrollbars(cm); } + var startWidth = cm.display.barWidth, startHeight = cm.display.barHeight; + updateScrollbarsInner(cm, measure); + for (var i = 0; i < 4 && startWidth != cm.display.barWidth || startHeight != cm.display.barHeight; i++) { + if (startWidth != cm.display.barWidth && cm.options.lineWrapping) + { updateHeightsInViewport(cm); } + updateScrollbarsInner(cm, measureForScrollbars(cm)); + startWidth = cm.display.barWidth; startHeight = cm.display.barHeight; + } + } + + // Re-synchronize the fake scrollbars with the actual size of the + // content. + function updateScrollbarsInner(cm, measure) { + var d = cm.display; + var sizes = d.scrollbars.update(measure); + + d.sizer.style.paddingRight = (d.barWidth = sizes.right) + "px"; + d.sizer.style.paddingBottom = (d.barHeight = sizes.bottom) + "px"; + d.heightForcer.style.borderBottom = sizes.bottom + "px solid transparent"; + + if (sizes.right && sizes.bottom) { + d.scrollbarFiller.style.display = "block"; + d.scrollbarFiller.style.height = sizes.bottom + "px"; + d.scrollbarFiller.style.width = sizes.right + "px"; + } else { d.scrollbarFiller.style.display = ""; } + if (sizes.bottom && cm.options.coverGutterNextToScrollbar && cm.options.fixedGutter) { + d.gutterFiller.style.display = "block"; + d.gutterFiller.style.height = sizes.bottom + "px"; + d.gutterFiller.style.width = measure.gutterWidth + "px"; + } else { d.gutterFiller.style.display = ""; } + } + + var scrollbarModel = {"native": NativeScrollbars, "null": NullScrollbars}; + + function initScrollbars(cm) { + if (cm.display.scrollbars) { + cm.display.scrollbars.clear(); + if (cm.display.scrollbars.addClass) + { rmClass(cm.display.wrapper, cm.display.scrollbars.addClass); } + } + + cm.display.scrollbars = new scrollbarModel[cm.options.scrollbarStyle](function (node) { + cm.display.wrapper.insertBefore(node, cm.display.scrollbarFiller); + // Prevent clicks in the scrollbars from killing focus + on(node, "mousedown", function () { + if (cm.state.focused) { setTimeout(function () { return cm.display.input.focus(); }, 0); } + }); + node.setAttribute("cm-not-content", "true"); + }, function (pos, axis) { + if (axis == "horizontal") { setScrollLeft(cm, pos); } + else { updateScrollTop(cm, pos); } + }, cm); + if (cm.display.scrollbars.addClass) + { addClass(cm.display.wrapper, cm.display.scrollbars.addClass); } + } + + // Operations are used to wrap a series of changes to the editor + // state in such a way that each change won't have to update the + // cursor and display (which would be awkward, slow, and + // error-prone). Instead, display updates are batched and then all + // combined and executed at once. + + var nextOpId = 0; + // Start a new operation. + function startOperation(cm) { + cm.curOp = { + cm: cm, + viewChanged: false, // Flag that indicates that lines might need to be redrawn + startHeight: cm.doc.height, // Used to detect need to update scrollbar + forceUpdate: false, // Used to force a redraw + updateInput: 0, // Whether to reset the input textarea + typing: false, // Whether this reset should be careful to leave existing text (for compositing) + changeObjs: null, // Accumulated changes, for firing change events + cursorActivityHandlers: null, // Set of handlers to fire cursorActivity on + cursorActivityCalled: 0, // Tracks which cursorActivity handlers have been called already + selectionChanged: false, // Whether the selection needs to be redrawn + updateMaxLine: false, // Set when the widest line needs to be determined anew + scrollLeft: null, scrollTop: null, // Intermediate scroll position, not pushed to DOM yet + scrollToPos: null, // Used to scroll to a specific position + focus: false, + id: ++nextOpId // Unique ID + }; + pushOperation(cm.curOp); + } + + // Finish an operation, updating the display and signalling delayed events + function endOperation(cm) { + var op = cm.curOp; + if (op) { finishOperation(op, function (group) { + for (var i = 0; i < group.ops.length; i++) + { group.ops[i].cm.curOp = null; } + endOperations(group); + }); } + } + + // The DOM updates done when an operation finishes are batched so + // that the minimum number of relayouts are required. + function endOperations(group) { + var ops = group.ops; + for (var i = 0; i < ops.length; i++) // Read DOM + { endOperation_R1(ops[i]); } + for (var i$1 = 0; i$1 < ops.length; i$1++) // Write DOM (maybe) + { endOperation_W1(ops[i$1]); } + for (var i$2 = 0; i$2 < ops.length; i$2++) // Read DOM + { endOperation_R2(ops[i$2]); } + for (var i$3 = 0; i$3 < ops.length; i$3++) // Write DOM (maybe) + { endOperation_W2(ops[i$3]); } + for (var i$4 = 0; i$4 < ops.length; i$4++) // Read DOM + { endOperation_finish(ops[i$4]); } + } + + function endOperation_R1(op) { + var cm = op.cm, display = cm.display; + maybeClipScrollbars(cm); + if (op.updateMaxLine) { findMaxLine(cm); } + + op.mustUpdate = op.viewChanged || op.forceUpdate || op.scrollTop != null || + op.scrollToPos && (op.scrollToPos.from.line < display.viewFrom || + op.scrollToPos.to.line >= display.viewTo) || + display.maxLineChanged && cm.options.lineWrapping; + op.update = op.mustUpdate && + new DisplayUpdate(cm, op.mustUpdate && {top: op.scrollTop, ensure: op.scrollToPos}, op.forceUpdate); + } + + function endOperation_W1(op) { + op.updatedDisplay = op.mustUpdate && updateDisplayIfNeeded(op.cm, op.update); + } + + function endOperation_R2(op) { + var cm = op.cm, display = cm.display; + if (op.updatedDisplay) { updateHeightsInViewport(cm); } + + op.barMeasure = measureForScrollbars(cm); + + // If the max line changed since it was last measured, measure it, + // and ensure the document's width matches it. + // updateDisplay_W2 will use these properties to do the actual resizing + if (display.maxLineChanged && !cm.options.lineWrapping) { + op.adjustWidthTo = measureChar(cm, display.maxLine, display.maxLine.text.length).left + 3; + cm.display.sizerWidth = op.adjustWidthTo; + op.barMeasure.scrollWidth = + Math.max(display.scroller.clientWidth, display.sizer.offsetLeft + op.adjustWidthTo + scrollGap(cm) + cm.display.barWidth); + op.maxScrollLeft = Math.max(0, display.sizer.offsetLeft + op.adjustWidthTo - displayWidth(cm)); + } + + if (op.updatedDisplay || op.selectionChanged) + { op.preparedSelection = display.input.prepareSelection(); } + } + + function endOperation_W2(op) { + var cm = op.cm; + + if (op.adjustWidthTo != null) { + cm.display.sizer.style.minWidth = op.adjustWidthTo + "px"; + if (op.maxScrollLeft < cm.doc.scrollLeft) + { setScrollLeft(cm, Math.min(cm.display.scroller.scrollLeft, op.maxScrollLeft), true); } + cm.display.maxLineChanged = false; + } + + var takeFocus = op.focus && op.focus == activeElt(); + if (op.preparedSelection) + { cm.display.input.showSelection(op.preparedSelection, takeFocus); } + if (op.updatedDisplay || op.startHeight != cm.doc.height) + { updateScrollbars(cm, op.barMeasure); } + if (op.updatedDisplay) + { setDocumentHeight(cm, op.barMeasure); } + + if (op.selectionChanged) { restartBlink(cm); } + + if (cm.state.focused && op.updateInput) + { cm.display.input.reset(op.typing); } + if (takeFocus) { ensureFocus(op.cm); } + } + + function endOperation_finish(op) { + var cm = op.cm, display = cm.display, doc = cm.doc; + + if (op.updatedDisplay) { postUpdateDisplay(cm, op.update); } + + // Abort mouse wheel delta measurement, when scrolling explicitly + if (display.wheelStartX != null && (op.scrollTop != null || op.scrollLeft != null || op.scrollToPos)) + { display.wheelStartX = display.wheelStartY = null; } + + // Propagate the scroll position to the actual DOM scroller + if (op.scrollTop != null) { setScrollTop(cm, op.scrollTop, op.forceScroll); } + + if (op.scrollLeft != null) { setScrollLeft(cm, op.scrollLeft, true, true); } + // If we need to scroll a specific position into view, do so. + if (op.scrollToPos) { + var rect = scrollPosIntoView(cm, clipPos(doc, op.scrollToPos.from), + clipPos(doc, op.scrollToPos.to), op.scrollToPos.margin); + maybeScrollWindow(cm, rect); + } + + // Fire events for markers that are hidden/unidden by editing or + // undoing + var hidden = op.maybeHiddenMarkers, unhidden = op.maybeUnhiddenMarkers; + if (hidden) { for (var i = 0; i < hidden.length; ++i) + { if (!hidden[i].lines.length) { signal(hidden[i], "hide"); } } } + if (unhidden) { for (var i$1 = 0; i$1 < unhidden.length; ++i$1) + { if (unhidden[i$1].lines.length) { signal(unhidden[i$1], "unhide"); } } } + + if (display.wrapper.offsetHeight) + { doc.scrollTop = cm.display.scroller.scrollTop; } + + // Fire change events, and delayed event handlers + if (op.changeObjs) + { signal(cm, "changes", cm, op.changeObjs); } + if (op.update) + { op.update.finish(); } + } + + // Run the given function in an operation + function runInOp(cm, f) { + if (cm.curOp) { return f() } + startOperation(cm); + try { return f() } + finally { endOperation(cm); } + } + // Wraps a function in an operation. Returns the wrapped function. + function operation(cm, f) { + return function() { + if (cm.curOp) { return f.apply(cm, arguments) } + startOperation(cm); + try { return f.apply(cm, arguments) } + finally { endOperation(cm); } + } + } + // Used to add methods to editor and doc instances, wrapping them in + // operations. + function methodOp(f) { + return function() { + if (this.curOp) { return f.apply(this, arguments) } + startOperation(this); + try { return f.apply(this, arguments) } + finally { endOperation(this); } + } + } + function docMethodOp(f) { + return function() { + var cm = this.cm; + if (!cm || cm.curOp) { return f.apply(this, arguments) } + startOperation(cm); + try { return f.apply(this, arguments) } + finally { endOperation(cm); } + } + } + + // HIGHLIGHT WORKER + + function startWorker(cm, time) { + if (cm.doc.highlightFrontier < cm.display.viewTo) + { cm.state.highlight.set(time, bind(highlightWorker, cm)); } + } + + function highlightWorker(cm) { + var doc = cm.doc; + if (doc.highlightFrontier >= cm.display.viewTo) { return } + var end = +new Date + cm.options.workTime; + var context = getContextBefore(cm, doc.highlightFrontier); + var changedLines = []; + + doc.iter(context.line, Math.min(doc.first + doc.size, cm.display.viewTo + 500), function (line) { + if (context.line >= cm.display.viewFrom) { // Visible + var oldStyles = line.styles; + var resetState = line.text.length > cm.options.maxHighlightLength ? copyState(doc.mode, context.state) : null; + var highlighted = highlightLine(cm, line, context, true); + if (resetState) { context.state = resetState; } + line.styles = highlighted.styles; + var oldCls = line.styleClasses, newCls = highlighted.classes; + if (newCls) { line.styleClasses = newCls; } + else if (oldCls) { line.styleClasses = null; } + var ischange = !oldStyles || oldStyles.length != line.styles.length || + oldCls != newCls && (!oldCls || !newCls || oldCls.bgClass != newCls.bgClass || oldCls.textClass != newCls.textClass); + for (var i = 0; !ischange && i < oldStyles.length; ++i) { ischange = oldStyles[i] != line.styles[i]; } + if (ischange) { changedLines.push(context.line); } + line.stateAfter = context.save(); + context.nextLine(); + } else { + if (line.text.length <= cm.options.maxHighlightLength) + { processLine(cm, line.text, context); } + line.stateAfter = context.line % 5 == 0 ? context.save() : null; + context.nextLine(); + } + if (+new Date > end) { + startWorker(cm, cm.options.workDelay); + return true + } + }); + doc.highlightFrontier = context.line; + doc.modeFrontier = Math.max(doc.modeFrontier, context.line); + if (changedLines.length) { runInOp(cm, function () { + for (var i = 0; i < changedLines.length; i++) + { regLineChange(cm, changedLines[i], "text"); } + }); } + } + + // DISPLAY DRAWING + + var DisplayUpdate = function(cm, viewport, force) { + var display = cm.display; + + this.viewport = viewport; + // Store some values that we'll need later (but don't want to force a relayout for) + this.visible = visibleLines(display, cm.doc, viewport); + this.editorIsHidden = !display.wrapper.offsetWidth; + this.wrapperHeight = display.wrapper.clientHeight; + this.wrapperWidth = display.wrapper.clientWidth; + this.oldDisplayWidth = displayWidth(cm); + this.force = force; + this.dims = getDimensions(cm); + this.events = []; + }; + + DisplayUpdate.prototype.signal = function (emitter, type) { + if (hasHandler(emitter, type)) + { this.events.push(arguments); } + }; + DisplayUpdate.prototype.finish = function () { + for (var i = 0; i < this.events.length; i++) + { signal.apply(null, this.events[i]); } + }; + + function maybeClipScrollbars(cm) { + var display = cm.display; + if (!display.scrollbarsClipped && display.scroller.offsetWidth) { + display.nativeBarWidth = display.scroller.offsetWidth - display.scroller.clientWidth; + display.heightForcer.style.height = scrollGap(cm) + "px"; + display.sizer.style.marginBottom = -display.nativeBarWidth + "px"; + display.sizer.style.borderRightWidth = scrollGap(cm) + "px"; + display.scrollbarsClipped = true; + } + } + + function selectionSnapshot(cm) { + if (cm.hasFocus()) { return null } + var active = activeElt(); + if (!active || !contains(cm.display.lineDiv, active)) { return null } + var result = {activeElt: active}; + if (window.getSelection) { + var sel = window.getSelection(); + if (sel.anchorNode && sel.extend && contains(cm.display.lineDiv, sel.anchorNode)) { + result.anchorNode = sel.anchorNode; + result.anchorOffset = sel.anchorOffset; + result.focusNode = sel.focusNode; + result.focusOffset = sel.focusOffset; + } + } + return result + } + + function restoreSelection(snapshot) { + if (!snapshot || !snapshot.activeElt || snapshot.activeElt == activeElt()) { return } + snapshot.activeElt.focus(); + if (!/^(INPUT|TEXTAREA)$/.test(snapshot.activeElt.nodeName) && + snapshot.anchorNode && contains(document.body, snapshot.anchorNode) && contains(document.body, snapshot.focusNode)) { + var sel = window.getSelection(), range = document.createRange(); + range.setEnd(snapshot.anchorNode, snapshot.anchorOffset); + range.collapse(false); + sel.removeAllRanges(); + sel.addRange(range); + sel.extend(snapshot.focusNode, snapshot.focusOffset); + } + } + + // Does the actual updating of the line display. Bails out + // (returning false) when there is nothing to be done and forced is + // false. + function updateDisplayIfNeeded(cm, update) { + var display = cm.display, doc = cm.doc; + + if (update.editorIsHidden) { + resetView(cm); + return false + } + + // Bail out if the visible area is already rendered and nothing changed. + if (!update.force && + update.visible.from >= display.viewFrom && update.visible.to <= display.viewTo && + (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo) && + display.renderedView == display.view && countDirtyView(cm) == 0) + { return false } + + if (maybeUpdateLineNumberWidth(cm)) { + resetView(cm); + update.dims = getDimensions(cm); + } + + // Compute a suitable new viewport (from & to) + var end = doc.first + doc.size; + var from = Math.max(update.visible.from - cm.options.viewportMargin, doc.first); + var to = Math.min(end, update.visible.to + cm.options.viewportMargin); + if (display.viewFrom < from && from - display.viewFrom < 20) { from = Math.max(doc.first, display.viewFrom); } + if (display.viewTo > to && display.viewTo - to < 20) { to = Math.min(end, display.viewTo); } + if (sawCollapsedSpans) { + from = visualLineNo(cm.doc, from); + to = visualLineEndNo(cm.doc, to); + } + + var different = from != display.viewFrom || to != display.viewTo || + display.lastWrapHeight != update.wrapperHeight || display.lastWrapWidth != update.wrapperWidth; + adjustView(cm, from, to); + + display.viewOffset = heightAtLine(getLine(cm.doc, display.viewFrom)); + // Position the mover div to align with the current scroll position + cm.display.mover.style.top = display.viewOffset + "px"; + + var toUpdate = countDirtyView(cm); + if (!different && toUpdate == 0 && !update.force && display.renderedView == display.view && + (display.updateLineNumbers == null || display.updateLineNumbers >= display.viewTo)) + { return false } + + // For big changes, we hide the enclosing element during the + // update, since that speeds up the operations on most browsers. + var selSnapshot = selectionSnapshot(cm); + if (toUpdate > 4) { display.lineDiv.style.display = "none"; } + patchDisplay(cm, display.updateLineNumbers, update.dims); + if (toUpdate > 4) { display.lineDiv.style.display = ""; } + display.renderedView = display.view; + // There might have been a widget with a focused element that got + // hidden or updated, if so re-focus it. + restoreSelection(selSnapshot); + + // Prevent selection and cursors from interfering with the scroll + // width and height. + removeChildren(display.cursorDiv); + removeChildren(display.selectionDiv); + display.gutters.style.height = display.sizer.style.minHeight = 0; + + if (different) { + display.lastWrapHeight = update.wrapperHeight; + display.lastWrapWidth = update.wrapperWidth; + startWorker(cm, 400); + } + + display.updateLineNumbers = null; + + return true + } + + function postUpdateDisplay(cm, update) { + var viewport = update.viewport; + + for (var first = true;; first = false) { + if (!first || !cm.options.lineWrapping || update.oldDisplayWidth == displayWidth(cm)) { + // Clip forced viewport to actual scrollable area. + if (viewport && viewport.top != null) + { viewport = {top: Math.min(cm.doc.height + paddingVert(cm.display) - displayHeight(cm), viewport.top)}; } + // Updated line heights might result in the drawn area not + // actually covering the viewport. Keep looping until it does. + update.visible = visibleLines(cm.display, cm.doc, viewport); + if (update.visible.from >= cm.display.viewFrom && update.visible.to <= cm.display.viewTo) + { break } + } else if (first) { + update.visible = visibleLines(cm.display, cm.doc, viewport); + } + if (!updateDisplayIfNeeded(cm, update)) { break } + updateHeightsInViewport(cm); + var barMeasure = measureForScrollbars(cm); + updateSelection(cm); + updateScrollbars(cm, barMeasure); + setDocumentHeight(cm, barMeasure); + update.force = false; + } + + update.signal(cm, "update", cm); + if (cm.display.viewFrom != cm.display.reportedViewFrom || cm.display.viewTo != cm.display.reportedViewTo) { + update.signal(cm, "viewportChange", cm, cm.display.viewFrom, cm.display.viewTo); + cm.display.reportedViewFrom = cm.display.viewFrom; cm.display.reportedViewTo = cm.display.viewTo; + } + } + + function updateDisplaySimple(cm, viewport) { + var update = new DisplayUpdate(cm, viewport); + if (updateDisplayIfNeeded(cm, update)) { + updateHeightsInViewport(cm); + postUpdateDisplay(cm, update); + var barMeasure = measureForScrollbars(cm); + updateSelection(cm); + updateScrollbars(cm, barMeasure); + setDocumentHeight(cm, barMeasure); + update.finish(); + } + } + + // Sync the actual display DOM structure with display.view, removing + // nodes for lines that are no longer in view, and creating the ones + // that are not there yet, and updating the ones that are out of + // date. + function patchDisplay(cm, updateNumbersFrom, dims) { + var display = cm.display, lineNumbers = cm.options.lineNumbers; + var container = display.lineDiv, cur = container.firstChild; + + function rm(node) { + var next = node.nextSibling; + // Works around a throw-scroll bug in OS X Webkit + if (webkit && mac && cm.display.currentWheelTarget == node) + { node.style.display = "none"; } + else + { node.parentNode.removeChild(node); } + return next + } + + var view = display.view, lineN = display.viewFrom; + // Loop over the elements in the view, syncing cur (the DOM nodes + // in display.lineDiv) with the view as we go. + for (var i = 0; i < view.length; i++) { + var lineView = view[i]; + if (lineView.hidden) ; else if (!lineView.node || lineView.node.parentNode != container) { // Not drawn yet + var node = buildLineElement(cm, lineView, lineN, dims); + container.insertBefore(node, cur); + } else { // Already drawn + while (cur != lineView.node) { cur = rm(cur); } + var updateNumber = lineNumbers && updateNumbersFrom != null && + updateNumbersFrom <= lineN && lineView.lineNumber; + if (lineView.changes) { + if (indexOf(lineView.changes, "gutter") > -1) { updateNumber = false; } + updateLineForChanges(cm, lineView, lineN, dims); + } + if (updateNumber) { + removeChildren(lineView.lineNumber); + lineView.lineNumber.appendChild(document.createTextNode(lineNumberFor(cm.options, lineN))); + } + cur = lineView.node.nextSibling; + } + lineN += lineView.size; + } + while (cur) { cur = rm(cur); } + } + + function updateGutterSpace(display) { + var width = display.gutters.offsetWidth; + display.sizer.style.marginLeft = width + "px"; + } + + function setDocumentHeight(cm, measure) { + cm.display.sizer.style.minHeight = measure.docHeight + "px"; + cm.display.heightForcer.style.top = measure.docHeight + "px"; + cm.display.gutters.style.height = (measure.docHeight + cm.display.barHeight + scrollGap(cm)) + "px"; + } + + // Re-align line numbers and gutter marks to compensate for + // horizontal scrolling. + function alignHorizontally(cm) { + var display = cm.display, view = display.view; + if (!display.alignWidgets && (!display.gutters.firstChild || !cm.options.fixedGutter)) { return } + var comp = compensateForHScroll(display) - display.scroller.scrollLeft + cm.doc.scrollLeft; + var gutterW = display.gutters.offsetWidth, left = comp + "px"; + for (var i = 0; i < view.length; i++) { if (!view[i].hidden) { + if (cm.options.fixedGutter) { + if (view[i].gutter) + { view[i].gutter.style.left = left; } + if (view[i].gutterBackground) + { view[i].gutterBackground.style.left = left; } + } + var align = view[i].alignable; + if (align) { for (var j = 0; j < align.length; j++) + { align[j].style.left = left; } } + } } + if (cm.options.fixedGutter) + { display.gutters.style.left = (comp + gutterW) + "px"; } + } + + // Used to ensure that the line number gutter is still the right + // size for the current document size. Returns true when an update + // is needed. + function maybeUpdateLineNumberWidth(cm) { + if (!cm.options.lineNumbers) { return false } + var doc = cm.doc, last = lineNumberFor(cm.options, doc.first + doc.size - 1), display = cm.display; + if (last.length != display.lineNumChars) { + var test = display.measure.appendChild(elt("div", [elt("div", last)], + "CodeMirror-linenumber CodeMirror-gutter-elt")); + var innerW = test.firstChild.offsetWidth, padding = test.offsetWidth - innerW; + display.lineGutter.style.width = ""; + display.lineNumInnerWidth = Math.max(innerW, display.lineGutter.offsetWidth - padding) + 1; + display.lineNumWidth = display.lineNumInnerWidth + padding; + display.lineNumChars = display.lineNumInnerWidth ? last.length : -1; + display.lineGutter.style.width = display.lineNumWidth + "px"; + updateGutterSpace(cm.display); + return true + } + return false + } + + function getGutters(gutters, lineNumbers) { + var result = [], sawLineNumbers = false; + for (var i = 0; i < gutters.length; i++) { + var name = gutters[i], style = null; + if (typeof name != "string") { style = name.style; name = name.className; } + if (name == "CodeMirror-linenumbers") { + if (!lineNumbers) { continue } + else { sawLineNumbers = true; } + } + result.push({className: name, style: style}); + } + if (lineNumbers && !sawLineNumbers) { result.push({className: "CodeMirror-linenumbers", style: null}); } + return result + } + + // Rebuild the gutter elements, ensure the margin to the left of the + // code matches their width. + function renderGutters(display) { + var gutters = display.gutters, specs = display.gutterSpecs; + removeChildren(gutters); + display.lineGutter = null; + for (var i = 0; i < specs.length; ++i) { + var ref = specs[i]; + var className = ref.className; + var style = ref.style; + var gElt = gutters.appendChild(elt("div", null, "CodeMirror-gutter " + className)); + if (style) { gElt.style.cssText = style; } + if (className == "CodeMirror-linenumbers") { + display.lineGutter = gElt; + gElt.style.width = (display.lineNumWidth || 1) + "px"; + } + } + gutters.style.display = specs.length ? "" : "none"; + updateGutterSpace(display); + } + + function updateGutters(cm) { + renderGutters(cm.display); + regChange(cm); + alignHorizontally(cm); + } + + // The display handles the DOM integration, both for input reading + // and content drawing. It holds references to DOM nodes and + // display-related state. + + function Display(place, doc, input, options) { + var d = this; + this.input = input; + + // Covers bottom-right square when both scrollbars are present. + d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler"); + d.scrollbarFiller.setAttribute("cm-not-content", "true"); + // Covers bottom of gutter when coverGutterNextToScrollbar is on + // and h scrollbar is present. + d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler"); + d.gutterFiller.setAttribute("cm-not-content", "true"); + // Will contain the actual code, positioned to cover the viewport. + d.lineDiv = eltP("div", null, "CodeMirror-code"); + // Elements are added to these to represent selection and cursors. + d.selectionDiv = elt("div", null, null, "position: relative; z-index: 1"); + d.cursorDiv = elt("div", null, "CodeMirror-cursors"); + // A visibility: hidden element used to find the size of things. + d.measure = elt("div", null, "CodeMirror-measure"); + // When lines outside of the viewport are measured, they are drawn in this. + d.lineMeasure = elt("div", null, "CodeMirror-measure"); + // Wraps everything that needs to exist inside the vertically-padded coordinate system + d.lineSpace = eltP("div", [d.measure, d.lineMeasure, d.selectionDiv, d.cursorDiv, d.lineDiv], + null, "position: relative; outline: none"); + var lines = eltP("div", [d.lineSpace], "CodeMirror-lines"); + // Moved around its parent to cover visible view. + d.mover = elt("div", [lines], null, "position: relative"); + // Set to the height of the document, allowing scrolling. + d.sizer = elt("div", [d.mover], "CodeMirror-sizer"); + d.sizerWidth = null; + // Behavior of elts with overflow: auto and padding is + // inconsistent across browsers. This is used to ensure the + // scrollable area is big enough. + d.heightForcer = elt("div", null, null, "position: absolute; height: " + scrollerGap + "px; width: 1px;"); + // Will contain the gutters, if any. + d.gutters = elt("div", null, "CodeMirror-gutters"); + d.lineGutter = null; + // Actual scrollable element. + d.scroller = elt("div", [d.sizer, d.heightForcer, d.gutters], "CodeMirror-scroll"); + d.scroller.setAttribute("tabIndex", "-1"); + // The element in which the editor lives. + d.wrapper = elt("div", [d.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror"); + + // Work around IE7 z-index bug (not perfect, hence IE7 not really being supported) + if (ie && ie_version < 8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0; } + if (!webkit && !(gecko && mobile)) { d.scroller.draggable = true; } + + if (place) { + if (place.appendChild) { place.appendChild(d.wrapper); } + else { place(d.wrapper); } + } + + // Current rendered range (may be bigger than the view window). + d.viewFrom = d.viewTo = doc.first; + d.reportedViewFrom = d.reportedViewTo = doc.first; + // Information about the rendered lines. + d.view = []; + d.renderedView = null; + // Holds info about a single rendered line when it was rendered + // for measurement, while not in view. + d.externalMeasured = null; + // Empty space (in pixels) above the view + d.viewOffset = 0; + d.lastWrapHeight = d.lastWrapWidth = 0; + d.updateLineNumbers = null; + + d.nativeBarWidth = d.barHeight = d.barWidth = 0; + d.scrollbarsClipped = false; + + // Used to only resize the line number gutter when necessary (when + // the amount of lines crosses a boundary that makes its width change) + d.lineNumWidth = d.lineNumInnerWidth = d.lineNumChars = null; + // Set to true when a non-horizontal-scrolling line widget is + // added. As an optimization, line widget aligning is skipped when + // this is false. + d.alignWidgets = false; + + d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null; + + // Tracks the maximum line length so that the horizontal scrollbar + // can be kept static when scrolling. + d.maxLine = null; + d.maxLineLength = 0; + d.maxLineChanged = false; + + // Used for measuring wheel scrolling granularity + d.wheelDX = d.wheelDY = d.wheelStartX = d.wheelStartY = null; + + // True when shift is held down. + d.shift = false; + + // Used to track whether anything happened since the context menu + // was opened. + d.selForContextMenu = null; + + d.activeTouch = null; + + d.gutterSpecs = getGutters(options.gutters, options.lineNumbers); + renderGutters(d); + + input.init(d); + } + + // Since the delta values reported on mouse wheel events are + // unstandardized between browsers and even browser versions, and + // generally horribly unpredictable, this code starts by measuring + // the scroll effect that the first few mouse wheel events have, + // and, from that, detects the way it can convert deltas to pixel + // offsets afterwards. + // + // The reason we want to know the amount a wheel event will scroll + // is that it gives us a chance to update the display before the + // actual scrolling happens, reducing flickering. + + var wheelSamples = 0, wheelPixelsPerUnit = null; + // Fill in a browser-detected starting value on browsers where we + // know one. These don't have to be accurate -- the result of them + // being wrong would just be a slight flicker on the first wheel + // scroll (if it is large enough). + if (ie) { wheelPixelsPerUnit = -.53; } + else if (gecko) { wheelPixelsPerUnit = 15; } + else if (chrome) { wheelPixelsPerUnit = -.7; } + else if (safari) { wheelPixelsPerUnit = -1/3; } + + function wheelEventDelta(e) { + var dx = e.wheelDeltaX, dy = e.wheelDeltaY; + if (dx == null && e.detail && e.axis == e.HORIZONTAL_AXIS) { dx = e.detail; } + if (dy == null && e.detail && e.axis == e.VERTICAL_AXIS) { dy = e.detail; } + else if (dy == null) { dy = e.wheelDelta; } + return {x: dx, y: dy} + } + function wheelEventPixels(e) { + var delta = wheelEventDelta(e); + delta.x *= wheelPixelsPerUnit; + delta.y *= wheelPixelsPerUnit; + return delta + } + + function onScrollWheel(cm, e) { + var delta = wheelEventDelta(e), dx = delta.x, dy = delta.y; + + var display = cm.display, scroll = display.scroller; + // Quit if there's nothing to scroll here + var canScrollX = scroll.scrollWidth > scroll.clientWidth; + var canScrollY = scroll.scrollHeight > scroll.clientHeight; + if (!(dx && canScrollX || dy && canScrollY)) { return } + + // Webkit browsers on OS X abort momentum scrolls when the target + // of the scroll event is removed from the scrollable element. + // This hack (see related code in patchDisplay) makes sure the + // element is kept around. + if (dy && mac && webkit) { + outer: for (var cur = e.target, view = display.view; cur != scroll; cur = cur.parentNode) { + for (var i = 0; i < view.length; i++) { + if (view[i].node == cur) { + cm.display.currentWheelTarget = cur; + break outer + } + } + } + } + + // On some browsers, horizontal scrolling will cause redraws to + // happen before the gutter has been realigned, causing it to + // wriggle around in a most unseemly way. When we have an + // estimated pixels/delta value, we just handle horizontal + // scrolling entirely here. It'll be slightly off from native, but + // better than glitching out. + if (dx && !gecko && !presto && wheelPixelsPerUnit != null) { + if (dy && canScrollY) + { updateScrollTop(cm, Math.max(0, scroll.scrollTop + dy * wheelPixelsPerUnit)); } + setScrollLeft(cm, Math.max(0, scroll.scrollLeft + dx * wheelPixelsPerUnit)); + // Only prevent default scrolling if vertical scrolling is + // actually possible. Otherwise, it causes vertical scroll + // jitter on OSX trackpads when deltaX is small and deltaY + // is large (issue #3579) + if (!dy || (dy && canScrollY)) + { e_preventDefault(e); } + display.wheelStartX = null; // Abort measurement, if in progress + return + } + + // 'Project' the visible viewport to cover the area that is being + // scrolled into view (if we know enough to estimate it). + if (dy && wheelPixelsPerUnit != null) { + var pixels = dy * wheelPixelsPerUnit; + var top = cm.doc.scrollTop, bot = top + display.wrapper.clientHeight; + if (pixels < 0) { top = Math.max(0, top + pixels - 50); } + else { bot = Math.min(cm.doc.height, bot + pixels + 50); } + updateDisplaySimple(cm, {top: top, bottom: bot}); + } + + if (wheelSamples < 20) { + if (display.wheelStartX == null) { + display.wheelStartX = scroll.scrollLeft; display.wheelStartY = scroll.scrollTop; + display.wheelDX = dx; display.wheelDY = dy; + setTimeout(function () { + if (display.wheelStartX == null) { return } + var movedX = scroll.scrollLeft - display.wheelStartX; + var movedY = scroll.scrollTop - display.wheelStartY; + var sample = (movedY && display.wheelDY && movedY / display.wheelDY) || + (movedX && display.wheelDX && movedX / display.wheelDX); + display.wheelStartX = display.wheelStartY = null; + if (!sample) { return } + wheelPixelsPerUnit = (wheelPixelsPerUnit * wheelSamples + sample) / (wheelSamples + 1); + ++wheelSamples; + }, 200); + } else { + display.wheelDX += dx; display.wheelDY += dy; + } + } + } + + // Selection objects are immutable. A new one is created every time + // the selection changes. A selection is one or more non-overlapping + // (and non-touching) ranges, sorted, and an integer that indicates + // which one is the primary selection (the one that's scrolled into + // view, that getCursor returns, etc). + var Selection = function(ranges, primIndex) { + this.ranges = ranges; + this.primIndex = primIndex; + }; + + Selection.prototype.primary = function () { return this.ranges[this.primIndex] }; + + Selection.prototype.equals = function (other) { + if (other == this) { return true } + if (other.primIndex != this.primIndex || other.ranges.length != this.ranges.length) { return false } + for (var i = 0; i < this.ranges.length; i++) { + var here = this.ranges[i], there = other.ranges[i]; + if (!equalCursorPos(here.anchor, there.anchor) || !equalCursorPos(here.head, there.head)) { return false } + } + return true + }; + + Selection.prototype.deepCopy = function () { + var out = []; + for (var i = 0; i < this.ranges.length; i++) + { out[i] = new Range(copyPos(this.ranges[i].anchor), copyPos(this.ranges[i].head)); } + return new Selection(out, this.primIndex) + }; + + Selection.prototype.somethingSelected = function () { + for (var i = 0; i < this.ranges.length; i++) + { if (!this.ranges[i].empty()) { return true } } + return false + }; + + Selection.prototype.contains = function (pos, end) { + if (!end) { end = pos; } + for (var i = 0; i < this.ranges.length; i++) { + var range = this.ranges[i]; + if (cmp(end, range.from()) >= 0 && cmp(pos, range.to()) <= 0) + { return i } + } + return -1 + }; + + var Range = function(anchor, head) { + this.anchor = anchor; this.head = head; + }; + + Range.prototype.from = function () { return minPos(this.anchor, this.head) }; + Range.prototype.to = function () { return maxPos(this.anchor, this.head) }; + Range.prototype.empty = function () { return this.head.line == this.anchor.line && this.head.ch == this.anchor.ch }; + + // Take an unsorted, potentially overlapping set of ranges, and + // build a selection out of it. 'Consumes' ranges array (modifying + // it). + function normalizeSelection(cm, ranges, primIndex) { + var mayTouch = cm && cm.options.selectionsMayTouch; + var prim = ranges[primIndex]; + ranges.sort(function (a, b) { return cmp(a.from(), b.from()); }); + primIndex = indexOf(ranges, prim); + for (var i = 1; i < ranges.length; i++) { + var cur = ranges[i], prev = ranges[i - 1]; + var diff = cmp(prev.to(), cur.from()); + if (mayTouch && !cur.empty() ? diff > 0 : diff >= 0) { + var from = minPos(prev.from(), cur.from()), to = maxPos(prev.to(), cur.to()); + var inv = prev.empty() ? cur.from() == cur.head : prev.from() == prev.head; + if (i <= primIndex) { --primIndex; } + ranges.splice(--i, 2, new Range(inv ? to : from, inv ? from : to)); + } + } + return new Selection(ranges, primIndex) + } + + function simpleSelection(anchor, head) { + return new Selection([new Range(anchor, head || anchor)], 0) + } + + // Compute the position of the end of a change (its 'to' property + // refers to the pre-change end). + function changeEnd(change) { + if (!change.text) { return change.to } + return Pos(change.from.line + change.text.length - 1, + lst(change.text).length + (change.text.length == 1 ? change.from.ch : 0)) + } + + // Adjust a position to refer to the post-change position of the + // same text, or the end of the change if the change covers it. + function adjustForChange(pos, change) { + if (cmp(pos, change.from) < 0) { return pos } + if (cmp(pos, change.to) <= 0) { return changeEnd(change) } + + var line = pos.line + change.text.length - (change.to.line - change.from.line) - 1, ch = pos.ch; + if (pos.line == change.to.line) { ch += changeEnd(change).ch - change.to.ch; } + return Pos(line, ch) + } + + function computeSelAfterChange(doc, change) { + var out = []; + for (var i = 0; i < doc.sel.ranges.length; i++) { + var range = doc.sel.ranges[i]; + out.push(new Range(adjustForChange(range.anchor, change), + adjustForChange(range.head, change))); + } + return normalizeSelection(doc.cm, out, doc.sel.primIndex) + } + + function offsetPos(pos, old, nw) { + if (pos.line == old.line) + { return Pos(nw.line, pos.ch - old.ch + nw.ch) } + else + { return Pos(nw.line + (pos.line - old.line), pos.ch) } + } + + // Used by replaceSelections to allow moving the selection to the + // start or around the replaced test. Hint may be "start" or "around". + function computeReplacedSel(doc, changes, hint) { + var out = []; + var oldPrev = Pos(doc.first, 0), newPrev = oldPrev; + for (var i = 0; i < changes.length; i++) { + var change = changes[i]; + var from = offsetPos(change.from, oldPrev, newPrev); + var to = offsetPos(changeEnd(change), oldPrev, newPrev); + oldPrev = change.to; + newPrev = to; + if (hint == "around") { + var range = doc.sel.ranges[i], inv = cmp(range.head, range.anchor) < 0; + out[i] = new Range(inv ? to : from, inv ? from : to); + } else { + out[i] = new Range(from, from); + } + } + return new Selection(out, doc.sel.primIndex) + } + + // Used to get the editor into a consistent state again when options change. + + function loadMode(cm) { + cm.doc.mode = getMode(cm.options, cm.doc.modeOption); + resetModeState(cm); + } + + function resetModeState(cm) { + cm.doc.iter(function (line) { + if (line.stateAfter) { line.stateAfter = null; } + if (line.styles) { line.styles = null; } + }); + cm.doc.modeFrontier = cm.doc.highlightFrontier = cm.doc.first; + startWorker(cm, 100); + cm.state.modeGen++; + if (cm.curOp) { regChange(cm); } + } + + // DOCUMENT DATA STRUCTURE + + // By default, updates that start and end at the beginning of a line + // are treated specially, in order to make the association of line + // widgets and marker elements with the text behave more intuitive. + function isWholeLineUpdate(doc, change) { + return change.from.ch == 0 && change.to.ch == 0 && lst(change.text) == "" && + (!doc.cm || doc.cm.options.wholeLineUpdateBefore) + } + + // Perform a change on the document data structure. + function updateDoc(doc, change, markedSpans, estimateHeight) { + function spansFor(n) {return markedSpans ? markedSpans[n] : null} + function update(line, text, spans) { + updateLine(line, text, spans, estimateHeight); + signalLater(line, "change", line, change); + } + function linesFor(start, end) { + var result = []; + for (var i = start; i < end; ++i) + { result.push(new Line(text[i], spansFor(i), estimateHeight)); } + return result + } + + var from = change.from, to = change.to, text = change.text; + var firstLine = getLine(doc, from.line), lastLine = getLine(doc, to.line); + var lastText = lst(text), lastSpans = spansFor(text.length - 1), nlines = to.line - from.line; + + // Adjust the line structure + if (change.full) { + doc.insert(0, linesFor(0, text.length)); + doc.remove(text.length, doc.size - text.length); + } else if (isWholeLineUpdate(doc, change)) { + // This is a whole-line replace. Treated specially to make + // sure line objects move the way they are supposed to. + var added = linesFor(0, text.length - 1); + update(lastLine, lastLine.text, lastSpans); + if (nlines) { doc.remove(from.line, nlines); } + if (added.length) { doc.insert(from.line, added); } + } else if (firstLine == lastLine) { + if (text.length == 1) { + update(firstLine, firstLine.text.slice(0, from.ch) + lastText + firstLine.text.slice(to.ch), lastSpans); + } else { + var added$1 = linesFor(1, text.length - 1); + added$1.push(new Line(lastText + firstLine.text.slice(to.ch), lastSpans, estimateHeight)); + update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); + doc.insert(from.line + 1, added$1); + } + } else if (text.length == 1) { + update(firstLine, firstLine.text.slice(0, from.ch) + text[0] + lastLine.text.slice(to.ch), spansFor(0)); + doc.remove(from.line + 1, nlines); + } else { + update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); + update(lastLine, lastText + lastLine.text.slice(to.ch), lastSpans); + var added$2 = linesFor(1, text.length - 1); + if (nlines > 1) { doc.remove(from.line + 1, nlines - 1); } + doc.insert(from.line + 1, added$2); + } + + signalLater(doc, "change", doc, change); + } + + // Call f for all linked documents. + function linkedDocs(doc, f, sharedHistOnly) { + function propagate(doc, skip, sharedHist) { + if (doc.linked) { for (var i = 0; i < doc.linked.length; ++i) { + var rel = doc.linked[i]; + if (rel.doc == skip) { continue } + var shared = sharedHist && rel.sharedHist; + if (sharedHistOnly && !shared) { continue } + f(rel.doc, shared); + propagate(rel.doc, doc, shared); + } } + } + propagate(doc, null, true); + } + + // Attach a document to an editor. + function attachDoc(cm, doc) { + if (doc.cm) { throw new Error("This document is already in use.") } + cm.doc = doc; + doc.cm = cm; + estimateLineHeights(cm); + loadMode(cm); + setDirectionClass(cm); + if (!cm.options.lineWrapping) { findMaxLine(cm); } + cm.options.mode = doc.modeOption; + regChange(cm); + } + + function setDirectionClass(cm) { + (cm.doc.direction == "rtl" ? addClass : rmClass)(cm.display.lineDiv, "CodeMirror-rtl"); + } + + function directionChanged(cm) { + runInOp(cm, function () { + setDirectionClass(cm); + regChange(cm); + }); + } + + function History(startGen) { + // Arrays of change events and selections. Doing something adds an + // event to done and clears undo. Undoing moves events from done + // to undone, redoing moves them in the other direction. + this.done = []; this.undone = []; + this.undoDepth = Infinity; + // Used to track when changes can be merged into a single undo + // event + this.lastModTime = this.lastSelTime = 0; + this.lastOp = this.lastSelOp = null; + this.lastOrigin = this.lastSelOrigin = null; + // Used by the isClean() method + this.generation = this.maxGeneration = startGen || 1; + } + + // Create a history change event from an updateDoc-style change + // object. + function historyChangeFromChange(doc, change) { + var histChange = {from: copyPos(change.from), to: changeEnd(change), text: getBetween(doc, change.from, change.to)}; + attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1); + linkedDocs(doc, function (doc) { return attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1); }, true); + return histChange + } + + // Pop all selection events off the end of a history array. Stop at + // a change event. + function clearSelectionEvents(array) { + while (array.length) { + var last = lst(array); + if (last.ranges) { array.pop(); } + else { break } + } + } + + // Find the top change event in the history. Pop off selection + // events that are in the way. + function lastChangeEvent(hist, force) { + if (force) { + clearSelectionEvents(hist.done); + return lst(hist.done) + } else if (hist.done.length && !lst(hist.done).ranges) { + return lst(hist.done) + } else if (hist.done.length > 1 && !hist.done[hist.done.length - 2].ranges) { + hist.done.pop(); + return lst(hist.done) + } + } + + // Register a change in the history. Merges changes that are within + // a single operation, or are close together with an origin that + // allows merging (starting with "+") into a single event. + function addChangeToHistory(doc, change, selAfter, opId) { + var hist = doc.history; + hist.undone.length = 0; + var time = +new Date, cur; + var last; + + if ((hist.lastOp == opId || + hist.lastOrigin == change.origin && change.origin && + ((change.origin.charAt(0) == "+" && hist.lastModTime > time - (doc.cm ? doc.cm.options.historyEventDelay : 500)) || + change.origin.charAt(0) == "*")) && + (cur = lastChangeEvent(hist, hist.lastOp == opId))) { + // Merge this change into the last event + last = lst(cur.changes); + if (cmp(change.from, change.to) == 0 && cmp(change.from, last.to) == 0) { + // Optimized case for simple insertion -- don't want to add + // new changesets for every character typed + last.to = changeEnd(change); + } else { + // Add new sub-event + cur.changes.push(historyChangeFromChange(doc, change)); + } + } else { + // Can not be merged, start a new event. + var before = lst(hist.done); + if (!before || !before.ranges) + { pushSelectionToHistory(doc.sel, hist.done); } + cur = {changes: [historyChangeFromChange(doc, change)], + generation: hist.generation}; + hist.done.push(cur); + while (hist.done.length > hist.undoDepth) { + hist.done.shift(); + if (!hist.done[0].ranges) { hist.done.shift(); } + } + } + hist.done.push(selAfter); + hist.generation = ++hist.maxGeneration; + hist.lastModTime = hist.lastSelTime = time; + hist.lastOp = hist.lastSelOp = opId; + hist.lastOrigin = hist.lastSelOrigin = change.origin; + + if (!last) { signal(doc, "historyAdded"); } + } + + function selectionEventCanBeMerged(doc, origin, prev, sel) { + var ch = origin.charAt(0); + return ch == "*" || + ch == "+" && + prev.ranges.length == sel.ranges.length && + prev.somethingSelected() == sel.somethingSelected() && + new Date - doc.history.lastSelTime <= (doc.cm ? doc.cm.options.historyEventDelay : 500) + } + + // Called whenever the selection changes, sets the new selection as + // the pending selection in the history, and pushes the old pending + // selection into the 'done' array when it was significantly + // different (in number of selected ranges, emptiness, or time). + function addSelectionToHistory(doc, sel, opId, options) { + var hist = doc.history, origin = options && options.origin; + + // A new event is started when the previous origin does not match + // the current, or the origins don't allow matching. Origins + // starting with * are always merged, those starting with + are + // merged when similar and close together in time. + if (opId == hist.lastSelOp || + (origin && hist.lastSelOrigin == origin && + (hist.lastModTime == hist.lastSelTime && hist.lastOrigin == origin || + selectionEventCanBeMerged(doc, origin, lst(hist.done), sel)))) + { hist.done[hist.done.length - 1] = sel; } + else + { pushSelectionToHistory(sel, hist.done); } + + hist.lastSelTime = +new Date; + hist.lastSelOrigin = origin; + hist.lastSelOp = opId; + if (options && options.clearRedo !== false) + { clearSelectionEvents(hist.undone); } + } + + function pushSelectionToHistory(sel, dest) { + var top = lst(dest); + if (!(top && top.ranges && top.equals(sel))) + { dest.push(sel); } + } + + // Used to store marked span information in the history. + function attachLocalSpans(doc, change, from, to) { + var existing = change["spans_" + doc.id], n = 0; + doc.iter(Math.max(doc.first, from), Math.min(doc.first + doc.size, to), function (line) { + if (line.markedSpans) + { (existing || (existing = change["spans_" + doc.id] = {}))[n] = line.markedSpans; } + ++n; + }); + } + + // When un/re-doing restores text containing marked spans, those + // that have been explicitly cleared should not be restored. + function removeClearedSpans(spans) { + if (!spans) { return null } + var out; + for (var i = 0; i < spans.length; ++i) { + if (spans[i].marker.explicitlyCleared) { if (!out) { out = spans.slice(0, i); } } + else if (out) { out.push(spans[i]); } + } + return !out ? spans : out.length ? out : null + } + + // Retrieve and filter the old marked spans stored in a change event. + function getOldSpans(doc, change) { + var found = change["spans_" + doc.id]; + if (!found) { return null } + var nw = []; + for (var i = 0; i < change.text.length; ++i) + { nw.push(removeClearedSpans(found[i])); } + return nw + } + + // Used for un/re-doing changes from the history. Combines the + // result of computing the existing spans with the set of spans that + // existed in the history (so that deleting around a span and then + // undoing brings back the span). + function mergeOldSpans(doc, change) { + var old = getOldSpans(doc, change); + var stretched = stretchSpansOverChange(doc, change); + if (!old) { return stretched } + if (!stretched) { return old } + + for (var i = 0; i < old.length; ++i) { + var oldCur = old[i], stretchCur = stretched[i]; + if (oldCur && stretchCur) { + spans: for (var j = 0; j < stretchCur.length; ++j) { + var span = stretchCur[j]; + for (var k = 0; k < oldCur.length; ++k) + { if (oldCur[k].marker == span.marker) { continue spans } } + oldCur.push(span); + } + } else if (stretchCur) { + old[i] = stretchCur; + } + } + return old + } + + // Used both to provide a JSON-safe object in .getHistory, and, when + // detaching a document, to split the history in two + function copyHistoryArray(events, newGroup, instantiateSel) { + var copy = []; + for (var i = 0; i < events.length; ++i) { + var event = events[i]; + if (event.ranges) { + copy.push(instantiateSel ? Selection.prototype.deepCopy.call(event) : event); + continue + } + var changes = event.changes, newChanges = []; + copy.push({changes: newChanges}); + for (var j = 0; j < changes.length; ++j) { + var change = changes[j], m = (void 0); + newChanges.push({from: change.from, to: change.to, text: change.text}); + if (newGroup) { for (var prop in change) { if (m = prop.match(/^spans_(\d+)$/)) { + if (indexOf(newGroup, Number(m[1])) > -1) { + lst(newChanges)[prop] = change[prop]; + delete change[prop]; + } + } } } + } + } + return copy + } + + // The 'scroll' parameter given to many of these indicated whether + // the new cursor position should be scrolled into view after + // modifying the selection. + + // If shift is held or the extend flag is set, extends a range to + // include a given position (and optionally a second position). + // Otherwise, simply returns the range between the given positions. + // Used for cursor motion and such. + function extendRange(range, head, other, extend) { + if (extend) { + var anchor = range.anchor; + if (other) { + var posBefore = cmp(head, anchor) < 0; + if (posBefore != (cmp(other, anchor) < 0)) { + anchor = head; + head = other; + } else if (posBefore != (cmp(head, other) < 0)) { + head = other; + } + } + return new Range(anchor, head) + } else { + return new Range(other || head, head) + } + } + + // Extend the primary selection range, discard the rest. + function extendSelection(doc, head, other, options, extend) { + if (extend == null) { extend = doc.cm && (doc.cm.display.shift || doc.extend); } + setSelection(doc, new Selection([extendRange(doc.sel.primary(), head, other, extend)], 0), options); + } + + // Extend all selections (pos is an array of selections with length + // equal the number of selections) + function extendSelections(doc, heads, options) { + var out = []; + var extend = doc.cm && (doc.cm.display.shift || doc.extend); + for (var i = 0; i < doc.sel.ranges.length; i++) + { out[i] = extendRange(doc.sel.ranges[i], heads[i], null, extend); } + var newSel = normalizeSelection(doc.cm, out, doc.sel.primIndex); + setSelection(doc, newSel, options); + } + + // Updates a single range in the selection. + function replaceOneSelection(doc, i, range, options) { + var ranges = doc.sel.ranges.slice(0); + ranges[i] = range; + setSelection(doc, normalizeSelection(doc.cm, ranges, doc.sel.primIndex), options); + } + + // Reset the selection to a single range. + function setSimpleSelection(doc, anchor, head, options) { + setSelection(doc, simpleSelection(anchor, head), options); + } + + // Give beforeSelectionChange handlers a change to influence a + // selection update. + function filterSelectionChange(doc, sel, options) { + var obj = { + ranges: sel.ranges, + update: function(ranges) { + this.ranges = []; + for (var i = 0; i < ranges.length; i++) + { this.ranges[i] = new Range(clipPos(doc, ranges[i].anchor), + clipPos(doc, ranges[i].head)); } + }, + origin: options && options.origin + }; + signal(doc, "beforeSelectionChange", doc, obj); + if (doc.cm) { signal(doc.cm, "beforeSelectionChange", doc.cm, obj); } + if (obj.ranges != sel.ranges) { return normalizeSelection(doc.cm, obj.ranges, obj.ranges.length - 1) } + else { return sel } + } + + function setSelectionReplaceHistory(doc, sel, options) { + var done = doc.history.done, last = lst(done); + if (last && last.ranges) { + done[done.length - 1] = sel; + setSelectionNoUndo(doc, sel, options); + } else { + setSelection(doc, sel, options); + } + } + + // Set a new selection. + function setSelection(doc, sel, options) { + setSelectionNoUndo(doc, sel, options); + addSelectionToHistory(doc, doc.sel, doc.cm ? doc.cm.curOp.id : NaN, options); + } + + function setSelectionNoUndo(doc, sel, options) { + if (hasHandler(doc, "beforeSelectionChange") || doc.cm && hasHandler(doc.cm, "beforeSelectionChange")) + { sel = filterSelectionChange(doc, sel, options); } + + var bias = options && options.bias || + (cmp(sel.primary().head, doc.sel.primary().head) < 0 ? -1 : 1); + setSelectionInner(doc, skipAtomicInSelection(doc, sel, bias, true)); + + if (!(options && options.scroll === false) && doc.cm) + { ensureCursorVisible(doc.cm); } + } + + function setSelectionInner(doc, sel) { + if (sel.equals(doc.sel)) { return } + + doc.sel = sel; + + if (doc.cm) { + doc.cm.curOp.updateInput = 1; + doc.cm.curOp.selectionChanged = true; + signalCursorActivity(doc.cm); + } + signalLater(doc, "cursorActivity", doc); + } + + // Verify that the selection does not partially select any atomic + // marked ranges. + function reCheckSelection(doc) { + setSelectionInner(doc, skipAtomicInSelection(doc, doc.sel, null, false)); + } + + // Return a selection that does not partially select any atomic + // ranges. + function skipAtomicInSelection(doc, sel, bias, mayClear) { + var out; + for (var i = 0; i < sel.ranges.length; i++) { + var range = sel.ranges[i]; + var old = sel.ranges.length == doc.sel.ranges.length && doc.sel.ranges[i]; + var newAnchor = skipAtomic(doc, range.anchor, old && old.anchor, bias, mayClear); + var newHead = skipAtomic(doc, range.head, old && old.head, bias, mayClear); + if (out || newAnchor != range.anchor || newHead != range.head) { + if (!out) { out = sel.ranges.slice(0, i); } + out[i] = new Range(newAnchor, newHead); + } + } + return out ? normalizeSelection(doc.cm, out, sel.primIndex) : sel + } + + function skipAtomicInner(doc, pos, oldPos, dir, mayClear) { + var line = getLine(doc, pos.line); + if (line.markedSpans) { for (var i = 0; i < line.markedSpans.length; ++i) { + var sp = line.markedSpans[i], m = sp.marker; + + // Determine if we should prevent the cursor being placed to the left/right of an atomic marker + // Historically this was determined using the inclusiveLeft/Right option, but the new way to control it + // is with selectLeft/Right + var preventCursorLeft = ("selectLeft" in m) ? !m.selectLeft : m.inclusiveLeft; + var preventCursorRight = ("selectRight" in m) ? !m.selectRight : m.inclusiveRight; + + if ((sp.from == null || (preventCursorLeft ? sp.from <= pos.ch : sp.from < pos.ch)) && + (sp.to == null || (preventCursorRight ? sp.to >= pos.ch : sp.to > pos.ch))) { + if (mayClear) { + signal(m, "beforeCursorEnter"); + if (m.explicitlyCleared) { + if (!line.markedSpans) { break } + else {--i; continue} + } + } + if (!m.atomic) { continue } + + if (oldPos) { + var near = m.find(dir < 0 ? 1 : -1), diff = (void 0); + if (dir < 0 ? preventCursorRight : preventCursorLeft) + { near = movePos(doc, near, -dir, near && near.line == pos.line ? line : null); } + if (near && near.line == pos.line && (diff = cmp(near, oldPos)) && (dir < 0 ? diff < 0 : diff > 0)) + { return skipAtomicInner(doc, near, pos, dir, mayClear) } + } + + var far = m.find(dir < 0 ? -1 : 1); + if (dir < 0 ? preventCursorLeft : preventCursorRight) + { far = movePos(doc, far, dir, far.line == pos.line ? line : null); } + return far ? skipAtomicInner(doc, far, pos, dir, mayClear) : null + } + } } + return pos + } + + // Ensure a given position is not inside an atomic range. + function skipAtomic(doc, pos, oldPos, bias, mayClear) { + var dir = bias || 1; + var found = skipAtomicInner(doc, pos, oldPos, dir, mayClear) || + (!mayClear && skipAtomicInner(doc, pos, oldPos, dir, true)) || + skipAtomicInner(doc, pos, oldPos, -dir, mayClear) || + (!mayClear && skipAtomicInner(doc, pos, oldPos, -dir, true)); + if (!found) { + doc.cantEdit = true; + return Pos(doc.first, 0) + } + return found + } + + function movePos(doc, pos, dir, line) { + if (dir < 0 && pos.ch == 0) { + if (pos.line > doc.first) { return clipPos(doc, Pos(pos.line - 1)) } + else { return null } + } else if (dir > 0 && pos.ch == (line || getLine(doc, pos.line)).text.length) { + if (pos.line < doc.first + doc.size - 1) { return Pos(pos.line + 1, 0) } + else { return null } + } else { + return new Pos(pos.line, pos.ch + dir) + } + } + + function selectAll(cm) { + cm.setSelection(Pos(cm.firstLine(), 0), Pos(cm.lastLine()), sel_dontScroll); + } + + // UPDATING + + // Allow "beforeChange" event handlers to influence a change + function filterChange(doc, change, update) { + var obj = { + canceled: false, + from: change.from, + to: change.to, + text: change.text, + origin: change.origin, + cancel: function () { return obj.canceled = true; } + }; + if (update) { obj.update = function (from, to, text, origin) { + if (from) { obj.from = clipPos(doc, from); } + if (to) { obj.to = clipPos(doc, to); } + if (text) { obj.text = text; } + if (origin !== undefined) { obj.origin = origin; } + }; } + signal(doc, "beforeChange", doc, obj); + if (doc.cm) { signal(doc.cm, "beforeChange", doc.cm, obj); } + + if (obj.canceled) { + if (doc.cm) { doc.cm.curOp.updateInput = 2; } + return null + } + return {from: obj.from, to: obj.to, text: obj.text, origin: obj.origin} + } + + // Apply a change to a document, and add it to the document's + // history, and propagating it to all linked documents. + function makeChange(doc, change, ignoreReadOnly) { + if (doc.cm) { + if (!doc.cm.curOp) { return operation(doc.cm, makeChange)(doc, change, ignoreReadOnly) } + if (doc.cm.state.suppressEdits) { return } + } + + if (hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")) { + change = filterChange(doc, change, true); + if (!change) { return } + } + + // Possibly split or suppress the update based on the presence + // of read-only spans in its range. + var split = sawReadOnlySpans && !ignoreReadOnly && removeReadOnlyRanges(doc, change.from, change.to); + if (split) { + for (var i = split.length - 1; i >= 0; --i) + { makeChangeInner(doc, {from: split[i].from, to: split[i].to, text: i ? [""] : change.text, origin: change.origin}); } + } else { + makeChangeInner(doc, change); + } + } + + function makeChangeInner(doc, change) { + if (change.text.length == 1 && change.text[0] == "" && cmp(change.from, change.to) == 0) { return } + var selAfter = computeSelAfterChange(doc, change); + addChangeToHistory(doc, change, selAfter, doc.cm ? doc.cm.curOp.id : NaN); + + makeChangeSingleDoc(doc, change, selAfter, stretchSpansOverChange(doc, change)); + var rebased = []; + + linkedDocs(doc, function (doc, sharedHist) { + if (!sharedHist && indexOf(rebased, doc.history) == -1) { + rebaseHist(doc.history, change); + rebased.push(doc.history); + } + makeChangeSingleDoc(doc, change, null, stretchSpansOverChange(doc, change)); + }); + } + + // Revert a change stored in a document's history. + function makeChangeFromHistory(doc, type, allowSelectionOnly) { + var suppress = doc.cm && doc.cm.state.suppressEdits; + if (suppress && !allowSelectionOnly) { return } + + var hist = doc.history, event, selAfter = doc.sel; + var source = type == "undo" ? hist.done : hist.undone, dest = type == "undo" ? hist.undone : hist.done; + + // Verify that there is a useable event (so that ctrl-z won't + // needlessly clear selection events) + var i = 0; + for (; i < source.length; i++) { + event = source[i]; + if (allowSelectionOnly ? event.ranges && !event.equals(doc.sel) : !event.ranges) + { break } + } + if (i == source.length) { return } + hist.lastOrigin = hist.lastSelOrigin = null; + + for (;;) { + event = source.pop(); + if (event.ranges) { + pushSelectionToHistory(event, dest); + if (allowSelectionOnly && !event.equals(doc.sel)) { + setSelection(doc, event, {clearRedo: false}); + return + } + selAfter = event; + } else if (suppress) { + source.push(event); + return + } else { break } + } + + // Build up a reverse change object to add to the opposite history + // stack (redo when undoing, and vice versa). + var antiChanges = []; + pushSelectionToHistory(selAfter, dest); + dest.push({changes: antiChanges, generation: hist.generation}); + hist.generation = event.generation || ++hist.maxGeneration; + + var filter = hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange"); + + var loop = function ( i ) { + var change = event.changes[i]; + change.origin = type; + if (filter && !filterChange(doc, change, false)) { + source.length = 0; + return {} + } + + antiChanges.push(historyChangeFromChange(doc, change)); + + var after = i ? computeSelAfterChange(doc, change) : lst(source); + makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change)); + if (!i && doc.cm) { doc.cm.scrollIntoView({from: change.from, to: changeEnd(change)}); } + var rebased = []; + + // Propagate to the linked documents + linkedDocs(doc, function (doc, sharedHist) { + if (!sharedHist && indexOf(rebased, doc.history) == -1) { + rebaseHist(doc.history, change); + rebased.push(doc.history); + } + makeChangeSingleDoc(doc, change, null, mergeOldSpans(doc, change)); + }); + }; + + for (var i$1 = event.changes.length - 1; i$1 >= 0; --i$1) { + var returned = loop( i$1 ); + + if ( returned ) return returned.v; + } + } + + // Sub-views need their line numbers shifted when text is added + // above or below them in the parent document. + function shiftDoc(doc, distance) { + if (distance == 0) { return } + doc.first += distance; + doc.sel = new Selection(map(doc.sel.ranges, function (range) { return new Range( + Pos(range.anchor.line + distance, range.anchor.ch), + Pos(range.head.line + distance, range.head.ch) + ); }), doc.sel.primIndex); + if (doc.cm) { + regChange(doc.cm, doc.first, doc.first - distance, distance); + for (var d = doc.cm.display, l = d.viewFrom; l < d.viewTo; l++) + { regLineChange(doc.cm, l, "gutter"); } + } + } + + // More lower-level change function, handling only a single document + // (not linked ones). + function makeChangeSingleDoc(doc, change, selAfter, spans) { + if (doc.cm && !doc.cm.curOp) + { return operation(doc.cm, makeChangeSingleDoc)(doc, change, selAfter, spans) } + + if (change.to.line < doc.first) { + shiftDoc(doc, change.text.length - 1 - (change.to.line - change.from.line)); + return + } + if (change.from.line > doc.lastLine()) { return } + + // Clip the change to the size of this doc + if (change.from.line < doc.first) { + var shift = change.text.length - 1 - (doc.first - change.from.line); + shiftDoc(doc, shift); + change = {from: Pos(doc.first, 0), to: Pos(change.to.line + shift, change.to.ch), + text: [lst(change.text)], origin: change.origin}; + } + var last = doc.lastLine(); + if (change.to.line > last) { + change = {from: change.from, to: Pos(last, getLine(doc, last).text.length), + text: [change.text[0]], origin: change.origin}; + } + + change.removed = getBetween(doc, change.from, change.to); + + if (!selAfter) { selAfter = computeSelAfterChange(doc, change); } + if (doc.cm) { makeChangeSingleDocInEditor(doc.cm, change, spans); } + else { updateDoc(doc, change, spans); } + setSelectionNoUndo(doc, selAfter, sel_dontScroll); + + if (doc.cantEdit && skipAtomic(doc, Pos(doc.firstLine(), 0))) + { doc.cantEdit = false; } + } + + // Handle the interaction of a change to a document with the editor + // that this document is part of. + function makeChangeSingleDocInEditor(cm, change, spans) { + var doc = cm.doc, display = cm.display, from = change.from, to = change.to; + + var recomputeMaxLength = false, checkWidthStart = from.line; + if (!cm.options.lineWrapping) { + checkWidthStart = lineNo(visualLine(getLine(doc, from.line))); + doc.iter(checkWidthStart, to.line + 1, function (line) { + if (line == display.maxLine) { + recomputeMaxLength = true; + return true + } + }); + } + + if (doc.sel.contains(change.from, change.to) > -1) + { signalCursorActivity(cm); } + + updateDoc(doc, change, spans, estimateHeight(cm)); + + if (!cm.options.lineWrapping) { + doc.iter(checkWidthStart, from.line + change.text.length, function (line) { + var len = lineLength(line); + if (len > display.maxLineLength) { + display.maxLine = line; + display.maxLineLength = len; + display.maxLineChanged = true; + recomputeMaxLength = false; + } + }); + if (recomputeMaxLength) { cm.curOp.updateMaxLine = true; } + } + + retreatFrontier(doc, from.line); + startWorker(cm, 400); + + var lendiff = change.text.length - (to.line - from.line) - 1; + // Remember that these lines changed, for updating the display + if (change.full) + { regChange(cm); } + else if (from.line == to.line && change.text.length == 1 && !isWholeLineUpdate(cm.doc, change)) + { regLineChange(cm, from.line, "text"); } + else + { regChange(cm, from.line, to.line + 1, lendiff); } + + var changesHandler = hasHandler(cm, "changes"), changeHandler = hasHandler(cm, "change"); + if (changeHandler || changesHandler) { + var obj = { + from: from, to: to, + text: change.text, + removed: change.removed, + origin: change.origin + }; + if (changeHandler) { signalLater(cm, "change", cm, obj); } + if (changesHandler) { (cm.curOp.changeObjs || (cm.curOp.changeObjs = [])).push(obj); } + } + cm.display.selForContextMenu = null; + } + + function replaceRange(doc, code, from, to, origin) { + var assign; + + if (!to) { to = from; } + if (cmp(to, from) < 0) { (assign = [to, from], from = assign[0], to = assign[1]); } + if (typeof code == "string") { code = doc.splitLines(code); } + makeChange(doc, {from: from, to: to, text: code, origin: origin}); + } + + // Rebasing/resetting history to deal with externally-sourced changes + + function rebaseHistSelSingle(pos, from, to, diff) { + if (to < pos.line) { + pos.line += diff; + } else if (from < pos.line) { + pos.line = from; + pos.ch = 0; + } + } + + // Tries to rebase an array of history events given a change in the + // document. If the change touches the same lines as the event, the + // event, and everything 'behind' it, is discarded. If the change is + // before the event, the event's positions are updated. Uses a + // copy-on-write scheme for the positions, to avoid having to + // reallocate them all on every rebase, but also avoid problems with + // shared position objects being unsafely updated. + function rebaseHistArray(array, from, to, diff) { + for (var i = 0; i < array.length; ++i) { + var sub = array[i], ok = true; + if (sub.ranges) { + if (!sub.copied) { sub = array[i] = sub.deepCopy(); sub.copied = true; } + for (var j = 0; j < sub.ranges.length; j++) { + rebaseHistSelSingle(sub.ranges[j].anchor, from, to, diff); + rebaseHistSelSingle(sub.ranges[j].head, from, to, diff); + } + continue + } + for (var j$1 = 0; j$1 < sub.changes.length; ++j$1) { + var cur = sub.changes[j$1]; + if (to < cur.from.line) { + cur.from = Pos(cur.from.line + diff, cur.from.ch); + cur.to = Pos(cur.to.line + diff, cur.to.ch); + } else if (from <= cur.to.line) { + ok = false; + break + } + } + if (!ok) { + array.splice(0, i + 1); + i = 0; + } + } + } + + function rebaseHist(hist, change) { + var from = change.from.line, to = change.to.line, diff = change.text.length - (to - from) - 1; + rebaseHistArray(hist.done, from, to, diff); + rebaseHistArray(hist.undone, from, to, diff); + } + + // Utility for applying a change to a line by handle or number, + // returning the number and optionally registering the line as + // changed. + function changeLine(doc, handle, changeType, op) { + var no = handle, line = handle; + if (typeof handle == "number") { line = getLine(doc, clipLine(doc, handle)); } + else { no = lineNo(handle); } + if (no == null) { return null } + if (op(line, no) && doc.cm) { regLineChange(doc.cm, no, changeType); } + return line + } + + // The document is represented as a BTree consisting of leaves, with + // chunk of lines in them, and branches, with up to ten leaves or + // other branch nodes below them. The top node is always a branch + // node, and is the document object itself (meaning it has + // additional methods and properties). + // + // All nodes have parent links. The tree is used both to go from + // line numbers to line objects, and to go from objects to numbers. + // It also indexes by height, and is used to convert between height + // and line object, and to find the total height of the document. + // + // See also http://marijnhaverbeke.nl/blog/codemirror-line-tree.html + + function LeafChunk(lines) { + this.lines = lines; + this.parent = null; + var height = 0; + for (var i = 0; i < lines.length; ++i) { + lines[i].parent = this; + height += lines[i].height; + } + this.height = height; + } + + LeafChunk.prototype = { + chunkSize: function() { return this.lines.length }, + + // Remove the n lines at offset 'at'. + removeInner: function(at, n) { + for (var i = at, e = at + n; i < e; ++i) { + var line = this.lines[i]; + this.height -= line.height; + cleanUpLine(line); + signalLater(line, "delete"); + } + this.lines.splice(at, n); + }, + + // Helper used to collapse a small branch into a single leaf. + collapse: function(lines) { + lines.push.apply(lines, this.lines); + }, + + // Insert the given array of lines at offset 'at', count them as + // having the given height. + insertInner: function(at, lines, height) { + this.height += height; + this.lines = this.lines.slice(0, at).concat(lines).concat(this.lines.slice(at)); + for (var i = 0; i < lines.length; ++i) { lines[i].parent = this; } + }, + + // Used to iterate over a part of the tree. + iterN: function(at, n, op) { + for (var e = at + n; at < e; ++at) + { if (op(this.lines[at])) { return true } } + } + }; + + function BranchChunk(children) { + this.children = children; + var size = 0, height = 0; + for (var i = 0; i < children.length; ++i) { + var ch = children[i]; + size += ch.chunkSize(); height += ch.height; + ch.parent = this; + } + this.size = size; + this.height = height; + this.parent = null; + } + + BranchChunk.prototype = { + chunkSize: function() { return this.size }, + + removeInner: function(at, n) { + this.size -= n; + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at < sz) { + var rm = Math.min(n, sz - at), oldHeight = child.height; + child.removeInner(at, rm); + this.height -= oldHeight - child.height; + if (sz == rm) { this.children.splice(i--, 1); child.parent = null; } + if ((n -= rm) == 0) { break } + at = 0; + } else { at -= sz; } + } + // If the result is smaller than 25 lines, ensure that it is a + // single leaf node. + if (this.size - n < 25 && + (this.children.length > 1 || !(this.children[0] instanceof LeafChunk))) { + var lines = []; + this.collapse(lines); + this.children = [new LeafChunk(lines)]; + this.children[0].parent = this; + } + }, + + collapse: function(lines) { + for (var i = 0; i < this.children.length; ++i) { this.children[i].collapse(lines); } + }, + + insertInner: function(at, lines, height) { + this.size += lines.length; + this.height += height; + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at <= sz) { + child.insertInner(at, lines, height); + if (child.lines && child.lines.length > 50) { + // To avoid memory thrashing when child.lines is huge (e.g. first view of a large file), it's never spliced. + // Instead, small slices are taken. They're taken in order because sequential memory accesses are fastest. + var remaining = child.lines.length % 25 + 25; + for (var pos = remaining; pos < child.lines.length;) { + var leaf = new LeafChunk(child.lines.slice(pos, pos += 25)); + child.height -= leaf.height; + this.children.splice(++i, 0, leaf); + leaf.parent = this; + } + child.lines = child.lines.slice(0, remaining); + this.maybeSpill(); + } + break + } + at -= sz; + } + }, + + // When a node has grown, check whether it should be split. + maybeSpill: function() { + if (this.children.length <= 10) { return } + var me = this; + do { + var spilled = me.children.splice(me.children.length - 5, 5); + var sibling = new BranchChunk(spilled); + if (!me.parent) { // Become the parent node + var copy = new BranchChunk(me.children); + copy.parent = me; + me.children = [copy, sibling]; + me = copy; + } else { + me.size -= sibling.size; + me.height -= sibling.height; + var myIndex = indexOf(me.parent.children, me); + me.parent.children.splice(myIndex + 1, 0, sibling); + } + sibling.parent = me.parent; + } while (me.children.length > 10) + me.parent.maybeSpill(); + }, + + iterN: function(at, n, op) { + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at < sz) { + var used = Math.min(n, sz - at); + if (child.iterN(at, used, op)) { return true } + if ((n -= used) == 0) { break } + at = 0; + } else { at -= sz; } + } + } + }; + + // Line widgets are block elements displayed above or below a line. + + var LineWidget = function(doc, node, options) { + if (options) { for (var opt in options) { if (options.hasOwnProperty(opt)) + { this[opt] = options[opt]; } } } + this.doc = doc; + this.node = node; + }; + + LineWidget.prototype.clear = function () { + var cm = this.doc.cm, ws = this.line.widgets, line = this.line, no = lineNo(line); + if (no == null || !ws) { return } + for (var i = 0; i < ws.length; ++i) { if (ws[i] == this) { ws.splice(i--, 1); } } + if (!ws.length) { line.widgets = null; } + var height = widgetHeight(this); + updateLineHeight(line, Math.max(0, line.height - height)); + if (cm) { + runInOp(cm, function () { + adjustScrollWhenAboveVisible(cm, line, -height); + regLineChange(cm, no, "widget"); + }); + signalLater(cm, "lineWidgetCleared", cm, this, no); + } + }; + + LineWidget.prototype.changed = function () { + var this$1 = this; + + var oldH = this.height, cm = this.doc.cm, line = this.line; + this.height = null; + var diff = widgetHeight(this) - oldH; + if (!diff) { return } + if (!lineIsHidden(this.doc, line)) { updateLineHeight(line, line.height + diff); } + if (cm) { + runInOp(cm, function () { + cm.curOp.forceUpdate = true; + adjustScrollWhenAboveVisible(cm, line, diff); + signalLater(cm, "lineWidgetChanged", cm, this$1, lineNo(line)); + }); + } + }; + eventMixin(LineWidget); + + function adjustScrollWhenAboveVisible(cm, line, diff) { + if (heightAtLine(line) < ((cm.curOp && cm.curOp.scrollTop) || cm.doc.scrollTop)) + { addToScrollTop(cm, diff); } + } + + function addLineWidget(doc, handle, node, options) { + var widget = new LineWidget(doc, node, options); + var cm = doc.cm; + if (cm && widget.noHScroll) { cm.display.alignWidgets = true; } + changeLine(doc, handle, "widget", function (line) { + var widgets = line.widgets || (line.widgets = []); + if (widget.insertAt == null) { widgets.push(widget); } + else { widgets.splice(Math.min(widgets.length - 1, Math.max(0, widget.insertAt)), 0, widget); } + widget.line = line; + if (cm && !lineIsHidden(doc, line)) { + var aboveVisible = heightAtLine(line) < doc.scrollTop; + updateLineHeight(line, line.height + widgetHeight(widget)); + if (aboveVisible) { addToScrollTop(cm, widget.height); } + cm.curOp.forceUpdate = true; + } + return true + }); + if (cm) { signalLater(cm, "lineWidgetAdded", cm, widget, typeof handle == "number" ? handle : lineNo(handle)); } + return widget + } + + // TEXTMARKERS + + // Created with markText and setBookmark methods. A TextMarker is a + // handle that can be used to clear or find a marked position in the + // document. Line objects hold arrays (markedSpans) containing + // {from, to, marker} object pointing to such marker objects, and + // indicating that such a marker is present on that line. Multiple + // lines may point to the same marker when it spans across lines. + // The spans will have null for their from/to properties when the + // marker continues beyond the start/end of the line. Markers have + // links back to the lines they currently touch. + + // Collapsed markers have unique ids, in order to be able to order + // them, which is needed for uniquely determining an outer marker + // when they overlap (they may nest, but not partially overlap). + var nextMarkerId = 0; + + var TextMarker = function(doc, type) { + this.lines = []; + this.type = type; + this.doc = doc; + this.id = ++nextMarkerId; + }; + + // Clear the marker. + TextMarker.prototype.clear = function () { + if (this.explicitlyCleared) { return } + var cm = this.doc.cm, withOp = cm && !cm.curOp; + if (withOp) { startOperation(cm); } + if (hasHandler(this, "clear")) { + var found = this.find(); + if (found) { signalLater(this, "clear", found.from, found.to); } + } + var min = null, max = null; + for (var i = 0; i < this.lines.length; ++i) { + var line = this.lines[i]; + var span = getMarkedSpanFor(line.markedSpans, this); + if (cm && !this.collapsed) { regLineChange(cm, lineNo(line), "text"); } + else if (cm) { + if (span.to != null) { max = lineNo(line); } + if (span.from != null) { min = lineNo(line); } + } + line.markedSpans = removeMarkedSpan(line.markedSpans, span); + if (span.from == null && this.collapsed && !lineIsHidden(this.doc, line) && cm) + { updateLineHeight(line, textHeight(cm.display)); } + } + if (cm && this.collapsed && !cm.options.lineWrapping) { for (var i$1 = 0; i$1 < this.lines.length; ++i$1) { + var visual = visualLine(this.lines[i$1]), len = lineLength(visual); + if (len > cm.display.maxLineLength) { + cm.display.maxLine = visual; + cm.display.maxLineLength = len; + cm.display.maxLineChanged = true; + } + } } + + if (min != null && cm && this.collapsed) { regChange(cm, min, max + 1); } + this.lines.length = 0; + this.explicitlyCleared = true; + if (this.atomic && this.doc.cantEdit) { + this.doc.cantEdit = false; + if (cm) { reCheckSelection(cm.doc); } + } + if (cm) { signalLater(cm, "markerCleared", cm, this, min, max); } + if (withOp) { endOperation(cm); } + if (this.parent) { this.parent.clear(); } + }; + + // Find the position of the marker in the document. Returns a {from, + // to} object by default. Side can be passed to get a specific side + // -- 0 (both), -1 (left), or 1 (right). When lineObj is true, the + // Pos objects returned contain a line object, rather than a line + // number (used to prevent looking up the same line twice). + TextMarker.prototype.find = function (side, lineObj) { + if (side == null && this.type == "bookmark") { side = 1; } + var from, to; + for (var i = 0; i < this.lines.length; ++i) { + var line = this.lines[i]; + var span = getMarkedSpanFor(line.markedSpans, this); + if (span.from != null) { + from = Pos(lineObj ? line : lineNo(line), span.from); + if (side == -1) { return from } + } + if (span.to != null) { + to = Pos(lineObj ? line : lineNo(line), span.to); + if (side == 1) { return to } + } + } + return from && {from: from, to: to} + }; + + // Signals that the marker's widget changed, and surrounding layout + // should be recomputed. + TextMarker.prototype.changed = function () { + var this$1 = this; + + var pos = this.find(-1, true), widget = this, cm = this.doc.cm; + if (!pos || !cm) { return } + runInOp(cm, function () { + var line = pos.line, lineN = lineNo(pos.line); + var view = findViewForLine(cm, lineN); + if (view) { + clearLineMeasurementCacheFor(view); + cm.curOp.selectionChanged = cm.curOp.forceUpdate = true; + } + cm.curOp.updateMaxLine = true; + if (!lineIsHidden(widget.doc, line) && widget.height != null) { + var oldHeight = widget.height; + widget.height = null; + var dHeight = widgetHeight(widget) - oldHeight; + if (dHeight) + { updateLineHeight(line, line.height + dHeight); } + } + signalLater(cm, "markerChanged", cm, this$1); + }); + }; + + TextMarker.prototype.attachLine = function (line) { + if (!this.lines.length && this.doc.cm) { + var op = this.doc.cm.curOp; + if (!op.maybeHiddenMarkers || indexOf(op.maybeHiddenMarkers, this) == -1) + { (op.maybeUnhiddenMarkers || (op.maybeUnhiddenMarkers = [])).push(this); } + } + this.lines.push(line); + }; + + TextMarker.prototype.detachLine = function (line) { + this.lines.splice(indexOf(this.lines, line), 1); + if (!this.lines.length && this.doc.cm) { + var op = this.doc.cm.curOp + ;(op.maybeHiddenMarkers || (op.maybeHiddenMarkers = [])).push(this); + } + }; + eventMixin(TextMarker); + + // Create a marker, wire it up to the right lines, and + function markText(doc, from, to, options, type) { + // Shared markers (across linked documents) are handled separately + // (markTextShared will call out to this again, once per + // document). + if (options && options.shared) { return markTextShared(doc, from, to, options, type) } + // Ensure we are in an operation. + if (doc.cm && !doc.cm.curOp) { return operation(doc.cm, markText)(doc, from, to, options, type) } + + var marker = new TextMarker(doc, type), diff = cmp(from, to); + if (options) { copyObj(options, marker, false); } + // Don't connect empty markers unless clearWhenEmpty is false + if (diff > 0 || diff == 0 && marker.clearWhenEmpty !== false) + { return marker } + if (marker.replacedWith) { + // Showing up as a widget implies collapsed (widget replaces text) + marker.collapsed = true; + marker.widgetNode = eltP("span", [marker.replacedWith], "CodeMirror-widget"); + if (!options.handleMouseEvents) { marker.widgetNode.setAttribute("cm-ignore-events", "true"); } + if (options.insertLeft) { marker.widgetNode.insertLeft = true; } + } + if (marker.collapsed) { + if (conflictingCollapsedRange(doc, from.line, from, to, marker) || + from.line != to.line && conflictingCollapsedRange(doc, to.line, from, to, marker)) + { throw new Error("Inserting collapsed marker partially overlapping an existing one") } + seeCollapsedSpans(); + } + + if (marker.addToHistory) + { addChangeToHistory(doc, {from: from, to: to, origin: "markText"}, doc.sel, NaN); } + + var curLine = from.line, cm = doc.cm, updateMaxLine; + doc.iter(curLine, to.line + 1, function (line) { + if (cm && marker.collapsed && !cm.options.lineWrapping && visualLine(line) == cm.display.maxLine) + { updateMaxLine = true; } + if (marker.collapsed && curLine != from.line) { updateLineHeight(line, 0); } + addMarkedSpan(line, new MarkedSpan(marker, + curLine == from.line ? from.ch : null, + curLine == to.line ? to.ch : null)); + ++curLine; + }); + // lineIsHidden depends on the presence of the spans, so needs a second pass + if (marker.collapsed) { doc.iter(from.line, to.line + 1, function (line) { + if (lineIsHidden(doc, line)) { updateLineHeight(line, 0); } + }); } + + if (marker.clearOnEnter) { on(marker, "beforeCursorEnter", function () { return marker.clear(); }); } + + if (marker.readOnly) { + seeReadOnlySpans(); + if (doc.history.done.length || doc.history.undone.length) + { doc.clearHistory(); } + } + if (marker.collapsed) { + marker.id = ++nextMarkerId; + marker.atomic = true; + } + if (cm) { + // Sync editor state + if (updateMaxLine) { cm.curOp.updateMaxLine = true; } + if (marker.collapsed) + { regChange(cm, from.line, to.line + 1); } + else if (marker.className || marker.startStyle || marker.endStyle || marker.css || + marker.attributes || marker.title) + { for (var i = from.line; i <= to.line; i++) { regLineChange(cm, i, "text"); } } + if (marker.atomic) { reCheckSelection(cm.doc); } + signalLater(cm, "markerAdded", cm, marker); + } + return marker + } + + // SHARED TEXTMARKERS + + // A shared marker spans multiple linked documents. It is + // implemented as a meta-marker-object controlling multiple normal + // markers. + var SharedTextMarker = function(markers, primary) { + this.markers = markers; + this.primary = primary; + for (var i = 0; i < markers.length; ++i) + { markers[i].parent = this; } + }; + + SharedTextMarker.prototype.clear = function () { + if (this.explicitlyCleared) { return } + this.explicitlyCleared = true; + for (var i = 0; i < this.markers.length; ++i) + { this.markers[i].clear(); } + signalLater(this, "clear"); + }; + + SharedTextMarker.prototype.find = function (side, lineObj) { + return this.primary.find(side, lineObj) + }; + eventMixin(SharedTextMarker); + + function markTextShared(doc, from, to, options, type) { + options = copyObj(options); + options.shared = false; + var markers = [markText(doc, from, to, options, type)], primary = markers[0]; + var widget = options.widgetNode; + linkedDocs(doc, function (doc) { + if (widget) { options.widgetNode = widget.cloneNode(true); } + markers.push(markText(doc, clipPos(doc, from), clipPos(doc, to), options, type)); + for (var i = 0; i < doc.linked.length; ++i) + { if (doc.linked[i].isParent) { return } } + primary = lst(markers); + }); + return new SharedTextMarker(markers, primary) + } + + function findSharedMarkers(doc) { + return doc.findMarks(Pos(doc.first, 0), doc.clipPos(Pos(doc.lastLine())), function (m) { return m.parent; }) + } + + function copySharedMarkers(doc, markers) { + for (var i = 0; i < markers.length; i++) { + var marker = markers[i], pos = marker.find(); + var mFrom = doc.clipPos(pos.from), mTo = doc.clipPos(pos.to); + if (cmp(mFrom, mTo)) { + var subMark = markText(doc, mFrom, mTo, marker.primary, marker.primary.type); + marker.markers.push(subMark); + subMark.parent = marker; + } + } + } + + function detachSharedMarkers(markers) { + var loop = function ( i ) { + var marker = markers[i], linked = [marker.primary.doc]; + linkedDocs(marker.primary.doc, function (d) { return linked.push(d); }); + for (var j = 0; j < marker.markers.length; j++) { + var subMarker = marker.markers[j]; + if (indexOf(linked, subMarker.doc) == -1) { + subMarker.parent = null; + marker.markers.splice(j--, 1); + } + } + }; + + for (var i = 0; i < markers.length; i++) loop( i ); + } + + var nextDocId = 0; + var Doc = function(text, mode, firstLine, lineSep, direction) { + if (!(this instanceof Doc)) { return new Doc(text, mode, firstLine, lineSep, direction) } + if (firstLine == null) { firstLine = 0; } + + BranchChunk.call(this, [new LeafChunk([new Line("", null)])]); + this.first = firstLine; + this.scrollTop = this.scrollLeft = 0; + this.cantEdit = false; + this.cleanGeneration = 1; + this.modeFrontier = this.highlightFrontier = firstLine; + var start = Pos(firstLine, 0); + this.sel = simpleSelection(start); + this.history = new History(null); + this.id = ++nextDocId; + this.modeOption = mode; + this.lineSep = lineSep; + this.direction = (direction == "rtl") ? "rtl" : "ltr"; + this.extend = false; + + if (typeof text == "string") { text = this.splitLines(text); } + updateDoc(this, {from: start, to: start, text: text}); + setSelection(this, simpleSelection(start), sel_dontScroll); + }; + + Doc.prototype = createObj(BranchChunk.prototype, { + constructor: Doc, + // Iterate over the document. Supports two forms -- with only one + // argument, it calls that for each line in the document. With + // three, it iterates over the range given by the first two (with + // the second being non-inclusive). + iter: function(from, to, op) { + if (op) { this.iterN(from - this.first, to - from, op); } + else { this.iterN(this.first, this.first + this.size, from); } + }, + + // Non-public interface for adding and removing lines. + insert: function(at, lines) { + var height = 0; + for (var i = 0; i < lines.length; ++i) { height += lines[i].height; } + this.insertInner(at - this.first, lines, height); + }, + remove: function(at, n) { this.removeInner(at - this.first, n); }, + + // From here, the methods are part of the public interface. Most + // are also available from CodeMirror (editor) instances. + + getValue: function(lineSep) { + var lines = getLines(this, this.first, this.first + this.size); + if (lineSep === false) { return lines } + return lines.join(lineSep || this.lineSeparator()) + }, + setValue: docMethodOp(function(code) { + var top = Pos(this.first, 0), last = this.first + this.size - 1; + makeChange(this, {from: top, to: Pos(last, getLine(this, last).text.length), + text: this.splitLines(code), origin: "setValue", full: true}, true); + if (this.cm) { scrollToCoords(this.cm, 0, 0); } + setSelection(this, simpleSelection(top), sel_dontScroll); + }), + replaceRange: function(code, from, to, origin) { + from = clipPos(this, from); + to = to ? clipPos(this, to) : from; + replaceRange(this, code, from, to, origin); + }, + getRange: function(from, to, lineSep) { + var lines = getBetween(this, clipPos(this, from), clipPos(this, to)); + if (lineSep === false) { return lines } + return lines.join(lineSep || this.lineSeparator()) + }, + + getLine: function(line) {var l = this.getLineHandle(line); return l && l.text}, + + getLineHandle: function(line) {if (isLine(this, line)) { return getLine(this, line) }}, + getLineNumber: function(line) {return lineNo(line)}, + + getLineHandleVisualStart: function(line) { + if (typeof line == "number") { line = getLine(this, line); } + return visualLine(line) + }, + + lineCount: function() {return this.size}, + firstLine: function() {return this.first}, + lastLine: function() {return this.first + this.size - 1}, + + clipPos: function(pos) {return clipPos(this, pos)}, + + getCursor: function(start) { + var range = this.sel.primary(), pos; + if (start == null || start == "head") { pos = range.head; } + else if (start == "anchor") { pos = range.anchor; } + else if (start == "end" || start == "to" || start === false) { pos = range.to(); } + else { pos = range.from(); } + return pos + }, + listSelections: function() { return this.sel.ranges }, + somethingSelected: function() {return this.sel.somethingSelected()}, + + setCursor: docMethodOp(function(line, ch, options) { + setSimpleSelection(this, clipPos(this, typeof line == "number" ? Pos(line, ch || 0) : line), null, options); + }), + setSelection: docMethodOp(function(anchor, head, options) { + setSimpleSelection(this, clipPos(this, anchor), clipPos(this, head || anchor), options); + }), + extendSelection: docMethodOp(function(head, other, options) { + extendSelection(this, clipPos(this, head), other && clipPos(this, other), options); + }), + extendSelections: docMethodOp(function(heads, options) { + extendSelections(this, clipPosArray(this, heads), options); + }), + extendSelectionsBy: docMethodOp(function(f, options) { + var heads = map(this.sel.ranges, f); + extendSelections(this, clipPosArray(this, heads), options); + }), + setSelections: docMethodOp(function(ranges, primary, options) { + if (!ranges.length) { return } + var out = []; + for (var i = 0; i < ranges.length; i++) + { out[i] = new Range(clipPos(this, ranges[i].anchor), + clipPos(this, ranges[i].head)); } + if (primary == null) { primary = Math.min(ranges.length - 1, this.sel.primIndex); } + setSelection(this, normalizeSelection(this.cm, out, primary), options); + }), + addSelection: docMethodOp(function(anchor, head, options) { + var ranges = this.sel.ranges.slice(0); + ranges.push(new Range(clipPos(this, anchor), clipPos(this, head || anchor))); + setSelection(this, normalizeSelection(this.cm, ranges, ranges.length - 1), options); + }), + + getSelection: function(lineSep) { + var ranges = this.sel.ranges, lines; + for (var i = 0; i < ranges.length; i++) { + var sel = getBetween(this, ranges[i].from(), ranges[i].to()); + lines = lines ? lines.concat(sel) : sel; + } + if (lineSep === false) { return lines } + else { return lines.join(lineSep || this.lineSeparator()) } + }, + getSelections: function(lineSep) { + var parts = [], ranges = this.sel.ranges; + for (var i = 0; i < ranges.length; i++) { + var sel = getBetween(this, ranges[i].from(), ranges[i].to()); + if (lineSep !== false) { sel = sel.join(lineSep || this.lineSeparator()); } + parts[i] = sel; + } + return parts + }, + replaceSelection: function(code, collapse, origin) { + var dup = []; + for (var i = 0; i < this.sel.ranges.length; i++) + { dup[i] = code; } + this.replaceSelections(dup, collapse, origin || "+input"); + }, + replaceSelections: docMethodOp(function(code, collapse, origin) { + var changes = [], sel = this.sel; + for (var i = 0; i < sel.ranges.length; i++) { + var range = sel.ranges[i]; + changes[i] = {from: range.from(), to: range.to(), text: this.splitLines(code[i]), origin: origin}; + } + var newSel = collapse && collapse != "end" && computeReplacedSel(this, changes, collapse); + for (var i$1 = changes.length - 1; i$1 >= 0; i$1--) + { makeChange(this, changes[i$1]); } + if (newSel) { setSelectionReplaceHistory(this, newSel); } + else if (this.cm) { ensureCursorVisible(this.cm); } + }), + undo: docMethodOp(function() {makeChangeFromHistory(this, "undo");}), + redo: docMethodOp(function() {makeChangeFromHistory(this, "redo");}), + undoSelection: docMethodOp(function() {makeChangeFromHistory(this, "undo", true);}), + redoSelection: docMethodOp(function() {makeChangeFromHistory(this, "redo", true);}), + + setExtending: function(val) {this.extend = val;}, + getExtending: function() {return this.extend}, + + historySize: function() { + var hist = this.history, done = 0, undone = 0; + for (var i = 0; i < hist.done.length; i++) { if (!hist.done[i].ranges) { ++done; } } + for (var i$1 = 0; i$1 < hist.undone.length; i$1++) { if (!hist.undone[i$1].ranges) { ++undone; } } + return {undo: done, redo: undone} + }, + clearHistory: function() { + var this$1 = this; + + this.history = new History(this.history.maxGeneration); + linkedDocs(this, function (doc) { return doc.history = this$1.history; }, true); + }, + + markClean: function() { + this.cleanGeneration = this.changeGeneration(true); + }, + changeGeneration: function(forceSplit) { + if (forceSplit) + { this.history.lastOp = this.history.lastSelOp = this.history.lastOrigin = null; } + return this.history.generation + }, + isClean: function (gen) { + return this.history.generation == (gen || this.cleanGeneration) + }, + + getHistory: function() { + return {done: copyHistoryArray(this.history.done), + undone: copyHistoryArray(this.history.undone)} + }, + setHistory: function(histData) { + var hist = this.history = new History(this.history.maxGeneration); + hist.done = copyHistoryArray(histData.done.slice(0), null, true); + hist.undone = copyHistoryArray(histData.undone.slice(0), null, true); + }, + + setGutterMarker: docMethodOp(function(line, gutterID, value) { + return changeLine(this, line, "gutter", function (line) { + var markers = line.gutterMarkers || (line.gutterMarkers = {}); + markers[gutterID] = value; + if (!value && isEmpty(markers)) { line.gutterMarkers = null; } + return true + }) + }), + + clearGutter: docMethodOp(function(gutterID) { + var this$1 = this; + + this.iter(function (line) { + if (line.gutterMarkers && line.gutterMarkers[gutterID]) { + changeLine(this$1, line, "gutter", function () { + line.gutterMarkers[gutterID] = null; + if (isEmpty(line.gutterMarkers)) { line.gutterMarkers = null; } + return true + }); + } + }); + }), + + lineInfo: function(line) { + var n; + if (typeof line == "number") { + if (!isLine(this, line)) { return null } + n = line; + line = getLine(this, line); + if (!line) { return null } + } else { + n = lineNo(line); + if (n == null) { return null } + } + return {line: n, handle: line, text: line.text, gutterMarkers: line.gutterMarkers, + textClass: line.textClass, bgClass: line.bgClass, wrapClass: line.wrapClass, + widgets: line.widgets} + }, + + addLineClass: docMethodOp(function(handle, where, cls) { + return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function (line) { + var prop = where == "text" ? "textClass" + : where == "background" ? "bgClass" + : where == "gutter" ? "gutterClass" : "wrapClass"; + if (!line[prop]) { line[prop] = cls; } + else if (classTest(cls).test(line[prop])) { return false } + else { line[prop] += " " + cls; } + return true + }) + }), + removeLineClass: docMethodOp(function(handle, where, cls) { + return changeLine(this, handle, where == "gutter" ? "gutter" : "class", function (line) { + var prop = where == "text" ? "textClass" + : where == "background" ? "bgClass" + : where == "gutter" ? "gutterClass" : "wrapClass"; + var cur = line[prop]; + if (!cur) { return false } + else if (cls == null) { line[prop] = null; } + else { + var found = cur.match(classTest(cls)); + if (!found) { return false } + var end = found.index + found[0].length; + line[prop] = cur.slice(0, found.index) + (!found.index || end == cur.length ? "" : " ") + cur.slice(end) || null; + } + return true + }) + }), + + addLineWidget: docMethodOp(function(handle, node, options) { + return addLineWidget(this, handle, node, options) + }), + removeLineWidget: function(widget) { widget.clear(); }, + + markText: function(from, to, options) { + return markText(this, clipPos(this, from), clipPos(this, to), options, options && options.type || "range") + }, + setBookmark: function(pos, options) { + var realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options), + insertLeft: options && options.insertLeft, + clearWhenEmpty: false, shared: options && options.shared, + handleMouseEvents: options && options.handleMouseEvents}; + pos = clipPos(this, pos); + return markText(this, pos, pos, realOpts, "bookmark") + }, + findMarksAt: function(pos) { + pos = clipPos(this, pos); + var markers = [], spans = getLine(this, pos.line).markedSpans; + if (spans) { for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if ((span.from == null || span.from <= pos.ch) && + (span.to == null || span.to >= pos.ch)) + { markers.push(span.marker.parent || span.marker); } + } } + return markers + }, + findMarks: function(from, to, filter) { + from = clipPos(this, from); to = clipPos(this, to); + var found = [], lineNo = from.line; + this.iter(from.line, to.line + 1, function (line) { + var spans = line.markedSpans; + if (spans) { for (var i = 0; i < spans.length; i++) { + var span = spans[i]; + if (!(span.to != null && lineNo == from.line && from.ch >= span.to || + span.from == null && lineNo != from.line || + span.from != null && lineNo == to.line && span.from >= to.ch) && + (!filter || filter(span.marker))) + { found.push(span.marker.parent || span.marker); } + } } + ++lineNo; + }); + return found + }, + getAllMarks: function() { + var markers = []; + this.iter(function (line) { + var sps = line.markedSpans; + if (sps) { for (var i = 0; i < sps.length; ++i) + { if (sps[i].from != null) { markers.push(sps[i].marker); } } } + }); + return markers + }, + + posFromIndex: function(off) { + var ch, lineNo = this.first, sepSize = this.lineSeparator().length; + this.iter(function (line) { + var sz = line.text.length + sepSize; + if (sz > off) { ch = off; return true } + off -= sz; + ++lineNo; + }); + return clipPos(this, Pos(lineNo, ch)) + }, + indexFromPos: function (coords) { + coords = clipPos(this, coords); + var index = coords.ch; + if (coords.line < this.first || coords.ch < 0) { return 0 } + var sepSize = this.lineSeparator().length; + this.iter(this.first, coords.line, function (line) { // iter aborts when callback returns a truthy value + index += line.text.length + sepSize; + }); + return index + }, + + copy: function(copyHistory) { + var doc = new Doc(getLines(this, this.first, this.first + this.size), + this.modeOption, this.first, this.lineSep, this.direction); + doc.scrollTop = this.scrollTop; doc.scrollLeft = this.scrollLeft; + doc.sel = this.sel; + doc.extend = false; + if (copyHistory) { + doc.history.undoDepth = this.history.undoDepth; + doc.setHistory(this.getHistory()); + } + return doc + }, + + linkedDoc: function(options) { + if (!options) { options = {}; } + var from = this.first, to = this.first + this.size; + if (options.from != null && options.from > from) { from = options.from; } + if (options.to != null && options.to < to) { to = options.to; } + var copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from, this.lineSep, this.direction); + if (options.sharedHist) { copy.history = this.history + ; }(this.linked || (this.linked = [])).push({doc: copy, sharedHist: options.sharedHist}); + copy.linked = [{doc: this, isParent: true, sharedHist: options.sharedHist}]; + copySharedMarkers(copy, findSharedMarkers(this)); + return copy + }, + unlinkDoc: function(other) { + if (other instanceof CodeMirror) { other = other.doc; } + if (this.linked) { for (var i = 0; i < this.linked.length; ++i) { + var link = this.linked[i]; + if (link.doc != other) { continue } + this.linked.splice(i, 1); + other.unlinkDoc(this); + detachSharedMarkers(findSharedMarkers(this)); + break + } } + // If the histories were shared, split them again + if (other.history == this.history) { + var splitIds = [other.id]; + linkedDocs(other, function (doc) { return splitIds.push(doc.id); }, true); + other.history = new History(null); + other.history.done = copyHistoryArray(this.history.done, splitIds); + other.history.undone = copyHistoryArray(this.history.undone, splitIds); + } + }, + iterLinkedDocs: function(f) {linkedDocs(this, f);}, + + getMode: function() {return this.mode}, + getEditor: function() {return this.cm}, + + splitLines: function(str) { + if (this.lineSep) { return str.split(this.lineSep) } + return splitLinesAuto(str) + }, + lineSeparator: function() { return this.lineSep || "\n" }, + + setDirection: docMethodOp(function (dir) { + if (dir != "rtl") { dir = "ltr"; } + if (dir == this.direction) { return } + this.direction = dir; + this.iter(function (line) { return line.order = null; }); + if (this.cm) { directionChanged(this.cm); } + }) + }); + + // Public alias. + Doc.prototype.eachLine = Doc.prototype.iter; + + // Kludge to work around strange IE behavior where it'll sometimes + // re-fire a series of drag-related events right after the drop (#1551) + var lastDrop = 0; + + function onDrop(e) { + var cm = this; + clearDragCursor(cm); + if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) + { return } + e_preventDefault(e); + if (ie) { lastDrop = +new Date; } + var pos = posFromMouse(cm, e, true), files = e.dataTransfer.files; + if (!pos || cm.isReadOnly()) { return } + // Might be a file drop, in which case we simply extract the text + // and insert it. + if (files && files.length && window.FileReader && window.File) { + var n = files.length, text = Array(n), read = 0; + var markAsReadAndPasteIfAllFilesAreRead = function () { + if (++read == n) { + operation(cm, function () { + pos = clipPos(cm.doc, pos); + var change = {from: pos, to: pos, + text: cm.doc.splitLines( + text.filter(function (t) { return t != null; }).join(cm.doc.lineSeparator())), + origin: "paste"}; + makeChange(cm.doc, change); + setSelectionReplaceHistory(cm.doc, simpleSelection(clipPos(cm.doc, pos), clipPos(cm.doc, changeEnd(change)))); + })(); + } + }; + var readTextFromFile = function (file, i) { + if (cm.options.allowDropFileTypes && + indexOf(cm.options.allowDropFileTypes, file.type) == -1) { + markAsReadAndPasteIfAllFilesAreRead(); + return + } + var reader = new FileReader; + reader.onerror = function () { return markAsReadAndPasteIfAllFilesAreRead(); }; + reader.onload = function () { + var content = reader.result; + if (/[\x00-\x08\x0e-\x1f]{2}/.test(content)) { + markAsReadAndPasteIfAllFilesAreRead(); + return + } + text[i] = content; + markAsReadAndPasteIfAllFilesAreRead(); + }; + reader.readAsText(file); + }; + for (var i = 0; i < files.length; i++) { readTextFromFile(files[i], i); } + } else { // Normal drop + // Don't do a replace if the drop happened inside of the selected text. + if (cm.state.draggingText && cm.doc.sel.contains(pos) > -1) { + cm.state.draggingText(e); + // Ensure the editor is re-focused + setTimeout(function () { return cm.display.input.focus(); }, 20); + return + } + try { + var text$1 = e.dataTransfer.getData("Text"); + if (text$1) { + var selected; + if (cm.state.draggingText && !cm.state.draggingText.copy) + { selected = cm.listSelections(); } + setSelectionNoUndo(cm.doc, simpleSelection(pos, pos)); + if (selected) { for (var i$1 = 0; i$1 < selected.length; ++i$1) + { replaceRange(cm.doc, "", selected[i$1].anchor, selected[i$1].head, "drag"); } } + cm.replaceSelection(text$1, "around", "paste"); + cm.display.input.focus(); + } + } + catch(e$1){} + } + } + + function onDragStart(cm, e) { + if (ie && (!cm.state.draggingText || +new Date - lastDrop < 100)) { e_stop(e); return } + if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) { return } + + e.dataTransfer.setData("Text", cm.getSelection()); + e.dataTransfer.effectAllowed = "copyMove"; + + // Use dummy image instead of default browsers image. + // Recent Safari (~6.0.2) have a tendency to segfault when this happens, so we don't do it there. + if (e.dataTransfer.setDragImage && !safari) { + var img = elt("img", null, null, "position: fixed; left: 0; top: 0;"); + img.src = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; + if (presto) { + img.width = img.height = 1; + cm.display.wrapper.appendChild(img); + // Force a relayout, or Opera won't use our image for some obscure reason + img._top = img.offsetTop; + } + e.dataTransfer.setDragImage(img, 0, 0); + if (presto) { img.parentNode.removeChild(img); } + } + } + + function onDragOver(cm, e) { + var pos = posFromMouse(cm, e); + if (!pos) { return } + var frag = document.createDocumentFragment(); + drawSelectionCursor(cm, pos, frag); + if (!cm.display.dragCursor) { + cm.display.dragCursor = elt("div", null, "CodeMirror-cursors CodeMirror-dragcursors"); + cm.display.lineSpace.insertBefore(cm.display.dragCursor, cm.display.cursorDiv); + } + removeChildrenAndAdd(cm.display.dragCursor, frag); + } + + function clearDragCursor(cm) { + if (cm.display.dragCursor) { + cm.display.lineSpace.removeChild(cm.display.dragCursor); + cm.display.dragCursor = null; + } + } + + // These must be handled carefully, because naively registering a + // handler for each editor will cause the editors to never be + // garbage collected. + + function forEachCodeMirror(f) { + if (!document.getElementsByClassName) { return } + var byClass = document.getElementsByClassName("CodeMirror"), editors = []; + for (var i = 0; i < byClass.length; i++) { + var cm = byClass[i].CodeMirror; + if (cm) { editors.push(cm); } + } + if (editors.length) { editors[0].operation(function () { + for (var i = 0; i < editors.length; i++) { f(editors[i]); } + }); } + } + + var globalsRegistered = false; + function ensureGlobalHandlers() { + if (globalsRegistered) { return } + registerGlobalHandlers(); + globalsRegistered = true; + } + function registerGlobalHandlers() { + // When the window resizes, we need to refresh active editors. + var resizeTimer; + on(window, "resize", function () { + if (resizeTimer == null) { resizeTimer = setTimeout(function () { + resizeTimer = null; + forEachCodeMirror(onResize); + }, 100); } + }); + // When the window loses focus, we want to show the editor as blurred + on(window, "blur", function () { return forEachCodeMirror(onBlur); }); + } + // Called when the window resizes + function onResize(cm) { + var d = cm.display; + // Might be a text scaling operation, clear size caches. + d.cachedCharWidth = d.cachedTextHeight = d.cachedPaddingH = null; + d.scrollbarsClipped = false; + cm.setSize(); + } + + var keyNames = { + 3: "Pause", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt", + 19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End", + 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert", + 46: "Delete", 59: ";", 61: "=", 91: "Mod", 92: "Mod", 93: "Mod", + 106: "*", 107: "=", 109: "-", 110: ".", 111: "/", 145: "ScrollLock", + 173: "-", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", + 221: "]", 222: "'", 224: "Mod", 63232: "Up", 63233: "Down", 63234: "Left", 63235: "Right", 63272: "Delete", + 63273: "Home", 63275: "End", 63276: "PageUp", 63277: "PageDown", 63302: "Insert" + }; + + // Number keys + for (var i = 0; i < 10; i++) { keyNames[i + 48] = keyNames[i + 96] = String(i); } + // Alphabetic keys + for (var i$1 = 65; i$1 <= 90; i$1++) { keyNames[i$1] = String.fromCharCode(i$1); } + // Function keys + for (var i$2 = 1; i$2 <= 12; i$2++) { keyNames[i$2 + 111] = keyNames[i$2 + 63235] = "F" + i$2; } + + var keyMap = {}; + + keyMap.basic = { + "Left": "goCharLeft", "Right": "goCharRight", "Up": "goLineUp", "Down": "goLineDown", + "End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown", + "Delete": "delCharAfter", "Backspace": "delCharBefore", "Shift-Backspace": "delCharBefore", + "Tab": "defaultTab", "Shift-Tab": "indentAuto", + "Enter": "newlineAndIndent", "Insert": "toggleOverwrite", + "Esc": "singleSelection" + }; + // Note that the save and find-related commands aren't defined by + // default. User code or addons can define them. Unknown commands + // are simply ignored. + keyMap.pcDefault = { + "Ctrl-A": "selectAll", "Ctrl-D": "deleteLine", "Ctrl-Z": "undo", "Shift-Ctrl-Z": "redo", "Ctrl-Y": "redo", + "Ctrl-Home": "goDocStart", "Ctrl-End": "goDocEnd", "Ctrl-Up": "goLineUp", "Ctrl-Down": "goLineDown", + "Ctrl-Left": "goGroupLeft", "Ctrl-Right": "goGroupRight", "Alt-Left": "goLineStart", "Alt-Right": "goLineEnd", + "Ctrl-Backspace": "delGroupBefore", "Ctrl-Delete": "delGroupAfter", "Ctrl-S": "save", "Ctrl-F": "find", + "Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll", + "Ctrl-[": "indentLess", "Ctrl-]": "indentMore", + "Ctrl-U": "undoSelection", "Shift-Ctrl-U": "redoSelection", "Alt-U": "redoSelection", + "fallthrough": "basic" + }; + // Very basic readline/emacs-style bindings, which are standard on Mac. + keyMap.emacsy = { + "Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown", + "Alt-F": "goWordRight", "Alt-B": "goWordLeft", "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd", + "Ctrl-V": "goPageDown", "Shift-Ctrl-V": "goPageUp", "Ctrl-D": "delCharAfter", "Ctrl-H": "delCharBefore", + "Alt-D": "delWordAfter", "Alt-Backspace": "delWordBefore", "Ctrl-K": "killLine", "Ctrl-T": "transposeChars", + "Ctrl-O": "openLine" + }; + keyMap.macDefault = { + "Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo", + "Cmd-Home": "goDocStart", "Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goGroupLeft", + "Alt-Right": "goGroupRight", "Cmd-Left": "goLineLeft", "Cmd-Right": "goLineRight", "Alt-Backspace": "delGroupBefore", + "Ctrl-Alt-Backspace": "delGroupAfter", "Alt-Delete": "delGroupAfter", "Cmd-S": "save", "Cmd-F": "find", + "Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll", + "Cmd-[": "indentLess", "Cmd-]": "indentMore", "Cmd-Backspace": "delWrappedLineLeft", "Cmd-Delete": "delWrappedLineRight", + "Cmd-U": "undoSelection", "Shift-Cmd-U": "redoSelection", "Ctrl-Up": "goDocStart", "Ctrl-Down": "goDocEnd", + "fallthrough": ["basic", "emacsy"] + }; + keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault; + + // KEYMAP DISPATCH + + function normalizeKeyName(name) { + var parts = name.split(/-(?!$)/); + name = parts[parts.length - 1]; + var alt, ctrl, shift, cmd; + for (var i = 0; i < parts.length - 1; i++) { + var mod = parts[i]; + if (/^(cmd|meta|m)$/i.test(mod)) { cmd = true; } + else if (/^a(lt)?$/i.test(mod)) { alt = true; } + else if (/^(c|ctrl|control)$/i.test(mod)) { ctrl = true; } + else if (/^s(hift)?$/i.test(mod)) { shift = true; } + else { throw new Error("Unrecognized modifier name: " + mod) } + } + if (alt) { name = "Alt-" + name; } + if (ctrl) { name = "Ctrl-" + name; } + if (cmd) { name = "Cmd-" + name; } + if (shift) { name = "Shift-" + name; } + return name + } + + // This is a kludge to keep keymaps mostly working as raw objects + // (backwards compatibility) while at the same time support features + // like normalization and multi-stroke key bindings. It compiles a + // new normalized keymap, and then updates the old object to reflect + // this. + function normalizeKeyMap(keymap) { + var copy = {}; + for (var keyname in keymap) { if (keymap.hasOwnProperty(keyname)) { + var value = keymap[keyname]; + if (/^(name|fallthrough|(de|at)tach)$/.test(keyname)) { continue } + if (value == "...") { delete keymap[keyname]; continue } + + var keys = map(keyname.split(" "), normalizeKeyName); + for (var i = 0; i < keys.length; i++) { + var val = (void 0), name = (void 0); + if (i == keys.length - 1) { + name = keys.join(" "); + val = value; + } else { + name = keys.slice(0, i + 1).join(" "); + val = "..."; + } + var prev = copy[name]; + if (!prev) { copy[name] = val; } + else if (prev != val) { throw new Error("Inconsistent bindings for " + name) } + } + delete keymap[keyname]; + } } + for (var prop in copy) { keymap[prop] = copy[prop]; } + return keymap + } + + function lookupKey(key, map, handle, context) { + map = getKeyMap(map); + var found = map.call ? map.call(key, context) : map[key]; + if (found === false) { return "nothing" } + if (found === "...") { return "multi" } + if (found != null && handle(found)) { return "handled" } + + if (map.fallthrough) { + if (Object.prototype.toString.call(map.fallthrough) != "[object Array]") + { return lookupKey(key, map.fallthrough, handle, context) } + for (var i = 0; i < map.fallthrough.length; i++) { + var result = lookupKey(key, map.fallthrough[i], handle, context); + if (result) { return result } + } + } + } + + // Modifier key presses don't count as 'real' key presses for the + // purpose of keymap fallthrough. + function isModifierKey(value) { + var name = typeof value == "string" ? value : keyNames[value.keyCode]; + return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod" + } + + function addModifierNames(name, event, noShift) { + var base = name; + if (event.altKey && base != "Alt") { name = "Alt-" + name; } + if ((flipCtrlCmd ? event.metaKey : event.ctrlKey) && base != "Ctrl") { name = "Ctrl-" + name; } + if ((flipCtrlCmd ? event.ctrlKey : event.metaKey) && base != "Mod") { name = "Cmd-" + name; } + if (!noShift && event.shiftKey && base != "Shift") { name = "Shift-" + name; } + return name + } + + // Look up the name of a key as indicated by an event object. + function keyName(event, noShift) { + if (presto && event.keyCode == 34 && event["char"]) { return false } + var name = keyNames[event.keyCode]; + if (name == null || event.altGraphKey) { return false } + // Ctrl-ScrollLock has keyCode 3, same as Ctrl-Pause, + // so we'll use event.code when available (Chrome 48+, FF 38+, Safari 10.1+) + if (event.keyCode == 3 && event.code) { name = event.code; } + return addModifierNames(name, event, noShift) + } + + function getKeyMap(val) { + return typeof val == "string" ? keyMap[val] : val + } + + // Helper for deleting text near the selection(s), used to implement + // backspace, delete, and similar functionality. + function deleteNearSelection(cm, compute) { + var ranges = cm.doc.sel.ranges, kill = []; + // Build up a set of ranges to kill first, merging overlapping + // ranges. + for (var i = 0; i < ranges.length; i++) { + var toKill = compute(ranges[i]); + while (kill.length && cmp(toKill.from, lst(kill).to) <= 0) { + var replaced = kill.pop(); + if (cmp(replaced.from, toKill.from) < 0) { + toKill.from = replaced.from; + break + } + } + kill.push(toKill); + } + // Next, remove those actual ranges. + runInOp(cm, function () { + for (var i = kill.length - 1; i >= 0; i--) + { replaceRange(cm.doc, "", kill[i].from, kill[i].to, "+delete"); } + ensureCursorVisible(cm); + }); + } + + function moveCharLogically(line, ch, dir) { + var target = skipExtendingChars(line.text, ch + dir, dir); + return target < 0 || target > line.text.length ? null : target + } + + function moveLogically(line, start, dir) { + var ch = moveCharLogically(line, start.ch, dir); + return ch == null ? null : new Pos(start.line, ch, dir < 0 ? "after" : "before") + } + + function endOfLine(visually, cm, lineObj, lineNo, dir) { + if (visually) { + if (cm.doc.direction == "rtl") { dir = -dir; } + var order = getOrder(lineObj, cm.doc.direction); + if (order) { + var part = dir < 0 ? lst(order) : order[0]; + var moveInStorageOrder = (dir < 0) == (part.level == 1); + var sticky = moveInStorageOrder ? "after" : "before"; + var ch; + // With a wrapped rtl chunk (possibly spanning multiple bidi parts), + // it could be that the last bidi part is not on the last visual line, + // since visual lines contain content order-consecutive chunks. + // Thus, in rtl, we are looking for the first (content-order) character + // in the rtl chunk that is on the last line (that is, the same line + // as the last (content-order) character). + if (part.level > 0 || cm.doc.direction == "rtl") { + var prep = prepareMeasureForLine(cm, lineObj); + ch = dir < 0 ? lineObj.text.length - 1 : 0; + var targetTop = measureCharPrepared(cm, prep, ch).top; + ch = findFirst(function (ch) { return measureCharPrepared(cm, prep, ch).top == targetTop; }, (dir < 0) == (part.level == 1) ? part.from : part.to - 1, ch); + if (sticky == "before") { ch = moveCharLogically(lineObj, ch, 1); } + } else { ch = dir < 0 ? part.to : part.from; } + return new Pos(lineNo, ch, sticky) + } + } + return new Pos(lineNo, dir < 0 ? lineObj.text.length : 0, dir < 0 ? "before" : "after") + } + + function moveVisually(cm, line, start, dir) { + var bidi = getOrder(line, cm.doc.direction); + if (!bidi) { return moveLogically(line, start, dir) } + if (start.ch >= line.text.length) { + start.ch = line.text.length; + start.sticky = "before"; + } else if (start.ch <= 0) { + start.ch = 0; + start.sticky = "after"; + } + var partPos = getBidiPartAt(bidi, start.ch, start.sticky), part = bidi[partPos]; + if (cm.doc.direction == "ltr" && part.level % 2 == 0 && (dir > 0 ? part.to > start.ch : part.from < start.ch)) { + // Case 1: We move within an ltr part in an ltr editor. Even with wrapped lines, + // nothing interesting happens. + return moveLogically(line, start, dir) + } + + var mv = function (pos, dir) { return moveCharLogically(line, pos instanceof Pos ? pos.ch : pos, dir); }; + var prep; + var getWrappedLineExtent = function (ch) { + if (!cm.options.lineWrapping) { return {begin: 0, end: line.text.length} } + prep = prep || prepareMeasureForLine(cm, line); + return wrappedLineExtentChar(cm, line, prep, ch) + }; + var wrappedLineExtent = getWrappedLineExtent(start.sticky == "before" ? mv(start, -1) : start.ch); + + if (cm.doc.direction == "rtl" || part.level == 1) { + var moveInStorageOrder = (part.level == 1) == (dir < 0); + var ch = mv(start, moveInStorageOrder ? 1 : -1); + if (ch != null && (!moveInStorageOrder ? ch >= part.from && ch >= wrappedLineExtent.begin : ch <= part.to && ch <= wrappedLineExtent.end)) { + // Case 2: We move within an rtl part or in an rtl editor on the same visual line + var sticky = moveInStorageOrder ? "before" : "after"; + return new Pos(start.line, ch, sticky) + } + } + + // Case 3: Could not move within this bidi part in this visual line, so leave + // the current bidi part + + var searchInVisualLine = function (partPos, dir, wrappedLineExtent) { + var getRes = function (ch, moveInStorageOrder) { return moveInStorageOrder + ? new Pos(start.line, mv(ch, 1), "before") + : new Pos(start.line, ch, "after"); }; + + for (; partPos >= 0 && partPos < bidi.length; partPos += dir) { + var part = bidi[partPos]; + var moveInStorageOrder = (dir > 0) == (part.level != 1); + var ch = moveInStorageOrder ? wrappedLineExtent.begin : mv(wrappedLineExtent.end, -1); + if (part.from <= ch && ch < part.to) { return getRes(ch, moveInStorageOrder) } + ch = moveInStorageOrder ? part.from : mv(part.to, -1); + if (wrappedLineExtent.begin <= ch && ch < wrappedLineExtent.end) { return getRes(ch, moveInStorageOrder) } + } + }; + + // Case 3a: Look for other bidi parts on the same visual line + var res = searchInVisualLine(partPos + dir, dir, wrappedLineExtent); + if (res) { return res } + + // Case 3b: Look for other bidi parts on the next visual line + var nextCh = dir > 0 ? wrappedLineExtent.end : mv(wrappedLineExtent.begin, -1); + if (nextCh != null && !(dir > 0 && nextCh == line.text.length)) { + res = searchInVisualLine(dir > 0 ? 0 : bidi.length - 1, dir, getWrappedLineExtent(nextCh)); + if (res) { return res } + } + + // Case 4: Nowhere to move + return null + } + + // Commands are parameter-less actions that can be performed on an + // editor, mostly used for keybindings. + var commands = { + selectAll: selectAll, + singleSelection: function (cm) { return cm.setSelection(cm.getCursor("anchor"), cm.getCursor("head"), sel_dontScroll); }, + killLine: function (cm) { return deleteNearSelection(cm, function (range) { + if (range.empty()) { + var len = getLine(cm.doc, range.head.line).text.length; + if (range.head.ch == len && range.head.line < cm.lastLine()) + { return {from: range.head, to: Pos(range.head.line + 1, 0)} } + else + { return {from: range.head, to: Pos(range.head.line, len)} } + } else { + return {from: range.from(), to: range.to()} + } + }); }, + deleteLine: function (cm) { return deleteNearSelection(cm, function (range) { return ({ + from: Pos(range.from().line, 0), + to: clipPos(cm.doc, Pos(range.to().line + 1, 0)) + }); }); }, + delLineLeft: function (cm) { return deleteNearSelection(cm, function (range) { return ({ + from: Pos(range.from().line, 0), to: range.from() + }); }); }, + delWrappedLineLeft: function (cm) { return deleteNearSelection(cm, function (range) { + var top = cm.charCoords(range.head, "div").top + 5; + var leftPos = cm.coordsChar({left: 0, top: top}, "div"); + return {from: leftPos, to: range.from()} + }); }, + delWrappedLineRight: function (cm) { return deleteNearSelection(cm, function (range) { + var top = cm.charCoords(range.head, "div").top + 5; + var rightPos = cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div"); + return {from: range.from(), to: rightPos } + }); }, + undo: function (cm) { return cm.undo(); }, + redo: function (cm) { return cm.redo(); }, + undoSelection: function (cm) { return cm.undoSelection(); }, + redoSelection: function (cm) { return cm.redoSelection(); }, + goDocStart: function (cm) { return cm.extendSelection(Pos(cm.firstLine(), 0)); }, + goDocEnd: function (cm) { return cm.extendSelection(Pos(cm.lastLine())); }, + goLineStart: function (cm) { return cm.extendSelectionsBy(function (range) { return lineStart(cm, range.head.line); }, + {origin: "+move", bias: 1} + ); }, + goLineStartSmart: function (cm) { return cm.extendSelectionsBy(function (range) { return lineStartSmart(cm, range.head); }, + {origin: "+move", bias: 1} + ); }, + goLineEnd: function (cm) { return cm.extendSelectionsBy(function (range) { return lineEnd(cm, range.head.line); }, + {origin: "+move", bias: -1} + ); }, + goLineRight: function (cm) { return cm.extendSelectionsBy(function (range) { + var top = cm.cursorCoords(range.head, "div").top + 5; + return cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div") + }, sel_move); }, + goLineLeft: function (cm) { return cm.extendSelectionsBy(function (range) { + var top = cm.cursorCoords(range.head, "div").top + 5; + return cm.coordsChar({left: 0, top: top}, "div") + }, sel_move); }, + goLineLeftSmart: function (cm) { return cm.extendSelectionsBy(function (range) { + var top = cm.cursorCoords(range.head, "div").top + 5; + var pos = cm.coordsChar({left: 0, top: top}, "div"); + if (pos.ch < cm.getLine(pos.line).search(/\S/)) { return lineStartSmart(cm, range.head) } + return pos + }, sel_move); }, + goLineUp: function (cm) { return cm.moveV(-1, "line"); }, + goLineDown: function (cm) { return cm.moveV(1, "line"); }, + goPageUp: function (cm) { return cm.moveV(-1, "page"); }, + goPageDown: function (cm) { return cm.moveV(1, "page"); }, + goCharLeft: function (cm) { return cm.moveH(-1, "char"); }, + goCharRight: function (cm) { return cm.moveH(1, "char"); }, + goColumnLeft: function (cm) { return cm.moveH(-1, "column"); }, + goColumnRight: function (cm) { return cm.moveH(1, "column"); }, + goWordLeft: function (cm) { return cm.moveH(-1, "word"); }, + goGroupRight: function (cm) { return cm.moveH(1, "group"); }, + goGroupLeft: function (cm) { return cm.moveH(-1, "group"); }, + goWordRight: function (cm) { return cm.moveH(1, "word"); }, + delCharBefore: function (cm) { return cm.deleteH(-1, "codepoint"); }, + delCharAfter: function (cm) { return cm.deleteH(1, "char"); }, + delWordBefore: function (cm) { return cm.deleteH(-1, "word"); }, + delWordAfter: function (cm) { return cm.deleteH(1, "word"); }, + delGroupBefore: function (cm) { return cm.deleteH(-1, "group"); }, + delGroupAfter: function (cm) { return cm.deleteH(1, "group"); }, + indentAuto: function (cm) { return cm.indentSelection("smart"); }, + indentMore: function (cm) { return cm.indentSelection("add"); }, + indentLess: function (cm) { return cm.indentSelection("subtract"); }, + insertTab: function (cm) { return cm.replaceSelection("\t"); }, + insertSoftTab: function (cm) { + var spaces = [], ranges = cm.listSelections(), tabSize = cm.options.tabSize; + for (var i = 0; i < ranges.length; i++) { + var pos = ranges[i].from(); + var col = countColumn(cm.getLine(pos.line), pos.ch, tabSize); + spaces.push(spaceStr(tabSize - col % tabSize)); + } + cm.replaceSelections(spaces); + }, + defaultTab: function (cm) { + if (cm.somethingSelected()) { cm.indentSelection("add"); } + else { cm.execCommand("insertTab"); } + }, + // Swap the two chars left and right of each selection's head. + // Move cursor behind the two swapped characters afterwards. + // + // Doesn't consider line feeds a character. + // Doesn't scan more than one line above to find a character. + // Doesn't do anything on an empty line. + // Doesn't do anything with non-empty selections. + transposeChars: function (cm) { return runInOp(cm, function () { + var ranges = cm.listSelections(), newSel = []; + for (var i = 0; i < ranges.length; i++) { + if (!ranges[i].empty()) { continue } + var cur = ranges[i].head, line = getLine(cm.doc, cur.line).text; + if (line) { + if (cur.ch == line.length) { cur = new Pos(cur.line, cur.ch - 1); } + if (cur.ch > 0) { + cur = new Pos(cur.line, cur.ch + 1); + cm.replaceRange(line.charAt(cur.ch - 1) + line.charAt(cur.ch - 2), + Pos(cur.line, cur.ch - 2), cur, "+transpose"); + } else if (cur.line > cm.doc.first) { + var prev = getLine(cm.doc, cur.line - 1).text; + if (prev) { + cur = new Pos(cur.line, 1); + cm.replaceRange(line.charAt(0) + cm.doc.lineSeparator() + + prev.charAt(prev.length - 1), + Pos(cur.line - 1, prev.length - 1), cur, "+transpose"); + } + } + } + newSel.push(new Range(cur, cur)); + } + cm.setSelections(newSel); + }); }, + newlineAndIndent: function (cm) { return runInOp(cm, function () { + var sels = cm.listSelections(); + for (var i = sels.length - 1; i >= 0; i--) + { cm.replaceRange(cm.doc.lineSeparator(), sels[i].anchor, sels[i].head, "+input"); } + sels = cm.listSelections(); + for (var i$1 = 0; i$1 < sels.length; i$1++) + { cm.indentLine(sels[i$1].from().line, null, true); } + ensureCursorVisible(cm); + }); }, + openLine: function (cm) { return cm.replaceSelection("\n", "start"); }, + toggleOverwrite: function (cm) { return cm.toggleOverwrite(); } + }; + + + function lineStart(cm, lineN) { + var line = getLine(cm.doc, lineN); + var visual = visualLine(line); + if (visual != line) { lineN = lineNo(visual); } + return endOfLine(true, cm, visual, lineN, 1) + } + function lineEnd(cm, lineN) { + var line = getLine(cm.doc, lineN); + var visual = visualLineEnd(line); + if (visual != line) { lineN = lineNo(visual); } + return endOfLine(true, cm, line, lineN, -1) + } + function lineStartSmart(cm, pos) { + var start = lineStart(cm, pos.line); + var line = getLine(cm.doc, start.line); + var order = getOrder(line, cm.doc.direction); + if (!order || order[0].level == 0) { + var firstNonWS = Math.max(start.ch, line.text.search(/\S/)); + var inWS = pos.line == start.line && pos.ch <= firstNonWS && pos.ch; + return Pos(start.line, inWS ? 0 : firstNonWS, start.sticky) + } + return start + } + + // Run a handler that was bound to a key. + function doHandleBinding(cm, bound, dropShift) { + if (typeof bound == "string") { + bound = commands[bound]; + if (!bound) { return false } + } + // Ensure previous input has been read, so that the handler sees a + // consistent view of the document + cm.display.input.ensurePolled(); + var prevShift = cm.display.shift, done = false; + try { + if (cm.isReadOnly()) { cm.state.suppressEdits = true; } + if (dropShift) { cm.display.shift = false; } + done = bound(cm) != Pass; + } finally { + cm.display.shift = prevShift; + cm.state.suppressEdits = false; + } + return done + } + + function lookupKeyForEditor(cm, name, handle) { + for (var i = 0; i < cm.state.keyMaps.length; i++) { + var result = lookupKey(name, cm.state.keyMaps[i], handle, cm); + if (result) { return result } + } + return (cm.options.extraKeys && lookupKey(name, cm.options.extraKeys, handle, cm)) + || lookupKey(name, cm.options.keyMap, handle, cm) + } + + // Note that, despite the name, this function is also used to check + // for bound mouse clicks. + + var stopSeq = new Delayed; + + function dispatchKey(cm, name, e, handle) { + var seq = cm.state.keySeq; + if (seq) { + if (isModifierKey(name)) { return "handled" } + if (/\'$/.test(name)) + { cm.state.keySeq = null; } + else + { stopSeq.set(50, function () { + if (cm.state.keySeq == seq) { + cm.state.keySeq = null; + cm.display.input.reset(); + } + }); } + if (dispatchKeyInner(cm, seq + " " + name, e, handle)) { return true } + } + return dispatchKeyInner(cm, name, e, handle) + } + + function dispatchKeyInner(cm, name, e, handle) { + var result = lookupKeyForEditor(cm, name, handle); + + if (result == "multi") + { cm.state.keySeq = name; } + if (result == "handled") + { signalLater(cm, "keyHandled", cm, name, e); } + + if (result == "handled" || result == "multi") { + e_preventDefault(e); + restartBlink(cm); + } + + return !!result + } + + // Handle a key from the keydown event. + function handleKeyBinding(cm, e) { + var name = keyName(e, true); + if (!name) { return false } + + if (e.shiftKey && !cm.state.keySeq) { + // First try to resolve full name (including 'Shift-'). Failing + // that, see if there is a cursor-motion command (starting with + // 'go') bound to the keyname without 'Shift-'. + return dispatchKey(cm, "Shift-" + name, e, function (b) { return doHandleBinding(cm, b, true); }) + || dispatchKey(cm, name, e, function (b) { + if (typeof b == "string" ? /^go[A-Z]/.test(b) : b.motion) + { return doHandleBinding(cm, b) } + }) + } else { + return dispatchKey(cm, name, e, function (b) { return doHandleBinding(cm, b); }) + } + } + + // Handle a key from the keypress event + function handleCharBinding(cm, e, ch) { + return dispatchKey(cm, "'" + ch + "'", e, function (b) { return doHandleBinding(cm, b, true); }) + } + + var lastStoppedKey = null; + function onKeyDown(e) { + var cm = this; + if (e.target && e.target != cm.display.input.getField()) { return } + cm.curOp.focus = activeElt(); + if (signalDOMEvent(cm, e)) { return } + // IE does strange things with escape. + if (ie && ie_version < 11 && e.keyCode == 27) { e.returnValue = false; } + var code = e.keyCode; + cm.display.shift = code == 16 || e.shiftKey; + var handled = handleKeyBinding(cm, e); + if (presto) { + lastStoppedKey = handled ? code : null; + // Opera has no cut event... we try to at least catch the key combo + if (!handled && code == 88 && !hasCopyEvent && (mac ? e.metaKey : e.ctrlKey)) + { cm.replaceSelection("", null, "cut"); } + } + if (gecko && !mac && !handled && code == 46 && e.shiftKey && !e.ctrlKey && document.execCommand) + { document.execCommand("cut"); } + + // Turn mouse into crosshair when Alt is held on Mac. + if (code == 18 && !/\bCodeMirror-crosshair\b/.test(cm.display.lineDiv.className)) + { showCrossHair(cm); } + } + + function showCrossHair(cm) { + var lineDiv = cm.display.lineDiv; + addClass(lineDiv, "CodeMirror-crosshair"); + + function up(e) { + if (e.keyCode == 18 || !e.altKey) { + rmClass(lineDiv, "CodeMirror-crosshair"); + off(document, "keyup", up); + off(document, "mouseover", up); + } + } + on(document, "keyup", up); + on(document, "mouseover", up); + } + + function onKeyUp(e) { + if (e.keyCode == 16) { this.doc.sel.shift = false; } + signalDOMEvent(this, e); + } + + function onKeyPress(e) { + var cm = this; + if (e.target && e.target != cm.display.input.getField()) { return } + if (eventInWidget(cm.display, e) || signalDOMEvent(cm, e) || e.ctrlKey && !e.altKey || mac && e.metaKey) { return } + var keyCode = e.keyCode, charCode = e.charCode; + if (presto && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return} + if ((presto && (!e.which || e.which < 10)) && handleKeyBinding(cm, e)) { return } + var ch = String.fromCharCode(charCode == null ? keyCode : charCode); + // Some browsers fire keypress events for backspace + if (ch == "\x08") { return } + if (handleCharBinding(cm, e, ch)) { return } + cm.display.input.onKeyPress(e); + } + + var DOUBLECLICK_DELAY = 400; + + var PastClick = function(time, pos, button) { + this.time = time; + this.pos = pos; + this.button = button; + }; + + PastClick.prototype.compare = function (time, pos, button) { + return this.time + DOUBLECLICK_DELAY > time && + cmp(pos, this.pos) == 0 && button == this.button + }; + + var lastClick, lastDoubleClick; + function clickRepeat(pos, button) { + var now = +new Date; + if (lastDoubleClick && lastDoubleClick.compare(now, pos, button)) { + lastClick = lastDoubleClick = null; + return "triple" + } else if (lastClick && lastClick.compare(now, pos, button)) { + lastDoubleClick = new PastClick(now, pos, button); + lastClick = null; + return "double" + } else { + lastClick = new PastClick(now, pos, button); + lastDoubleClick = null; + return "single" + } + } + + // A mouse down can be a single click, double click, triple click, + // start of selection drag, start of text drag, new cursor + // (ctrl-click), rectangle drag (alt-drag), or xwin + // middle-click-paste. Or it might be a click on something we should + // not interfere with, such as a scrollbar or widget. + function onMouseDown(e) { + var cm = this, display = cm.display; + if (signalDOMEvent(cm, e) || display.activeTouch && display.input.supportsTouch()) { return } + display.input.ensurePolled(); + display.shift = e.shiftKey; + + if (eventInWidget(display, e)) { + if (!webkit) { + // Briefly turn off draggability, to allow widgets to do + // normal dragging things. + display.scroller.draggable = false; + setTimeout(function () { return display.scroller.draggable = true; }, 100); + } + return + } + if (clickInGutter(cm, e)) { return } + var pos = posFromMouse(cm, e), button = e_button(e), repeat = pos ? clickRepeat(pos, button) : "single"; + window.focus(); + + // #3261: make sure, that we're not starting a second selection + if (button == 1 && cm.state.selectingText) + { cm.state.selectingText(e); } + + if (pos && handleMappedButton(cm, button, pos, repeat, e)) { return } + + if (button == 1) { + if (pos) { leftButtonDown(cm, pos, repeat, e); } + else if (e_target(e) == display.scroller) { e_preventDefault(e); } + } else if (button == 2) { + if (pos) { extendSelection(cm.doc, pos); } + setTimeout(function () { return display.input.focus(); }, 20); + } else if (button == 3) { + if (captureRightClick) { cm.display.input.onContextMenu(e); } + else { delayBlurEvent(cm); } + } + } + + function handleMappedButton(cm, button, pos, repeat, event) { + var name = "Click"; + if (repeat == "double") { name = "Double" + name; } + else if (repeat == "triple") { name = "Triple" + name; } + name = (button == 1 ? "Left" : button == 2 ? "Middle" : "Right") + name; + + return dispatchKey(cm, addModifierNames(name, event), event, function (bound) { + if (typeof bound == "string") { bound = commands[bound]; } + if (!bound) { return false } + var done = false; + try { + if (cm.isReadOnly()) { cm.state.suppressEdits = true; } + done = bound(cm, pos) != Pass; + } finally { + cm.state.suppressEdits = false; + } + return done + }) + } + + function configureMouse(cm, repeat, event) { + var option = cm.getOption("configureMouse"); + var value = option ? option(cm, repeat, event) : {}; + if (value.unit == null) { + var rect = chromeOS ? event.shiftKey && event.metaKey : event.altKey; + value.unit = rect ? "rectangle" : repeat == "single" ? "char" : repeat == "double" ? "word" : "line"; + } + if (value.extend == null || cm.doc.extend) { value.extend = cm.doc.extend || event.shiftKey; } + if (value.addNew == null) { value.addNew = mac ? event.metaKey : event.ctrlKey; } + if (value.moveOnDrag == null) { value.moveOnDrag = !(mac ? event.altKey : event.ctrlKey); } + return value + } + + function leftButtonDown(cm, pos, repeat, event) { + if (ie) { setTimeout(bind(ensureFocus, cm), 0); } + else { cm.curOp.focus = activeElt(); } + + var behavior = configureMouse(cm, repeat, event); + + var sel = cm.doc.sel, contained; + if (cm.options.dragDrop && dragAndDrop && !cm.isReadOnly() && + repeat == "single" && (contained = sel.contains(pos)) > -1 && + (cmp((contained = sel.ranges[contained]).from(), pos) < 0 || pos.xRel > 0) && + (cmp(contained.to(), pos) > 0 || pos.xRel < 0)) + { leftButtonStartDrag(cm, event, pos, behavior); } + else + { leftButtonSelect(cm, event, pos, behavior); } + } + + // Start a text drag. When it ends, see if any dragging actually + // happen, and treat as a click if it didn't. + function leftButtonStartDrag(cm, event, pos, behavior) { + var display = cm.display, moved = false; + var dragEnd = operation(cm, function (e) { + if (webkit) { display.scroller.draggable = false; } + cm.state.draggingText = false; + off(display.wrapper.ownerDocument, "mouseup", dragEnd); + off(display.wrapper.ownerDocument, "mousemove", mouseMove); + off(display.scroller, "dragstart", dragStart); + off(display.scroller, "drop", dragEnd); + if (!moved) { + e_preventDefault(e); + if (!behavior.addNew) + { extendSelection(cm.doc, pos, null, null, behavior.extend); } + // Work around unexplainable focus problem in IE9 (#2127) and Chrome (#3081) + if ((webkit && !safari) || ie && ie_version == 9) + { setTimeout(function () {display.wrapper.ownerDocument.body.focus({preventScroll: true}); display.input.focus();}, 20); } + else + { display.input.focus(); } + } + }); + var mouseMove = function(e2) { + moved = moved || Math.abs(event.clientX - e2.clientX) + Math.abs(event.clientY - e2.clientY) >= 10; + }; + var dragStart = function () { return moved = true; }; + // Let the drag handler handle this. + if (webkit) { display.scroller.draggable = true; } + cm.state.draggingText = dragEnd; + dragEnd.copy = !behavior.moveOnDrag; + // IE's approach to draggable + if (display.scroller.dragDrop) { display.scroller.dragDrop(); } + on(display.wrapper.ownerDocument, "mouseup", dragEnd); + on(display.wrapper.ownerDocument, "mousemove", mouseMove); + on(display.scroller, "dragstart", dragStart); + on(display.scroller, "drop", dragEnd); + + delayBlurEvent(cm); + setTimeout(function () { return display.input.focus(); }, 20); + } + + function rangeForUnit(cm, pos, unit) { + if (unit == "char") { return new Range(pos, pos) } + if (unit == "word") { return cm.findWordAt(pos) } + if (unit == "line") { return new Range(Pos(pos.line, 0), clipPos(cm.doc, Pos(pos.line + 1, 0))) } + var result = unit(cm, pos); + return new Range(result.from, result.to) + } + + // Normal selection, as opposed to text dragging. + function leftButtonSelect(cm, event, start, behavior) { + var display = cm.display, doc = cm.doc; + e_preventDefault(event); + + var ourRange, ourIndex, startSel = doc.sel, ranges = startSel.ranges; + if (behavior.addNew && !behavior.extend) { + ourIndex = doc.sel.contains(start); + if (ourIndex > -1) + { ourRange = ranges[ourIndex]; } + else + { ourRange = new Range(start, start); } + } else { + ourRange = doc.sel.primary(); + ourIndex = doc.sel.primIndex; + } + + if (behavior.unit == "rectangle") { + if (!behavior.addNew) { ourRange = new Range(start, start); } + start = posFromMouse(cm, event, true, true); + ourIndex = -1; + } else { + var range = rangeForUnit(cm, start, behavior.unit); + if (behavior.extend) + { ourRange = extendRange(ourRange, range.anchor, range.head, behavior.extend); } + else + { ourRange = range; } + } + + if (!behavior.addNew) { + ourIndex = 0; + setSelection(doc, new Selection([ourRange], 0), sel_mouse); + startSel = doc.sel; + } else if (ourIndex == -1) { + ourIndex = ranges.length; + setSelection(doc, normalizeSelection(cm, ranges.concat([ourRange]), ourIndex), + {scroll: false, origin: "*mouse"}); + } else if (ranges.length > 1 && ranges[ourIndex].empty() && behavior.unit == "char" && !behavior.extend) { + setSelection(doc, normalizeSelection(cm, ranges.slice(0, ourIndex).concat(ranges.slice(ourIndex + 1)), 0), + {scroll: false, origin: "*mouse"}); + startSel = doc.sel; + } else { + replaceOneSelection(doc, ourIndex, ourRange, sel_mouse); + } + + var lastPos = start; + function extendTo(pos) { + if (cmp(lastPos, pos) == 0) { return } + lastPos = pos; + + if (behavior.unit == "rectangle") { + var ranges = [], tabSize = cm.options.tabSize; + var startCol = countColumn(getLine(doc, start.line).text, start.ch, tabSize); + var posCol = countColumn(getLine(doc, pos.line).text, pos.ch, tabSize); + var left = Math.min(startCol, posCol), right = Math.max(startCol, posCol); + for (var line = Math.min(start.line, pos.line), end = Math.min(cm.lastLine(), Math.max(start.line, pos.line)); + line <= end; line++) { + var text = getLine(doc, line).text, leftPos = findColumn(text, left, tabSize); + if (left == right) + { ranges.push(new Range(Pos(line, leftPos), Pos(line, leftPos))); } + else if (text.length > leftPos) + { ranges.push(new Range(Pos(line, leftPos), Pos(line, findColumn(text, right, tabSize)))); } + } + if (!ranges.length) { ranges.push(new Range(start, start)); } + setSelection(doc, normalizeSelection(cm, startSel.ranges.slice(0, ourIndex).concat(ranges), ourIndex), + {origin: "*mouse", scroll: false}); + cm.scrollIntoView(pos); + } else { + var oldRange = ourRange; + var range = rangeForUnit(cm, pos, behavior.unit); + var anchor = oldRange.anchor, head; + if (cmp(range.anchor, anchor) > 0) { + head = range.head; + anchor = minPos(oldRange.from(), range.anchor); + } else { + head = range.anchor; + anchor = maxPos(oldRange.to(), range.head); + } + var ranges$1 = startSel.ranges.slice(0); + ranges$1[ourIndex] = bidiSimplify(cm, new Range(clipPos(doc, anchor), head)); + setSelection(doc, normalizeSelection(cm, ranges$1, ourIndex), sel_mouse); + } + } + + var editorSize = display.wrapper.getBoundingClientRect(); + // Used to ensure timeout re-tries don't fire when another extend + // happened in the meantime (clearTimeout isn't reliable -- at + // least on Chrome, the timeouts still happen even when cleared, + // if the clear happens after their scheduled firing time). + var counter = 0; + + function extend(e) { + var curCount = ++counter; + var cur = posFromMouse(cm, e, true, behavior.unit == "rectangle"); + if (!cur) { return } + if (cmp(cur, lastPos) != 0) { + cm.curOp.focus = activeElt(); + extendTo(cur); + var visible = visibleLines(display, doc); + if (cur.line >= visible.to || cur.line < visible.from) + { setTimeout(operation(cm, function () {if (counter == curCount) { extend(e); }}), 150); } + } else { + var outside = e.clientY < editorSize.top ? -20 : e.clientY > editorSize.bottom ? 20 : 0; + if (outside) { setTimeout(operation(cm, function () { + if (counter != curCount) { return } + display.scroller.scrollTop += outside; + extend(e); + }), 50); } + } + } + + function done(e) { + cm.state.selectingText = false; + counter = Infinity; + // If e is null or undefined we interpret this as someone trying + // to explicitly cancel the selection rather than the user + // letting go of the mouse button. + if (e) { + e_preventDefault(e); + display.input.focus(); + } + off(display.wrapper.ownerDocument, "mousemove", move); + off(display.wrapper.ownerDocument, "mouseup", up); + doc.history.lastSelOrigin = null; + } + + var move = operation(cm, function (e) { + if (e.buttons === 0 || !e_button(e)) { done(e); } + else { extend(e); } + }); + var up = operation(cm, done); + cm.state.selectingText = up; + on(display.wrapper.ownerDocument, "mousemove", move); + on(display.wrapper.ownerDocument, "mouseup", up); + } + + // Used when mouse-selecting to adjust the anchor to the proper side + // of a bidi jump depending on the visual position of the head. + function bidiSimplify(cm, range) { + var anchor = range.anchor; + var head = range.head; + var anchorLine = getLine(cm.doc, anchor.line); + if (cmp(anchor, head) == 0 && anchor.sticky == head.sticky) { return range } + var order = getOrder(anchorLine); + if (!order) { return range } + var index = getBidiPartAt(order, anchor.ch, anchor.sticky), part = order[index]; + if (part.from != anchor.ch && part.to != anchor.ch) { return range } + var boundary = index + ((part.from == anchor.ch) == (part.level != 1) ? 0 : 1); + if (boundary == 0 || boundary == order.length) { return range } + + // Compute the relative visual position of the head compared to the + // anchor (<0 is to the left, >0 to the right) + var leftSide; + if (head.line != anchor.line) { + leftSide = (head.line - anchor.line) * (cm.doc.direction == "ltr" ? 1 : -1) > 0; + } else { + var headIndex = getBidiPartAt(order, head.ch, head.sticky); + var dir = headIndex - index || (head.ch - anchor.ch) * (part.level == 1 ? -1 : 1); + if (headIndex == boundary - 1 || headIndex == boundary) + { leftSide = dir < 0; } + else + { leftSide = dir > 0; } + } + + var usePart = order[boundary + (leftSide ? -1 : 0)]; + var from = leftSide == (usePart.level == 1); + var ch = from ? usePart.from : usePart.to, sticky = from ? "after" : "before"; + return anchor.ch == ch && anchor.sticky == sticky ? range : new Range(new Pos(anchor.line, ch, sticky), head) + } + + + // Determines whether an event happened in the gutter, and fires the + // handlers for the corresponding event. + function gutterEvent(cm, e, type, prevent) { + var mX, mY; + if (e.touches) { + mX = e.touches[0].clientX; + mY = e.touches[0].clientY; + } else { + try { mX = e.clientX; mY = e.clientY; } + catch(e$1) { return false } + } + if (mX >= Math.floor(cm.display.gutters.getBoundingClientRect().right)) { return false } + if (prevent) { e_preventDefault(e); } + + var display = cm.display; + var lineBox = display.lineDiv.getBoundingClientRect(); + + if (mY > lineBox.bottom || !hasHandler(cm, type)) { return e_defaultPrevented(e) } + mY -= lineBox.top - display.viewOffset; + + for (var i = 0; i < cm.display.gutterSpecs.length; ++i) { + var g = display.gutters.childNodes[i]; + if (g && g.getBoundingClientRect().right >= mX) { + var line = lineAtHeight(cm.doc, mY); + var gutter = cm.display.gutterSpecs[i]; + signal(cm, type, cm, line, gutter.className, e); + return e_defaultPrevented(e) + } + } + } + + function clickInGutter(cm, e) { + return gutterEvent(cm, e, "gutterClick", true) + } + + // CONTEXT MENU HANDLING + + // To make the context menu work, we need to briefly unhide the + // textarea (making it as unobtrusive as possible) to let the + // right-click take effect on it. + function onContextMenu(cm, e) { + if (eventInWidget(cm.display, e) || contextMenuInGutter(cm, e)) { return } + if (signalDOMEvent(cm, e, "contextmenu")) { return } + if (!captureRightClick) { cm.display.input.onContextMenu(e); } + } + + function contextMenuInGutter(cm, e) { + if (!hasHandler(cm, "gutterContextMenu")) { return false } + return gutterEvent(cm, e, "gutterContextMenu", false) + } + + function themeChanged(cm) { + cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-s-\S+/g, "") + + cm.options.theme.replace(/(^|\s)\s*/g, " cm-s-"); + clearCaches(cm); + } + + var Init = {toString: function(){return "CodeMirror.Init"}}; + + var defaults = {}; + var optionHandlers = {}; + + function defineOptions(CodeMirror) { + var optionHandlers = CodeMirror.optionHandlers; + + function option(name, deflt, handle, notOnInit) { + CodeMirror.defaults[name] = deflt; + if (handle) { optionHandlers[name] = + notOnInit ? function (cm, val, old) {if (old != Init) { handle(cm, val, old); }} : handle; } + } + + CodeMirror.defineOption = option; + + // Passed to option handlers when there is no old value. + CodeMirror.Init = Init; + + // These two are, on init, called from the constructor because they + // have to be initialized before the editor can start at all. + option("value", "", function (cm, val) { return cm.setValue(val); }, true); + option("mode", null, function (cm, val) { + cm.doc.modeOption = val; + loadMode(cm); + }, true); + + option("indentUnit", 2, loadMode, true); + option("indentWithTabs", false); + option("smartIndent", true); + option("tabSize", 4, function (cm) { + resetModeState(cm); + clearCaches(cm); + regChange(cm); + }, true); + + option("lineSeparator", null, function (cm, val) { + cm.doc.lineSep = val; + if (!val) { return } + var newBreaks = [], lineNo = cm.doc.first; + cm.doc.iter(function (line) { + for (var pos = 0;;) { + var found = line.text.indexOf(val, pos); + if (found == -1) { break } + pos = found + val.length; + newBreaks.push(Pos(lineNo, found)); + } + lineNo++; + }); + for (var i = newBreaks.length - 1; i >= 0; i--) + { replaceRange(cm.doc, val, newBreaks[i], Pos(newBreaks[i].line, newBreaks[i].ch + val.length)); } + }); + option("specialChars", /[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u200b-\u200c\u200e\u200f\u2028\u2029\ufeff\ufff9-\ufffc]/g, function (cm, val, old) { + cm.state.specialChars = new RegExp(val.source + (val.test("\t") ? "" : "|\t"), "g"); + if (old != Init) { cm.refresh(); } + }); + option("specialCharPlaceholder", defaultSpecialCharPlaceholder, function (cm) { return cm.refresh(); }, true); + option("electricChars", true); + option("inputStyle", mobile ? "contenteditable" : "textarea", function () { + throw new Error("inputStyle can not (yet) be changed in a running editor") // FIXME + }, true); + option("spellcheck", false, function (cm, val) { return cm.getInputField().spellcheck = val; }, true); + option("autocorrect", false, function (cm, val) { return cm.getInputField().autocorrect = val; }, true); + option("autocapitalize", false, function (cm, val) { return cm.getInputField().autocapitalize = val; }, true); + option("rtlMoveVisually", !windows); + option("wholeLineUpdateBefore", true); + + option("theme", "default", function (cm) { + themeChanged(cm); + updateGutters(cm); + }, true); + option("keyMap", "default", function (cm, val, old) { + var next = getKeyMap(val); + var prev = old != Init && getKeyMap(old); + if (prev && prev.detach) { prev.detach(cm, next); } + if (next.attach) { next.attach(cm, prev || null); } + }); + option("extraKeys", null); + option("configureMouse", null); + + option("lineWrapping", false, wrappingChanged, true); + option("gutters", [], function (cm, val) { + cm.display.gutterSpecs = getGutters(val, cm.options.lineNumbers); + updateGutters(cm); + }, true); + option("fixedGutter", true, function (cm, val) { + cm.display.gutters.style.left = val ? compensateForHScroll(cm.display) + "px" : "0"; + cm.refresh(); + }, true); + option("coverGutterNextToScrollbar", false, function (cm) { return updateScrollbars(cm); }, true); + option("scrollbarStyle", "native", function (cm) { + initScrollbars(cm); + updateScrollbars(cm); + cm.display.scrollbars.setScrollTop(cm.doc.scrollTop); + cm.display.scrollbars.setScrollLeft(cm.doc.scrollLeft); + }, true); + option("lineNumbers", false, function (cm, val) { + cm.display.gutterSpecs = getGutters(cm.options.gutters, val); + updateGutters(cm); + }, true); + option("firstLineNumber", 1, updateGutters, true); + option("lineNumberFormatter", function (integer) { return integer; }, updateGutters, true); + option("showCursorWhenSelecting", false, updateSelection, true); + + option("resetSelectionOnContextMenu", true); + option("lineWiseCopyCut", true); + option("pasteLinesPerSelection", true); + option("selectionsMayTouch", false); + + option("readOnly", false, function (cm, val) { + if (val == "nocursor") { + onBlur(cm); + cm.display.input.blur(); + } + cm.display.input.readOnlyChanged(val); + }); + + option("screenReaderLabel", null, function (cm, val) { + val = (val === '') ? null : val; + cm.display.input.screenReaderLabelChanged(val); + }); + + option("disableInput", false, function (cm, val) {if (!val) { cm.display.input.reset(); }}, true); + option("dragDrop", true, dragDropChanged); + option("allowDropFileTypes", null); + + option("cursorBlinkRate", 530); + option("cursorScrollMargin", 0); + option("cursorHeight", 1, updateSelection, true); + option("singleCursorHeightPerLine", true, updateSelection, true); + option("workTime", 100); + option("workDelay", 100); + option("flattenSpans", true, resetModeState, true); + option("addModeClass", false, resetModeState, true); + option("pollInterval", 100); + option("undoDepth", 200, function (cm, val) { return cm.doc.history.undoDepth = val; }); + option("historyEventDelay", 1250); + option("viewportMargin", 10, function (cm) { return cm.refresh(); }, true); + option("maxHighlightLength", 10000, resetModeState, true); + option("moveInputWithCursor", true, function (cm, val) { + if (!val) { cm.display.input.resetPosition(); } + }); + + option("tabindex", null, function (cm, val) { return cm.display.input.getField().tabIndex = val || ""; }); + option("autofocus", null); + option("direction", "ltr", function (cm, val) { return cm.doc.setDirection(val); }, true); + option("phrases", null); + } + + function dragDropChanged(cm, value, old) { + var wasOn = old && old != Init; + if (!value != !wasOn) { + var funcs = cm.display.dragFunctions; + var toggle = value ? on : off; + toggle(cm.display.scroller, "dragstart", funcs.start); + toggle(cm.display.scroller, "dragenter", funcs.enter); + toggle(cm.display.scroller, "dragover", funcs.over); + toggle(cm.display.scroller, "dragleave", funcs.leave); + toggle(cm.display.scroller, "drop", funcs.drop); + } + } + + function wrappingChanged(cm) { + if (cm.options.lineWrapping) { + addClass(cm.display.wrapper, "CodeMirror-wrap"); + cm.display.sizer.style.minWidth = ""; + cm.display.sizerWidth = null; + } else { + rmClass(cm.display.wrapper, "CodeMirror-wrap"); + findMaxLine(cm); + } + estimateLineHeights(cm); + regChange(cm); + clearCaches(cm); + setTimeout(function () { return updateScrollbars(cm); }, 100); + } + + // A CodeMirror instance represents an editor. This is the object + // that user code is usually dealing with. + + function CodeMirror(place, options) { + var this$1 = this; + + if (!(this instanceof CodeMirror)) { return new CodeMirror(place, options) } + + this.options = options = options ? copyObj(options) : {}; + // Determine effective options based on given values and defaults. + copyObj(defaults, options, false); + + var doc = options.value; + if (typeof doc == "string") { doc = new Doc(doc, options.mode, null, options.lineSeparator, options.direction); } + else if (options.mode) { doc.modeOption = options.mode; } + this.doc = doc; + + var input = new CodeMirror.inputStyles[options.inputStyle](this); + var display = this.display = new Display(place, doc, input, options); + display.wrapper.CodeMirror = this; + themeChanged(this); + if (options.lineWrapping) + { this.display.wrapper.className += " CodeMirror-wrap"; } + initScrollbars(this); + + this.state = { + keyMaps: [], // stores maps added by addKeyMap + overlays: [], // highlighting overlays, as added by addOverlay + modeGen: 0, // bumped when mode/overlay changes, used to invalidate highlighting info + overwrite: false, + delayingBlurEvent: false, + focused: false, + suppressEdits: false, // used to disable editing during key handlers when in readOnly mode + pasteIncoming: -1, cutIncoming: -1, // help recognize paste/cut edits in input.poll + selectingText: false, + draggingText: false, + highlight: new Delayed(), // stores highlight worker timeout + keySeq: null, // Unfinished key sequence + specialChars: null + }; + + if (options.autofocus && !mobile) { display.input.focus(); } + + // Override magic textarea content restore that IE sometimes does + // on our hidden textarea on reload + if (ie && ie_version < 11) { setTimeout(function () { return this$1.display.input.reset(true); }, 20); } + + registerEventHandlers(this); + ensureGlobalHandlers(); + + startOperation(this); + this.curOp.forceUpdate = true; + attachDoc(this, doc); + + if ((options.autofocus && !mobile) || this.hasFocus()) + { setTimeout(function () { + if (this$1.hasFocus() && !this$1.state.focused) { onFocus(this$1); } + }, 20); } + else + { onBlur(this); } + + for (var opt in optionHandlers) { if (optionHandlers.hasOwnProperty(opt)) + { optionHandlers[opt](this, options[opt], Init); } } + maybeUpdateLineNumberWidth(this); + if (options.finishInit) { options.finishInit(this); } + for (var i = 0; i < initHooks.length; ++i) { initHooks[i](this); } + endOperation(this); + // Suppress optimizelegibility in Webkit, since it breaks text + // measuring on line wrapping boundaries. + if (webkit && options.lineWrapping && + getComputedStyle(display.lineDiv).textRendering == "optimizelegibility") + { display.lineDiv.style.textRendering = "auto"; } + } + + // The default configuration options. + CodeMirror.defaults = defaults; + // Functions to run when options are changed. + CodeMirror.optionHandlers = optionHandlers; + + // Attach the necessary event handlers when initializing the editor + function registerEventHandlers(cm) { + var d = cm.display; + on(d.scroller, "mousedown", operation(cm, onMouseDown)); + // Older IE's will not fire a second mousedown for a double click + if (ie && ie_version < 11) + { on(d.scroller, "dblclick", operation(cm, function (e) { + if (signalDOMEvent(cm, e)) { return } + var pos = posFromMouse(cm, e); + if (!pos || clickInGutter(cm, e) || eventInWidget(cm.display, e)) { return } + e_preventDefault(e); + var word = cm.findWordAt(pos); + extendSelection(cm.doc, word.anchor, word.head); + })); } + else + { on(d.scroller, "dblclick", function (e) { return signalDOMEvent(cm, e) || e_preventDefault(e); }); } + // Some browsers fire contextmenu *after* opening the menu, at + // which point we can't mess with it anymore. Context menu is + // handled in onMouseDown for these browsers. + on(d.scroller, "contextmenu", function (e) { return onContextMenu(cm, e); }); + on(d.input.getField(), "contextmenu", function (e) { + if (!d.scroller.contains(e.target)) { onContextMenu(cm, e); } + }); + + // Used to suppress mouse event handling when a touch happens + var touchFinished, prevTouch = {end: 0}; + function finishTouch() { + if (d.activeTouch) { + touchFinished = setTimeout(function () { return d.activeTouch = null; }, 1000); + prevTouch = d.activeTouch; + prevTouch.end = +new Date; + } + } + function isMouseLikeTouchEvent(e) { + if (e.touches.length != 1) { return false } + var touch = e.touches[0]; + return touch.radiusX <= 1 && touch.radiusY <= 1 + } + function farAway(touch, other) { + if (other.left == null) { return true } + var dx = other.left - touch.left, dy = other.top - touch.top; + return dx * dx + dy * dy > 20 * 20 + } + on(d.scroller, "touchstart", function (e) { + if (!signalDOMEvent(cm, e) && !isMouseLikeTouchEvent(e) && !clickInGutter(cm, e)) { + d.input.ensurePolled(); + clearTimeout(touchFinished); + var now = +new Date; + d.activeTouch = {start: now, moved: false, + prev: now - prevTouch.end <= 300 ? prevTouch : null}; + if (e.touches.length == 1) { + d.activeTouch.left = e.touches[0].pageX; + d.activeTouch.top = e.touches[0].pageY; + } + } + }); + on(d.scroller, "touchmove", function () { + if (d.activeTouch) { d.activeTouch.moved = true; } + }); + on(d.scroller, "touchend", function (e) { + var touch = d.activeTouch; + if (touch && !eventInWidget(d, e) && touch.left != null && + !touch.moved && new Date - touch.start < 300) { + var pos = cm.coordsChar(d.activeTouch, "page"), range; + if (!touch.prev || farAway(touch, touch.prev)) // Single tap + { range = new Range(pos, pos); } + else if (!touch.prev.prev || farAway(touch, touch.prev.prev)) // Double tap + { range = cm.findWordAt(pos); } + else // Triple tap + { range = new Range(Pos(pos.line, 0), clipPos(cm.doc, Pos(pos.line + 1, 0))); } + cm.setSelection(range.anchor, range.head); + cm.focus(); + e_preventDefault(e); + } + finishTouch(); + }); + on(d.scroller, "touchcancel", finishTouch); + + // Sync scrolling between fake scrollbars and real scrollable + // area, ensure viewport is updated when scrolling. + on(d.scroller, "scroll", function () { + if (d.scroller.clientHeight) { + updateScrollTop(cm, d.scroller.scrollTop); + setScrollLeft(cm, d.scroller.scrollLeft, true); + signal(cm, "scroll", cm); + } + }); + + // Listen to wheel events in order to try and update the viewport on time. + on(d.scroller, "mousewheel", function (e) { return onScrollWheel(cm, e); }); + on(d.scroller, "DOMMouseScroll", function (e) { return onScrollWheel(cm, e); }); + + // Prevent wrapper from ever scrolling + on(d.wrapper, "scroll", function () { return d.wrapper.scrollTop = d.wrapper.scrollLeft = 0; }); + + d.dragFunctions = { + enter: function (e) {if (!signalDOMEvent(cm, e)) { e_stop(e); }}, + over: function (e) {if (!signalDOMEvent(cm, e)) { onDragOver(cm, e); e_stop(e); }}, + start: function (e) { return onDragStart(cm, e); }, + drop: operation(cm, onDrop), + leave: function (e) {if (!signalDOMEvent(cm, e)) { clearDragCursor(cm); }} + }; + + var inp = d.input.getField(); + on(inp, "keyup", function (e) { return onKeyUp.call(cm, e); }); + on(inp, "keydown", operation(cm, onKeyDown)); + on(inp, "keypress", operation(cm, onKeyPress)); + on(inp, "focus", function (e) { return onFocus(cm, e); }); + on(inp, "blur", function (e) { return onBlur(cm, e); }); + } + + var initHooks = []; + CodeMirror.defineInitHook = function (f) { return initHooks.push(f); }; + + // Indent the given line. The how parameter can be "smart", + // "add"/null, "subtract", or "prev". When aggressive is false + // (typically set to true for forced single-line indents), empty + // lines are not indented, and places where the mode returns Pass + // are left alone. + function indentLine(cm, n, how, aggressive) { + var doc = cm.doc, state; + if (how == null) { how = "add"; } + if (how == "smart") { + // Fall back to "prev" when the mode doesn't have an indentation + // method. + if (!doc.mode.indent) { how = "prev"; } + else { state = getContextBefore(cm, n).state; } + } + + var tabSize = cm.options.tabSize; + var line = getLine(doc, n), curSpace = countColumn(line.text, null, tabSize); + if (line.stateAfter) { line.stateAfter = null; } + var curSpaceString = line.text.match(/^\s*/)[0], indentation; + if (!aggressive && !/\S/.test(line.text)) { + indentation = 0; + how = "not"; + } else if (how == "smart") { + indentation = doc.mode.indent(state, line.text.slice(curSpaceString.length), line.text); + if (indentation == Pass || indentation > 150) { + if (!aggressive) { return } + how = "prev"; + } + } + if (how == "prev") { + if (n > doc.first) { indentation = countColumn(getLine(doc, n-1).text, null, tabSize); } + else { indentation = 0; } + } else if (how == "add") { + indentation = curSpace + cm.options.indentUnit; + } else if (how == "subtract") { + indentation = curSpace - cm.options.indentUnit; + } else if (typeof how == "number") { + indentation = curSpace + how; + } + indentation = Math.max(0, indentation); + + var indentString = "", pos = 0; + if (cm.options.indentWithTabs) + { for (var i = Math.floor(indentation / tabSize); i; --i) {pos += tabSize; indentString += "\t";} } + if (pos < indentation) { indentString += spaceStr(indentation - pos); } + + if (indentString != curSpaceString) { + replaceRange(doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input"); + line.stateAfter = null; + return true + } else { + // Ensure that, if the cursor was in the whitespace at the start + // of the line, it is moved to the end of that space. + for (var i$1 = 0; i$1 < doc.sel.ranges.length; i$1++) { + var range = doc.sel.ranges[i$1]; + if (range.head.line == n && range.head.ch < curSpaceString.length) { + var pos$1 = Pos(n, curSpaceString.length); + replaceOneSelection(doc, i$1, new Range(pos$1, pos$1)); + break + } + } + } + } + + // This will be set to a {lineWise: bool, text: [string]} object, so + // that, when pasting, we know what kind of selections the copied + // text was made out of. + var lastCopied = null; + + function setLastCopied(newLastCopied) { + lastCopied = newLastCopied; + } + + function applyTextInput(cm, inserted, deleted, sel, origin) { + var doc = cm.doc; + cm.display.shift = false; + if (!sel) { sel = doc.sel; } + + var recent = +new Date - 200; + var paste = origin == "paste" || cm.state.pasteIncoming > recent; + var textLines = splitLinesAuto(inserted), multiPaste = null; + // When pasting N lines into N selections, insert one line per selection + if (paste && sel.ranges.length > 1) { + if (lastCopied && lastCopied.text.join("\n") == inserted) { + if (sel.ranges.length % lastCopied.text.length == 0) { + multiPaste = []; + for (var i = 0; i < lastCopied.text.length; i++) + { multiPaste.push(doc.splitLines(lastCopied.text[i])); } + } + } else if (textLines.length == sel.ranges.length && cm.options.pasteLinesPerSelection) { + multiPaste = map(textLines, function (l) { return [l]; }); + } + } + + var updateInput = cm.curOp.updateInput; + // Normal behavior is to insert the new text into every selection + for (var i$1 = sel.ranges.length - 1; i$1 >= 0; i$1--) { + var range = sel.ranges[i$1]; + var from = range.from(), to = range.to(); + if (range.empty()) { + if (deleted && deleted > 0) // Handle deletion + { from = Pos(from.line, from.ch - deleted); } + else if (cm.state.overwrite && !paste) // Handle overwrite + { to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + lst(textLines).length)); } + else if (paste && lastCopied && lastCopied.lineWise && lastCopied.text.join("\n") == textLines.join("\n")) + { from = to = Pos(from.line, 0); } + } + var changeEvent = {from: from, to: to, text: multiPaste ? multiPaste[i$1 % multiPaste.length] : textLines, + origin: origin || (paste ? "paste" : cm.state.cutIncoming > recent ? "cut" : "+input")}; + makeChange(cm.doc, changeEvent); + signalLater(cm, "inputRead", cm, changeEvent); + } + if (inserted && !paste) + { triggerElectric(cm, inserted); } + + ensureCursorVisible(cm); + if (cm.curOp.updateInput < 2) { cm.curOp.updateInput = updateInput; } + cm.curOp.typing = true; + cm.state.pasteIncoming = cm.state.cutIncoming = -1; + } + + function handlePaste(e, cm) { + var pasted = e.clipboardData && e.clipboardData.getData("Text"); + if (pasted) { + e.preventDefault(); + if (!cm.isReadOnly() && !cm.options.disableInput) + { runInOp(cm, function () { return applyTextInput(cm, pasted, 0, null, "paste"); }); } + return true + } + } + + function triggerElectric(cm, inserted) { + // When an 'electric' character is inserted, immediately trigger a reindent + if (!cm.options.electricChars || !cm.options.smartIndent) { return } + var sel = cm.doc.sel; + + for (var i = sel.ranges.length - 1; i >= 0; i--) { + var range = sel.ranges[i]; + if (range.head.ch > 100 || (i && sel.ranges[i - 1].head.line == range.head.line)) { continue } + var mode = cm.getModeAt(range.head); + var indented = false; + if (mode.electricChars) { + for (var j = 0; j < mode.electricChars.length; j++) + { if (inserted.indexOf(mode.electricChars.charAt(j)) > -1) { + indented = indentLine(cm, range.head.line, "smart"); + break + } } + } else if (mode.electricInput) { + if (mode.electricInput.test(getLine(cm.doc, range.head.line).text.slice(0, range.head.ch))) + { indented = indentLine(cm, range.head.line, "smart"); } + } + if (indented) { signalLater(cm, "electricInput", cm, range.head.line); } + } + } + + function copyableRanges(cm) { + var text = [], ranges = []; + for (var i = 0; i < cm.doc.sel.ranges.length; i++) { + var line = cm.doc.sel.ranges[i].head.line; + var lineRange = {anchor: Pos(line, 0), head: Pos(line + 1, 0)}; + ranges.push(lineRange); + text.push(cm.getRange(lineRange.anchor, lineRange.head)); + } + return {text: text, ranges: ranges} + } + + function disableBrowserMagic(field, spellcheck, autocorrect, autocapitalize) { + field.setAttribute("autocorrect", autocorrect ? "" : "off"); + field.setAttribute("autocapitalize", autocapitalize ? "" : "off"); + field.setAttribute("spellcheck", !!spellcheck); + } + + function hiddenTextarea() { + var te = elt("textarea", null, null, "position: absolute; bottom: -1em; padding: 0; width: 1px; height: 1em; outline: none"); + var div = elt("div", [te], null, "overflow: hidden; position: relative; width: 3px; height: 0px;"); + // The textarea is kept positioned near the cursor to prevent the + // fact that it'll be scrolled into view on input from scrolling + // our fake cursor out of view. On webkit, when wrap=off, paste is + // very slow. So make the area wide instead. + if (webkit) { te.style.width = "1000px"; } + else { te.setAttribute("wrap", "off"); } + // If border: 0; -- iOS fails to open keyboard (issue #1287) + if (ios) { te.style.border = "1px solid black"; } + disableBrowserMagic(te); + return div + } + + // The publicly visible API. Note that methodOp(f) means + // 'wrap f in an operation, performed on its `this` parameter'. + + // This is not the complete set of editor methods. Most of the + // methods defined on the Doc type are also injected into + // CodeMirror.prototype, for backwards compatibility and + // convenience. + + function addEditorMethods(CodeMirror) { + var optionHandlers = CodeMirror.optionHandlers; + + var helpers = CodeMirror.helpers = {}; + + CodeMirror.prototype = { + constructor: CodeMirror, + focus: function(){window.focus(); this.display.input.focus();}, + + setOption: function(option, value) { + var options = this.options, old = options[option]; + if (options[option] == value && option != "mode") { return } + options[option] = value; + if (optionHandlers.hasOwnProperty(option)) + { operation(this, optionHandlers[option])(this, value, old); } + signal(this, "optionChange", this, option); + }, + + getOption: function(option) {return this.options[option]}, + getDoc: function() {return this.doc}, + + addKeyMap: function(map, bottom) { + this.state.keyMaps[bottom ? "push" : "unshift"](getKeyMap(map)); + }, + removeKeyMap: function(map) { + var maps = this.state.keyMaps; + for (var i = 0; i < maps.length; ++i) + { if (maps[i] == map || maps[i].name == map) { + maps.splice(i, 1); + return true + } } + }, + + addOverlay: methodOp(function(spec, options) { + var mode = spec.token ? spec : CodeMirror.getMode(this.options, spec); + if (mode.startState) { throw new Error("Overlays may not be stateful.") } + insertSorted(this.state.overlays, + {mode: mode, modeSpec: spec, opaque: options && options.opaque, + priority: (options && options.priority) || 0}, + function (overlay) { return overlay.priority; }); + this.state.modeGen++; + regChange(this); + }), + removeOverlay: methodOp(function(spec) { + var overlays = this.state.overlays; + for (var i = 0; i < overlays.length; ++i) { + var cur = overlays[i].modeSpec; + if (cur == spec || typeof spec == "string" && cur.name == spec) { + overlays.splice(i, 1); + this.state.modeGen++; + regChange(this); + return + } + } + }), + + indentLine: methodOp(function(n, dir, aggressive) { + if (typeof dir != "string" && typeof dir != "number") { + if (dir == null) { dir = this.options.smartIndent ? "smart" : "prev"; } + else { dir = dir ? "add" : "subtract"; } + } + if (isLine(this.doc, n)) { indentLine(this, n, dir, aggressive); } + }), + indentSelection: methodOp(function(how) { + var ranges = this.doc.sel.ranges, end = -1; + for (var i = 0; i < ranges.length; i++) { + var range = ranges[i]; + if (!range.empty()) { + var from = range.from(), to = range.to(); + var start = Math.max(end, from.line); + end = Math.min(this.lastLine(), to.line - (to.ch ? 0 : 1)) + 1; + for (var j = start; j < end; ++j) + { indentLine(this, j, how); } + var newRanges = this.doc.sel.ranges; + if (from.ch == 0 && ranges.length == newRanges.length && newRanges[i].from().ch > 0) + { replaceOneSelection(this.doc, i, new Range(from, newRanges[i].to()), sel_dontScroll); } + } else if (range.head.line > end) { + indentLine(this, range.head.line, how, true); + end = range.head.line; + if (i == this.doc.sel.primIndex) { ensureCursorVisible(this); } + } + } + }), + + // Fetch the parser token for a given character. Useful for hacks + // that want to inspect the mode state (say, for completion). + getTokenAt: function(pos, precise) { + return takeToken(this, pos, precise) + }, + + getLineTokens: function(line, precise) { + return takeToken(this, Pos(line), precise, true) + }, + + getTokenTypeAt: function(pos) { + pos = clipPos(this.doc, pos); + var styles = getLineStyles(this, getLine(this.doc, pos.line)); + var before = 0, after = (styles.length - 1) / 2, ch = pos.ch; + var type; + if (ch == 0) { type = styles[2]; } + else { for (;;) { + var mid = (before + after) >> 1; + if ((mid ? styles[mid * 2 - 1] : 0) >= ch) { after = mid; } + else if (styles[mid * 2 + 1] < ch) { before = mid + 1; } + else { type = styles[mid * 2 + 2]; break } + } } + var cut = type ? type.indexOf("overlay ") : -1; + return cut < 0 ? type : cut == 0 ? null : type.slice(0, cut - 1) + }, + + getModeAt: function(pos) { + var mode = this.doc.mode; + if (!mode.innerMode) { return mode } + return CodeMirror.innerMode(mode, this.getTokenAt(pos).state).mode + }, + + getHelper: function(pos, type) { + return this.getHelpers(pos, type)[0] + }, + + getHelpers: function(pos, type) { + var found = []; + if (!helpers.hasOwnProperty(type)) { return found } + var help = helpers[type], mode = this.getModeAt(pos); + if (typeof mode[type] == "string") { + if (help[mode[type]]) { found.push(help[mode[type]]); } + } else if (mode[type]) { + for (var i = 0; i < mode[type].length; i++) { + var val = help[mode[type][i]]; + if (val) { found.push(val); } + } + } else if (mode.helperType && help[mode.helperType]) { + found.push(help[mode.helperType]); + } else if (help[mode.name]) { + found.push(help[mode.name]); + } + for (var i$1 = 0; i$1 < help._global.length; i$1++) { + var cur = help._global[i$1]; + if (cur.pred(mode, this) && indexOf(found, cur.val) == -1) + { found.push(cur.val); } + } + return found + }, + + getStateAfter: function(line, precise) { + var doc = this.doc; + line = clipLine(doc, line == null ? doc.first + doc.size - 1: line); + return getContextBefore(this, line + 1, precise).state + }, + + cursorCoords: function(start, mode) { + var pos, range = this.doc.sel.primary(); + if (start == null) { pos = range.head; } + else if (typeof start == "object") { pos = clipPos(this.doc, start); } + else { pos = start ? range.from() : range.to(); } + return cursorCoords(this, pos, mode || "page") + }, + + charCoords: function(pos, mode) { + return charCoords(this, clipPos(this.doc, pos), mode || "page") + }, + + coordsChar: function(coords, mode) { + coords = fromCoordSystem(this, coords, mode || "page"); + return coordsChar(this, coords.left, coords.top) + }, + + lineAtHeight: function(height, mode) { + height = fromCoordSystem(this, {top: height, left: 0}, mode || "page").top; + return lineAtHeight(this.doc, height + this.display.viewOffset) + }, + heightAtLine: function(line, mode, includeWidgets) { + var end = false, lineObj; + if (typeof line == "number") { + var last = this.doc.first + this.doc.size - 1; + if (line < this.doc.first) { line = this.doc.first; } + else if (line > last) { line = last; end = true; } + lineObj = getLine(this.doc, line); + } else { + lineObj = line; + } + return intoCoordSystem(this, lineObj, {top: 0, left: 0}, mode || "page", includeWidgets || end).top + + (end ? this.doc.height - heightAtLine(lineObj) : 0) + }, + + defaultTextHeight: function() { return textHeight(this.display) }, + defaultCharWidth: function() { return charWidth(this.display) }, + + getViewport: function() { return {from: this.display.viewFrom, to: this.display.viewTo}}, + + addWidget: function(pos, node, scroll, vert, horiz) { + var display = this.display; + pos = cursorCoords(this, clipPos(this.doc, pos)); + var top = pos.bottom, left = pos.left; + node.style.position = "absolute"; + node.setAttribute("cm-ignore-events", "true"); + this.display.input.setUneditable(node); + display.sizer.appendChild(node); + if (vert == "over") { + top = pos.top; + } else if (vert == "above" || vert == "near") { + var vspace = Math.max(display.wrapper.clientHeight, this.doc.height), + hspace = Math.max(display.sizer.clientWidth, display.lineSpace.clientWidth); + // Default to positioning above (if specified and possible); otherwise default to positioning below + if ((vert == 'above' || pos.bottom + node.offsetHeight > vspace) && pos.top > node.offsetHeight) + { top = pos.top - node.offsetHeight; } + else if (pos.bottom + node.offsetHeight <= vspace) + { top = pos.bottom; } + if (left + node.offsetWidth > hspace) + { left = hspace - node.offsetWidth; } + } + node.style.top = top + "px"; + node.style.left = node.style.right = ""; + if (horiz == "right") { + left = display.sizer.clientWidth - node.offsetWidth; + node.style.right = "0px"; + } else { + if (horiz == "left") { left = 0; } + else if (horiz == "middle") { left = (display.sizer.clientWidth - node.offsetWidth) / 2; } + node.style.left = left + "px"; + } + if (scroll) + { scrollIntoView(this, {left: left, top: top, right: left + node.offsetWidth, bottom: top + node.offsetHeight}); } + }, + + triggerOnKeyDown: methodOp(onKeyDown), + triggerOnKeyPress: methodOp(onKeyPress), + triggerOnKeyUp: onKeyUp, + triggerOnMouseDown: methodOp(onMouseDown), + + execCommand: function(cmd) { + if (commands.hasOwnProperty(cmd)) + { return commands[cmd].call(null, this) } + }, + + triggerElectric: methodOp(function(text) { triggerElectric(this, text); }), + + findPosH: function(from, amount, unit, visually) { + var dir = 1; + if (amount < 0) { dir = -1; amount = -amount; } + var cur = clipPos(this.doc, from); + for (var i = 0; i < amount; ++i) { + cur = findPosH(this.doc, cur, dir, unit, visually); + if (cur.hitSide) { break } + } + return cur + }, + + moveH: methodOp(function(dir, unit) { + var this$1 = this; + + this.extendSelectionsBy(function (range) { + if (this$1.display.shift || this$1.doc.extend || range.empty()) + { return findPosH(this$1.doc, range.head, dir, unit, this$1.options.rtlMoveVisually) } + else + { return dir < 0 ? range.from() : range.to() } + }, sel_move); + }), + + deleteH: methodOp(function(dir, unit) { + var sel = this.doc.sel, doc = this.doc; + if (sel.somethingSelected()) + { doc.replaceSelection("", null, "+delete"); } + else + { deleteNearSelection(this, function (range) { + var other = findPosH(doc, range.head, dir, unit, false); + return dir < 0 ? {from: other, to: range.head} : {from: range.head, to: other} + }); } + }), + + findPosV: function(from, amount, unit, goalColumn) { + var dir = 1, x = goalColumn; + if (amount < 0) { dir = -1; amount = -amount; } + var cur = clipPos(this.doc, from); + for (var i = 0; i < amount; ++i) { + var coords = cursorCoords(this, cur, "div"); + if (x == null) { x = coords.left; } + else { coords.left = x; } + cur = findPosV(this, coords, dir, unit); + if (cur.hitSide) { break } + } + return cur + }, + + moveV: methodOp(function(dir, unit) { + var this$1 = this; + + var doc = this.doc, goals = []; + var collapse = !this.display.shift && !doc.extend && doc.sel.somethingSelected(); + doc.extendSelectionsBy(function (range) { + if (collapse) + { return dir < 0 ? range.from() : range.to() } + var headPos = cursorCoords(this$1, range.head, "div"); + if (range.goalColumn != null) { headPos.left = range.goalColumn; } + goals.push(headPos.left); + var pos = findPosV(this$1, headPos, dir, unit); + if (unit == "page" && range == doc.sel.primary()) + { addToScrollTop(this$1, charCoords(this$1, pos, "div").top - headPos.top); } + return pos + }, sel_move); + if (goals.length) { for (var i = 0; i < doc.sel.ranges.length; i++) + { doc.sel.ranges[i].goalColumn = goals[i]; } } + }), + + // Find the word at the given position (as returned by coordsChar). + findWordAt: function(pos) { + var doc = this.doc, line = getLine(doc, pos.line).text; + var start = pos.ch, end = pos.ch; + if (line) { + var helper = this.getHelper(pos, "wordChars"); + if ((pos.sticky == "before" || end == line.length) && start) { --start; } else { ++end; } + var startChar = line.charAt(start); + var check = isWordChar(startChar, helper) + ? function (ch) { return isWordChar(ch, helper); } + : /\s/.test(startChar) ? function (ch) { return /\s/.test(ch); } + : function (ch) { return (!/\s/.test(ch) && !isWordChar(ch)); }; + while (start > 0 && check(line.charAt(start - 1))) { --start; } + while (end < line.length && check(line.charAt(end))) { ++end; } + } + return new Range(Pos(pos.line, start), Pos(pos.line, end)) + }, + + toggleOverwrite: function(value) { + if (value != null && value == this.state.overwrite) { return } + if (this.state.overwrite = !this.state.overwrite) + { addClass(this.display.cursorDiv, "CodeMirror-overwrite"); } + else + { rmClass(this.display.cursorDiv, "CodeMirror-overwrite"); } + + signal(this, "overwriteToggle", this, this.state.overwrite); + }, + hasFocus: function() { return this.display.input.getField() == activeElt() }, + isReadOnly: function() { return !!(this.options.readOnly || this.doc.cantEdit) }, + + scrollTo: methodOp(function (x, y) { scrollToCoords(this, x, y); }), + getScrollInfo: function() { + var scroller = this.display.scroller; + return {left: scroller.scrollLeft, top: scroller.scrollTop, + height: scroller.scrollHeight - scrollGap(this) - this.display.barHeight, + width: scroller.scrollWidth - scrollGap(this) - this.display.barWidth, + clientHeight: displayHeight(this), clientWidth: displayWidth(this)} + }, + + scrollIntoView: methodOp(function(range, margin) { + if (range == null) { + range = {from: this.doc.sel.primary().head, to: null}; + if (margin == null) { margin = this.options.cursorScrollMargin; } + } else if (typeof range == "number") { + range = {from: Pos(range, 0), to: null}; + } else if (range.from == null) { + range = {from: range, to: null}; + } + if (!range.to) { range.to = range.from; } + range.margin = margin || 0; + + if (range.from.line != null) { + scrollToRange(this, range); + } else { + scrollToCoordsRange(this, range.from, range.to, range.margin); + } + }), + + setSize: methodOp(function(width, height) { + var this$1 = this; + + var interpret = function (val) { return typeof val == "number" || /^\d+$/.test(String(val)) ? val + "px" : val; }; + if (width != null) { this.display.wrapper.style.width = interpret(width); } + if (height != null) { this.display.wrapper.style.height = interpret(height); } + if (this.options.lineWrapping) { clearLineMeasurementCache(this); } + var lineNo = this.display.viewFrom; + this.doc.iter(lineNo, this.display.viewTo, function (line) { + if (line.widgets) { for (var i = 0; i < line.widgets.length; i++) + { if (line.widgets[i].noHScroll) { regLineChange(this$1, lineNo, "widget"); break } } } + ++lineNo; + }); + this.curOp.forceUpdate = true; + signal(this, "refresh", this); + }), + + operation: function(f){return runInOp(this, f)}, + startOperation: function(){return startOperation(this)}, + endOperation: function(){return endOperation(this)}, + + refresh: methodOp(function() { + var oldHeight = this.display.cachedTextHeight; + regChange(this); + this.curOp.forceUpdate = true; + clearCaches(this); + scrollToCoords(this, this.doc.scrollLeft, this.doc.scrollTop); + updateGutterSpace(this.display); + if (oldHeight == null || Math.abs(oldHeight - textHeight(this.display)) > .5 || this.options.lineWrapping) + { estimateLineHeights(this); } + signal(this, "refresh", this); + }), + + swapDoc: methodOp(function(doc) { + var old = this.doc; + old.cm = null; + // Cancel the current text selection if any (#5821) + if (this.state.selectingText) { this.state.selectingText(); } + attachDoc(this, doc); + clearCaches(this); + this.display.input.reset(); + scrollToCoords(this, doc.scrollLeft, doc.scrollTop); + this.curOp.forceScroll = true; + signalLater(this, "swapDoc", this, old); + return old + }), + + phrase: function(phraseText) { + var phrases = this.options.phrases; + return phrases && Object.prototype.hasOwnProperty.call(phrases, phraseText) ? phrases[phraseText] : phraseText + }, + + getInputField: function(){return this.display.input.getField()}, + getWrapperElement: function(){return this.display.wrapper}, + getScrollerElement: function(){return this.display.scroller}, + getGutterElement: function(){return this.display.gutters} + }; + eventMixin(CodeMirror); + + CodeMirror.registerHelper = function(type, name, value) { + if (!helpers.hasOwnProperty(type)) { helpers[type] = CodeMirror[type] = {_global: []}; } + helpers[type][name] = value; + }; + CodeMirror.registerGlobalHelper = function(type, name, predicate, value) { + CodeMirror.registerHelper(type, name, value); + helpers[type]._global.push({pred: predicate, val: value}); + }; + } + + // Used for horizontal relative motion. Dir is -1 or 1 (left or + // right), unit can be "codepoint", "char", "column" (like char, but + // doesn't cross line boundaries), "word" (across next word), or + // "group" (to the start of next group of word or + // non-word-non-whitespace chars). The visually param controls + // whether, in right-to-left text, direction 1 means to move towards + // the next index in the string, or towards the character to the right + // of the current position. The resulting position will have a + // hitSide=true property if it reached the end of the document. + function findPosH(doc, pos, dir, unit, visually) { + var oldPos = pos; + var origDir = dir; + var lineObj = getLine(doc, pos.line); + var lineDir = visually && doc.direction == "rtl" ? -dir : dir; + function findNextLine() { + var l = pos.line + lineDir; + if (l < doc.first || l >= doc.first + doc.size) { return false } + pos = new Pos(l, pos.ch, pos.sticky); + return lineObj = getLine(doc, l) + } + function moveOnce(boundToLine) { + var next; + if (unit == "codepoint") { + var ch = lineObj.text.charCodeAt(pos.ch + (unit > 0 ? 0 : -1)); + if (isNaN(ch)) { next = null; } + else { next = new Pos(pos.line, Math.max(0, Math.min(lineObj.text.length, pos.ch + dir * (ch >= 0xD800 && ch < 0xDC00 ? 2 : 1))), + -dir); } + } else if (visually) { + next = moveVisually(doc.cm, lineObj, pos, dir); + } else { + next = moveLogically(lineObj, pos, dir); + } + if (next == null) { + if (!boundToLine && findNextLine()) + { pos = endOfLine(visually, doc.cm, lineObj, pos.line, lineDir); } + else + { return false } + } else { + pos = next; + } + return true + } + + if (unit == "char" || unit == "codepoint") { + moveOnce(); + } else if (unit == "column") { + moveOnce(true); + } else if (unit == "word" || unit == "group") { + var sawType = null, group = unit == "group"; + var helper = doc.cm && doc.cm.getHelper(pos, "wordChars"); + for (var first = true;; first = false) { + if (dir < 0 && !moveOnce(!first)) { break } + var cur = lineObj.text.charAt(pos.ch) || "\n"; + var type = isWordChar(cur, helper) ? "w" + : group && cur == "\n" ? "n" + : !group || /\s/.test(cur) ? null + : "p"; + if (group && !first && !type) { type = "s"; } + if (sawType && sawType != type) { + if (dir < 0) {dir = 1; moveOnce(); pos.sticky = "after";} + break + } + + if (type) { sawType = type; } + if (dir > 0 && !moveOnce(!first)) { break } + } + } + var result = skipAtomic(doc, pos, oldPos, origDir, true); + if (equalCursorPos(oldPos, result)) { result.hitSide = true; } + return result + } + + // For relative vertical movement. Dir may be -1 or 1. Unit can be + // "page" or "line". The resulting position will have a hitSide=true + // property if it reached the end of the document. + function findPosV(cm, pos, dir, unit) { + var doc = cm.doc, x = pos.left, y; + if (unit == "page") { + var pageSize = Math.min(cm.display.wrapper.clientHeight, window.innerHeight || document.documentElement.clientHeight); + var moveAmount = Math.max(pageSize - .5 * textHeight(cm.display), 3); + y = (dir > 0 ? pos.bottom : pos.top) + dir * moveAmount; + + } else if (unit == "line") { + y = dir > 0 ? pos.bottom + 3 : pos.top - 3; + } + var target; + for (;;) { + target = coordsChar(cm, x, y); + if (!target.outside) { break } + if (dir < 0 ? y <= 0 : y >= doc.height) { target.hitSide = true; break } + y += dir * 5; + } + return target + } + + // CONTENTEDITABLE INPUT STYLE + + var ContentEditableInput = function(cm) { + this.cm = cm; + this.lastAnchorNode = this.lastAnchorOffset = this.lastFocusNode = this.lastFocusOffset = null; + this.polling = new Delayed(); + this.composing = null; + this.gracePeriod = false; + this.readDOMTimeout = null; + }; + + ContentEditableInput.prototype.init = function (display) { + var this$1 = this; + + var input = this, cm = input.cm; + var div = input.div = display.lineDiv; + disableBrowserMagic(div, cm.options.spellcheck, cm.options.autocorrect, cm.options.autocapitalize); + + function belongsToInput(e) { + for (var t = e.target; t; t = t.parentNode) { + if (t == div) { return true } + if (/\bCodeMirror-(?:line)?widget\b/.test(t.className)) { break } + } + return false + } + + on(div, "paste", function (e) { + if (!belongsToInput(e) || signalDOMEvent(cm, e) || handlePaste(e, cm)) { return } + // IE doesn't fire input events, so we schedule a read for the pasted content in this way + if (ie_version <= 11) { setTimeout(operation(cm, function () { return this$1.updateFromDOM(); }), 20); } + }); + + on(div, "compositionstart", function (e) { + this$1.composing = {data: e.data, done: false}; + }); + on(div, "compositionupdate", function (e) { + if (!this$1.composing) { this$1.composing = {data: e.data, done: false}; } + }); + on(div, "compositionend", function (e) { + if (this$1.composing) { + if (e.data != this$1.composing.data) { this$1.readFromDOMSoon(); } + this$1.composing.done = true; + } + }); + + on(div, "touchstart", function () { return input.forceCompositionEnd(); }); + + on(div, "input", function () { + if (!this$1.composing) { this$1.readFromDOMSoon(); } + }); + + function onCopyCut(e) { + if (!belongsToInput(e) || signalDOMEvent(cm, e)) { return } + if (cm.somethingSelected()) { + setLastCopied({lineWise: false, text: cm.getSelections()}); + if (e.type == "cut") { cm.replaceSelection("", null, "cut"); } + } else if (!cm.options.lineWiseCopyCut) { + return + } else { + var ranges = copyableRanges(cm); + setLastCopied({lineWise: true, text: ranges.text}); + if (e.type == "cut") { + cm.operation(function () { + cm.setSelections(ranges.ranges, 0, sel_dontScroll); + cm.replaceSelection("", null, "cut"); + }); + } + } + if (e.clipboardData) { + e.clipboardData.clearData(); + var content = lastCopied.text.join("\n"); + // iOS exposes the clipboard API, but seems to discard content inserted into it + e.clipboardData.setData("Text", content); + if (e.clipboardData.getData("Text") == content) { + e.preventDefault(); + return + } + } + // Old-fashioned briefly-focus-a-textarea hack + var kludge = hiddenTextarea(), te = kludge.firstChild; + cm.display.lineSpace.insertBefore(kludge, cm.display.lineSpace.firstChild); + te.value = lastCopied.text.join("\n"); + var hadFocus = document.activeElement; + selectInput(te); + setTimeout(function () { + cm.display.lineSpace.removeChild(kludge); + hadFocus.focus(); + if (hadFocus == div) { input.showPrimarySelection(); } + }, 50); + } + on(div, "copy", onCopyCut); + on(div, "cut", onCopyCut); + }; + + ContentEditableInput.prototype.screenReaderLabelChanged = function (label) { + // Label for screenreaders, accessibility + if(label) { + this.div.setAttribute('aria-label', label); + } else { + this.div.removeAttribute('aria-label'); + } + }; + + ContentEditableInput.prototype.prepareSelection = function () { + var result = prepareSelection(this.cm, false); + result.focus = document.activeElement == this.div; + return result + }; + + ContentEditableInput.prototype.showSelection = function (info, takeFocus) { + if (!info || !this.cm.display.view.length) { return } + if (info.focus || takeFocus) { this.showPrimarySelection(); } + this.showMultipleSelections(info); + }; + + ContentEditableInput.prototype.getSelection = function () { + return this.cm.display.wrapper.ownerDocument.getSelection() + }; + + ContentEditableInput.prototype.showPrimarySelection = function () { + var sel = this.getSelection(), cm = this.cm, prim = cm.doc.sel.primary(); + var from = prim.from(), to = prim.to(); + + if (cm.display.viewTo == cm.display.viewFrom || from.line >= cm.display.viewTo || to.line < cm.display.viewFrom) { + sel.removeAllRanges(); + return + } + + var curAnchor = domToPos(cm, sel.anchorNode, sel.anchorOffset); + var curFocus = domToPos(cm, sel.focusNode, sel.focusOffset); + if (curAnchor && !curAnchor.bad && curFocus && !curFocus.bad && + cmp(minPos(curAnchor, curFocus), from) == 0 && + cmp(maxPos(curAnchor, curFocus), to) == 0) + { return } + + var view = cm.display.view; + var start = (from.line >= cm.display.viewFrom && posToDOM(cm, from)) || + {node: view[0].measure.map[2], offset: 0}; + var end = to.line < cm.display.viewTo && posToDOM(cm, to); + if (!end) { + var measure = view[view.length - 1].measure; + var map = measure.maps ? measure.maps[measure.maps.length - 1] : measure.map; + end = {node: map[map.length - 1], offset: map[map.length - 2] - map[map.length - 3]}; + } + + if (!start || !end) { + sel.removeAllRanges(); + return + } + + var old = sel.rangeCount && sel.getRangeAt(0), rng; + try { rng = range(start.node, start.offset, end.offset, end.node); } + catch(e) {} // Our model of the DOM might be outdated, in which case the range we try to set can be impossible + if (rng) { + if (!gecko && cm.state.focused) { + sel.collapse(start.node, start.offset); + if (!rng.collapsed) { + sel.removeAllRanges(); + sel.addRange(rng); + } + } else { + sel.removeAllRanges(); + sel.addRange(rng); + } + if (old && sel.anchorNode == null) { sel.addRange(old); } + else if (gecko) { this.startGracePeriod(); } + } + this.rememberSelection(); + }; + + ContentEditableInput.prototype.startGracePeriod = function () { + var this$1 = this; + + clearTimeout(this.gracePeriod); + this.gracePeriod = setTimeout(function () { + this$1.gracePeriod = false; + if (this$1.selectionChanged()) + { this$1.cm.operation(function () { return this$1.cm.curOp.selectionChanged = true; }); } + }, 20); + }; + + ContentEditableInput.prototype.showMultipleSelections = function (info) { + removeChildrenAndAdd(this.cm.display.cursorDiv, info.cursors); + removeChildrenAndAdd(this.cm.display.selectionDiv, info.selection); + }; + + ContentEditableInput.prototype.rememberSelection = function () { + var sel = this.getSelection(); + this.lastAnchorNode = sel.anchorNode; this.lastAnchorOffset = sel.anchorOffset; + this.lastFocusNode = sel.focusNode; this.lastFocusOffset = sel.focusOffset; + }; + + ContentEditableInput.prototype.selectionInEditor = function () { + var sel = this.getSelection(); + if (!sel.rangeCount) { return false } + var node = sel.getRangeAt(0).commonAncestorContainer; + return contains(this.div, node) + }; + + ContentEditableInput.prototype.focus = function () { + if (this.cm.options.readOnly != "nocursor") { + if (!this.selectionInEditor() || document.activeElement != this.div) + { this.showSelection(this.prepareSelection(), true); } + this.div.focus(); + } + }; + ContentEditableInput.prototype.blur = function () { this.div.blur(); }; + ContentEditableInput.prototype.getField = function () { return this.div }; + + ContentEditableInput.prototype.supportsTouch = function () { return true }; + + ContentEditableInput.prototype.receivedFocus = function () { + var input = this; + if (this.selectionInEditor()) + { this.pollSelection(); } + else + { runInOp(this.cm, function () { return input.cm.curOp.selectionChanged = true; }); } + + function poll() { + if (input.cm.state.focused) { + input.pollSelection(); + input.polling.set(input.cm.options.pollInterval, poll); + } + } + this.polling.set(this.cm.options.pollInterval, poll); + }; + + ContentEditableInput.prototype.selectionChanged = function () { + var sel = this.getSelection(); + return sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset || + sel.focusNode != this.lastFocusNode || sel.focusOffset != this.lastFocusOffset + }; + + ContentEditableInput.prototype.pollSelection = function () { + if (this.readDOMTimeout != null || this.gracePeriod || !this.selectionChanged()) { return } + var sel = this.getSelection(), cm = this.cm; + // On Android Chrome (version 56, at least), backspacing into an + // uneditable block element will put the cursor in that element, + // and then, because it's not editable, hide the virtual keyboard. + // Because Android doesn't allow us to actually detect backspace + // presses in a sane way, this code checks for when that happens + // and simulates a backspace press in this case. + if (android && chrome && this.cm.display.gutterSpecs.length && isInGutter(sel.anchorNode)) { + this.cm.triggerOnKeyDown({type: "keydown", keyCode: 8, preventDefault: Math.abs}); + this.blur(); + this.focus(); + return + } + if (this.composing) { return } + this.rememberSelection(); + var anchor = domToPos(cm, sel.anchorNode, sel.anchorOffset); + var head = domToPos(cm, sel.focusNode, sel.focusOffset); + if (anchor && head) { runInOp(cm, function () { + setSelection(cm.doc, simpleSelection(anchor, head), sel_dontScroll); + if (anchor.bad || head.bad) { cm.curOp.selectionChanged = true; } + }); } + }; + + ContentEditableInput.prototype.pollContent = function () { + if (this.readDOMTimeout != null) { + clearTimeout(this.readDOMTimeout); + this.readDOMTimeout = null; + } + + var cm = this.cm, display = cm.display, sel = cm.doc.sel.primary(); + var from = sel.from(), to = sel.to(); + if (from.ch == 0 && from.line > cm.firstLine()) + { from = Pos(from.line - 1, getLine(cm.doc, from.line - 1).length); } + if (to.ch == getLine(cm.doc, to.line).text.length && to.line < cm.lastLine()) + { to = Pos(to.line + 1, 0); } + if (from.line < display.viewFrom || to.line > display.viewTo - 1) { return false } + + var fromIndex, fromLine, fromNode; + if (from.line == display.viewFrom || (fromIndex = findViewIndex(cm, from.line)) == 0) { + fromLine = lineNo(display.view[0].line); + fromNode = display.view[0].node; + } else { + fromLine = lineNo(display.view[fromIndex].line); + fromNode = display.view[fromIndex - 1].node.nextSibling; + } + var toIndex = findViewIndex(cm, to.line); + var toLine, toNode; + if (toIndex == display.view.length - 1) { + toLine = display.viewTo - 1; + toNode = display.lineDiv.lastChild; + } else { + toLine = lineNo(display.view[toIndex + 1].line) - 1; + toNode = display.view[toIndex + 1].node.previousSibling; + } + + if (!fromNode) { return false } + var newText = cm.doc.splitLines(domTextBetween(cm, fromNode, toNode, fromLine, toLine)); + var oldText = getBetween(cm.doc, Pos(fromLine, 0), Pos(toLine, getLine(cm.doc, toLine).text.length)); + while (newText.length > 1 && oldText.length > 1) { + if (lst(newText) == lst(oldText)) { newText.pop(); oldText.pop(); toLine--; } + else if (newText[0] == oldText[0]) { newText.shift(); oldText.shift(); fromLine++; } + else { break } + } + + var cutFront = 0, cutEnd = 0; + var newTop = newText[0], oldTop = oldText[0], maxCutFront = Math.min(newTop.length, oldTop.length); + while (cutFront < maxCutFront && newTop.charCodeAt(cutFront) == oldTop.charCodeAt(cutFront)) + { ++cutFront; } + var newBot = lst(newText), oldBot = lst(oldText); + var maxCutEnd = Math.min(newBot.length - (newText.length == 1 ? cutFront : 0), + oldBot.length - (oldText.length == 1 ? cutFront : 0)); + while (cutEnd < maxCutEnd && + newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1)) + { ++cutEnd; } + // Try to move start of change to start of selection if ambiguous + if (newText.length == 1 && oldText.length == 1 && fromLine == from.line) { + while (cutFront && cutFront > from.ch && + newBot.charCodeAt(newBot.length - cutEnd - 1) == oldBot.charCodeAt(oldBot.length - cutEnd - 1)) { + cutFront--; + cutEnd++; + } + } + + newText[newText.length - 1] = newBot.slice(0, newBot.length - cutEnd).replace(/^\u200b+/, ""); + newText[0] = newText[0].slice(cutFront).replace(/\u200b+$/, ""); + + var chFrom = Pos(fromLine, cutFront); + var chTo = Pos(toLine, oldText.length ? lst(oldText).length - cutEnd : 0); + if (newText.length > 1 || newText[0] || cmp(chFrom, chTo)) { + replaceRange(cm.doc, newText, chFrom, chTo, "+input"); + return true + } + }; + + ContentEditableInput.prototype.ensurePolled = function () { + this.forceCompositionEnd(); + }; + ContentEditableInput.prototype.reset = function () { + this.forceCompositionEnd(); + }; + ContentEditableInput.prototype.forceCompositionEnd = function () { + if (!this.composing) { return } + clearTimeout(this.readDOMTimeout); + this.composing = null; + this.updateFromDOM(); + this.div.blur(); + this.div.focus(); + }; + ContentEditableInput.prototype.readFromDOMSoon = function () { + var this$1 = this; + + if (this.readDOMTimeout != null) { return } + this.readDOMTimeout = setTimeout(function () { + this$1.readDOMTimeout = null; + if (this$1.composing) { + if (this$1.composing.done) { this$1.composing = null; } + else { return } + } + this$1.updateFromDOM(); + }, 80); + }; + + ContentEditableInput.prototype.updateFromDOM = function () { + var this$1 = this; + + if (this.cm.isReadOnly() || !this.pollContent()) + { runInOp(this.cm, function () { return regChange(this$1.cm); }); } + }; + + ContentEditableInput.prototype.setUneditable = function (node) { + node.contentEditable = "false"; + }; + + ContentEditableInput.prototype.onKeyPress = function (e) { + if (e.charCode == 0 || this.composing) { return } + e.preventDefault(); + if (!this.cm.isReadOnly()) + { operation(this.cm, applyTextInput)(this.cm, String.fromCharCode(e.charCode == null ? e.keyCode : e.charCode), 0); } + }; + + ContentEditableInput.prototype.readOnlyChanged = function (val) { + this.div.contentEditable = String(val != "nocursor"); + }; + + ContentEditableInput.prototype.onContextMenu = function () {}; + ContentEditableInput.prototype.resetPosition = function () {}; + + ContentEditableInput.prototype.needsContentAttribute = true; + + function posToDOM(cm, pos) { + var view = findViewForLine(cm, pos.line); + if (!view || view.hidden) { return null } + var line = getLine(cm.doc, pos.line); + var info = mapFromLineView(view, line, pos.line); + + var order = getOrder(line, cm.doc.direction), side = "left"; + if (order) { + var partPos = getBidiPartAt(order, pos.ch); + side = partPos % 2 ? "right" : "left"; + } + var result = nodeAndOffsetInLineMap(info.map, pos.ch, side); + result.offset = result.collapse == "right" ? result.end : result.start; + return result + } + + function isInGutter(node) { + for (var scan = node; scan; scan = scan.parentNode) + { if (/CodeMirror-gutter-wrapper/.test(scan.className)) { return true } } + return false + } + + function badPos(pos, bad) { if (bad) { pos.bad = true; } return pos } + + function domTextBetween(cm, from, to, fromLine, toLine) { + var text = "", closing = false, lineSep = cm.doc.lineSeparator(), extraLinebreak = false; + function recognizeMarker(id) { return function (marker) { return marker.id == id; } } + function close() { + if (closing) { + text += lineSep; + if (extraLinebreak) { text += lineSep; } + closing = extraLinebreak = false; + } + } + function addText(str) { + if (str) { + close(); + text += str; + } + } + function walk(node) { + if (node.nodeType == 1) { + var cmText = node.getAttribute("cm-text"); + if (cmText) { + addText(cmText); + return + } + var markerID = node.getAttribute("cm-marker"), range; + if (markerID) { + var found = cm.findMarks(Pos(fromLine, 0), Pos(toLine + 1, 0), recognizeMarker(+markerID)); + if (found.length && (range = found[0].find(0))) + { addText(getBetween(cm.doc, range.from, range.to).join(lineSep)); } + return + } + if (node.getAttribute("contenteditable") == "false") { return } + var isBlock = /^(pre|div|p|li|table|br)$/i.test(node.nodeName); + if (!/^br$/i.test(node.nodeName) && node.textContent.length == 0) { return } + + if (isBlock) { close(); } + for (var i = 0; i < node.childNodes.length; i++) + { walk(node.childNodes[i]); } + + if (/^(pre|p)$/i.test(node.nodeName)) { extraLinebreak = true; } + if (isBlock) { closing = true; } + } else if (node.nodeType == 3) { + addText(node.nodeValue.replace(/\u200b/g, "").replace(/\u00a0/g, " ")); + } + } + for (;;) { + walk(from); + if (from == to) { break } + from = from.nextSibling; + extraLinebreak = false; + } + return text + } + + function domToPos(cm, node, offset) { + var lineNode; + if (node == cm.display.lineDiv) { + lineNode = cm.display.lineDiv.childNodes[offset]; + if (!lineNode) { return badPos(cm.clipPos(Pos(cm.display.viewTo - 1)), true) } + node = null; offset = 0; + } else { + for (lineNode = node;; lineNode = lineNode.parentNode) { + if (!lineNode || lineNode == cm.display.lineDiv) { return null } + if (lineNode.parentNode && lineNode.parentNode == cm.display.lineDiv) { break } + } + } + for (var i = 0; i < cm.display.view.length; i++) { + var lineView = cm.display.view[i]; + if (lineView.node == lineNode) + { return locateNodeInLineView(lineView, node, offset) } + } + } + + function locateNodeInLineView(lineView, node, offset) { + var wrapper = lineView.text.firstChild, bad = false; + if (!node || !contains(wrapper, node)) { return badPos(Pos(lineNo(lineView.line), 0), true) } + if (node == wrapper) { + bad = true; + node = wrapper.childNodes[offset]; + offset = 0; + if (!node) { + var line = lineView.rest ? lst(lineView.rest) : lineView.line; + return badPos(Pos(lineNo(line), line.text.length), bad) + } + } + + var textNode = node.nodeType == 3 ? node : null, topNode = node; + if (!textNode && node.childNodes.length == 1 && node.firstChild.nodeType == 3) { + textNode = node.firstChild; + if (offset) { offset = textNode.nodeValue.length; } + } + while (topNode.parentNode != wrapper) { topNode = topNode.parentNode; } + var measure = lineView.measure, maps = measure.maps; + + function find(textNode, topNode, offset) { + for (var i = -1; i < (maps ? maps.length : 0); i++) { + var map = i < 0 ? measure.map : maps[i]; + for (var j = 0; j < map.length; j += 3) { + var curNode = map[j + 2]; + if (curNode == textNode || curNode == topNode) { + var line = lineNo(i < 0 ? lineView.line : lineView.rest[i]); + var ch = map[j] + offset; + if (offset < 0 || curNode != textNode) { ch = map[j + (offset ? 1 : 0)]; } + return Pos(line, ch) + } + } + } + } + var found = find(textNode, topNode, offset); + if (found) { return badPos(found, bad) } + + // FIXME this is all really shaky. might handle the few cases it needs to handle, but likely to cause problems + for (var after = topNode.nextSibling, dist = textNode ? textNode.nodeValue.length - offset : 0; after; after = after.nextSibling) { + found = find(after, after.firstChild, 0); + if (found) + { return badPos(Pos(found.line, found.ch - dist), bad) } + else + { dist += after.textContent.length; } + } + for (var before = topNode.previousSibling, dist$1 = offset; before; before = before.previousSibling) { + found = find(before, before.firstChild, -1); + if (found) + { return badPos(Pos(found.line, found.ch + dist$1), bad) } + else + { dist$1 += before.textContent.length; } + } + } + + // TEXTAREA INPUT STYLE + + var TextareaInput = function(cm) { + this.cm = cm; + // See input.poll and input.reset + this.prevInput = ""; + + // Flag that indicates whether we expect input to appear real soon + // now (after some event like 'keypress' or 'input') and are + // polling intensively. + this.pollingFast = false; + // Self-resetting timeout for the poller + this.polling = new Delayed(); + // Used to work around IE issue with selection being forgotten when focus moves away from textarea + this.hasSelection = false; + this.composing = null; + }; + + TextareaInput.prototype.init = function (display) { + var this$1 = this; + + var input = this, cm = this.cm; + this.createField(display); + var te = this.textarea; + + display.wrapper.insertBefore(this.wrapper, display.wrapper.firstChild); + + // Needed to hide big blue blinking cursor on Mobile Safari (doesn't seem to work in iOS 8 anymore) + if (ios) { te.style.width = "0px"; } + + on(te, "input", function () { + if (ie && ie_version >= 9 && this$1.hasSelection) { this$1.hasSelection = null; } + input.poll(); + }); + + on(te, "paste", function (e) { + if (signalDOMEvent(cm, e) || handlePaste(e, cm)) { return } + + cm.state.pasteIncoming = +new Date; + input.fastPoll(); + }); + + function prepareCopyCut(e) { + if (signalDOMEvent(cm, e)) { return } + if (cm.somethingSelected()) { + setLastCopied({lineWise: false, text: cm.getSelections()}); + } else if (!cm.options.lineWiseCopyCut) { + return + } else { + var ranges = copyableRanges(cm); + setLastCopied({lineWise: true, text: ranges.text}); + if (e.type == "cut") { + cm.setSelections(ranges.ranges, null, sel_dontScroll); + } else { + input.prevInput = ""; + te.value = ranges.text.join("\n"); + selectInput(te); + } + } + if (e.type == "cut") { cm.state.cutIncoming = +new Date; } + } + on(te, "cut", prepareCopyCut); + on(te, "copy", prepareCopyCut); + + on(display.scroller, "paste", function (e) { + if (eventInWidget(display, e) || signalDOMEvent(cm, e)) { return } + if (!te.dispatchEvent) { + cm.state.pasteIncoming = +new Date; + input.focus(); + return + } + + // Pass the `paste` event to the textarea so it's handled by its event listener. + var event = new Event("paste"); + event.clipboardData = e.clipboardData; + te.dispatchEvent(event); + }); + + // Prevent normal selection in the editor (we handle our own) + on(display.lineSpace, "selectstart", function (e) { + if (!eventInWidget(display, e)) { e_preventDefault(e); } + }); + + on(te, "compositionstart", function () { + var start = cm.getCursor("from"); + if (input.composing) { input.composing.range.clear(); } + input.composing = { + start: start, + range: cm.markText(start, cm.getCursor("to"), {className: "CodeMirror-composing"}) + }; + }); + on(te, "compositionend", function () { + if (input.composing) { + input.poll(); + input.composing.range.clear(); + input.composing = null; + } + }); + }; + + TextareaInput.prototype.createField = function (_display) { + // Wraps and hides input textarea + this.wrapper = hiddenTextarea(); + // The semihidden textarea that is focused when the editor is + // focused, and receives input. + this.textarea = this.wrapper.firstChild; + }; + + TextareaInput.prototype.screenReaderLabelChanged = function (label) { + // Label for screenreaders, accessibility + if(label) { + this.textarea.setAttribute('aria-label', label); + } else { + this.textarea.removeAttribute('aria-label'); + } + }; + + TextareaInput.prototype.prepareSelection = function () { + // Redraw the selection and/or cursor + var cm = this.cm, display = cm.display, doc = cm.doc; + var result = prepareSelection(cm); + + // Move the hidden textarea near the cursor to prevent scrolling artifacts + if (cm.options.moveInputWithCursor) { + var headPos = cursorCoords(cm, doc.sel.primary().head, "div"); + var wrapOff = display.wrapper.getBoundingClientRect(), lineOff = display.lineDiv.getBoundingClientRect(); + result.teTop = Math.max(0, Math.min(display.wrapper.clientHeight - 10, + headPos.top + lineOff.top - wrapOff.top)); + result.teLeft = Math.max(0, Math.min(display.wrapper.clientWidth - 10, + headPos.left + lineOff.left - wrapOff.left)); + } + + return result + }; + + TextareaInput.prototype.showSelection = function (drawn) { + var cm = this.cm, display = cm.display; + removeChildrenAndAdd(display.cursorDiv, drawn.cursors); + removeChildrenAndAdd(display.selectionDiv, drawn.selection); + if (drawn.teTop != null) { + this.wrapper.style.top = drawn.teTop + "px"; + this.wrapper.style.left = drawn.teLeft + "px"; + } + }; + + // Reset the input to correspond to the selection (or to be empty, + // when not typing and nothing is selected) + TextareaInput.prototype.reset = function (typing) { + if (this.contextMenuPending || this.composing) { return } + var cm = this.cm; + if (cm.somethingSelected()) { + this.prevInput = ""; + var content = cm.getSelection(); + this.textarea.value = content; + if (cm.state.focused) { selectInput(this.textarea); } + if (ie && ie_version >= 9) { this.hasSelection = content; } + } else if (!typing) { + this.prevInput = this.textarea.value = ""; + if (ie && ie_version >= 9) { this.hasSelection = null; } + } + }; + + TextareaInput.prototype.getField = function () { return this.textarea }; + + TextareaInput.prototype.supportsTouch = function () { return false }; + + TextareaInput.prototype.focus = function () { + if (this.cm.options.readOnly != "nocursor" && (!mobile || activeElt() != this.textarea)) { + try { this.textarea.focus(); } + catch (e) {} // IE8 will throw if the textarea is display: none or not in DOM + } + }; + + TextareaInput.prototype.blur = function () { this.textarea.blur(); }; + + TextareaInput.prototype.resetPosition = function () { + this.wrapper.style.top = this.wrapper.style.left = 0; + }; + + TextareaInput.prototype.receivedFocus = function () { this.slowPoll(); }; + + // Poll for input changes, using the normal rate of polling. This + // runs as long as the editor is focused. + TextareaInput.prototype.slowPoll = function () { + var this$1 = this; + + if (this.pollingFast) { return } + this.polling.set(this.cm.options.pollInterval, function () { + this$1.poll(); + if (this$1.cm.state.focused) { this$1.slowPoll(); } + }); + }; + + // When an event has just come in that is likely to add or change + // something in the input textarea, we poll faster, to ensure that + // the change appears on the screen quickly. + TextareaInput.prototype.fastPoll = function () { + var missed = false, input = this; + input.pollingFast = true; + function p() { + var changed = input.poll(); + if (!changed && !missed) {missed = true; input.polling.set(60, p);} + else {input.pollingFast = false; input.slowPoll();} + } + input.polling.set(20, p); + }; + + // Read input from the textarea, and update the document to match. + // When something is selected, it is present in the textarea, and + // selected (unless it is huge, in which case a placeholder is + // used). When nothing is selected, the cursor sits after previously + // seen text (can be empty), which is stored in prevInput (we must + // not reset the textarea when typing, because that breaks IME). + TextareaInput.prototype.poll = function () { + var this$1 = this; + + var cm = this.cm, input = this.textarea, prevInput = this.prevInput; + // Since this is called a *lot*, try to bail out as cheaply as + // possible when it is clear that nothing happened. hasSelection + // will be the case when there is a lot of text in the textarea, + // in which case reading its value would be expensive. + if (this.contextMenuPending || !cm.state.focused || + (hasSelection(input) && !prevInput && !this.composing) || + cm.isReadOnly() || cm.options.disableInput || cm.state.keySeq) + { return false } + + var text = input.value; + // If nothing changed, bail. + if (text == prevInput && !cm.somethingSelected()) { return false } + // Work around nonsensical selection resetting in IE9/10, and + // inexplicable appearance of private area unicode characters on + // some key combos in Mac (#2689). + if (ie && ie_version >= 9 && this.hasSelection === text || + mac && /[\uf700-\uf7ff]/.test(text)) { + cm.display.input.reset(); + return false + } + + if (cm.doc.sel == cm.display.selForContextMenu) { + var first = text.charCodeAt(0); + if (first == 0x200b && !prevInput) { prevInput = "\u200b"; } + if (first == 0x21da) { this.reset(); return this.cm.execCommand("undo") } + } + // Find the part of the input that is actually new + var same = 0, l = Math.min(prevInput.length, text.length); + while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) { ++same; } + + runInOp(cm, function () { + applyTextInput(cm, text.slice(same), prevInput.length - same, + null, this$1.composing ? "*compose" : null); + + // Don't leave long text in the textarea, since it makes further polling slow + if (text.length > 1000 || text.indexOf("\n") > -1) { input.value = this$1.prevInput = ""; } + else { this$1.prevInput = text; } + + if (this$1.composing) { + this$1.composing.range.clear(); + this$1.composing.range = cm.markText(this$1.composing.start, cm.getCursor("to"), + {className: "CodeMirror-composing"}); + } + }); + return true + }; + + TextareaInput.prototype.ensurePolled = function () { + if (this.pollingFast && this.poll()) { this.pollingFast = false; } + }; + + TextareaInput.prototype.onKeyPress = function () { + if (ie && ie_version >= 9) { this.hasSelection = null; } + this.fastPoll(); + }; + + TextareaInput.prototype.onContextMenu = function (e) { + var input = this, cm = input.cm, display = cm.display, te = input.textarea; + if (input.contextMenuPending) { input.contextMenuPending(); } + var pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop; + if (!pos || presto) { return } // Opera is difficult. + + // Reset the current text selection only if the click is done outside of the selection + // and 'resetSelectionOnContextMenu' option is true. + var reset = cm.options.resetSelectionOnContextMenu; + if (reset && cm.doc.sel.contains(pos) == -1) + { operation(cm, setSelection)(cm.doc, simpleSelection(pos), sel_dontScroll); } + + var oldCSS = te.style.cssText, oldWrapperCSS = input.wrapper.style.cssText; + var wrapperBox = input.wrapper.offsetParent.getBoundingClientRect(); + input.wrapper.style.cssText = "position: static"; + te.style.cssText = "position: absolute; width: 30px; height: 30px;\n top: " + (e.clientY - wrapperBox.top - 5) + "px; left: " + (e.clientX - wrapperBox.left - 5) + "px;\n z-index: 1000; background: " + (ie ? "rgba(255, 255, 255, .05)" : "transparent") + ";\n outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);"; + var oldScrollY; + if (webkit) { oldScrollY = window.scrollY; } // Work around Chrome issue (#2712) + display.input.focus(); + if (webkit) { window.scrollTo(null, oldScrollY); } + display.input.reset(); + // Adds "Select all" to context menu in FF + if (!cm.somethingSelected()) { te.value = input.prevInput = " "; } + input.contextMenuPending = rehide; + display.selForContextMenu = cm.doc.sel; + clearTimeout(display.detectingSelectAll); + + // Select-all will be greyed out if there's nothing to select, so + // this adds a zero-width space so that we can later check whether + // it got selected. + function prepareSelectAllHack() { + if (te.selectionStart != null) { + var selected = cm.somethingSelected(); + var extval = "\u200b" + (selected ? te.value : ""); + te.value = "\u21da"; // Used to catch context-menu undo + te.value = extval; + input.prevInput = selected ? "" : "\u200b"; + te.selectionStart = 1; te.selectionEnd = extval.length; + // Re-set this, in case some other handler touched the + // selection in the meantime. + display.selForContextMenu = cm.doc.sel; + } + } + function rehide() { + if (input.contextMenuPending != rehide) { return } + input.contextMenuPending = false; + input.wrapper.style.cssText = oldWrapperCSS; + te.style.cssText = oldCSS; + if (ie && ie_version < 9) { display.scrollbars.setScrollTop(display.scroller.scrollTop = scrollPos); } + + // Try to detect the user choosing select-all + if (te.selectionStart != null) { + if (!ie || (ie && ie_version < 9)) { prepareSelectAllHack(); } + var i = 0, poll = function () { + if (display.selForContextMenu == cm.doc.sel && te.selectionStart == 0 && + te.selectionEnd > 0 && input.prevInput == "\u200b") { + operation(cm, selectAll)(cm); + } else if (i++ < 10) { + display.detectingSelectAll = setTimeout(poll, 500); + } else { + display.selForContextMenu = null; + display.input.reset(); + } + }; + display.detectingSelectAll = setTimeout(poll, 200); + } + } + + if (ie && ie_version >= 9) { prepareSelectAllHack(); } + if (captureRightClick) { + e_stop(e); + var mouseup = function () { + off(window, "mouseup", mouseup); + setTimeout(rehide, 20); + }; + on(window, "mouseup", mouseup); + } else { + setTimeout(rehide, 50); + } + }; + + TextareaInput.prototype.readOnlyChanged = function (val) { + if (!val) { this.reset(); } + this.textarea.disabled = val == "nocursor"; + this.textarea.readOnly = !!val; + }; + + TextareaInput.prototype.setUneditable = function () {}; + + TextareaInput.prototype.needsContentAttribute = false; + + function fromTextArea(textarea, options) { + options = options ? copyObj(options) : {}; + options.value = textarea.value; + if (!options.tabindex && textarea.tabIndex) + { options.tabindex = textarea.tabIndex; } + if (!options.placeholder && textarea.placeholder) + { options.placeholder = textarea.placeholder; } + // Set autofocus to true if this textarea is focused, or if it has + // autofocus and no other element is focused. + if (options.autofocus == null) { + var hasFocus = activeElt(); + options.autofocus = hasFocus == textarea || + textarea.getAttribute("autofocus") != null && hasFocus == document.body; + } + + function save() {textarea.value = cm.getValue();} + + var realSubmit; + if (textarea.form) { + on(textarea.form, "submit", save); + // Deplorable hack to make the submit method do the right thing. + if (!options.leaveSubmitMethodAlone) { + var form = textarea.form; + realSubmit = form.submit; + try { + var wrappedSubmit = form.submit = function () { + save(); + form.submit = realSubmit; + form.submit(); + form.submit = wrappedSubmit; + }; + } catch(e) {} + } + } + + options.finishInit = function (cm) { + cm.save = save; + cm.getTextArea = function () { return textarea; }; + cm.toTextArea = function () { + cm.toTextArea = isNaN; // Prevent this from being ran twice + save(); + textarea.parentNode.removeChild(cm.getWrapperElement()); + textarea.style.display = ""; + if (textarea.form) { + off(textarea.form, "submit", save); + if (!options.leaveSubmitMethodAlone && typeof textarea.form.submit == "function") + { textarea.form.submit = realSubmit; } + } + }; + }; + + textarea.style.display = "none"; + var cm = CodeMirror(function (node) { return textarea.parentNode.insertBefore(node, textarea.nextSibling); }, + options); + return cm + } + + function addLegacyProps(CodeMirror) { + CodeMirror.off = off; + CodeMirror.on = on; + CodeMirror.wheelEventPixels = wheelEventPixels; + CodeMirror.Doc = Doc; + CodeMirror.splitLines = splitLinesAuto; + CodeMirror.countColumn = countColumn; + CodeMirror.findColumn = findColumn; + CodeMirror.isWordChar = isWordCharBasic; + CodeMirror.Pass = Pass; + CodeMirror.signal = signal; + CodeMirror.Line = Line; + CodeMirror.changeEnd = changeEnd; + CodeMirror.scrollbarModel = scrollbarModel; + CodeMirror.Pos = Pos; + CodeMirror.cmpPos = cmp; + CodeMirror.modes = modes; + CodeMirror.mimeModes = mimeModes; + CodeMirror.resolveMode = resolveMode; + CodeMirror.getMode = getMode; + CodeMirror.modeExtensions = modeExtensions; + CodeMirror.extendMode = extendMode; + CodeMirror.copyState = copyState; + CodeMirror.startState = startState; + CodeMirror.innerMode = innerMode; + CodeMirror.commands = commands; + CodeMirror.keyMap = keyMap; + CodeMirror.keyName = keyName; + CodeMirror.isModifierKey = isModifierKey; + CodeMirror.lookupKey = lookupKey; + CodeMirror.normalizeKeyMap = normalizeKeyMap; + CodeMirror.StringStream = StringStream; + CodeMirror.SharedTextMarker = SharedTextMarker; + CodeMirror.TextMarker = TextMarker; + CodeMirror.LineWidget = LineWidget; + CodeMirror.e_preventDefault = e_preventDefault; + CodeMirror.e_stopPropagation = e_stopPropagation; + CodeMirror.e_stop = e_stop; + CodeMirror.addClass = addClass; + CodeMirror.contains = contains; + CodeMirror.rmClass = rmClass; + CodeMirror.keyNames = keyNames; + } + + // EDITOR CONSTRUCTOR + + defineOptions(CodeMirror); + + addEditorMethods(CodeMirror); + + // Set up methods on CodeMirror's prototype to redirect to the editor's document. + var dontDelegate = "iter insert remove copy getEditor constructor".split(" "); + for (var prop in Doc.prototype) { if (Doc.prototype.hasOwnProperty(prop) && indexOf(dontDelegate, prop) < 0) + { CodeMirror.prototype[prop] = (function(method) { + return function() {return method.apply(this.doc, arguments)} + })(Doc.prototype[prop]); } } + + eventMixin(Doc); + CodeMirror.inputStyles = {"textarea": TextareaInput, "contenteditable": ContentEditableInput}; + + // Extra arguments are stored as the mode's dependencies, which is + // used by (legacy) mechanisms like loadmode.js to automatically + // load a mode. (Preferred mechanism is the require/define calls.) + CodeMirror.defineMode = function(name/*, mode, …*/) { + if (!CodeMirror.defaults.mode && name != "null") { CodeMirror.defaults.mode = name; } + defineMode.apply(this, arguments); + }; + + CodeMirror.defineMIME = defineMIME; + + // Minimal default mode. + CodeMirror.defineMode("null", function () { return ({token: function (stream) { return stream.skipToEnd(); }}); }); + CodeMirror.defineMIME("text/plain", "null"); + + // EXTENSIONS + + CodeMirror.defineExtension = function (name, func) { + CodeMirror.prototype[name] = func; + }; + CodeMirror.defineDocExtension = function (name, func) { + Doc.prototype[name] = func; + }; + + CodeMirror.fromTextArea = fromTextArea; + + addLegacyProps(CodeMirror); + + CodeMirror.version = "5.58.1"; + + return CodeMirror; + +}))); diff --git a/play/js/codemirror/mode/javascript/javascript.js b/play/js/codemirror/mode/javascript/javascript.js new file mode 100644 index 00000000..66e5a308 --- /dev/null +++ b/play/js/codemirror/mode/javascript/javascript.js @@ -0,0 +1,934 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { +"use strict"; + +CodeMirror.defineMode("javascript", function(config, parserConfig) { + var indentUnit = config.indentUnit; + var statementIndent = parserConfig.statementIndent; + var jsonldMode = parserConfig.jsonld; + var jsonMode = parserConfig.json || jsonldMode; + var isTS = parserConfig.typescript; + var wordRE = parserConfig.wordCharacters || /[\w$\xa1-\uffff]/; + + // Tokenizer + + var keywords = function(){ + function kw(type) {return {type: type, style: "keyword"};} + var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c"), D = kw("keyword d"); + var operator = kw("operator"), atom = {type: "atom", style: "atom"}; + + return { + "if": kw("if"), "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B, + "return": D, "break": D, "continue": D, "new": kw("new"), "delete": C, "void": C, "throw": C, + "debugger": kw("debugger"), "var": kw("var"), "const": kw("var"), "let": kw("var"), + "function": kw("function"), "catch": kw("catch"), + "for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"), + "in": operator, "typeof": operator, "instanceof": operator, + "true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom, + "this": kw("this"), "class": kw("class"), "super": kw("atom"), + "yield": C, "export": kw("export"), "import": kw("import"), "extends": C, + "await": C + }; + }(); + + var isOperatorChar = /[+\-*&%=<>!?|~^@]/; + var isJsonldKeyword = /^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/; + + function readRegexp(stream) { + var escaped = false, next, inSet = false; + while ((next = stream.next()) != null) { + if (!escaped) { + if (next == "/" && !inSet) return; + if (next == "[") inSet = true; + else if (inSet && next == "]") inSet = false; + } + escaped = !escaped && next == "\\"; + } + } + + // Used as scratch variables to communicate multiple values without + // consing up tons of objects. + var type, content; + function ret(tp, style, cont) { + type = tp; content = cont; + return style; + } + function tokenBase(stream, state) { + var ch = stream.next(); + if (ch == '"' || ch == "'") { + state.tokenize = tokenString(ch); + return state.tokenize(stream, state); + } else if (ch == "." && stream.match(/^\d[\d_]*(?:[eE][+\-]?[\d_]+)?/)) { + return ret("number", "number"); + } else if (ch == "." && stream.match("..")) { + return ret("spread", "meta"); + } else if (/[\[\]{}\(\),;\:\.]/.test(ch)) { + return ret(ch); + } else if (ch == "=" && stream.eat(">")) { + return ret("=>", "operator"); + } else if (ch == "0" && stream.match(/^(?:x[\dA-Fa-f_]+|o[0-7_]+|b[01_]+)n?/)) { + return ret("number", "number"); + } else if (/\d/.test(ch)) { + stream.match(/^[\d_]*(?:n|(?:\.[\d_]*)?(?:[eE][+\-]?[\d_]+)?)?/); + return ret("number", "number"); + } else if (ch == "/") { + if (stream.eat("*")) { + state.tokenize = tokenComment; + return tokenComment(stream, state); + } else if (stream.eat("/")) { + stream.skipToEnd(); + return ret("comment", "comment"); + } else if (expressionAllowed(stream, state, 1)) { + readRegexp(stream); + stream.match(/^\b(([gimyus])(?![gimyus]*\2))+\b/); + return ret("regexp", "string-2"); + } else { + stream.eat("="); + return ret("operator", "operator", stream.current()); + } + } else if (ch == "`") { + state.tokenize = tokenQuasi; + return tokenQuasi(stream, state); + } else if (ch == "#" && stream.peek() == "!") { + stream.skipToEnd(); + return ret("meta", "meta"); + } else if (ch == "#" && stream.eatWhile(wordRE)) { + return ret("variable", "property") + } else if (ch == "<" && stream.match("!--") || + (ch == "-" && stream.match("->") && !/\S/.test(stream.string.slice(0, stream.start)))) { + stream.skipToEnd() + return ret("comment", "comment") + } else if (isOperatorChar.test(ch)) { + if (ch != ">" || !state.lexical || state.lexical.type != ">") { + if (stream.eat("=")) { + if (ch == "!" || ch == "=") stream.eat("=") + } else if (/[<>*+\-|&?]/.test(ch)) { + stream.eat(ch) + if (ch == ">") stream.eat(ch) + } + } + if (ch == "?" && stream.eat(".")) return ret(".") + return ret("operator", "operator", stream.current()); + } else if (wordRE.test(ch)) { + stream.eatWhile(wordRE); + var word = stream.current() + if (state.lastType != ".") { + if (keywords.propertyIsEnumerable(word)) { + var kw = keywords[word] + return ret(kw.type, kw.style, word) + } + if (word == "async" && stream.match(/^(\s|\/\*.*?\*\/)*[\[\(\w]/, false)) + return ret("async", "keyword", word) + } + return ret("variable", "variable", word) + } + } + + function tokenString(quote) { + return function(stream, state) { + var escaped = false, next; + if (jsonldMode && stream.peek() == "@" && stream.match(isJsonldKeyword)){ + state.tokenize = tokenBase; + return ret("jsonld-keyword", "meta"); + } + while ((next = stream.next()) != null) { + if (next == quote && !escaped) break; + escaped = !escaped && next == "\\"; + } + if (!escaped) state.tokenize = tokenBase; + return ret("string", "string"); + }; + } + + function tokenComment(stream, state) { + var maybeEnd = false, ch; + while (ch = stream.next()) { + if (ch == "/" && maybeEnd) { + state.tokenize = tokenBase; + break; + } + maybeEnd = (ch == "*"); + } + return ret("comment", "comment"); + } + + function tokenQuasi(stream, state) { + var escaped = false, next; + while ((next = stream.next()) != null) { + if (!escaped && (next == "`" || next == "$" && stream.eat("{"))) { + state.tokenize = tokenBase; + break; + } + escaped = !escaped && next == "\\"; + } + return ret("quasi", "string-2", stream.current()); + } + + var brackets = "([{}])"; + // This is a crude lookahead trick to try and notice that we're + // parsing the argument patterns for a fat-arrow function before we + // actually hit the arrow token. It only works if the arrow is on + // the same line as the arguments and there's no strange noise + // (comments) in between. Fallback is to only notice when we hit the + // arrow, and not declare the arguments as locals for the arrow + // body. + function findFatArrow(stream, state) { + if (state.fatArrowAt) state.fatArrowAt = null; + var arrow = stream.string.indexOf("=>", stream.start); + if (arrow < 0) return; + + if (isTS) { // Try to skip TypeScript return type declarations after the arguments + var m = /:\s*(?:\w+(?:<[^>]*>|\[\])?|\{[^}]*\})\s*$/.exec(stream.string.slice(stream.start, arrow)) + if (m) arrow = m.index + } + + var depth = 0, sawSomething = false; + for (var pos = arrow - 1; pos >= 0; --pos) { + var ch = stream.string.charAt(pos); + var bracket = brackets.indexOf(ch); + if (bracket >= 0 && bracket < 3) { + if (!depth) { ++pos; break; } + if (--depth == 0) { if (ch == "(") sawSomething = true; break; } + } else if (bracket >= 3 && bracket < 6) { + ++depth; + } else if (wordRE.test(ch)) { + sawSomething = true; + } else if (/["'\/`]/.test(ch)) { + for (;; --pos) { + if (pos == 0) return + var next = stream.string.charAt(pos - 1) + if (next == ch && stream.string.charAt(pos - 2) != "\\") { pos--; break } + } + } else if (sawSomething && !depth) { + ++pos; + break; + } + } + if (sawSomething && !depth) state.fatArrowAt = pos; + } + + // Parser + + var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true, "regexp": true, "this": true, "jsonld-keyword": true}; + + function JSLexical(indented, column, type, align, prev, info) { + this.indented = indented; + this.column = column; + this.type = type; + this.prev = prev; + this.info = info; + if (align != null) this.align = align; + } + + function inScope(state, varname) { + for (var v = state.localVars; v; v = v.next) + if (v.name == varname) return true; + for (var cx = state.context; cx; cx = cx.prev) { + for (var v = cx.vars; v; v = v.next) + if (v.name == varname) return true; + } + } + + function parseJS(state, style, type, content, stream) { + var cc = state.cc; + // Communicate our context to the combinators. + // (Less wasteful than consing up a hundred closures on every call.) + cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc; cx.style = style; + + if (!state.lexical.hasOwnProperty("align")) + state.lexical.align = true; + + while(true) { + var combinator = cc.length ? cc.pop() : jsonMode ? expression : statement; + if (combinator(type, content)) { + while(cc.length && cc[cc.length - 1].lex) + cc.pop()(); + if (cx.marked) return cx.marked; + if (type == "variable" && inScope(state, content)) return "variable-2"; + return style; + } + } + } + + // Combinator utils + + var cx = {state: null, column: null, marked: null, cc: null}; + function pass() { + for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]); + } + function cont() { + pass.apply(null, arguments); + return true; + } + function inList(name, list) { + for (var v = list; v; v = v.next) if (v.name == name) return true + return false; + } + function register(varname) { + var state = cx.state; + cx.marked = "def"; + if (state.context) { + if (state.lexical.info == "var" && state.context && state.context.block) { + // FIXME function decls are also not block scoped + var newContext = registerVarScoped(varname, state.context) + if (newContext != null) { + state.context = newContext + return + } + } else if (!inList(varname, state.localVars)) { + state.localVars = new Var(varname, state.localVars) + return + } + } + // Fall through means this is global + if (parserConfig.globalVars && !inList(varname, state.globalVars)) + state.globalVars = new Var(varname, state.globalVars) + } + function registerVarScoped(varname, context) { + if (!context) { + return null + } else if (context.block) { + var inner = registerVarScoped(varname, context.prev) + if (!inner) return null + if (inner == context.prev) return context + return new Context(inner, context.vars, true) + } else if (inList(varname, context.vars)) { + return context + } else { + return new Context(context.prev, new Var(varname, context.vars), false) + } + } + + function isModifier(name) { + return name == "public" || name == "private" || name == "protected" || name == "abstract" || name == "readonly" + } + + // Combinators + + function Context(prev, vars, block) { this.prev = prev; this.vars = vars; this.block = block } + function Var(name, next) { this.name = name; this.next = next } + + var defaultVars = new Var("this", new Var("arguments", null)) + function pushcontext() { + cx.state.context = new Context(cx.state.context, cx.state.localVars, false) + cx.state.localVars = defaultVars + } + function pushblockcontext() { + cx.state.context = new Context(cx.state.context, cx.state.localVars, true) + cx.state.localVars = null + } + function popcontext() { + cx.state.localVars = cx.state.context.vars + cx.state.context = cx.state.context.prev + } + popcontext.lex = true + function pushlex(type, info) { + var result = function() { + var state = cx.state, indent = state.indented; + if (state.lexical.type == "stat") indent = state.lexical.indented; + else for (var outer = state.lexical; outer && outer.type == ")" && outer.align; outer = outer.prev) + indent = outer.indented; + state.lexical = new JSLexical(indent, cx.stream.column(), type, null, state.lexical, info); + }; + result.lex = true; + return result; + } + function poplex() { + var state = cx.state; + if (state.lexical.prev) { + if (state.lexical.type == ")") + state.indented = state.lexical.indented; + state.lexical = state.lexical.prev; + } + } + poplex.lex = true; + + function expect(wanted) { + function exp(type) { + if (type == wanted) return cont(); + else if (wanted == ";" || type == "}" || type == ")" || type == "]") return pass(); + else return cont(exp); + }; + return exp; + } + + function statement(type, value) { + if (type == "var") return cont(pushlex("vardef", value), vardef, expect(";"), poplex); + if (type == "keyword a") return cont(pushlex("form"), parenExpr, statement, poplex); + if (type == "keyword b") return cont(pushlex("form"), statement, poplex); + if (type == "keyword d") return cx.stream.match(/^\s*$/, false) ? cont() : cont(pushlex("stat"), maybeexpression, expect(";"), poplex); + if (type == "debugger") return cont(expect(";")); + if (type == "{") return cont(pushlex("}"), pushblockcontext, block, poplex, popcontext); + if (type == ";") return cont(); + if (type == "if") { + if (cx.state.lexical.info == "else" && cx.state.cc[cx.state.cc.length - 1] == poplex) + cx.state.cc.pop()(); + return cont(pushlex("form"), parenExpr, statement, poplex, maybeelse); + } + if (type == "function") return cont(functiondef); + if (type == "for") return cont(pushlex("form"), forspec, statement, poplex); + if (type == "class" || (isTS && value == "interface")) { + cx.marked = "keyword" + return cont(pushlex("form", type == "class" ? type : value), className, poplex) + } + if (type == "variable") { + if (isTS && value == "declare") { + cx.marked = "keyword" + return cont(statement) + } else if (isTS && (value == "module" || value == "enum" || value == "type") && cx.stream.match(/^\s*\w/, false)) { + cx.marked = "keyword" + if (value == "enum") return cont(enumdef); + else if (value == "type") return cont(typename, expect("operator"), typeexpr, expect(";")); + else return cont(pushlex("form"), pattern, expect("{"), pushlex("}"), block, poplex, poplex) + } else if (isTS && value == "namespace") { + cx.marked = "keyword" + return cont(pushlex("form"), expression, statement, poplex) + } else if (isTS && value == "abstract") { + cx.marked = "keyword" + return cont(statement) + } else { + return cont(pushlex("stat"), maybelabel); + } + } + if (type == "switch") return cont(pushlex("form"), parenExpr, expect("{"), pushlex("}", "switch"), pushblockcontext, + block, poplex, poplex, popcontext); + if (type == "case") return cont(expression, expect(":")); + if (type == "default") return cont(expect(":")); + if (type == "catch") return cont(pushlex("form"), pushcontext, maybeCatchBinding, statement, poplex, popcontext); + if (type == "export") return cont(pushlex("stat"), afterExport, poplex); + if (type == "import") return cont(pushlex("stat"), afterImport, poplex); + if (type == "async") return cont(statement) + if (value == "@") return cont(expression, statement) + return pass(pushlex("stat"), expression, expect(";"), poplex); + } + function maybeCatchBinding(type) { + if (type == "(") return cont(funarg, expect(")")) + } + function expression(type, value) { + return expressionInner(type, value, false); + } + function expressionNoComma(type, value) { + return expressionInner(type, value, true); + } + function parenExpr(type) { + if (type != "(") return pass() + return cont(pushlex(")"), maybeexpression, expect(")"), poplex) + } + function expressionInner(type, value, noComma) { + if (cx.state.fatArrowAt == cx.stream.start) { + var body = noComma ? arrowBodyNoComma : arrowBody; + if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, expect("=>"), body, popcontext); + else if (type == "variable") return pass(pushcontext, pattern, expect("=>"), body, popcontext); + } + + var maybeop = noComma ? maybeoperatorNoComma : maybeoperatorComma; + if (atomicTypes.hasOwnProperty(type)) return cont(maybeop); + if (type == "function") return cont(functiondef, maybeop); + if (type == "class" || (isTS && value == "interface")) { cx.marked = "keyword"; return cont(pushlex("form"), classExpression, poplex); } + if (type == "keyword c" || type == "async") return cont(noComma ? expressionNoComma : expression); + if (type == "(") return cont(pushlex(")"), maybeexpression, expect(")"), poplex, maybeop); + if (type == "operator" || type == "spread") return cont(noComma ? expressionNoComma : expression); + if (type == "[") return cont(pushlex("]"), arrayLiteral, poplex, maybeop); + if (type == "{") return contCommasep(objprop, "}", null, maybeop); + if (type == "quasi") return pass(quasi, maybeop); + if (type == "new") return cont(maybeTarget(noComma)); + if (type == "import") return cont(expression); + return cont(); + } + function maybeexpression(type) { + if (type.match(/[;\}\)\],]/)) return pass(); + return pass(expression); + } + + function maybeoperatorComma(type, value) { + if (type == ",") return cont(maybeexpression); + return maybeoperatorNoComma(type, value, false); + } + function maybeoperatorNoComma(type, value, noComma) { + var me = noComma == false ? maybeoperatorComma : maybeoperatorNoComma; + var expr = noComma == false ? expression : expressionNoComma; + if (type == "=>") return cont(pushcontext, noComma ? arrowBodyNoComma : arrowBody, popcontext); + if (type == "operator") { + if (/\+\+|--/.test(value) || isTS && value == "!") return cont(me); + if (isTS && value == "<" && cx.stream.match(/^([^<>]|<[^<>]*>)*>\s*\(/, false)) + return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, me); + if (value == "?") return cont(expression, expect(":"), expr); + return cont(expr); + } + if (type == "quasi") { return pass(quasi, me); } + if (type == ";") return; + if (type == "(") return contCommasep(expressionNoComma, ")", "call", me); + if (type == ".") return cont(property, me); + if (type == "[") return cont(pushlex("]"), maybeexpression, expect("]"), poplex, me); + if (isTS && value == "as") { cx.marked = "keyword"; return cont(typeexpr, me) } + if (type == "regexp") { + cx.state.lastType = cx.marked = "operator" + cx.stream.backUp(cx.stream.pos - cx.stream.start - 1) + return cont(expr) + } + } + function quasi(type, value) { + if (type != "quasi") return pass(); + if (value.slice(value.length - 2) != "${") return cont(quasi); + return cont(expression, continueQuasi); + } + function continueQuasi(type) { + if (type == "}") { + cx.marked = "string-2"; + cx.state.tokenize = tokenQuasi; + return cont(quasi); + } + } + function arrowBody(type) { + findFatArrow(cx.stream, cx.state); + return pass(type == "{" ? statement : expression); + } + function arrowBodyNoComma(type) { + findFatArrow(cx.stream, cx.state); + return pass(type == "{" ? statement : expressionNoComma); + } + function maybeTarget(noComma) { + return function(type) { + if (type == ".") return cont(noComma ? targetNoComma : target); + else if (type == "variable" && isTS) return cont(maybeTypeArgs, noComma ? maybeoperatorNoComma : maybeoperatorComma) + else return pass(noComma ? expressionNoComma : expression); + }; + } + function target(_, value) { + if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorComma); } + } + function targetNoComma(_, value) { + if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorNoComma); } + } + function maybelabel(type) { + if (type == ":") return cont(poplex, statement); + return pass(maybeoperatorComma, expect(";"), poplex); + } + function property(type) { + if (type == "variable") {cx.marked = "property"; return cont();} + } + function objprop(type, value) { + if (type == "async") { + cx.marked = "property"; + return cont(objprop); + } else if (type == "variable" || cx.style == "keyword") { + cx.marked = "property"; + if (value == "get" || value == "set") return cont(getterSetter); + var m // Work around fat-arrow-detection complication for detecting typescript typed arrow params + if (isTS && cx.state.fatArrowAt == cx.stream.start && (m = cx.stream.match(/^\s*:\s*/, false))) + cx.state.fatArrowAt = cx.stream.pos + m[0].length + return cont(afterprop); + } else if (type == "number" || type == "string") { + cx.marked = jsonldMode ? "property" : (cx.style + " property"); + return cont(afterprop); + } else if (type == "jsonld-keyword") { + return cont(afterprop); + } else if (isTS && isModifier(value)) { + cx.marked = "keyword" + return cont(objprop) + } else if (type == "[") { + return cont(expression, maybetype, expect("]"), afterprop); + } else if (type == "spread") { + return cont(expressionNoComma, afterprop); + } else if (value == "*") { + cx.marked = "keyword"; + return cont(objprop); + } else if (type == ":") { + return pass(afterprop) + } + } + function getterSetter(type) { + if (type != "variable") return pass(afterprop); + cx.marked = "property"; + return cont(functiondef); + } + function afterprop(type) { + if (type == ":") return cont(expressionNoComma); + if (type == "(") return pass(functiondef); + } + function commasep(what, end, sep) { + function proceed(type, value) { + if (sep ? sep.indexOf(type) > -1 : type == ",") { + var lex = cx.state.lexical; + if (lex.info == "call") lex.pos = (lex.pos || 0) + 1; + return cont(function(type, value) { + if (type == end || value == end) return pass() + return pass(what) + }, proceed); + } + if (type == end || value == end) return cont(); + if (sep && sep.indexOf(";") > -1) return pass(what) + return cont(expect(end)); + } + return function(type, value) { + if (type == end || value == end) return cont(); + return pass(what, proceed); + }; + } + function contCommasep(what, end, info) { + for (var i = 3; i < arguments.length; i++) + cx.cc.push(arguments[i]); + return cont(pushlex(end, info), commasep(what, end), poplex); + } + function block(type) { + if (type == "}") return cont(); + return pass(statement, block); + } + function maybetype(type, value) { + if (isTS) { + if (type == ":") return cont(typeexpr); + if (value == "?") return cont(maybetype); + } + } + function maybetypeOrIn(type, value) { + if (isTS && (type == ":" || value == "in")) return cont(typeexpr) + } + function mayberettype(type) { + if (isTS && type == ":") { + if (cx.stream.match(/^\s*\w+\s+is\b/, false)) return cont(expression, isKW, typeexpr) + else return cont(typeexpr) + } + } + function isKW(_, value) { + if (value == "is") { + cx.marked = "keyword" + return cont() + } + } + function typeexpr(type, value) { + if (value == "keyof" || value == "typeof" || value == "infer") { + cx.marked = "keyword" + return cont(value == "typeof" ? expressionNoComma : typeexpr) + } + if (type == "variable" || value == "void") { + cx.marked = "type" + return cont(afterType) + } + if (value == "|" || value == "&") return cont(typeexpr) + if (type == "string" || type == "number" || type == "atom") return cont(afterType); + if (type == "[") return cont(pushlex("]"), commasep(typeexpr, "]", ","), poplex, afterType) + if (type == "{") return cont(pushlex("}"), commasep(typeprop, "}", ",;"), poplex, afterType) + if (type == "(") return cont(commasep(typearg, ")"), maybeReturnType, afterType) + if (type == "<") return cont(commasep(typeexpr, ">"), typeexpr) + } + function maybeReturnType(type) { + if (type == "=>") return cont(typeexpr) + } + function typeprop(type, value) { + if (type == "variable" || cx.style == "keyword") { + cx.marked = "property" + return cont(typeprop) + } else if (value == "?" || type == "number" || type == "string") { + return cont(typeprop) + } else if (type == ":") { + return cont(typeexpr) + } else if (type == "[") { + return cont(expect("variable"), maybetypeOrIn, expect("]"), typeprop) + } else if (type == "(") { + return pass(functiondecl, typeprop) + } + } + function typearg(type, value) { + if (type == "variable" && cx.stream.match(/^\s*[?:]/, false) || value == "?") return cont(typearg) + if (type == ":") return cont(typeexpr) + if (type == "spread") return cont(typearg) + return pass(typeexpr) + } + function afterType(type, value) { + if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType) + if (value == "|" || type == "." || value == "&") return cont(typeexpr) + if (type == "[") return cont(typeexpr, expect("]"), afterType) + if (value == "extends" || value == "implements") { cx.marked = "keyword"; return cont(typeexpr) } + if (value == "?") return cont(typeexpr, expect(":"), typeexpr) + } + function maybeTypeArgs(_, value) { + if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType) + } + function typeparam() { + return pass(typeexpr, maybeTypeDefault) + } + function maybeTypeDefault(_, value) { + if (value == "=") return cont(typeexpr) + } + function vardef(_, value) { + if (value == "enum") {cx.marked = "keyword"; return cont(enumdef)} + return pass(pattern, maybetype, maybeAssign, vardefCont); + } + function pattern(type, value) { + if (isTS && isModifier(value)) { cx.marked = "keyword"; return cont(pattern) } + if (type == "variable") { register(value); return cont(); } + if (type == "spread") return cont(pattern); + if (type == "[") return contCommasep(eltpattern, "]"); + if (type == "{") return contCommasep(proppattern, "}"); + } + function proppattern(type, value) { + if (type == "variable" && !cx.stream.match(/^\s*:/, false)) { + register(value); + return cont(maybeAssign); + } + if (type == "variable") cx.marked = "property"; + if (type == "spread") return cont(pattern); + if (type == "}") return pass(); + if (type == "[") return cont(expression, expect(']'), expect(':'), proppattern); + return cont(expect(":"), pattern, maybeAssign); + } + function eltpattern() { + return pass(pattern, maybeAssign) + } + function maybeAssign(_type, value) { + if (value == "=") return cont(expressionNoComma); + } + function vardefCont(type) { + if (type == ",") return cont(vardef); + } + function maybeelse(type, value) { + if (type == "keyword b" && value == "else") return cont(pushlex("form", "else"), statement, poplex); + } + function forspec(type, value) { + if (value == "await") return cont(forspec); + if (type == "(") return cont(pushlex(")"), forspec1, poplex); + } + function forspec1(type) { + if (type == "var") return cont(vardef, forspec2); + if (type == "variable") return cont(forspec2); + return pass(forspec2) + } + function forspec2(type, value) { + if (type == ")") return cont() + if (type == ";") return cont(forspec2) + if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression, forspec2) } + return pass(expression, forspec2) + } + function functiondef(type, value) { + if (value == "*") {cx.marked = "keyword"; return cont(functiondef);} + if (type == "variable") {register(value); return cont(functiondef);} + if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, mayberettype, statement, popcontext); + if (isTS && value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, functiondef) + } + function functiondecl(type, value) { + if (value == "*") {cx.marked = "keyword"; return cont(functiondecl);} + if (type == "variable") {register(value); return cont(functiondecl);} + if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, mayberettype, popcontext); + if (isTS && value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, functiondecl) + } + function typename(type, value) { + if (type == "keyword" || type == "variable") { + cx.marked = "type" + return cont(typename) + } else if (value == "<") { + return cont(pushlex(">"), commasep(typeparam, ">"), poplex) + } + } + function funarg(type, value) { + if (value == "@") cont(expression, funarg) + if (type == "spread") return cont(funarg); + if (isTS && isModifier(value)) { cx.marked = "keyword"; return cont(funarg); } + if (isTS && type == "this") return cont(maybetype, maybeAssign) + return pass(pattern, maybetype, maybeAssign); + } + function classExpression(type, value) { + // Class expressions may have an optional name. + if (type == "variable") return className(type, value); + return classNameAfter(type, value); + } + function className(type, value) { + if (type == "variable") {register(value); return cont(classNameAfter);} + } + function classNameAfter(type, value) { + if (value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, classNameAfter) + if (value == "extends" || value == "implements" || (isTS && type == ",")) { + if (value == "implements") cx.marked = "keyword"; + return cont(isTS ? typeexpr : expression, classNameAfter); + } + if (type == "{") return cont(pushlex("}"), classBody, poplex); + } + function classBody(type, value) { + if (type == "async" || + (type == "variable" && + (value == "static" || value == "get" || value == "set" || (isTS && isModifier(value))) && + cx.stream.match(/^\s+[\w$\xa1-\uffff]/, false))) { + cx.marked = "keyword"; + return cont(classBody); + } + if (type == "variable" || cx.style == "keyword") { + cx.marked = "property"; + return cont(classfield, classBody); + } + if (type == "number" || type == "string") return cont(classfield, classBody); + if (type == "[") + return cont(expression, maybetype, expect("]"), classfield, classBody) + if (value == "*") { + cx.marked = "keyword"; + return cont(classBody); + } + if (isTS && type == "(") return pass(functiondecl, classBody) + if (type == ";" || type == ",") return cont(classBody); + if (type == "}") return cont(); + if (value == "@") return cont(expression, classBody) + } + function classfield(type, value) { + if (value == "?") return cont(classfield) + if (type == ":") return cont(typeexpr, maybeAssign) + if (value == "=") return cont(expressionNoComma) + var context = cx.state.lexical.prev, isInterface = context && context.info == "interface" + return pass(isInterface ? functiondecl : functiondef) + } + function afterExport(type, value) { + if (value == "*") { cx.marked = "keyword"; return cont(maybeFrom, expect(";")); } + if (value == "default") { cx.marked = "keyword"; return cont(expression, expect(";")); } + if (type == "{") return cont(commasep(exportField, "}"), maybeFrom, expect(";")); + return pass(statement); + } + function exportField(type, value) { + if (value == "as") { cx.marked = "keyword"; return cont(expect("variable")); } + if (type == "variable") return pass(expressionNoComma, exportField); + } + function afterImport(type) { + if (type == "string") return cont(); + if (type == "(") return pass(expression); + return pass(importSpec, maybeMoreImports, maybeFrom); + } + function importSpec(type, value) { + if (type == "{") return contCommasep(importSpec, "}"); + if (type == "variable") register(value); + if (value == "*") cx.marked = "keyword"; + return cont(maybeAs); + } + function maybeMoreImports(type) { + if (type == ",") return cont(importSpec, maybeMoreImports) + } + function maybeAs(_type, value) { + if (value == "as") { cx.marked = "keyword"; return cont(importSpec); } + } + function maybeFrom(_type, value) { + if (value == "from") { cx.marked = "keyword"; return cont(expression); } + } + function arrayLiteral(type) { + if (type == "]") return cont(); + return pass(commasep(expressionNoComma, "]")); + } + function enumdef() { + return pass(pushlex("form"), pattern, expect("{"), pushlex("}"), commasep(enummember, "}"), poplex, poplex) + } + function enummember() { + return pass(pattern, maybeAssign); + } + + function isContinuedStatement(state, textAfter) { + return state.lastType == "operator" || state.lastType == "," || + isOperatorChar.test(textAfter.charAt(0)) || + /[,.]/.test(textAfter.charAt(0)); + } + + function expressionAllowed(stream, state, backUp) { + return state.tokenize == tokenBase && + /^(?:operator|sof|keyword [bcd]|case|new|export|default|spread|[\[{}\(,;:]|=>)$/.test(state.lastType) || + (state.lastType == "quasi" && /\{\s*$/.test(stream.string.slice(0, stream.pos - (backUp || 0)))) + } + + // Interface + + return { + startState: function(basecolumn) { + var state = { + tokenize: tokenBase, + lastType: "sof", + cc: [], + lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false), + localVars: parserConfig.localVars, + context: parserConfig.localVars && new Context(null, null, false), + indented: basecolumn || 0 + }; + if (parserConfig.globalVars && typeof parserConfig.globalVars == "object") + state.globalVars = parserConfig.globalVars; + return state; + }, + + token: function(stream, state) { + if (stream.sol()) { + if (!state.lexical.hasOwnProperty("align")) + state.lexical.align = false; + state.indented = stream.indentation(); + findFatArrow(stream, state); + } + if (state.tokenize != tokenComment && stream.eatSpace()) return null; + var style = state.tokenize(stream, state); + if (type == "comment") return style; + state.lastType = type == "operator" && (content == "++" || content == "--") ? "incdec" : type; + return parseJS(state, style, type, content, stream); + }, + + indent: function(state, textAfter) { + if (state.tokenize == tokenComment) return CodeMirror.Pass; + if (state.tokenize != tokenBase) return 0; + var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical, top + // Kludge to prevent 'maybelse' from blocking lexical scope pops + if (!/^\s*else\b/.test(textAfter)) for (var i = state.cc.length - 1; i >= 0; --i) { + var c = state.cc[i]; + if (c == poplex) lexical = lexical.prev; + else if (c != maybeelse) break; + } + while ((lexical.type == "stat" || lexical.type == "form") && + (firstChar == "}" || ((top = state.cc[state.cc.length - 1]) && + (top == maybeoperatorComma || top == maybeoperatorNoComma) && + !/^[,\.=+\-*:?[\(]/.test(textAfter)))) + lexical = lexical.prev; + if (statementIndent && lexical.type == ")" && lexical.prev.type == "stat") + lexical = lexical.prev; + var type = lexical.type, closing = firstChar == type; + + if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info.length + 1 : 0); + else if (type == "form" && firstChar == "{") return lexical.indented; + else if (type == "form") return lexical.indented + indentUnit; + else if (type == "stat") + return lexical.indented + (isContinuedStatement(state, textAfter) ? statementIndent || indentUnit : 0); + else if (lexical.info == "switch" && !closing && parserConfig.doubleIndentSwitch != false) + return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit); + else if (lexical.align) return lexical.column + (closing ? 0 : 1); + else return lexical.indented + (closing ? 0 : indentUnit); + }, + + electricInput: /^\s*(?:case .*?:|default:|\{|\})$/, + blockCommentStart: jsonMode ? null : "/*", + blockCommentEnd: jsonMode ? null : "*/", + blockCommentContinue: jsonMode ? null : " * ", + lineComment: jsonMode ? null : "//", + fold: "brace", + closeBrackets: "()[]{}''\"\"``", + + helperType: jsonMode ? "json" : "javascript", + jsonldMode: jsonldMode, + jsonMode: jsonMode, + + expressionAllowed: expressionAllowed, + + skipExpression: function(state) { + var top = state.cc[state.cc.length - 1] + if (top == expression || top == expressionNoComma) state.cc.pop() + } + }; +}); + +CodeMirror.registerHelper("wordChars", "javascript", /[\w$]/); + +CodeMirror.defineMIME("text/javascript", "javascript"); +CodeMirror.defineMIME("text/ecmascript", "javascript"); +CodeMirror.defineMIME("application/javascript", "javascript"); +CodeMirror.defineMIME("application/x-javascript", "javascript"); +CodeMirror.defineMIME("application/ecmascript", "javascript"); +CodeMirror.defineMIME("application/json", {name: "javascript", json: true}); +CodeMirror.defineMIME("application/x-json", {name: "javascript", json: true}); +CodeMirror.defineMIME("application/ld+json", {name: "javascript", jsonld: true}); +CodeMirror.defineMIME("text/typescript", { name: "javascript", typescript: true }); +CodeMirror.defineMIME("application/typescript", { name: "javascript", typescript: true }); + +}); diff --git a/play/js/d3/d3.js b/play/js/d3/d3.js new file mode 100644 index 00000000..8868e425 --- /dev/null +++ b/play/js/d3/d3.js @@ -0,0 +1,9504 @@ +!function() { + var d3 = { + version: "3.5.5" + }; + var d3_arraySlice = [].slice, d3_array = function(list) { + return d3_arraySlice.call(list); + }; + var d3_document = this.document; + function d3_documentElement(node) { + return node && (node.ownerDocument || node.document || node).documentElement; + } + function d3_window(node) { + return node && (node.ownerDocument && node.ownerDocument.defaultView || node.document && node || node.defaultView); + } + if (d3_document) { + try { + d3_array(d3_document.documentElement.childNodes)[0].nodeType; + } catch (e) { + d3_array = function(list) { + var i = list.length, array = new Array(i); + while (i--) array[i] = list[i]; + return array; + }; + } + } + if (!Date.now) Date.now = function() { + return +new Date(); + }; + if (d3_document) { + try { + d3_document.createElement("DIV").style.setProperty("opacity", 0, ""); + } catch (error) { + var d3_element_prototype = this.Element.prototype, d3_element_setAttribute = d3_element_prototype.setAttribute, d3_element_setAttributeNS = d3_element_prototype.setAttributeNS, d3_style_prototype = this.CSSStyleDeclaration.prototype, d3_style_setProperty = d3_style_prototype.setProperty; + d3_element_prototype.setAttribute = function(name, value) { + d3_element_setAttribute.call(this, name, value + ""); + }; + d3_element_prototype.setAttributeNS = function(space, local, value) { + d3_element_setAttributeNS.call(this, space, local, value + ""); + }; + d3_style_prototype.setProperty = function(name, value, priority) { + d3_style_setProperty.call(this, name, value + "", priority); + }; + } + } + d3.ascending = d3_ascending; + function d3_ascending(a, b) { + return a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN; + } + d3.descending = function(a, b) { + return b < a ? -1 : b > a ? 1 : b >= a ? 0 : NaN; + }; + d3.min = function(array, f) { + var i = -1, n = array.length, a, b; + if (arguments.length === 1) { + while (++i < n) if ((b = array[i]) != null && b >= b) { + a = b; + break; + } + while (++i < n) if ((b = array[i]) != null && a > b) a = b; + } else { + while (++i < n) if ((b = f.call(array, array[i], i)) != null && b >= b) { + a = b; + break; + } + while (++i < n) if ((b = f.call(array, array[i], i)) != null && a > b) a = b; + } + return a; + }; + d3.max = function(array, f) { + var i = -1, n = array.length, a, b; + if (arguments.length === 1) { + while (++i < n) if ((b = array[i]) != null && b >= b) { + a = b; + break; + } + while (++i < n) if ((b = array[i]) != null && b > a) a = b; + } else { + while (++i < n) if ((b = f.call(array, array[i], i)) != null && b >= b) { + a = b; + break; + } + while (++i < n) if ((b = f.call(array, array[i], i)) != null && b > a) a = b; + } + return a; + }; + d3.extent = function(array, f) { + var i = -1, n = array.length, a, b, c; + if (arguments.length === 1) { + while (++i < n) if ((b = array[i]) != null && b >= b) { + a = c = b; + break; + } + while (++i < n) if ((b = array[i]) != null) { + if (a > b) a = b; + if (c < b) c = b; + } + } else { + while (++i < n) if ((b = f.call(array, array[i], i)) != null && b >= b) { + a = c = b; + break; + } + while (++i < n) if ((b = f.call(array, array[i], i)) != null) { + if (a > b) a = b; + if (c < b) c = b; + } + } + return [ a, c ]; + }; + function d3_number(x) { + return x === null ? NaN : +x; + } + function d3_numeric(x) { + return !isNaN(x); + } + d3.sum = function(array, f) { + var s = 0, n = array.length, a, i = -1; + if (arguments.length === 1) { + while (++i < n) if (d3_numeric(a = +array[i])) s += a; + } else { + while (++i < n) if (d3_numeric(a = +f.call(array, array[i], i))) s += a; + } + return s; + }; + d3.mean = function(array, f) { + var s = 0, n = array.length, a, i = -1, j = n; + if (arguments.length === 1) { + while (++i < n) if (d3_numeric(a = d3_number(array[i]))) s += a; else --j; + } else { + while (++i < n) if (d3_numeric(a = d3_number(f.call(array, array[i], i)))) s += a; else --j; + } + if (j) return s / j; + }; + d3.quantile = function(values, p) { + var H = (values.length - 1) * p + 1, h = Math.floor(H), v = +values[h - 1], e = H - h; + return e ? v + e * (values[h] - v) : v; + }; + d3.median = function(array, f) { + var numbers = [], n = array.length, a, i = -1; + if (arguments.length === 1) { + while (++i < n) if (d3_numeric(a = d3_number(array[i]))) numbers.push(a); + } else { + while (++i < n) if (d3_numeric(a = d3_number(f.call(array, array[i], i)))) numbers.push(a); + } + if (numbers.length) return d3.quantile(numbers.sort(d3_ascending), .5); + }; + d3.variance = function(array, f) { + var n = array.length, m = 0, a, d, s = 0, i = -1, j = 0; + if (arguments.length === 1) { + while (++i < n) { + if (d3_numeric(a = d3_number(array[i]))) { + d = a - m; + m += d / ++j; + s += d * (a - m); + } + } + } else { + while (++i < n) { + if (d3_numeric(a = d3_number(f.call(array, array[i], i)))) { + d = a - m; + m += d / ++j; + s += d * (a - m); + } + } + } + if (j > 1) return s / (j - 1); + }; + d3.deviation = function() { + var v = d3.variance.apply(this, arguments); + return v ? Math.sqrt(v) : v; + }; + function d3_bisector(compare) { + return { + left: function(a, x, lo, hi) { + if (arguments.length < 3) lo = 0; + if (arguments.length < 4) hi = a.length; + while (lo < hi) { + var mid = lo + hi >>> 1; + if (compare(a[mid], x) < 0) lo = mid + 1; else hi = mid; + } + return lo; + }, + right: function(a, x, lo, hi) { + if (arguments.length < 3) lo = 0; + if (arguments.length < 4) hi = a.length; + while (lo < hi) { + var mid = lo + hi >>> 1; + if (compare(a[mid], x) > 0) hi = mid; else lo = mid + 1; + } + return lo; + } + }; + } + var d3_bisect = d3_bisector(d3_ascending); + d3.bisectLeft = d3_bisect.left; + d3.bisect = d3.bisectRight = d3_bisect.right; + d3.bisector = function(f) { + return d3_bisector(f.length === 1 ? function(d, x) { + return d3_ascending(f(d), x); + } : f); + }; + d3.shuffle = function(array, i0, i1) { + if ((m = arguments.length) < 3) { + i1 = array.length; + if (m < 2) i0 = 0; + } + var m = i1 - i0, t, i; + while (m) { + i = Math.random() * m-- | 0; + t = array[m + i0], array[m + i0] = array[i + i0], array[i + i0] = t; + } + return array; + }; + d3.permute = function(array, indexes) { + var i = indexes.length, permutes = new Array(i); + while (i--) permutes[i] = array[indexes[i]]; + return permutes; + }; + d3.pairs = function(array) { + var i = 0, n = array.length - 1, p0, p1 = array[0], pairs = new Array(n < 0 ? 0 : n); + while (i < n) pairs[i] = [ p0 = p1, p1 = array[++i] ]; + return pairs; + }; + d3.zip = function() { + if (!(n = arguments.length)) return []; + for (var i = -1, m = d3.min(arguments, d3_zipLength), zips = new Array(m); ++i < m; ) { + for (var j = -1, n, zip = zips[i] = new Array(n); ++j < n; ) { + zip[j] = arguments[j][i]; + } + } + return zips; + }; + function d3_zipLength(d) { + return d.length; + } + d3.transpose = function(matrix) { + return d3.zip.apply(d3, matrix); + }; + d3.keys = function(map) { + var keys = []; + for (var key in map) keys.push(key); + return keys; + }; + d3.values = function(map) { + var values = []; + for (var key in map) values.push(map[key]); + return values; + }; + d3.entries = function(map) { + var entries = []; + for (var key in map) entries.push({ + key: key, + value: map[key] + }); + return entries; + }; + d3.merge = function(arrays) { + var n = arrays.length, m, i = -1, j = 0, merged, array; + while (++i < n) j += arrays[i].length; + merged = new Array(j); + while (--n >= 0) { + array = arrays[n]; + m = array.length; + while (--m >= 0) { + merged[--j] = array[m]; + } + } + return merged; + }; + var abs = Math.abs; + d3.range = function(start, stop, step) { + if (arguments.length < 3) { + step = 1; + if (arguments.length < 2) { + stop = start; + start = 0; + } + } + if ((stop - start) / step === Infinity) throw new Error("infinite range"); + var range = [], k = d3_range_integerScale(abs(step)), i = -1, j; + start *= k, stop *= k, step *= k; + if (step < 0) while ((j = start + step * ++i) > stop) range.push(j / k); else while ((j = start + step * ++i) < stop) range.push(j / k); + return range; + }; + function d3_range_integerScale(x) { + var k = 1; + while (x * k % 1) k *= 10; + return k; + } + function d3_class(ctor, properties) { + for (var key in properties) { + Object.defineProperty(ctor.prototype, key, { + value: properties[key], + enumerable: false + }); + } + } + d3.map = function(object, f) { + var map = new d3_Map(); + if (object instanceof d3_Map) { + object.forEach(function(key, value) { + map.set(key, value); + }); + } else if (Array.isArray(object)) { + var i = -1, n = object.length, o; + if (arguments.length === 1) while (++i < n) map.set(i, object[i]); else while (++i < n) map.set(f.call(object, o = object[i], i), o); + } else { + for (var key in object) map.set(key, object[key]); + } + return map; + }; + function d3_Map() { + this._ = Object.create(null); + } + var d3_map_proto = "__proto__", d3_map_zero = "\x00"; + d3_class(d3_Map, { + has: d3_map_has, + get: function(key) { + return this._[d3_map_escape(key)]; + }, + set: function(key, value) { + return this._[d3_map_escape(key)] = value; + }, + remove: d3_map_remove, + keys: d3_map_keys, + values: function() { + var values = []; + for (var key in this._) values.push(this._[key]); + return values; + }, + entries: function() { + var entries = []; + for (var key in this._) entries.push({ + key: d3_map_unescape(key), + value: this._[key] + }); + return entries; + }, + size: d3_map_size, + empty: d3_map_empty, + forEach: function(f) { + for (var key in this._) f.call(this, d3_map_unescape(key), this._[key]); + } + }); + function d3_map_escape(key) { + return (key += "") === d3_map_proto || key[0] === d3_map_zero ? d3_map_zero + key : key; + } + function d3_map_unescape(key) { + return (key += "")[0] === d3_map_zero ? key.slice(1) : key; + } + function d3_map_has(key) { + return d3_map_escape(key) in this._; + } + function d3_map_remove(key) { + return (key = d3_map_escape(key)) in this._ && delete this._[key]; + } + function d3_map_keys() { + var keys = []; + for (var key in this._) keys.push(d3_map_unescape(key)); + return keys; + } + function d3_map_size() { + var size = 0; + for (var key in this._) ++size; + return size; + } + function d3_map_empty() { + for (var key in this._) return false; + return true; + } + d3.nest = function() { + var nest = {}, keys = [], sortKeys = [], sortValues, rollup; + function map(mapType, array, depth) { + if (depth >= keys.length) return rollup ? rollup.call(nest, array) : sortValues ? array.sort(sortValues) : array; + var i = -1, n = array.length, key = keys[depth++], keyValue, object, setter, valuesByKey = new d3_Map(), values; + while (++i < n) { + if (values = valuesByKey.get(keyValue = key(object = array[i]))) { + values.push(object); + } else { + valuesByKey.set(keyValue, [ object ]); + } + } + if (mapType) { + object = mapType(); + setter = function(keyValue, values) { + object.set(keyValue, map(mapType, values, depth)); + }; + } else { + object = {}; + setter = function(keyValue, values) { + object[keyValue] = map(mapType, values, depth); + }; + } + valuesByKey.forEach(setter); + return object; + } + function entries(map, depth) { + if (depth >= keys.length) return map; + var array = [], sortKey = sortKeys[depth++]; + map.forEach(function(key, keyMap) { + array.push({ + key: key, + values: entries(keyMap, depth) + }); + }); + return sortKey ? array.sort(function(a, b) { + return sortKey(a.key, b.key); + }) : array; + } + nest.map = function(array, mapType) { + return map(mapType, array, 0); + }; + nest.entries = function(array) { + return entries(map(d3.map, array, 0), 0); + }; + nest.key = function(d) { + keys.push(d); + return nest; + }; + nest.sortKeys = function(order) { + sortKeys[keys.length - 1] = order; + return nest; + }; + nest.sortValues = function(order) { + sortValues = order; + return nest; + }; + nest.rollup = function(f) { + rollup = f; + return nest; + }; + return nest; + }; + d3.set = function(array) { + var set = new d3_Set(); + if (array) for (var i = 0, n = array.length; i < n; ++i) set.add(array[i]); + return set; + }; + function d3_Set() { + this._ = Object.create(null); + } + d3_class(d3_Set, { + has: d3_map_has, + add: function(key) { + this._[d3_map_escape(key += "")] = true; + return key; + }, + remove: d3_map_remove, + values: d3_map_keys, + size: d3_map_size, + empty: d3_map_empty, + forEach: function(f) { + for (var key in this._) f.call(this, d3_map_unescape(key)); + } + }); + d3.behavior = {}; + function d3_identity(d) { + return d; + } + d3.rebind = function(target, source) { + var i = 1, n = arguments.length, method; + while (++i < n) target[method = arguments[i]] = d3_rebind(target, source, source[method]); + return target; + }; + function d3_rebind(target, source, method) { + return function() { + var value = method.apply(source, arguments); + return value === source ? target : value; + }; + } + function d3_vendorSymbol(object, name) { + if (name in object) return name; + name = name.charAt(0).toUpperCase() + name.slice(1); + for (var i = 0, n = d3_vendorPrefixes.length; i < n; ++i) { + var prefixName = d3_vendorPrefixes[i] + name; + if (prefixName in object) return prefixName; + } + } + var d3_vendorPrefixes = [ "webkit", "ms", "moz", "Moz", "o", "O" ]; + function d3_noop() {} + d3.dispatch = function() { + var dispatch = new d3_dispatch(), i = -1, n = arguments.length; + while (++i < n) dispatch[arguments[i]] = d3_dispatch_event(dispatch); + return dispatch; + }; + function d3_dispatch() {} + d3_dispatch.prototype.on = function(type, listener) { + var i = type.indexOf("."), name = ""; + if (i >= 0) { + name = type.slice(i + 1); + type = type.slice(0, i); + } + if (type) return arguments.length < 2 ? this[type].on(name) : this[type].on(name, listener); + if (arguments.length === 2) { + if (listener == null) for (type in this) { + if (this.hasOwnProperty(type)) this[type].on(name, null); + } + return this; + } + }; + function d3_dispatch_event(dispatch) { + var listeners = [], listenerByName = new d3_Map(); + function event() { + var z = listeners, i = -1, n = z.length, l; + while (++i < n) if (l = z[i].on) l.apply(this, arguments); + return dispatch; + } + event.on = function(name, listener) { + var l = listenerByName.get(name), i; + if (arguments.length < 2) return l && l.on; + if (l) { + l.on = null; + listeners = listeners.slice(0, i = listeners.indexOf(l)).concat(listeners.slice(i + 1)); + listenerByName.remove(name); + } + if (listener) listeners.push(listenerByName.set(name, { + on: listener + })); + return dispatch; + }; + return event; + } + d3.event = null; + function d3_eventPreventDefault() { + d3.event.preventDefault(); + } + function d3_eventSource() { + var e = d3.event, s; + while (s = e.sourceEvent) e = s; + return e; + } + function d3_eventDispatch(target) { + var dispatch = new d3_dispatch(), i = 0, n = arguments.length; + while (++i < n) dispatch[arguments[i]] = d3_dispatch_event(dispatch); + dispatch.of = function(thiz, argumentz) { + return function(e1) { + try { + var e0 = e1.sourceEvent = d3.event; + e1.target = target; + d3.event = e1; + dispatch[e1.type].apply(thiz, argumentz); + } finally { + d3.event = e0; + } + }; + }; + return dispatch; + } + d3.requote = function(s) { + return s.replace(d3_requote_re, "\\$&"); + }; + var d3_requote_re = /[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g; + var d3_subclass = {}.__proto__ ? function(object, prototype) { + object.__proto__ = prototype; + } : function(object, prototype) { + for (var property in prototype) object[property] = prototype[property]; + }; + function d3_selection(groups) { + d3_subclass(groups, d3_selectionPrototype); + return groups; + } + var d3_select = function(s, n) { + return n.querySelector(s); + }, d3_selectAll = function(s, n) { + return n.querySelectorAll(s); + }, d3_selectMatches = function(n, s) { + var d3_selectMatcher = n.matches || n[d3_vendorSymbol(n, "matchesSelector")]; + d3_selectMatches = function(n, s) { + return d3_selectMatcher.call(n, s); + }; + return d3_selectMatches(n, s); + }; + if (typeof Sizzle === "function") { + d3_select = function(s, n) { + return Sizzle(s, n)[0] || null; + }; + d3_selectAll = Sizzle; + d3_selectMatches = Sizzle.matchesSelector; + } + d3.selection = function() { + return d3.select(d3_document.documentElement); + }; + var d3_selectionPrototype = d3.selection.prototype = []; + d3_selectionPrototype.select = function(selector) { + var subgroups = [], subgroup, subnode, group, node; + selector = d3_selection_selector(selector); + for (var j = -1, m = this.length; ++j < m; ) { + subgroups.push(subgroup = []); + subgroup.parentNode = (group = this[j]).parentNode; + for (var i = -1, n = group.length; ++i < n; ) { + if (node = group[i]) { + subgroup.push(subnode = selector.call(node, node.__data__, i, j)); + if (subnode && "__data__" in node) subnode.__data__ = node.__data__; + } else { + subgroup.push(null); + } + } + } + return d3_selection(subgroups); + }; + function d3_selection_selector(selector) { + return typeof selector === "function" ? selector : function() { + return d3_select(selector, this); + }; + } + d3_selectionPrototype.selectAll = function(selector) { + var subgroups = [], subgroup, node; + selector = d3_selection_selectorAll(selector); + for (var j = -1, m = this.length; ++j < m; ) { + for (var group = this[j], i = -1, n = group.length; ++i < n; ) { + if (node = group[i]) { + subgroups.push(subgroup = d3_array(selector.call(node, node.__data__, i, j))); + subgroup.parentNode = node; + } + } + } + return d3_selection(subgroups); + }; + function d3_selection_selectorAll(selector) { + return typeof selector === "function" ? selector : function() { + return d3_selectAll(selector, this); + }; + } + var d3_nsPrefix = { + svg: "http://www.w3.org/2000/svg", + xhtml: "http://www.w3.org/1999/xhtml", + xlink: "http://www.w3.org/1999/xlink", + xml: "http://www.w3.org/XML/1998/namespace", + xmlns: "http://www.w3.org/2000/xmlns/" + }; + d3.ns = { + prefix: d3_nsPrefix, + qualify: function(name) { + var i = name.indexOf(":"), prefix = name; + if (i >= 0) { + prefix = name.slice(0, i); + name = name.slice(i + 1); + } + return d3_nsPrefix.hasOwnProperty(prefix) ? { + space: d3_nsPrefix[prefix], + local: name + } : name; + } + }; + d3_selectionPrototype.attr = function(name, value) { + if (arguments.length < 2) { + if (typeof name === "string") { + var node = this.node(); + name = d3.ns.qualify(name); + return name.local ? node.getAttributeNS(name.space, name.local) : node.getAttribute(name); + } + for (value in name) this.each(d3_selection_attr(value, name[value])); + return this; + } + return this.each(d3_selection_attr(name, value)); + }; + function d3_selection_attr(name, value) { + name = d3.ns.qualify(name); + function attrNull() { + this.removeAttribute(name); + } + function attrNullNS() { + this.removeAttributeNS(name.space, name.local); + } + function attrConstant() { + this.setAttribute(name, value); + } + function attrConstantNS() { + this.setAttributeNS(name.space, name.local, value); + } + function attrFunction() { + var x = value.apply(this, arguments); + if (x == null) this.removeAttribute(name); else this.setAttribute(name, x); + } + function attrFunctionNS() { + var x = value.apply(this, arguments); + if (x == null) this.removeAttributeNS(name.space, name.local); else this.setAttributeNS(name.space, name.local, x); + } + return value == null ? name.local ? attrNullNS : attrNull : typeof value === "function" ? name.local ? attrFunctionNS : attrFunction : name.local ? attrConstantNS : attrConstant; + } + function d3_collapse(s) { + return s.trim().replace(/\s+/g, " "); + } + d3_selectionPrototype.classed = function(name, value) { + if (arguments.length < 2) { + if (typeof name === "string") { + var node = this.node(), n = (name = d3_selection_classes(name)).length, i = -1; + if (value = node.classList) { + while (++i < n) if (!value.contains(name[i])) return false; + } else { + value = node.getAttribute("class"); + while (++i < n) if (!d3_selection_classedRe(name[i]).test(value)) return false; + } + return true; + } + for (value in name) this.each(d3_selection_classed(value, name[value])); + return this; + } + return this.each(d3_selection_classed(name, value)); + }; + function d3_selection_classedRe(name) { + return new RegExp("(?:^|\\s+)" + d3.requote(name) + "(?:\\s+|$)", "g"); + } + function d3_selection_classes(name) { + return (name + "").trim().split(/^|\s+/); + } + function d3_selection_classed(name, value) { + name = d3_selection_classes(name).map(d3_selection_classedName); + var n = name.length; + function classedConstant() { + var i = -1; + while (++i < n) name[i](this, value); + } + function classedFunction() { + var i = -1, x = value.apply(this, arguments); + while (++i < n) name[i](this, x); + } + return typeof value === "function" ? classedFunction : classedConstant; + } + function d3_selection_classedName(name) { + var re = d3_selection_classedRe(name); + return function(node, value) { + if (c = node.classList) return value ? c.add(name) : c.remove(name); + var c = node.getAttribute("class") || ""; + if (value) { + re.lastIndex = 0; + if (!re.test(c)) node.setAttribute("class", d3_collapse(c + " " + name)); + } else { + node.setAttribute("class", d3_collapse(c.replace(re, " "))); + } + }; + } + d3_selectionPrototype.style = function(name, value, priority) { + var n = arguments.length; + if (n < 3) { + if (typeof name !== "string") { + if (n < 2) value = ""; + for (priority in name) this.each(d3_selection_style(priority, name[priority], value)); + return this; + } + if (n < 2) { + var node = this.node(); + return d3_window(node).getComputedStyle(node, null).getPropertyValue(name); + } + priority = ""; + } + return this.each(d3_selection_style(name, value, priority)); + }; + function d3_selection_style(name, value, priority) { + function styleNull() { + this.style.removeProperty(name); + } + function styleConstant() { + this.style.setProperty(name, value, priority); + } + function styleFunction() { + var x = value.apply(this, arguments); + if (x == null) this.style.removeProperty(name); else this.style.setProperty(name, x, priority); + } + return value == null ? styleNull : typeof value === "function" ? styleFunction : styleConstant; + } + d3_selectionPrototype.property = function(name, value) { + if (arguments.length < 2) { + if (typeof name === "string") return this.node()[name]; + for (value in name) this.each(d3_selection_property(value, name[value])); + return this; + } + return this.each(d3_selection_property(name, value)); + }; + function d3_selection_property(name, value) { + function propertyNull() { + delete this[name]; + } + function propertyConstant() { + this[name] = value; + } + function propertyFunction() { + var x = value.apply(this, arguments); + if (x == null) delete this[name]; else this[name] = x; + } + return value == null ? propertyNull : typeof value === "function" ? propertyFunction : propertyConstant; + } + d3_selectionPrototype.text = function(value) { + return arguments.length ? this.each(typeof value === "function" ? function() { + var v = value.apply(this, arguments); + this.textContent = v == null ? "" : v; + } : value == null ? function() { + this.textContent = ""; + } : function() { + this.textContent = value; + }) : this.node().textContent; + }; + d3_selectionPrototype.html = function(value) { + return arguments.length ? this.each(typeof value === "function" ? function() { + var v = value.apply(this, arguments); + this.innerHTML = v == null ? "" : v; + } : value == null ? function() { + this.innerHTML = ""; + } : function() { + this.innerHTML = value; + }) : this.node().innerHTML; + }; + d3_selectionPrototype.append = function(name) { + name = d3_selection_creator(name); + return this.select(function() { + return this.appendChild(name.apply(this, arguments)); + }); + }; + function d3_selection_creator(name) { + function create() { + var document = this.ownerDocument, namespace = this.namespaceURI; + return namespace ? document.createElementNS(namespace, name) : document.createElement(name); + } + function createNS() { + return this.ownerDocument.createElementNS(name.space, name.local); + } + return typeof name === "function" ? name : (name = d3.ns.qualify(name)).local ? createNS : create; + } + d3_selectionPrototype.insert = function(name, before) { + name = d3_selection_creator(name); + before = d3_selection_selector(before); + return this.select(function() { + return this.insertBefore(name.apply(this, arguments), before.apply(this, arguments) || null); + }); + }; + d3_selectionPrototype.remove = function() { + return this.each(d3_selectionRemove); + }; + function d3_selectionRemove() { + var parent = this.parentNode; + if (parent) parent.removeChild(this); + } + d3_selectionPrototype.data = function(value, key) { + var i = -1, n = this.length, group, node; + if (!arguments.length) { + value = new Array(n = (group = this[0]).length); + while (++i < n) { + if (node = group[i]) { + value[i] = node.__data__; + } + } + return value; + } + function bind(group, groupData) { + var i, n = group.length, m = groupData.length, n0 = Math.min(n, m), updateNodes = new Array(m), enterNodes = new Array(m), exitNodes = new Array(n), node, nodeData; + if (key) { + var nodeByKeyValue = new d3_Map(), keyValues = new Array(n), keyValue; + for (i = -1; ++i < n; ) { + if (nodeByKeyValue.has(keyValue = key.call(node = group[i], node.__data__, i))) { + exitNodes[i] = node; + } else { + nodeByKeyValue.set(keyValue, node); + } + keyValues[i] = keyValue; + } + for (i = -1; ++i < m; ) { + if (!(node = nodeByKeyValue.get(keyValue = key.call(groupData, nodeData = groupData[i], i)))) { + enterNodes[i] = d3_selection_dataNode(nodeData); + } else if (node !== true) { + updateNodes[i] = node; + node.__data__ = nodeData; + } + nodeByKeyValue.set(keyValue, true); + } + for (i = -1; ++i < n; ) { + if (nodeByKeyValue.get(keyValues[i]) !== true) { + exitNodes[i] = group[i]; + } + } + } else { + for (i = -1; ++i < n0; ) { + node = group[i]; + nodeData = groupData[i]; + if (node) { + node.__data__ = nodeData; + updateNodes[i] = node; + } else { + enterNodes[i] = d3_selection_dataNode(nodeData); + } + } + for (;i < m; ++i) { + enterNodes[i] = d3_selection_dataNode(groupData[i]); + } + for (;i < n; ++i) { + exitNodes[i] = group[i]; + } + } + enterNodes.update = updateNodes; + enterNodes.parentNode = updateNodes.parentNode = exitNodes.parentNode = group.parentNode; + enter.push(enterNodes); + update.push(updateNodes); + exit.push(exitNodes); + } + var enter = d3_selection_enter([]), update = d3_selection([]), exit = d3_selection([]); + if (typeof value === "function") { + while (++i < n) { + bind(group = this[i], value.call(group, group.parentNode.__data__, i)); + } + } else { + while (++i < n) { + bind(group = this[i], value); + } + } + update.enter = function() { + return enter; + }; + update.exit = function() { + return exit; + }; + return update; + }; + function d3_selection_dataNode(data) { + return { + __data__: data + }; + } + d3_selectionPrototype.datum = function(value) { + return arguments.length ? this.property("__data__", value) : this.property("__data__"); + }; + d3_selectionPrototype.filter = function(filter) { + var subgroups = [], subgroup, group, node; + if (typeof filter !== "function") filter = d3_selection_filter(filter); + for (var j = 0, m = this.length; j < m; j++) { + subgroups.push(subgroup = []); + subgroup.parentNode = (group = this[j]).parentNode; + for (var i = 0, n = group.length; i < n; i++) { + if ((node = group[i]) && filter.call(node, node.__data__, i, j)) { + subgroup.push(node); + } + } + } + return d3_selection(subgroups); + }; + function d3_selection_filter(selector) { + return function() { + return d3_selectMatches(this, selector); + }; + } + d3_selectionPrototype.order = function() { + for (var j = -1, m = this.length; ++j < m; ) { + for (var group = this[j], i = group.length - 1, next = group[i], node; --i >= 0; ) { + if (node = group[i]) { + if (next && next !== node.nextSibling) next.parentNode.insertBefore(node, next); + next = node; + } + } + } + return this; + }; + d3_selectionPrototype.sort = function(comparator) { + comparator = d3_selection_sortComparator.apply(this, arguments); + for (var j = -1, m = this.length; ++j < m; ) this[j].sort(comparator); + return this.order(); + }; + function d3_selection_sortComparator(comparator) { + if (!arguments.length) comparator = d3_ascending; + return function(a, b) { + return a && b ? comparator(a.__data__, b.__data__) : !a - !b; + }; + } + d3_selectionPrototype.each = function(callback) { + return d3_selection_each(this, function(node, i, j) { + callback.call(node, node.__data__, i, j); + }); + }; + function d3_selection_each(groups, callback) { + for (var j = 0, m = groups.length; j < m; j++) { + for (var group = groups[j], i = 0, n = group.length, node; i < n; i++) { + if (node = group[i]) callback(node, i, j); + } + } + return groups; + } + d3_selectionPrototype.call = function(callback) { + var args = d3_array(arguments); + callback.apply(args[0] = this, args); + return this; + }; + d3_selectionPrototype.empty = function() { + return !this.node(); + }; + d3_selectionPrototype.node = function() { + for (var j = 0, m = this.length; j < m; j++) { + for (var group = this[j], i = 0, n = group.length; i < n; i++) { + var node = group[i]; + if (node) return node; + } + } + return null; + }; + d3_selectionPrototype.size = function() { + var n = 0; + d3_selection_each(this, function() { + ++n; + }); + return n; + }; + function d3_selection_enter(selection) { + d3_subclass(selection, d3_selection_enterPrototype); + return selection; + } + var d3_selection_enterPrototype = []; + d3.selection.enter = d3_selection_enter; + d3.selection.enter.prototype = d3_selection_enterPrototype; + d3_selection_enterPrototype.append = d3_selectionPrototype.append; + d3_selection_enterPrototype.empty = d3_selectionPrototype.empty; + d3_selection_enterPrototype.node = d3_selectionPrototype.node; + d3_selection_enterPrototype.call = d3_selectionPrototype.call; + d3_selection_enterPrototype.size = d3_selectionPrototype.size; + d3_selection_enterPrototype.select = function(selector) { + var subgroups = [], subgroup, subnode, upgroup, group, node; + for (var j = -1, m = this.length; ++j < m; ) { + upgroup = (group = this[j]).update; + subgroups.push(subgroup = []); + subgroup.parentNode = group.parentNode; + for (var i = -1, n = group.length; ++i < n; ) { + if (node = group[i]) { + subgroup.push(upgroup[i] = subnode = selector.call(group.parentNode, node.__data__, i, j)); + subnode.__data__ = node.__data__; + } else { + subgroup.push(null); + } + } + } + return d3_selection(subgroups); + }; + d3_selection_enterPrototype.insert = function(name, before) { + if (arguments.length < 2) before = d3_selection_enterInsertBefore(this); + return d3_selectionPrototype.insert.call(this, name, before); + }; + function d3_selection_enterInsertBefore(enter) { + var i0, j0; + return function(d, i, j) { + var group = enter[j].update, n = group.length, node; + if (j != j0) j0 = j, i0 = 0; + if (i >= i0) i0 = i + 1; + while (!(node = group[i0]) && ++i0 < n) ; + return node; + }; + } + d3.select = function(node) { + var group; + if (typeof node === "string") { + group = [ d3_select(node, d3_document) ]; + group.parentNode = d3_document.documentElement; + } else { + group = [ node ]; + group.parentNode = d3_documentElement(node); + } + return d3_selection([ group ]); + }; + d3.selectAll = function(nodes) { + var group; + if (typeof nodes === "string") { + group = d3_array(d3_selectAll(nodes, d3_document)); + group.parentNode = d3_document.documentElement; + } else { + group = nodes; + group.parentNode = null; + } + return d3_selection([ group ]); + }; + d3_selectionPrototype.on = function(type, listener, capture) { + var n = arguments.length; + if (n < 3) { + if (typeof type !== "string") { + if (n < 2) listener = false; + for (capture in type) this.each(d3_selection_on(capture, type[capture], listener)); + return this; + } + if (n < 2) return (n = this.node()["__on" + type]) && n._; + capture = false; + } + return this.each(d3_selection_on(type, listener, capture)); + }; + function d3_selection_on(type, listener, capture) { + var name = "__on" + type, i = type.indexOf("."), wrap = d3_selection_onListener; + if (i > 0) type = type.slice(0, i); + var filter = d3_selection_onFilters.get(type); + if (filter) type = filter, wrap = d3_selection_onFilter; + function onRemove() { + var l = this[name]; + if (l) { + this.removeEventListener(type, l, l.$); + delete this[name]; + } + } + function onAdd() { + var l = wrap(listener, d3_array(arguments)); + onRemove.call(this); + this.addEventListener(type, this[name] = l, l.$ = capture); + l._ = listener; + } + function removeAll() { + var re = new RegExp("^__on([^.]+)" + d3.requote(type) + "$"), match; + for (var name in this) { + if (match = name.match(re)) { + var l = this[name]; + this.removeEventListener(match[1], l, l.$); + delete this[name]; + } + } + } + return i ? listener ? onAdd : onRemove : listener ? d3_noop : removeAll; + } + var d3_selection_onFilters = d3.map({ + mouseenter: "mouseover", + mouseleave: "mouseout" + }); + if (d3_document) { + d3_selection_onFilters.forEach(function(k) { + if ("on" + k in d3_document) d3_selection_onFilters.remove(k); + }); + } + function d3_selection_onListener(listener, argumentz) { + return function(e) { + var o = d3.event; + d3.event = e; + argumentz[0] = this.__data__; + try { + listener.apply(this, argumentz); + } finally { + d3.event = o; + } + }; + } + function d3_selection_onFilter(listener, argumentz) { + var l = d3_selection_onListener(listener, argumentz); + return function(e) { + var target = this, related = e.relatedTarget; + if (!related || related !== target && !(related.compareDocumentPosition(target) & 8)) { + l.call(target, e); + } + }; + } + var d3_event_dragSelect, d3_event_dragId = 0; + function d3_event_dragSuppress(node) { + var name = ".dragsuppress-" + ++d3_event_dragId, click = "click" + name, w = d3.select(d3_window(node)).on("touchmove" + name, d3_eventPreventDefault).on("dragstart" + name, d3_eventPreventDefault).on("selectstart" + name, d3_eventPreventDefault); + if (d3_event_dragSelect == null) { + d3_event_dragSelect = "onselectstart" in node ? false : d3_vendorSymbol(node.style, "userSelect"); + } + if (d3_event_dragSelect) { + var style = d3_documentElement(node).style, select = style[d3_event_dragSelect]; + style[d3_event_dragSelect] = "none"; + } + return function(suppressClick) { + w.on(name, null); + if (d3_event_dragSelect) style[d3_event_dragSelect] = select; + if (suppressClick) { + var off = function() { + w.on(click, null); + }; + w.on(click, function() { + d3_eventPreventDefault(); + off(); + }, true); + setTimeout(off, 0); + } + }; + } + d3.mouse = function(container) { + return d3_mousePoint(container, d3_eventSource()); + }; + var d3_mouse_bug44083 = this.navigator && /WebKit/.test(this.navigator.userAgent) ? -1 : 0; + function d3_mousePoint(container, e) { + if (e.changedTouches) e = e.changedTouches[0]; + var svg = container.ownerSVGElement || container; + if (svg.createSVGPoint) { + var point = svg.createSVGPoint(); + if (d3_mouse_bug44083 < 0) { + var window = d3_window(container); + if (window.scrollX || window.scrollY) { + svg = d3.select("body").append("svg").style({ + position: "absolute", + top: 0, + left: 0, + margin: 0, + padding: 0, + border: "none" + }, "important"); + var ctm = svg[0][0].getScreenCTM(); + d3_mouse_bug44083 = !(ctm.f || ctm.e); + svg.remove(); + } + } + if (d3_mouse_bug44083) point.x = e.pageX, point.y = e.pageY; else point.x = e.clientX, + point.y = e.clientY; + point = point.matrixTransform(container.getScreenCTM().inverse()); + return [ point.x, point.y ]; + } + var rect = container.getBoundingClientRect(); + return [ e.clientX - rect.left - container.clientLeft, e.clientY - rect.top - container.clientTop ]; + } + d3.touch = function(container, touches, identifier) { + if (arguments.length < 3) identifier = touches, touches = d3_eventSource().changedTouches; + if (touches) for (var i = 0, n = touches.length, touch; i < n; ++i) { + if ((touch = touches[i]).identifier === identifier) { + return d3_mousePoint(container, touch); + } + } + }; + d3.behavior.drag = function() { + var event = d3_eventDispatch(drag, "drag", "dragstart", "dragend"), origin = null, mousedown = dragstart(d3_noop, d3.mouse, d3_window, "mousemove", "mouseup"), touchstart = dragstart(d3_behavior_dragTouchId, d3.touch, d3_identity, "touchmove", "touchend"); + function drag() { + this.on("mousedown.drag", mousedown).on("touchstart.drag", touchstart); + } + function dragstart(id, position, subject, move, end) { + return function() { + var that = this, target = d3.event.target, parent = that.parentNode, dispatch = event.of(that, arguments), dragged = 0, dragId = id(), dragName = ".drag" + (dragId == null ? "" : "-" + dragId), dragOffset, dragSubject = d3.select(subject(target)).on(move + dragName, moved).on(end + dragName, ended), dragRestore = d3_event_dragSuppress(target), position0 = position(parent, dragId); + if (origin) { + dragOffset = origin.apply(that, arguments); + dragOffset = [ dragOffset.x - position0[0], dragOffset.y - position0[1] ]; + } else { + dragOffset = [ 0, 0 ]; + } + dispatch({ + type: "dragstart" + }); + function moved() { + var position1 = position(parent, dragId), dx, dy; + if (!position1) return; + dx = position1[0] - position0[0]; + dy = position1[1] - position0[1]; + dragged |= dx | dy; + position0 = position1; + dispatch({ + type: "drag", + x: position1[0] + dragOffset[0], + y: position1[1] + dragOffset[1], + dx: dx, + dy: dy + }); + } + function ended() { + if (!position(parent, dragId)) return; + dragSubject.on(move + dragName, null).on(end + dragName, null); + dragRestore(dragged && d3.event.target === target); + dispatch({ + type: "dragend" + }); + } + }; + } + drag.origin = function(x) { + if (!arguments.length) return origin; + origin = x; + return drag; + }; + return d3.rebind(drag, event, "on"); + }; + function d3_behavior_dragTouchId() { + return d3.event.changedTouches[0].identifier; + } + d3.touches = function(container, touches) { + if (arguments.length < 2) touches = d3_eventSource().touches; + return touches ? d3_array(touches).map(function(touch) { + var point = d3_mousePoint(container, touch); + point.identifier = touch.identifier; + return point; + }) : []; + }; + var ε = 1e-6, ε2 = ε * ε, π = Math.PI, τ = 2 * π, τε = τ - ε, halfπ = π / 2, d3_radians = π / 180, d3_degrees = 180 / π; + function d3_sgn(x) { + return x > 0 ? 1 : x < 0 ? -1 : 0; + } + function d3_cross2d(a, b, c) { + return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]); + } + function d3_acos(x) { + return x > 1 ? 0 : x < -1 ? π : Math.acos(x); + } + function d3_asin(x) { + return x > 1 ? halfπ : x < -1 ? -halfπ : Math.asin(x); + } + function d3_sinh(x) { + return ((x = Math.exp(x)) - 1 / x) / 2; + } + function d3_cosh(x) { + return ((x = Math.exp(x)) + 1 / x) / 2; + } + function d3_tanh(x) { + return ((x = Math.exp(2 * x)) - 1) / (x + 1); + } + function d3_haversin(x) { + return (x = Math.sin(x / 2)) * x; + } + var ρ = Math.SQRT2, ρ2 = 2, ρ4 = 4; + d3.interpolateZoom = function(p0, p1) { + var ux0 = p0[0], uy0 = p0[1], w0 = p0[2], ux1 = p1[0], uy1 = p1[1], w1 = p1[2]; + var dx = ux1 - ux0, dy = uy1 - uy0, d2 = dx * dx + dy * dy, d1 = Math.sqrt(d2), b0 = (w1 * w1 - w0 * w0 + ρ4 * d2) / (2 * w0 * ρ2 * d1), b1 = (w1 * w1 - w0 * w0 - ρ4 * d2) / (2 * w1 * ρ2 * d1), r0 = Math.log(Math.sqrt(b0 * b0 + 1) - b0), r1 = Math.log(Math.sqrt(b1 * b1 + 1) - b1), dr = r1 - r0, S = (dr || Math.log(w1 / w0)) / ρ; + function interpolate(t) { + var s = t * S; + if (dr) { + var coshr0 = d3_cosh(r0), u = w0 / (ρ2 * d1) * (coshr0 * d3_tanh(ρ * s + r0) - d3_sinh(r0)); + return [ ux0 + u * dx, uy0 + u * dy, w0 * coshr0 / d3_cosh(ρ * s + r0) ]; + } + return [ ux0 + t * dx, uy0 + t * dy, w0 * Math.exp(ρ * s) ]; + } + interpolate.duration = S * 1e3; + return interpolate; + }; + d3.behavior.zoom = function() { + var view = { + x: 0, + y: 0, + k: 1 + }, translate0, center0, center, size = [ 960, 500 ], scaleExtent = d3_behavior_zoomInfinity, duration = 250, zooming = 0, mousedown = "mousedown.zoom", mousemove = "mousemove.zoom", mouseup = "mouseup.zoom", mousewheelTimer, touchstart = "touchstart.zoom", touchtime, event = d3_eventDispatch(zoom, "zoomstart", "zoom", "zoomend"), x0, x1, y0, y1; + if (!d3_behavior_zoomWheel) { + d3_behavior_zoomWheel = "onwheel" in d3_document ? (d3_behavior_zoomDelta = function() { + return -d3.event.deltaY * (d3.event.deltaMode ? 120 : 1); + }, "wheel") : "onmousewheel" in d3_document ? (d3_behavior_zoomDelta = function() { + return d3.event.wheelDelta; + }, "mousewheel") : (d3_behavior_zoomDelta = function() { + return -d3.event.detail; + }, "MozMousePixelScroll"); + } + function zoom(g) { + g.on(mousedown, mousedowned).on(d3_behavior_zoomWheel + ".zoom", mousewheeled).on("dblclick.zoom", dblclicked).on(touchstart, touchstarted); + } + zoom.event = function(g) { + g.each(function() { + var dispatch = event.of(this, arguments), view1 = view; + if (d3_transitionInheritId) { + d3.select(this).transition().each("start.zoom", function() { + view = this.__chart__ || { + x: 0, + y: 0, + k: 1 + }; + zoomstarted(dispatch); + }).tween("zoom:zoom", function() { + var dx = size[0], dy = size[1], cx = center0 ? center0[0] : dx / 2, cy = center0 ? center0[1] : dy / 2, i = d3.interpolateZoom([ (cx - view.x) / view.k, (cy - view.y) / view.k, dx / view.k ], [ (cx - view1.x) / view1.k, (cy - view1.y) / view1.k, dx / view1.k ]); + return function(t) { + var l = i(t), k = dx / l[2]; + this.__chart__ = view = { + x: cx - l[0] * k, + y: cy - l[1] * k, + k: k + }; + zoomed(dispatch); + }; + }).each("interrupt.zoom", function() { + zoomended(dispatch); + }).each("end.zoom", function() { + zoomended(dispatch); + }); + } else { + this.__chart__ = view; + zoomstarted(dispatch); + zoomed(dispatch); + zoomended(dispatch); + } + }); + }; + zoom.translate = function(_) { + if (!arguments.length) return [ view.x, view.y ]; + view = { + x: +_[0], + y: +_[1], + k: view.k + }; + rescale(); + return zoom; + }; + zoom.scale = function(_) { + if (!arguments.length) return view.k; + view = { + x: view.x, + y: view.y, + k: +_ + }; + rescale(); + return zoom; + }; + zoom.scaleExtent = function(_) { + if (!arguments.length) return scaleExtent; + scaleExtent = _ == null ? d3_behavior_zoomInfinity : [ +_[0], +_[1] ]; + return zoom; + }; + zoom.center = function(_) { + if (!arguments.length) return center; + center = _ && [ +_[0], +_[1] ]; + return zoom; + }; + zoom.size = function(_) { + if (!arguments.length) return size; + size = _ && [ +_[0], +_[1] ]; + return zoom; + }; + zoom.duration = function(_) { + if (!arguments.length) return duration; + duration = +_; + return zoom; + }; + zoom.x = function(z) { + if (!arguments.length) return x1; + x1 = z; + x0 = z.copy(); + view = { + x: 0, + y: 0, + k: 1 + }; + return zoom; + }; + zoom.y = function(z) { + if (!arguments.length) return y1; + y1 = z; + y0 = z.copy(); + view = { + x: 0, + y: 0, + k: 1 + }; + return zoom; + }; + function location(p) { + return [ (p[0] - view.x) / view.k, (p[1] - view.y) / view.k ]; + } + function point(l) { + return [ l[0] * view.k + view.x, l[1] * view.k + view.y ]; + } + function scaleTo(s) { + view.k = Math.max(scaleExtent[0], Math.min(scaleExtent[1], s)); + } + function translateTo(p, l) { + l = point(l); + view.x += p[0] - l[0]; + view.y += p[1] - l[1]; + } + function zoomTo(that, p, l, k) { + that.__chart__ = { + x: view.x, + y: view.y, + k: view.k + }; + scaleTo(Math.pow(2, k)); + translateTo(center0 = p, l); + that = d3.select(that); + if (duration > 0) that = that.transition().duration(duration); + that.call(zoom.event); + } + function rescale() { + if (x1) x1.domain(x0.range().map(function(x) { + return (x - view.x) / view.k; + }).map(x0.invert)); + if (y1) y1.domain(y0.range().map(function(y) { + return (y - view.y) / view.k; + }).map(y0.invert)); + } + function zoomstarted(dispatch) { + if (!zooming++) dispatch({ + type: "zoomstart" + }); + } + function zoomed(dispatch) { + rescale(); + dispatch({ + type: "zoom", + scale: view.k, + translate: [ view.x, view.y ] + }); + } + function zoomended(dispatch) { + if (!--zooming) dispatch({ + type: "zoomend" + }); + center0 = null; + } + function mousedowned() { + var that = this, target = d3.event.target, dispatch = event.of(that, arguments), dragged = 0, subject = d3.select(d3_window(that)).on(mousemove, moved).on(mouseup, ended), location0 = location(d3.mouse(that)), dragRestore = d3_event_dragSuppress(that); + d3_selection_interrupt.call(that); + zoomstarted(dispatch); + function moved() { + dragged = 1; + translateTo(d3.mouse(that), location0); + zoomed(dispatch); + } + function ended() { + subject.on(mousemove, null).on(mouseup, null); + dragRestore(dragged && d3.event.target === target); + zoomended(dispatch); + } + } + function touchstarted() { + var that = this, dispatch = event.of(that, arguments), locations0 = {}, distance0 = 0, scale0, zoomName = ".zoom-" + d3.event.changedTouches[0].identifier, touchmove = "touchmove" + zoomName, touchend = "touchend" + zoomName, targets = [], subject = d3.select(that), dragRestore = d3_event_dragSuppress(that); + started(); + zoomstarted(dispatch); + subject.on(mousedown, null).on(touchstart, started); + function relocate() { + var touches = d3.touches(that); + scale0 = view.k; + touches.forEach(function(t) { + if (t.identifier in locations0) locations0[t.identifier] = location(t); + }); + return touches; + } + function started() { + var target = d3.event.target; + d3.select(target).on(touchmove, moved).on(touchend, ended); + targets.push(target); + var changed = d3.event.changedTouches; + for (var i = 0, n = changed.length; i < n; ++i) { + locations0[changed[i].identifier] = null; + } + var touches = relocate(), now = Date.now(); + if (touches.length === 1) { + if (now - touchtime < 500) { + var p = touches[0]; + zoomTo(that, p, locations0[p.identifier], Math.floor(Math.log(view.k) / Math.LN2) + 1); + d3_eventPreventDefault(); + } + touchtime = now; + } else if (touches.length > 1) { + var p = touches[0], q = touches[1], dx = p[0] - q[0], dy = p[1] - q[1]; + distance0 = dx * dx + dy * dy; + } + } + function moved() { + var touches = d3.touches(that), p0, l0, p1, l1; + d3_selection_interrupt.call(that); + for (var i = 0, n = touches.length; i < n; ++i, l1 = null) { + p1 = touches[i]; + if (l1 = locations0[p1.identifier]) { + if (l0) break; + p0 = p1, l0 = l1; + } + } + if (l1) { + var distance1 = (distance1 = p1[0] - p0[0]) * distance1 + (distance1 = p1[1] - p0[1]) * distance1, scale1 = distance0 && Math.sqrt(distance1 / distance0); + p0 = [ (p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2 ]; + l0 = [ (l0[0] + l1[0]) / 2, (l0[1] + l1[1]) / 2 ]; + scaleTo(scale1 * scale0); + } + touchtime = null; + translateTo(p0, l0); + zoomed(dispatch); + } + function ended() { + if (d3.event.touches.length) { + var changed = d3.event.changedTouches; + for (var i = 0, n = changed.length; i < n; ++i) { + delete locations0[changed[i].identifier]; + } + for (var identifier in locations0) { + return void relocate(); + } + } + d3.selectAll(targets).on(zoomName, null); + subject.on(mousedown, mousedowned).on(touchstart, touchstarted); + dragRestore(); + zoomended(dispatch); + } + } + function mousewheeled() { + var dispatch = event.of(this, arguments); + if (mousewheelTimer) clearTimeout(mousewheelTimer); else translate0 = location(center0 = center || d3.mouse(this)), + d3_selection_interrupt.call(this), zoomstarted(dispatch); + mousewheelTimer = setTimeout(function() { + mousewheelTimer = null; + zoomended(dispatch); + }, 50); + d3_eventPreventDefault(); + scaleTo(Math.pow(2, d3_behavior_zoomDelta() * .002) * view.k); + translateTo(center0, translate0); + zoomed(dispatch); + } + function dblclicked() { + var p = d3.mouse(this), k = Math.log(view.k) / Math.LN2; + zoomTo(this, p, location(p), d3.event.shiftKey ? Math.ceil(k) - 1 : Math.floor(k) + 1); + } + return d3.rebind(zoom, event, "on"); + }; + var d3_behavior_zoomInfinity = [ 0, Infinity ], d3_behavior_zoomDelta, d3_behavior_zoomWheel; + d3.color = d3_color; + function d3_color() {} + d3_color.prototype.toString = function() { + return this.rgb() + ""; + }; + d3.hsl = d3_hsl; + function d3_hsl(h, s, l) { + return this instanceof d3_hsl ? void (this.h = +h, this.s = +s, this.l = +l) : arguments.length < 2 ? h instanceof d3_hsl ? new d3_hsl(h.h, h.s, h.l) : d3_rgb_parse("" + h, d3_rgb_hsl, d3_hsl) : new d3_hsl(h, s, l); + } + var d3_hslPrototype = d3_hsl.prototype = new d3_color(); + d3_hslPrototype.brighter = function(k) { + k = Math.pow(.7, arguments.length ? k : 1); + return new d3_hsl(this.h, this.s, this.l / k); + }; + d3_hslPrototype.darker = function(k) { + k = Math.pow(.7, arguments.length ? k : 1); + return new d3_hsl(this.h, this.s, k * this.l); + }; + d3_hslPrototype.rgb = function() { + return d3_hsl_rgb(this.h, this.s, this.l); + }; + function d3_hsl_rgb(h, s, l) { + var m1, m2; + h = isNaN(h) ? 0 : (h %= 360) < 0 ? h + 360 : h; + s = isNaN(s) ? 0 : s < 0 ? 0 : s > 1 ? 1 : s; + l = l < 0 ? 0 : l > 1 ? 1 : l; + m2 = l <= .5 ? l * (1 + s) : l + s - l * s; + m1 = 2 * l - m2; + function v(h) { + if (h > 360) h -= 360; else if (h < 0) h += 360; + if (h < 60) return m1 + (m2 - m1) * h / 60; + if (h < 180) return m2; + if (h < 240) return m1 + (m2 - m1) * (240 - h) / 60; + return m1; + } + function vv(h) { + return Math.round(v(h) * 255); + } + return new d3_rgb(vv(h + 120), vv(h), vv(h - 120)); + } + d3.hcl = d3_hcl; + function d3_hcl(h, c, l) { + return this instanceof d3_hcl ? void (this.h = +h, this.c = +c, this.l = +l) : arguments.length < 2 ? h instanceof d3_hcl ? new d3_hcl(h.h, h.c, h.l) : h instanceof d3_lab ? d3_lab_hcl(h.l, h.a, h.b) : d3_lab_hcl((h = d3_rgb_lab((h = d3.rgb(h)).r, h.g, h.b)).l, h.a, h.b) : new d3_hcl(h, c, l); + } + var d3_hclPrototype = d3_hcl.prototype = new d3_color(); + d3_hclPrototype.brighter = function(k) { + return new d3_hcl(this.h, this.c, Math.min(100, this.l + d3_lab_K * (arguments.length ? k : 1))); + }; + d3_hclPrototype.darker = function(k) { + return new d3_hcl(this.h, this.c, Math.max(0, this.l - d3_lab_K * (arguments.length ? k : 1))); + }; + d3_hclPrototype.rgb = function() { + return d3_hcl_lab(this.h, this.c, this.l).rgb(); + }; + function d3_hcl_lab(h, c, l) { + if (isNaN(h)) h = 0; + if (isNaN(c)) c = 0; + return new d3_lab(l, Math.cos(h *= d3_radians) * c, Math.sin(h) * c); + } + d3.lab = d3_lab; + function d3_lab(l, a, b) { + return this instanceof d3_lab ? void (this.l = +l, this.a = +a, this.b = +b) : arguments.length < 2 ? l instanceof d3_lab ? new d3_lab(l.l, l.a, l.b) : l instanceof d3_hcl ? d3_hcl_lab(l.h, l.c, l.l) : d3_rgb_lab((l = d3_rgb(l)).r, l.g, l.b) : new d3_lab(l, a, b); + } + var d3_lab_K = 18; + var d3_lab_X = .95047, d3_lab_Y = 1, d3_lab_Z = 1.08883; + var d3_labPrototype = d3_lab.prototype = new d3_color(); + d3_labPrototype.brighter = function(k) { + return new d3_lab(Math.min(100, this.l + d3_lab_K * (arguments.length ? k : 1)), this.a, this.b); + }; + d3_labPrototype.darker = function(k) { + return new d3_lab(Math.max(0, this.l - d3_lab_K * (arguments.length ? k : 1)), this.a, this.b); + }; + d3_labPrototype.rgb = function() { + return d3_lab_rgb(this.l, this.a, this.b); + }; + function d3_lab_rgb(l, a, b) { + var y = (l + 16) / 116, x = y + a / 500, z = y - b / 200; + x = d3_lab_xyz(x) * d3_lab_X; + y = d3_lab_xyz(y) * d3_lab_Y; + z = d3_lab_xyz(z) * d3_lab_Z; + return new d3_rgb(d3_xyz_rgb(3.2404542 * x - 1.5371385 * y - .4985314 * z), d3_xyz_rgb(-.969266 * x + 1.8760108 * y + .041556 * z), d3_xyz_rgb(.0556434 * x - .2040259 * y + 1.0572252 * z)); + } + function d3_lab_hcl(l, a, b) { + return l > 0 ? new d3_hcl(Math.atan2(b, a) * d3_degrees, Math.sqrt(a * a + b * b), l) : new d3_hcl(NaN, NaN, l); + } + function d3_lab_xyz(x) { + return x > .206893034 ? x * x * x : (x - 4 / 29) / 7.787037; + } + function d3_xyz_lab(x) { + return x > .008856 ? Math.pow(x, 1 / 3) : 7.787037 * x + 4 / 29; + } + function d3_xyz_rgb(r) { + return Math.round(255 * (r <= .00304 ? 12.92 * r : 1.055 * Math.pow(r, 1 / 2.4) - .055)); + } + d3.rgb = d3_rgb; + function d3_rgb(r, g, b) { + return this instanceof d3_rgb ? void (this.r = ~~r, this.g = ~~g, this.b = ~~b) : arguments.length < 2 ? r instanceof d3_rgb ? new d3_rgb(r.r, r.g, r.b) : d3_rgb_parse("" + r, d3_rgb, d3_hsl_rgb) : new d3_rgb(r, g, b); + } + function d3_rgbNumber(value) { + return new d3_rgb(value >> 16, value >> 8 & 255, value & 255); + } + function d3_rgbString(value) { + return d3_rgbNumber(value) + ""; + } + var d3_rgbPrototype = d3_rgb.prototype = new d3_color(); + d3_rgbPrototype.brighter = function(k) { + k = Math.pow(.7, arguments.length ? k : 1); + var r = this.r, g = this.g, b = this.b, i = 30; + if (!r && !g && !b) return new d3_rgb(i, i, i); + if (r && r < i) r = i; + if (g && g < i) g = i; + if (b && b < i) b = i; + return new d3_rgb(Math.min(255, r / k), Math.min(255, g / k), Math.min(255, b / k)); + }; + d3_rgbPrototype.darker = function(k) { + k = Math.pow(.7, arguments.length ? k : 1); + return new d3_rgb(k * this.r, k * this.g, k * this.b); + }; + d3_rgbPrototype.hsl = function() { + return d3_rgb_hsl(this.r, this.g, this.b); + }; + d3_rgbPrototype.toString = function() { + return "#" + d3_rgb_hex(this.r) + d3_rgb_hex(this.g) + d3_rgb_hex(this.b); + }; + function d3_rgb_hex(v) { + return v < 16 ? "0" + Math.max(0, v).toString(16) : Math.min(255, v).toString(16); + } + function d3_rgb_parse(format, rgb, hsl) { + var r = 0, g = 0, b = 0, m1, m2, color; + m1 = /([a-z]+)\((.*)\)/i.exec(format); + if (m1) { + m2 = m1[2].split(","); + switch (m1[1]) { + case "hsl": + { + return hsl(parseFloat(m2[0]), parseFloat(m2[1]) / 100, parseFloat(m2[2]) / 100); + } + + case "rgb": + { + return rgb(d3_rgb_parseNumber(m2[0]), d3_rgb_parseNumber(m2[1]), d3_rgb_parseNumber(m2[2])); + } + } + } + if (color = d3_rgb_names.get(format.toLowerCase())) { + return rgb(color.r, color.g, color.b); + } + if (format != null && format.charAt(0) === "#" && !isNaN(color = parseInt(format.slice(1), 16))) { + if (format.length === 4) { + r = (color & 3840) >> 4; + r = r >> 4 | r; + g = color & 240; + g = g >> 4 | g; + b = color & 15; + b = b << 4 | b; + } else if (format.length === 7) { + r = (color & 16711680) >> 16; + g = (color & 65280) >> 8; + b = color & 255; + } + } + return rgb(r, g, b); + } + function d3_rgb_hsl(r, g, b) { + var min = Math.min(r /= 255, g /= 255, b /= 255), max = Math.max(r, g, b), d = max - min, h, s, l = (max + min) / 2; + if (d) { + s = l < .5 ? d / (max + min) : d / (2 - max - min); + if (r == max) h = (g - b) / d + (g < b ? 6 : 0); else if (g == max) h = (b - r) / d + 2; else h = (r - g) / d + 4; + h *= 60; + } else { + h = NaN; + s = l > 0 && l < 1 ? 0 : h; + } + return new d3_hsl(h, s, l); + } + function d3_rgb_lab(r, g, b) { + r = d3_rgb_xyz(r); + g = d3_rgb_xyz(g); + b = d3_rgb_xyz(b); + var x = d3_xyz_lab((.4124564 * r + .3575761 * g + .1804375 * b) / d3_lab_X), y = d3_xyz_lab((.2126729 * r + .7151522 * g + .072175 * b) / d3_lab_Y), z = d3_xyz_lab((.0193339 * r + .119192 * g + .9503041 * b) / d3_lab_Z); + return d3_lab(116 * y - 16, 500 * (x - y), 200 * (y - z)); + } + function d3_rgb_xyz(r) { + return (r /= 255) <= .04045 ? r / 12.92 : Math.pow((r + .055) / 1.055, 2.4); + } + function d3_rgb_parseNumber(c) { + var f = parseFloat(c); + return c.charAt(c.length - 1) === "%" ? Math.round(f * 2.55) : f; + } + var d3_rgb_names = d3.map({ + aliceblue: 15792383, + antiquewhite: 16444375, + aqua: 65535, + aquamarine: 8388564, + azure: 15794175, + beige: 16119260, + bisque: 16770244, + black: 0, + blanchedalmond: 16772045, + blue: 255, + blueviolet: 9055202, + brown: 10824234, + burlywood: 14596231, + cadetblue: 6266528, + chartreuse: 8388352, + chocolate: 13789470, + coral: 16744272, + cornflowerblue: 6591981, + cornsilk: 16775388, + crimson: 14423100, + cyan: 65535, + darkblue: 139, + darkcyan: 35723, + darkgoldenrod: 12092939, + darkgray: 11119017, + darkgreen: 25600, + darkgrey: 11119017, + darkkhaki: 12433259, + darkmagenta: 9109643, + darkolivegreen: 5597999, + darkorange: 16747520, + darkorchid: 10040012, + darkred: 9109504, + darksalmon: 15308410, + darkseagreen: 9419919, + darkslateblue: 4734347, + darkslategray: 3100495, + darkslategrey: 3100495, + darkturquoise: 52945, + darkviolet: 9699539, + deeppink: 16716947, + deepskyblue: 49151, + dimgray: 6908265, + dimgrey: 6908265, + dodgerblue: 2003199, + firebrick: 11674146, + floralwhite: 16775920, + forestgreen: 2263842, + fuchsia: 16711935, + gainsboro: 14474460, + ghostwhite: 16316671, + gold: 16766720, + goldenrod: 14329120, + gray: 8421504, + green: 32768, + greenyellow: 11403055, + grey: 8421504, + honeydew: 15794160, + hotpink: 16738740, + indianred: 13458524, + indigo: 4915330, + ivory: 16777200, + khaki: 15787660, + lavender: 15132410, + lavenderblush: 16773365, + lawngreen: 8190976, + lemonchiffon: 16775885, + lightblue: 11393254, + lightcoral: 15761536, + lightcyan: 14745599, + lightgoldenrodyellow: 16448210, + lightgray: 13882323, + lightgreen: 9498256, + lightgrey: 13882323, + lightpink: 16758465, + lightsalmon: 16752762, + lightseagreen: 2142890, + lightskyblue: 8900346, + lightslategray: 7833753, + lightslategrey: 7833753, + lightsteelblue: 11584734, + lightyellow: 16777184, + lime: 65280, + limegreen: 3329330, + linen: 16445670, + magenta: 16711935, + maroon: 8388608, + mediumaquamarine: 6737322, + mediumblue: 205, + mediumorchid: 12211667, + mediumpurple: 9662683, + mediumseagreen: 3978097, + mediumslateblue: 8087790, + mediumspringgreen: 64154, + mediumturquoise: 4772300, + mediumvioletred: 13047173, + midnightblue: 1644912, + mintcream: 16121850, + mistyrose: 16770273, + moccasin: 16770229, + navajowhite: 16768685, + navy: 128, + oldlace: 16643558, + olive: 8421376, + olivedrab: 7048739, + orange: 16753920, + orangered: 16729344, + orchid: 14315734, + palegoldenrod: 15657130, + palegreen: 10025880, + paleturquoise: 11529966, + palevioletred: 14381203, + papayawhip: 16773077, + peachpuff: 16767673, + peru: 13468991, + pink: 16761035, + plum: 14524637, + powderblue: 11591910, + purple: 8388736, + rebeccapurple: 6697881, + red: 16711680, + rosybrown: 12357519, + royalblue: 4286945, + saddlebrown: 9127187, + salmon: 16416882, + sandybrown: 16032864, + seagreen: 3050327, + seashell: 16774638, + sienna: 10506797, + silver: 12632256, + skyblue: 8900331, + slateblue: 6970061, + slategray: 7372944, + slategrey: 7372944, + snow: 16775930, + springgreen: 65407, + steelblue: 4620980, + tan: 13808780, + teal: 32896, + thistle: 14204888, + tomato: 16737095, + turquoise: 4251856, + violet: 15631086, + wheat: 16113331, + white: 16777215, + whitesmoke: 16119285, + yellow: 16776960, + yellowgreen: 10145074 + }); + d3_rgb_names.forEach(function(key, value) { + d3_rgb_names.set(key, d3_rgbNumber(value)); + }); + function d3_functor(v) { + return typeof v === "function" ? v : function() { + return v; + }; + } + d3.functor = d3_functor; + d3.xhr = d3_xhrType(d3_identity); + function d3_xhrType(response) { + return function(url, mimeType, callback) { + if (arguments.length === 2 && typeof mimeType === "function") callback = mimeType, + mimeType = null; + return d3_xhr(url, mimeType, response, callback); + }; + } + function d3_xhr(url, mimeType, response, callback) { + var xhr = {}, dispatch = d3.dispatch("beforesend", "progress", "load", "error"), headers = {}, request = new XMLHttpRequest(), responseType = null; + if (this.XDomainRequest && !("withCredentials" in request) && /^(http(s)?:)?\/\//.test(url)) request = new XDomainRequest(); + "onload" in request ? request.onload = request.onerror = respond : request.onreadystatechange = function() { + request.readyState > 3 && respond(); + }; + function respond() { + var status = request.status, result; + if (!status && d3_xhrHasResponse(request) || status >= 200 && status < 300 || status === 304) { + try { + result = response.call(xhr, request); + } catch (e) { + dispatch.error.call(xhr, e); + return; + } + dispatch.load.call(xhr, result); + } else { + dispatch.error.call(xhr, request); + } + } + request.onprogress = function(event) { + var o = d3.event; + d3.event = event; + try { + dispatch.progress.call(xhr, request); + } finally { + d3.event = o; + } + }; + xhr.header = function(name, value) { + name = (name + "").toLowerCase(); + if (arguments.length < 2) return headers[name]; + if (value == null) delete headers[name]; else headers[name] = value + ""; + return xhr; + }; + xhr.mimeType = function(value) { + if (!arguments.length) return mimeType; + mimeType = value == null ? null : value + ""; + return xhr; + }; + xhr.responseType = function(value) { + if (!arguments.length) return responseType; + responseType = value; + return xhr; + }; + xhr.response = function(value) { + response = value; + return xhr; + }; + [ "get", "post" ].forEach(function(method) { + xhr[method] = function() { + return xhr.send.apply(xhr, [ method ].concat(d3_array(arguments))); + }; + }); + xhr.send = function(method, data, callback) { + if (arguments.length === 2 && typeof data === "function") callback = data, data = null; + request.open(method, url, true); + if (mimeType != null && !("accept" in headers)) headers["accept"] = mimeType + ",*/*"; + if (request.setRequestHeader) for (var name in headers) request.setRequestHeader(name, headers[name]); + if (mimeType != null && request.overrideMimeType) request.overrideMimeType(mimeType); + if (responseType != null) request.responseType = responseType; + if (callback != null) xhr.on("error", callback).on("load", function(request) { + callback(null, request); + }); + dispatch.beforesend.call(xhr, request); + request.send(data == null ? null : data); + return xhr; + }; + xhr.abort = function() { + request.abort(); + return xhr; + }; + d3.rebind(xhr, dispatch, "on"); + return callback == null ? xhr : xhr.get(d3_xhr_fixCallback(callback)); + } + function d3_xhr_fixCallback(callback) { + return callback.length === 1 ? function(error, request) { + callback(error == null ? request : null); + } : callback; + } + function d3_xhrHasResponse(request) { + var type = request.responseType; + return type && type !== "text" ? request.response : request.responseText; + } + d3.dsv = function(delimiter, mimeType) { + var reFormat = new RegExp('["' + delimiter + "\n]"), delimiterCode = delimiter.charCodeAt(0); + function dsv(url, row, callback) { + if (arguments.length < 3) callback = row, row = null; + var xhr = d3_xhr(url, mimeType, row == null ? response : typedResponse(row), callback); + xhr.row = function(_) { + return arguments.length ? xhr.response((row = _) == null ? response : typedResponse(_)) : row; + }; + return xhr; + } + function response(request) { + return dsv.parse(request.responseText); + } + function typedResponse(f) { + return function(request) { + return dsv.parse(request.responseText, f); + }; + } + dsv.parse = function(text, f) { + var o; + return dsv.parseRows(text, function(row, i) { + if (o) return o(row, i - 1); + var a = new Function("d", "return {" + row.map(function(name, i) { + return JSON.stringify(name) + ": d[" + i + "]"; + }).join(",") + "}"); + o = f ? function(row, i) { + return f(a(row), i); + } : a; + }); + }; + dsv.parseRows = function(text, f) { + var EOL = {}, EOF = {}, rows = [], N = text.length, I = 0, n = 0, t, eol; + function token() { + if (I >= N) return EOF; + if (eol) return eol = false, EOL; + var j = I; + if (text.charCodeAt(j) === 34) { + var i = j; + while (i++ < N) { + if (text.charCodeAt(i) === 34) { + if (text.charCodeAt(i + 1) !== 34) break; + ++i; + } + } + I = i + 2; + var c = text.charCodeAt(i + 1); + if (c === 13) { + eol = true; + if (text.charCodeAt(i + 2) === 10) ++I; + } else if (c === 10) { + eol = true; + } + return text.slice(j + 1, i).replace(/""/g, '"'); + } + while (I < N) { + var c = text.charCodeAt(I++), k = 1; + if (c === 10) eol = true; else if (c === 13) { + eol = true; + if (text.charCodeAt(I) === 10) ++I, ++k; + } else if (c !== delimiterCode) continue; + return text.slice(j, I - k); + } + return text.slice(j); + } + while ((t = token()) !== EOF) { + var a = []; + while (t !== EOL && t !== EOF) { + a.push(t); + t = token(); + } + if (f && (a = f(a, n++)) == null) continue; + rows.push(a); + } + return rows; + }; + dsv.format = function(rows) { + if (Array.isArray(rows[0])) return dsv.formatRows(rows); + var fieldSet = new d3_Set(), fields = []; + rows.forEach(function(row) { + for (var field in row) { + if (!fieldSet.has(field)) { + fields.push(fieldSet.add(field)); + } + } + }); + return [ fields.map(formatValue).join(delimiter) ].concat(rows.map(function(row) { + return fields.map(function(field) { + return formatValue(row[field]); + }).join(delimiter); + })).join("\n"); + }; + dsv.formatRows = function(rows) { + return rows.map(formatRow).join("\n"); + }; + function formatRow(row) { + return row.map(formatValue).join(delimiter); + } + function formatValue(text) { + return reFormat.test(text) ? '"' + text.replace(/\"/g, '""') + '"' : text; + } + return dsv; + }; + d3.csv = d3.dsv(",", "text/csv"); + d3.tsv = d3.dsv(" ", "text/tab-separated-values"); + var d3_timer_queueHead, d3_timer_queueTail, d3_timer_interval, d3_timer_timeout, d3_timer_active, d3_timer_frame = this[d3_vendorSymbol(this, "requestAnimationFrame")] || function(callback) { + setTimeout(callback, 17); + }; + d3.timer = function(callback, delay, then) { + var n = arguments.length; + if (n < 2) delay = 0; + if (n < 3) then = Date.now(); + var time = then + delay, timer = { + c: callback, + t: time, + f: false, + n: null + }; + if (d3_timer_queueTail) d3_timer_queueTail.n = timer; else d3_timer_queueHead = timer; + d3_timer_queueTail = timer; + if (!d3_timer_interval) { + d3_timer_timeout = clearTimeout(d3_timer_timeout); + d3_timer_interval = 1; + d3_timer_frame(d3_timer_step); + } + }; + function d3_timer_step() { + var now = d3_timer_mark(), delay = d3_timer_sweep() - now; + if (delay > 24) { + if (isFinite(delay)) { + clearTimeout(d3_timer_timeout); + d3_timer_timeout = setTimeout(d3_timer_step, delay); + } + d3_timer_interval = 0; + } else { + d3_timer_interval = 1; + d3_timer_frame(d3_timer_step); + } + } + d3.timer.flush = function() { + d3_timer_mark(); + d3_timer_sweep(); + }; + function d3_timer_mark() { + var now = Date.now(); + d3_timer_active = d3_timer_queueHead; + while (d3_timer_active) { + if (now >= d3_timer_active.t) d3_timer_active.f = d3_timer_active.c(now - d3_timer_active.t); + d3_timer_active = d3_timer_active.n; + } + return now; + } + function d3_timer_sweep() { + var t0, t1 = d3_timer_queueHead, time = Infinity; + while (t1) { + if (t1.f) { + t1 = t0 ? t0.n = t1.n : d3_timer_queueHead = t1.n; + } else { + if (t1.t < time) time = t1.t; + t1 = (t0 = t1).n; + } + } + d3_timer_queueTail = t0; + return time; + } + function d3_format_precision(x, p) { + return p - (x ? Math.ceil(Math.log(x) / Math.LN10) : 1); + } + d3.round = function(x, n) { + return n ? Math.round(x * (n = Math.pow(10, n))) / n : Math.round(x); + }; + var d3_formatPrefixes = [ "y", "z", "a", "f", "p", "n", "µ", "m", "", "k", "M", "G", "T", "P", "E", "Z", "Y" ].map(d3_formatPrefix); + d3.formatPrefix = function(value, precision) { + var i = 0; + if (value) { + if (value < 0) value *= -1; + if (precision) value = d3.round(value, d3_format_precision(value, precision)); + i = 1 + Math.floor(1e-12 + Math.log(value) / Math.LN10); + i = Math.max(-24, Math.min(24, Math.floor((i - 1) / 3) * 3)); + } + return d3_formatPrefixes[8 + i / 3]; + }; + function d3_formatPrefix(d, i) { + var k = Math.pow(10, abs(8 - i) * 3); + return { + scale: i > 8 ? function(d) { + return d / k; + } : function(d) { + return d * k; + }, + symbol: d + }; + } + function d3_locale_numberFormat(locale) { + var locale_decimal = locale.decimal, locale_thousands = locale.thousands, locale_grouping = locale.grouping, locale_currency = locale.currency, formatGroup = locale_grouping && locale_thousands ? function(value, width) { + var i = value.length, t = [], j = 0, g = locale_grouping[0], length = 0; + while (i > 0 && g > 0) { + if (length + g + 1 > width) g = Math.max(1, width - length); + t.push(value.substring(i -= g, i + g)); + if ((length += g + 1) > width) break; + g = locale_grouping[j = (j + 1) % locale_grouping.length]; + } + return t.reverse().join(locale_thousands); + } : d3_identity; + return function(specifier) { + var match = d3_format_re.exec(specifier), fill = match[1] || " ", align = match[2] || ">", sign = match[3] || "-", symbol = match[4] || "", zfill = match[5], width = +match[6], comma = match[7], precision = match[8], type = match[9], scale = 1, prefix = "", suffix = "", integer = false, exponent = true; + if (precision) precision = +precision.substring(1); + if (zfill || fill === "0" && align === "=") { + zfill = fill = "0"; + align = "="; + } + switch (type) { + case "n": + comma = true; + type = "g"; + break; + + case "%": + scale = 100; + suffix = "%"; + type = "f"; + break; + + case "p": + scale = 100; + suffix = "%"; + type = "r"; + break; + + case "b": + case "o": + case "x": + case "X": + if (symbol === "#") prefix = "0" + type.toLowerCase(); + + case "c": + exponent = false; + + case "d": + integer = true; + precision = 0; + break; + + case "s": + scale = -1; + type = "r"; + break; + } + if (symbol === "$") prefix = locale_currency[0], suffix = locale_currency[1]; + if (type == "r" && !precision) type = "g"; + if (precision != null) { + if (type == "g") precision = Math.max(1, Math.min(21, precision)); else if (type == "e" || type == "f") precision = Math.max(0, Math.min(20, precision)); + } + type = d3_format_types.get(type) || d3_format_typeDefault; + var zcomma = zfill && comma; + return function(value) { + var fullSuffix = suffix; + if (integer && value % 1) return ""; + var negative = value < 0 || value === 0 && 1 / value < 0 ? (value = -value, "-") : sign === "-" ? "" : sign; + if (scale < 0) { + var unit = d3.formatPrefix(value, precision); + value = unit.scale(value); + fullSuffix = unit.symbol + suffix; + } else { + value *= scale; + } + value = type(value, precision); + var i = value.lastIndexOf("."), before, after; + if (i < 0) { + var j = exponent ? value.lastIndexOf("e") : -1; + if (j < 0) before = value, after = ""; else before = value.substring(0, j), after = value.substring(j); + } else { + before = value.substring(0, i); + after = locale_decimal + value.substring(i + 1); + } + if (!zfill && comma) before = formatGroup(before, Infinity); + var length = prefix.length + before.length + after.length + (zcomma ? 0 : negative.length), padding = length < width ? new Array(length = width - length + 1).join(fill) : ""; + if (zcomma) before = formatGroup(padding + before, padding.length ? width - after.length : Infinity); + negative += prefix; + value = before + after; + return (align === "<" ? negative + value + padding : align === ">" ? padding + negative + value : align === "^" ? padding.substring(0, length >>= 1) + negative + value + padding.substring(length) : negative + (zcomma ? value : padding + value)) + fullSuffix; + }; + }; + } + var d3_format_re = /(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i; + var d3_format_types = d3.map({ + b: function(x) { + return x.toString(2); + }, + c: function(x) { + return String.fromCharCode(x); + }, + o: function(x) { + return x.toString(8); + }, + x: function(x) { + return x.toString(16); + }, + X: function(x) { + return x.toString(16).toUpperCase(); + }, + g: function(x, p) { + return x.toPrecision(p); + }, + e: function(x, p) { + return x.toExponential(p); + }, + f: function(x, p) { + return x.toFixed(p); + }, + r: function(x, p) { + return (x = d3.round(x, d3_format_precision(x, p))).toFixed(Math.max(0, Math.min(20, d3_format_precision(x * (1 + 1e-15), p)))); + } + }); + function d3_format_typeDefault(x) { + return x + ""; + } + var d3_time = d3.time = {}, d3_date = Date; + function d3_date_utc() { + this._ = new Date(arguments.length > 1 ? Date.UTC.apply(this, arguments) : arguments[0]); + } + d3_date_utc.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: function() { + return this._.getUTCSeconds(); + }, + getTime: function() { + return this._.getTime(); + }, + getTimezoneOffset: function() { + return 0; + }, + valueOf: function() { + return this._.valueOf(); + }, + setDate: function() { + d3_time_prototype.setUTCDate.apply(this._, arguments); + }, + setDay: function() { + d3_time_prototype.setUTCDay.apply(this._, arguments); + }, + setFullYear: function() { + d3_time_prototype.setUTCFullYear.apply(this._, arguments); + }, + setHours: function() { + d3_time_prototype.setUTCHours.apply(this._, arguments); + }, + setMilliseconds: function() { + d3_time_prototype.setUTCMilliseconds.apply(this._, arguments); + }, + setMinutes: function() { + d3_time_prototype.setUTCMinutes.apply(this._, arguments); + }, + setMonth: function() { + d3_time_prototype.setUTCMonth.apply(this._, arguments); + }, + setSeconds: function() { + d3_time_prototype.setUTCSeconds.apply(this._, arguments); + }, + setTime: function() { + d3_time_prototype.setTime.apply(this._, arguments); + } + }; + var d3_time_prototype = Date.prototype; + function d3_time_interval(local, step, number) { + function round(date) { + var d0 = local(date), d1 = offset(d0, 1); + return date - d0 < d1 - date ? d0 : d1; + } + function ceil(date) { + step(date = local(new d3_date(date - 1)), 1); + return date; + } + function offset(date, k) { + step(date = new d3_date(+date), k); + return date; + } + function range(t0, t1, dt) { + var time = ceil(t0), times = []; + if (dt > 1) { + while (time < t1) { + if (!(number(time) % dt)) times.push(new Date(+time)); + step(time, 1); + } + } else { + while (time < t1) times.push(new Date(+time)), step(time, 1); + } + return times; + } + function range_utc(t0, t1, dt) { + try { + d3_date = d3_date_utc; + var utc = new d3_date_utc(); + utc._ = t0; + return range(utc, t1, dt); + } finally { + d3_date = Date; + } + } + local.floor = local; + local.round = round; + local.ceil = ceil; + local.offset = offset; + local.range = range; + var utc = local.utc = d3_time_interval_utc(local); + utc.floor = utc; + utc.round = d3_time_interval_utc(round); + utc.ceil = d3_time_interval_utc(ceil); + utc.offset = d3_time_interval_utc(offset); + utc.range = range_utc; + return local; + } + function d3_time_interval_utc(method) { + return function(date, k) { + try { + d3_date = d3_date_utc; + var utc = new d3_date_utc(); + utc._ = date; + return method(utc, k)._; + } finally { + d3_date = Date; + } + }; + } + d3_time.year = d3_time_interval(function(date) { + date = d3_time.day(date); + date.setMonth(0, 1); + return date; + }, function(date, offset) { + date.setFullYear(date.getFullYear() + offset); + }, function(date) { + return date.getFullYear(); + }); + d3_time.years = d3_time.year.range; + d3_time.years.utc = d3_time.year.utc.range; + d3_time.day = d3_time_interval(function(date) { + var day = new d3_date(2e3, 0); + day.setFullYear(date.getFullYear(), date.getMonth(), date.getDate()); + return day; + }, function(date, offset) { + date.setDate(date.getDate() + offset); + }, function(date) { + return date.getDate() - 1; + }); + d3_time.days = d3_time.day.range; + d3_time.days.utc = d3_time.day.utc.range; + d3_time.dayOfYear = function(date) { + var year = d3_time.year(date); + return Math.floor((date - year - (date.getTimezoneOffset() - year.getTimezoneOffset()) * 6e4) / 864e5); + }; + [ "sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday" ].forEach(function(day, i) { + i = 7 - i; + var interval = d3_time[day] = d3_time_interval(function(date) { + (date = d3_time.day(date)).setDate(date.getDate() - (date.getDay() + i) % 7); + return date; + }, function(date, offset) { + date.setDate(date.getDate() + Math.floor(offset) * 7); + }, function(date) { + var day = d3_time.year(date).getDay(); + return Math.floor((d3_time.dayOfYear(date) + (day + i) % 7) / 7) - (day !== i); + }); + d3_time[day + "s"] = interval.range; + d3_time[day + "s"].utc = interval.utc.range; + d3_time[day + "OfYear"] = function(date) { + var day = d3_time.year(date).getDay(); + return Math.floor((d3_time.dayOfYear(date) + (day + i) % 7) / 7); + }; + }); + d3_time.week = d3_time.sunday; + d3_time.weeks = d3_time.sunday.range; + d3_time.weeks.utc = d3_time.sunday.utc.range; + d3_time.weekOfYear = d3_time.sundayOfYear; + function d3_locale_timeFormat(locale) { + var locale_dateTime = locale.dateTime, locale_date = locale.date, locale_time = locale.time, locale_periods = locale.periods, locale_days = locale.days, locale_shortDays = locale.shortDays, locale_months = locale.months, locale_shortMonths = locale.shortMonths; + function d3_time_format(template) { + var n = template.length; + function format(date) { + var string = [], i = -1, j = 0, c, p, f; + while (++i < n) { + if (template.charCodeAt(i) === 37) { + string.push(template.slice(j, i)); + if ((p = d3_time_formatPads[c = template.charAt(++i)]) != null) c = template.charAt(++i); + if (f = d3_time_formats[c]) c = f(date, p == null ? c === "e" ? " " : "0" : p); + string.push(c); + j = i + 1; + } + } + string.push(template.slice(j, i)); + return string.join(""); + } + format.parse = function(string) { + var d = { + y: 1900, + m: 0, + d: 1, + H: 0, + M: 0, + S: 0, + L: 0, + Z: null + }, i = d3_time_parse(d, template, string, 0); + if (i != string.length) return null; + if ("p" in d) d.H = d.H % 12 + d.p * 12; + var localZ = d.Z != null && d3_date !== d3_date_utc, date = new (localZ ? d3_date_utc : d3_date)(); + if ("j" in d) date.setFullYear(d.y, 0, d.j); else if ("w" in d && ("W" in d || "U" in d)) { + date.setFullYear(d.y, 0, 1); + date.setFullYear(d.y, 0, "W" in d ? (d.w + 6) % 7 + d.W * 7 - (date.getDay() + 5) % 7 : d.w + d.U * 7 - (date.getDay() + 6) % 7); + } else date.setFullYear(d.y, d.m, d.d); + date.setHours(d.H + (d.Z / 100 | 0), d.M + d.Z % 100, d.S, d.L); + return localZ ? date._ : date; + }; + format.toString = function() { + return template; + }; + return format; + } + function d3_time_parse(date, template, string, j) { + var c, p, t, i = 0, n = template.length, m = string.length; + while (i < n) { + if (j >= m) return -1; + c = template.charCodeAt(i++); + if (c === 37) { + t = template.charAt(i++); + p = d3_time_parsers[t in d3_time_formatPads ? template.charAt(i++) : t]; + if (!p || (j = p(date, string, j)) < 0) return -1; + } else if (c != string.charCodeAt(j++)) { + return -1; + } + } + return j; + } + d3_time_format.utc = function(template) { + var local = d3_time_format(template); + function format(date) { + try { + d3_date = d3_date_utc; + var utc = new d3_date(); + utc._ = date; + return local(utc); + } finally { + d3_date = Date; + } + } + format.parse = function(string) { + try { + d3_date = d3_date_utc; + var date = local.parse(string); + return date && date._; + } finally { + d3_date = Date; + } + }; + format.toString = local.toString; + return format; + }; + d3_time_format.multi = d3_time_format.utc.multi = d3_time_formatMulti; + var d3_time_periodLookup = d3.map(), d3_time_dayRe = d3_time_formatRe(locale_days), d3_time_dayLookup = d3_time_formatLookup(locale_days), d3_time_dayAbbrevRe = d3_time_formatRe(locale_shortDays), d3_time_dayAbbrevLookup = d3_time_formatLookup(locale_shortDays), d3_time_monthRe = d3_time_formatRe(locale_months), d3_time_monthLookup = d3_time_formatLookup(locale_months), d3_time_monthAbbrevRe = d3_time_formatRe(locale_shortMonths), d3_time_monthAbbrevLookup = d3_time_formatLookup(locale_shortMonths); + locale_periods.forEach(function(p, i) { + d3_time_periodLookup.set(p.toLowerCase(), i); + }); + var d3_time_formats = { + a: function(d) { + return locale_shortDays[d.getDay()]; + }, + A: function(d) { + return locale_days[d.getDay()]; + }, + b: function(d) { + return locale_shortMonths[d.getMonth()]; + }, + B: function(d) { + return locale_months[d.getMonth()]; + }, + c: d3_time_format(locale_dateTime), + d: function(d, p) { + return d3_time_formatPad(d.getDate(), p, 2); + }, + e: function(d, p) { + return d3_time_formatPad(d.getDate(), p, 2); + }, + H: function(d, p) { + return d3_time_formatPad(d.getHours(), p, 2); + }, + I: function(d, p) { + return d3_time_formatPad(d.getHours() % 12 || 12, p, 2); + }, + j: function(d, p) { + return d3_time_formatPad(1 + d3_time.dayOfYear(d), p, 3); + }, + L: function(d, p) { + return d3_time_formatPad(d.getMilliseconds(), p, 3); + }, + m: function(d, p) { + return d3_time_formatPad(d.getMonth() + 1, p, 2); + }, + M: function(d, p) { + return d3_time_formatPad(d.getMinutes(), p, 2); + }, + p: function(d) { + return locale_periods[+(d.getHours() >= 12)]; + }, + S: function(d, p) { + return d3_time_formatPad(d.getSeconds(), p, 2); + }, + U: function(d, p) { + return d3_time_formatPad(d3_time.sundayOfYear(d), p, 2); + }, + w: function(d) { + return d.getDay(); + }, + W: function(d, p) { + return d3_time_formatPad(d3_time.mondayOfYear(d), p, 2); + }, + x: d3_time_format(locale_date), + X: d3_time_format(locale_time), + y: function(d, p) { + return d3_time_formatPad(d.getFullYear() % 100, p, 2); + }, + Y: function(d, p) { + return d3_time_formatPad(d.getFullYear() % 1e4, p, 4); + }, + Z: d3_time_zone, + "%": function() { + return "%"; + } + }; + var d3_time_parsers = { + a: d3_time_parseWeekdayAbbrev, + A: d3_time_parseWeekday, + b: d3_time_parseMonthAbbrev, + B: d3_time_parseMonth, + c: d3_time_parseLocaleFull, + d: d3_time_parseDay, + e: d3_time_parseDay, + H: d3_time_parseHour24, + I: d3_time_parseHour24, + j: d3_time_parseDayOfYear, + L: d3_time_parseMilliseconds, + m: d3_time_parseMonthNumber, + M: d3_time_parseMinutes, + p: d3_time_parseAmPm, + S: d3_time_parseSeconds, + U: d3_time_parseWeekNumberSunday, + w: d3_time_parseWeekdayNumber, + W: d3_time_parseWeekNumberMonday, + x: d3_time_parseLocaleDate, + X: d3_time_parseLocaleTime, + y: d3_time_parseYear, + Y: d3_time_parseFullYear, + Z: d3_time_parseZone, + "%": d3_time_parseLiteralPercent + }; + function d3_time_parseWeekdayAbbrev(date, string, i) { + d3_time_dayAbbrevRe.lastIndex = 0; + var n = d3_time_dayAbbrevRe.exec(string.slice(i)); + return n ? (date.w = d3_time_dayAbbrevLookup.get(n[0].toLowerCase()), i + n[0].length) : -1; + } + function d3_time_parseWeekday(date, string, i) { + d3_time_dayRe.lastIndex = 0; + var n = d3_time_dayRe.exec(string.slice(i)); + return n ? (date.w = d3_time_dayLookup.get(n[0].toLowerCase()), i + n[0].length) : -1; + } + function d3_time_parseMonthAbbrev(date, string, i) { + d3_time_monthAbbrevRe.lastIndex = 0; + var n = d3_time_monthAbbrevRe.exec(string.slice(i)); + return n ? (date.m = d3_time_monthAbbrevLookup.get(n[0].toLowerCase()), i + n[0].length) : -1; + } + function d3_time_parseMonth(date, string, i) { + d3_time_monthRe.lastIndex = 0; + var n = d3_time_monthRe.exec(string.slice(i)); + return n ? (date.m = d3_time_monthLookup.get(n[0].toLowerCase()), i + n[0].length) : -1; + } + function d3_time_parseLocaleFull(date, string, i) { + return d3_time_parse(date, d3_time_formats.c.toString(), string, i); + } + function d3_time_parseLocaleDate(date, string, i) { + return d3_time_parse(date, d3_time_formats.x.toString(), string, i); + } + function d3_time_parseLocaleTime(date, string, i) { + return d3_time_parse(date, d3_time_formats.X.toString(), string, i); + } + function d3_time_parseAmPm(date, string, i) { + var n = d3_time_periodLookup.get(string.slice(i, i += 2).toLowerCase()); + return n == null ? -1 : (date.p = n, i); + } + return d3_time_format; + } + var d3_time_formatPads = { + "-": "", + _: " ", + "0": "0" + }, d3_time_numberRe = /^\s*\d+/, d3_time_percentRe = /^%/; + function d3_time_formatPad(value, fill, width) { + var sign = value < 0 ? "-" : "", string = (sign ? -value : value) + "", length = string.length; + return sign + (length < width ? new Array(width - length + 1).join(fill) + string : string); + } + function d3_time_formatRe(names) { + return new RegExp("^(?:" + names.map(d3.requote).join("|") + ")", "i"); + } + function d3_time_formatLookup(names) { + var map = new d3_Map(), i = -1, n = names.length; + while (++i < n) map.set(names[i].toLowerCase(), i); + return map; + } + function d3_time_parseWeekdayNumber(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i, i + 1)); + return n ? (date.w = +n[0], i + n[0].length) : -1; + } + function d3_time_parseWeekNumberSunday(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i)); + return n ? (date.U = +n[0], i + n[0].length) : -1; + } + function d3_time_parseWeekNumberMonday(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i)); + return n ? (date.W = +n[0], i + n[0].length) : -1; + } + function d3_time_parseFullYear(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i, i + 4)); + return n ? (date.y = +n[0], i + n[0].length) : -1; + } + function d3_time_parseYear(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i, i + 2)); + return n ? (date.y = d3_time_expandYear(+n[0]), i + n[0].length) : -1; + } + function d3_time_parseZone(date, string, i) { + return /^[+-]\d{4}$/.test(string = string.slice(i, i + 5)) ? (date.Z = -string, + i + 5) : -1; + } + function d3_time_expandYear(d) { + return d + (d > 68 ? 1900 : 2e3); + } + function d3_time_parseMonthNumber(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i, i + 2)); + return n ? (date.m = n[0] - 1, i + n[0].length) : -1; + } + function d3_time_parseDay(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i, i + 2)); + return n ? (date.d = +n[0], i + n[0].length) : -1; + } + function d3_time_parseDayOfYear(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i, i + 3)); + return n ? (date.j = +n[0], i + n[0].length) : -1; + } + function d3_time_parseHour24(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i, i + 2)); + return n ? (date.H = +n[0], i + n[0].length) : -1; + } + function d3_time_parseMinutes(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i, i + 2)); + return n ? (date.M = +n[0], i + n[0].length) : -1; + } + function d3_time_parseSeconds(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i, i + 2)); + return n ? (date.S = +n[0], i + n[0].length) : -1; + } + function d3_time_parseMilliseconds(date, string, i) { + d3_time_numberRe.lastIndex = 0; + var n = d3_time_numberRe.exec(string.slice(i, i + 3)); + return n ? (date.L = +n[0], i + n[0].length) : -1; + } + function d3_time_zone(d) { + var z = d.getTimezoneOffset(), zs = z > 0 ? "-" : "+", zh = abs(z) / 60 | 0, zm = abs(z) % 60; + return zs + d3_time_formatPad(zh, "0", 2) + d3_time_formatPad(zm, "0", 2); + } + function d3_time_parseLiteralPercent(date, string, i) { + d3_time_percentRe.lastIndex = 0; + var n = d3_time_percentRe.exec(string.slice(i, i + 1)); + return n ? i + n[0].length : -1; + } + function d3_time_formatMulti(formats) { + var n = formats.length, i = -1; + while (++i < n) formats[i][0] = this(formats[i][0]); + return function(date) { + var i = 0, f = formats[i]; + while (!f[1](date)) f = formats[++i]; + return f[0](date); + }; + } + d3.locale = function(locale) { + return { + numberFormat: d3_locale_numberFormat(locale), + timeFormat: d3_locale_timeFormat(locale) + }; + }; + var d3_locale_enUS = d3.locale({ + decimal: ".", + thousands: ",", + grouping: [ 3 ], + currency: [ "$", "" ], + dateTime: "%a %b %e %X %Y", + date: "%m/%d/%Y", + time: "%H:%M:%S", + periods: [ "AM", "PM" ], + days: [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" ], + shortDays: [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" ], + months: [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ], + shortMonths: [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ] + }); + d3.format = d3_locale_enUS.numberFormat; + d3.geo = {}; + function d3_adder() {} + d3_adder.prototype = { + s: 0, + t: 0, + add: function(y) { + d3_adderSum(y, this.t, d3_adderTemp); + d3_adderSum(d3_adderTemp.s, this.s, this); + if (this.s) this.t += d3_adderTemp.t; else this.s = d3_adderTemp.t; + }, + reset: function() { + this.s = this.t = 0; + }, + valueOf: function() { + return this.s; + } + }; + var d3_adderTemp = new d3_adder(); + function d3_adderSum(a, b, o) { + var x = o.s = a + b, bv = x - a, av = x - bv; + o.t = a - av + (b - bv); + } + d3.geo.stream = function(object, listener) { + if (object && d3_geo_streamObjectType.hasOwnProperty(object.type)) { + d3_geo_streamObjectType[object.type](object, listener); + } else { + d3_geo_streamGeometry(object, listener); + } + }; + function d3_geo_streamGeometry(geometry, listener) { + if (geometry && d3_geo_streamGeometryType.hasOwnProperty(geometry.type)) { + d3_geo_streamGeometryType[geometry.type](geometry, listener); + } + } + var d3_geo_streamObjectType = { + Feature: function(feature, listener) { + d3_geo_streamGeometry(feature.geometry, listener); + }, + FeatureCollection: function(object, listener) { + var features = object.features, i = -1, n = features.length; + while (++i < n) d3_geo_streamGeometry(features[i].geometry, listener); + } + }; + var d3_geo_streamGeometryType = { + Sphere: function(object, listener) { + listener.sphere(); + }, + Point: function(object, listener) { + object = object.coordinates; + listener.point(object[0], object[1], object[2]); + }, + MultiPoint: function(object, listener) { + var coordinates = object.coordinates, i = -1, n = coordinates.length; + while (++i < n) object = coordinates[i], listener.point(object[0], object[1], object[2]); + }, + LineString: function(object, listener) { + d3_geo_streamLine(object.coordinates, listener, 0); + }, + MultiLineString: function(object, listener) { + var coordinates = object.coordinates, i = -1, n = coordinates.length; + while (++i < n) d3_geo_streamLine(coordinates[i], listener, 0); + }, + Polygon: function(object, listener) { + d3_geo_streamPolygon(object.coordinates, listener); + }, + MultiPolygon: function(object, listener) { + var coordinates = object.coordinates, i = -1, n = coordinates.length; + while (++i < n) d3_geo_streamPolygon(coordinates[i], listener); + }, + GeometryCollection: function(object, listener) { + var geometries = object.geometries, i = -1, n = geometries.length; + while (++i < n) d3_geo_streamGeometry(geometries[i], listener); + } + }; + function d3_geo_streamLine(coordinates, listener, closed) { + var i = -1, n = coordinates.length - closed, coordinate; + listener.lineStart(); + while (++i < n) coordinate = coordinates[i], listener.point(coordinate[0], coordinate[1], coordinate[2]); + listener.lineEnd(); + } + function d3_geo_streamPolygon(coordinates, listener) { + var i = -1, n = coordinates.length; + listener.polygonStart(); + while (++i < n) d3_geo_streamLine(coordinates[i], listener, 1); + listener.polygonEnd(); + } + d3.geo.area = function(object) { + d3_geo_areaSum = 0; + d3.geo.stream(object, d3_geo_area); + return d3_geo_areaSum; + }; + var d3_geo_areaSum, d3_geo_areaRingSum = new d3_adder(); + var d3_geo_area = { + sphere: function() { + d3_geo_areaSum += 4 * π; + }, + point: d3_noop, + lineStart: d3_noop, + lineEnd: d3_noop, + polygonStart: function() { + d3_geo_areaRingSum.reset(); + d3_geo_area.lineStart = d3_geo_areaRingStart; + }, + polygonEnd: function() { + var area = 2 * d3_geo_areaRingSum; + d3_geo_areaSum += area < 0 ? 4 * π + area : area; + d3_geo_area.lineStart = d3_geo_area.lineEnd = d3_geo_area.point = d3_noop; + } + }; + function d3_geo_areaRingStart() { + var λ00, φ00, λ0, cosφ0, sinφ0; + d3_geo_area.point = function(λ, φ) { + d3_geo_area.point = nextPoint; + λ0 = (λ00 = λ) * d3_radians, cosφ0 = Math.cos(φ = (φ00 = φ) * d3_radians / 2 + π / 4), + sinφ0 = Math.sin(φ); + }; + function nextPoint(λ, φ) { + λ *= d3_radians; + φ = φ * d3_radians / 2 + π / 4; + var dλ = λ - λ0, sdλ = dλ >= 0 ? 1 : -1, adλ = sdλ * dλ, cosφ = Math.cos(φ), sinφ = Math.sin(φ), k = sinφ0 * sinφ, u = cosφ0 * cosφ + k * Math.cos(adλ), v = k * sdλ * Math.sin(adλ); + d3_geo_areaRingSum.add(Math.atan2(v, u)); + λ0 = λ, cosφ0 = cosφ, sinφ0 = sinφ; + } + d3_geo_area.lineEnd = function() { + nextPoint(λ00, φ00); + }; + } + function d3_geo_cartesian(spherical) { + var λ = spherical[0], φ = spherical[1], cosφ = Math.cos(φ); + return [ cosφ * Math.cos(λ), cosφ * Math.sin(λ), Math.sin(φ) ]; + } + function d3_geo_cartesianDot(a, b) { + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; + } + function d3_geo_cartesianCross(a, b) { + return [ a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0] ]; + } + function d3_geo_cartesianAdd(a, b) { + a[0] += b[0]; + a[1] += b[1]; + a[2] += b[2]; + } + function d3_geo_cartesianScale(vector, k) { + return [ vector[0] * k, vector[1] * k, vector[2] * k ]; + } + function d3_geo_cartesianNormalize(d) { + var l = Math.sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]); + d[0] /= l; + d[1] /= l; + d[2] /= l; + } + function d3_geo_spherical(cartesian) { + return [ Math.atan2(cartesian[1], cartesian[0]), d3_asin(cartesian[2]) ]; + } + function d3_geo_sphericalEqual(a, b) { + return abs(a[0] - b[0]) < ε && abs(a[1] - b[1]) < ε; + } + d3.geo.bounds = function() { + var λ0, φ0, λ1, φ1, λ_, λ__, φ__, p0, dλSum, ranges, range; + var bound = { + point: point, + lineStart: lineStart, + lineEnd: lineEnd, + polygonStart: function() { + bound.point = ringPoint; + bound.lineStart = ringStart; + bound.lineEnd = ringEnd; + dλSum = 0; + d3_geo_area.polygonStart(); + }, + polygonEnd: function() { + d3_geo_area.polygonEnd(); + bound.point = point; + bound.lineStart = lineStart; + bound.lineEnd = lineEnd; + if (d3_geo_areaRingSum < 0) λ0 = -(λ1 = 180), φ0 = -(φ1 = 90); else if (dλSum > ε) φ1 = 90; else if (dλSum < -ε) φ0 = -90; + range[0] = λ0, range[1] = λ1; + } + }; + function point(λ, φ) { + ranges.push(range = [ λ0 = λ, λ1 = λ ]); + if (φ < φ0) φ0 = φ; + if (φ > φ1) φ1 = φ; + } + function linePoint(λ, φ) { + var p = d3_geo_cartesian([ λ * d3_radians, φ * d3_radians ]); + if (p0) { + var normal = d3_geo_cartesianCross(p0, p), equatorial = [ normal[1], -normal[0], 0 ], inflection = d3_geo_cartesianCross(equatorial, normal); + d3_geo_cartesianNormalize(inflection); + inflection = d3_geo_spherical(inflection); + var dλ = λ - λ_, s = dλ > 0 ? 1 : -1, λi = inflection[0] * d3_degrees * s, antimeridian = abs(dλ) > 180; + if (antimeridian ^ (s * λ_ < λi && λi < s * λ)) { + var φi = inflection[1] * d3_degrees; + if (φi > φ1) φ1 = φi; + } else if (λi = (λi + 360) % 360 - 180, antimeridian ^ (s * λ_ < λi && λi < s * λ)) { + var φi = -inflection[1] * d3_degrees; + if (φi < φ0) φ0 = φi; + } else { + if (φ < φ0) φ0 = φ; + if (φ > φ1) φ1 = φ; + } + if (antimeridian) { + if (λ < λ_) { + if (angle(λ0, λ) > angle(λ0, λ1)) λ1 = λ; + } else { + if (angle(λ, λ1) > angle(λ0, λ1)) λ0 = λ; + } + } else { + if (λ1 >= λ0) { + if (λ < λ0) λ0 = λ; + if (λ > λ1) λ1 = λ; + } else { + if (λ > λ_) { + if (angle(λ0, λ) > angle(λ0, λ1)) λ1 = λ; + } else { + if (angle(λ, λ1) > angle(λ0, λ1)) λ0 = λ; + } + } + } + } else { + point(λ, φ); + } + p0 = p, λ_ = λ; + } + function lineStart() { + bound.point = linePoint; + } + function lineEnd() { + range[0] = λ0, range[1] = λ1; + bound.point = point; + p0 = null; + } + function ringPoint(λ, φ) { + if (p0) { + var dλ = λ - λ_; + dλSum += abs(dλ) > 180 ? dλ + (dλ > 0 ? 360 : -360) : dλ; + } else λ__ = λ, φ__ = φ; + d3_geo_area.point(λ, φ); + linePoint(λ, φ); + } + function ringStart() { + d3_geo_area.lineStart(); + } + function ringEnd() { + ringPoint(λ__, φ__); + d3_geo_area.lineEnd(); + if (abs(dλSum) > ε) λ0 = -(λ1 = 180); + range[0] = λ0, range[1] = λ1; + p0 = null; + } + function angle(λ0, λ1) { + return (λ1 -= λ0) < 0 ? λ1 + 360 : λ1; + } + function compareRanges(a, b) { + return a[0] - b[0]; + } + function withinRange(x, range) { + return range[0] <= range[1] ? range[0] <= x && x <= range[1] : x < range[0] || range[1] < x; + } + return function(feature) { + φ1 = λ1 = -(λ0 = φ0 = Infinity); + ranges = []; + d3.geo.stream(feature, bound); + var n = ranges.length; + if (n) { + ranges.sort(compareRanges); + for (var i = 1, a = ranges[0], b, merged = [ a ]; i < n; ++i) { + b = ranges[i]; + if (withinRange(b[0], a) || withinRange(b[1], a)) { + if (angle(a[0], b[1]) > angle(a[0], a[1])) a[1] = b[1]; + if (angle(b[0], a[1]) > angle(a[0], a[1])) a[0] = b[0]; + } else { + merged.push(a = b); + } + } + var best = -Infinity, dλ; + for (var n = merged.length - 1, i = 0, a = merged[n], b; i <= n; a = b, ++i) { + b = merged[i]; + if ((dλ = angle(a[1], b[0])) > best) best = dλ, λ0 = b[0], λ1 = a[1]; + } + } + ranges = range = null; + return λ0 === Infinity || φ0 === Infinity ? [ [ NaN, NaN ], [ NaN, NaN ] ] : [ [ λ0, φ0 ], [ λ1, φ1 ] ]; + }; + }(); + d3.geo.centroid = function(object) { + d3_geo_centroidW0 = d3_geo_centroidW1 = d3_geo_centroidX0 = d3_geo_centroidY0 = d3_geo_centroidZ0 = d3_geo_centroidX1 = d3_geo_centroidY1 = d3_geo_centroidZ1 = d3_geo_centroidX2 = d3_geo_centroidY2 = d3_geo_centroidZ2 = 0; + d3.geo.stream(object, d3_geo_centroid); + var x = d3_geo_centroidX2, y = d3_geo_centroidY2, z = d3_geo_centroidZ2, m = x * x + y * y + z * z; + if (m < ε2) { + x = d3_geo_centroidX1, y = d3_geo_centroidY1, z = d3_geo_centroidZ1; + if (d3_geo_centroidW1 < ε) x = d3_geo_centroidX0, y = d3_geo_centroidY0, z = d3_geo_centroidZ0; + m = x * x + y * y + z * z; + if (m < ε2) return [ NaN, NaN ]; + } + return [ Math.atan2(y, x) * d3_degrees, d3_asin(z / Math.sqrt(m)) * d3_degrees ]; + }; + var d3_geo_centroidW0, d3_geo_centroidW1, d3_geo_centroidX0, d3_geo_centroidY0, d3_geo_centroidZ0, d3_geo_centroidX1, d3_geo_centroidY1, d3_geo_centroidZ1, d3_geo_centroidX2, d3_geo_centroidY2, d3_geo_centroidZ2; + var d3_geo_centroid = { + sphere: d3_noop, + point: d3_geo_centroidPoint, + lineStart: d3_geo_centroidLineStart, + lineEnd: d3_geo_centroidLineEnd, + polygonStart: function() { + d3_geo_centroid.lineStart = d3_geo_centroidRingStart; + }, + polygonEnd: function() { + d3_geo_centroid.lineStart = d3_geo_centroidLineStart; + } + }; + function d3_geo_centroidPoint(λ, φ) { + λ *= d3_radians; + var cosφ = Math.cos(φ *= d3_radians); + d3_geo_centroidPointXYZ(cosφ * Math.cos(λ), cosφ * Math.sin(λ), Math.sin(φ)); + } + function d3_geo_centroidPointXYZ(x, y, z) { + ++d3_geo_centroidW0; + d3_geo_centroidX0 += (x - d3_geo_centroidX0) / d3_geo_centroidW0; + d3_geo_centroidY0 += (y - d3_geo_centroidY0) / d3_geo_centroidW0; + d3_geo_centroidZ0 += (z - d3_geo_centroidZ0) / d3_geo_centroidW0; + } + function d3_geo_centroidLineStart() { + var x0, y0, z0; + d3_geo_centroid.point = function(λ, φ) { + λ *= d3_radians; + var cosφ = Math.cos(φ *= d3_radians); + x0 = cosφ * Math.cos(λ); + y0 = cosφ * Math.sin(λ); + z0 = Math.sin(φ); + d3_geo_centroid.point = nextPoint; + d3_geo_centroidPointXYZ(x0, y0, z0); + }; + function nextPoint(λ, φ) { + λ *= d3_radians; + var cosφ = Math.cos(φ *= d3_radians), x = cosφ * Math.cos(λ), y = cosφ * Math.sin(λ), z = Math.sin(φ), w = Math.atan2(Math.sqrt((w = y0 * z - z0 * y) * w + (w = z0 * x - x0 * z) * w + (w = x0 * y - y0 * x) * w), x0 * x + y0 * y + z0 * z); + d3_geo_centroidW1 += w; + d3_geo_centroidX1 += w * (x0 + (x0 = x)); + d3_geo_centroidY1 += w * (y0 + (y0 = y)); + d3_geo_centroidZ1 += w * (z0 + (z0 = z)); + d3_geo_centroidPointXYZ(x0, y0, z0); + } + } + function d3_geo_centroidLineEnd() { + d3_geo_centroid.point = d3_geo_centroidPoint; + } + function d3_geo_centroidRingStart() { + var λ00, φ00, x0, y0, z0; + d3_geo_centroid.point = function(λ, φ) { + λ00 = λ, φ00 = φ; + d3_geo_centroid.point = nextPoint; + λ *= d3_radians; + var cosφ = Math.cos(φ *= d3_radians); + x0 = cosφ * Math.cos(λ); + y0 = cosφ * Math.sin(λ); + z0 = Math.sin(φ); + d3_geo_centroidPointXYZ(x0, y0, z0); + }; + d3_geo_centroid.lineEnd = function() { + nextPoint(λ00, φ00); + d3_geo_centroid.lineEnd = d3_geo_centroidLineEnd; + d3_geo_centroid.point = d3_geo_centroidPoint; + }; + function nextPoint(λ, φ) { + λ *= d3_radians; + var cosφ = Math.cos(φ *= d3_radians), x = cosφ * Math.cos(λ), y = cosφ * Math.sin(λ), z = Math.sin(φ), cx = y0 * z - z0 * y, cy = z0 * x - x0 * z, cz = x0 * y - y0 * x, m = Math.sqrt(cx * cx + cy * cy + cz * cz), u = x0 * x + y0 * y + z0 * z, v = m && -d3_acos(u) / m, w = Math.atan2(m, u); + d3_geo_centroidX2 += v * cx; + d3_geo_centroidY2 += v * cy; + d3_geo_centroidZ2 += v * cz; + d3_geo_centroidW1 += w; + d3_geo_centroidX1 += w * (x0 + (x0 = x)); + d3_geo_centroidY1 += w * (y0 + (y0 = y)); + d3_geo_centroidZ1 += w * (z0 + (z0 = z)); + d3_geo_centroidPointXYZ(x0, y0, z0); + } + } + function d3_geo_compose(a, b) { + function compose(x, y) { + return x = a(x, y), b(x[0], x[1]); + } + if (a.invert && b.invert) compose.invert = function(x, y) { + return x = b.invert(x, y), x && a.invert(x[0], x[1]); + }; + return compose; + } + function d3_true() { + return true; + } + function d3_geo_clipPolygon(segments, compare, clipStartInside, interpolate, listener) { + var subject = [], clip = []; + segments.forEach(function(segment) { + if ((n = segment.length - 1) <= 0) return; + var n, p0 = segment[0], p1 = segment[n]; + if (d3_geo_sphericalEqual(p0, p1)) { + listener.lineStart(); + for (var i = 0; i < n; ++i) listener.point((p0 = segment[i])[0], p0[1]); + listener.lineEnd(); + return; + } + var a = new d3_geo_clipPolygonIntersection(p0, segment, null, true), b = new d3_geo_clipPolygonIntersection(p0, null, a, false); + a.o = b; + subject.push(a); + clip.push(b); + a = new d3_geo_clipPolygonIntersection(p1, segment, null, false); + b = new d3_geo_clipPolygonIntersection(p1, null, a, true); + a.o = b; + subject.push(a); + clip.push(b); + }); + clip.sort(compare); + d3_geo_clipPolygonLinkCircular(subject); + d3_geo_clipPolygonLinkCircular(clip); + if (!subject.length) return; + for (var i = 0, entry = clipStartInside, n = clip.length; i < n; ++i) { + clip[i].e = entry = !entry; + } + var start = subject[0], points, point; + while (1) { + var current = start, isSubject = true; + while (current.v) if ((current = current.n) === start) return; + points = current.z; + listener.lineStart(); + do { + current.v = current.o.v = true; + if (current.e) { + if (isSubject) { + for (var i = 0, n = points.length; i < n; ++i) listener.point((point = points[i])[0], point[1]); + } else { + interpolate(current.x, current.n.x, 1, listener); + } + current = current.n; + } else { + if (isSubject) { + points = current.p.z; + for (var i = points.length - 1; i >= 0; --i) listener.point((point = points[i])[0], point[1]); + } else { + interpolate(current.x, current.p.x, -1, listener); + } + current = current.p; + } + current = current.o; + points = current.z; + isSubject = !isSubject; + } while (!current.v); + listener.lineEnd(); + } + } + function d3_geo_clipPolygonLinkCircular(array) { + if (!(n = array.length)) return; + var n, i = 0, a = array[0], b; + while (++i < n) { + a.n = b = array[i]; + b.p = a; + a = b; + } + a.n = b = array[0]; + b.p = a; + } + function d3_geo_clipPolygonIntersection(point, points, other, entry) { + this.x = point; + this.z = points; + this.o = other; + this.e = entry; + this.v = false; + this.n = this.p = null; + } + function d3_geo_clip(pointVisible, clipLine, interpolate, clipStart) { + return function(rotate, listener) { + var line = clipLine(listener), rotatedClipStart = rotate.invert(clipStart[0], clipStart[1]); + var clip = { + point: point, + lineStart: lineStart, + lineEnd: lineEnd, + polygonStart: function() { + clip.point = pointRing; + clip.lineStart = ringStart; + clip.lineEnd = ringEnd; + segments = []; + polygon = []; + }, + polygonEnd: function() { + clip.point = point; + clip.lineStart = lineStart; + clip.lineEnd = lineEnd; + segments = d3.merge(segments); + var clipStartInside = d3_geo_pointInPolygon(rotatedClipStart, polygon); + if (segments.length) { + if (!polygonStarted) listener.polygonStart(), polygonStarted = true; + d3_geo_clipPolygon(segments, d3_geo_clipSort, clipStartInside, interpolate, listener); + } else if (clipStartInside) { + if (!polygonStarted) listener.polygonStart(), polygonStarted = true; + listener.lineStart(); + interpolate(null, null, 1, listener); + listener.lineEnd(); + } + if (polygonStarted) listener.polygonEnd(), polygonStarted = false; + segments = polygon = null; + }, + sphere: function() { + listener.polygonStart(); + listener.lineStart(); + interpolate(null, null, 1, listener); + listener.lineEnd(); + listener.polygonEnd(); + } + }; + function point(λ, φ) { + var point = rotate(λ, φ); + if (pointVisible(λ = point[0], φ = point[1])) listener.point(λ, φ); + } + function pointLine(λ, φ) { + var point = rotate(λ, φ); + line.point(point[0], point[1]); + } + function lineStart() { + clip.point = pointLine; + line.lineStart(); + } + function lineEnd() { + clip.point = point; + line.lineEnd(); + } + var segments; + var buffer = d3_geo_clipBufferListener(), ringListener = clipLine(buffer), polygonStarted = false, polygon, ring; + function pointRing(λ, φ) { + ring.push([ λ, φ ]); + var point = rotate(λ, φ); + ringListener.point(point[0], point[1]); + } + function ringStart() { + ringListener.lineStart(); + ring = []; + } + function ringEnd() { + pointRing(ring[0][0], ring[0][1]); + ringListener.lineEnd(); + var clean = ringListener.clean(), ringSegments = buffer.buffer(), segment, n = ringSegments.length; + ring.pop(); + polygon.push(ring); + ring = null; + if (!n) return; + if (clean & 1) { + segment = ringSegments[0]; + var n = segment.length - 1, i = -1, point; + if (n > 0) { + if (!polygonStarted) listener.polygonStart(), polygonStarted = true; + listener.lineStart(); + while (++i < n) listener.point((point = segment[i])[0], point[1]); + listener.lineEnd(); + } + return; + } + if (n > 1 && clean & 2) ringSegments.push(ringSegments.pop().concat(ringSegments.shift())); + segments.push(ringSegments.filter(d3_geo_clipSegmentLength1)); + } + return clip; + }; + } + function d3_geo_clipSegmentLength1(segment) { + return segment.length > 1; + } + function d3_geo_clipBufferListener() { + var lines = [], line; + return { + lineStart: function() { + lines.push(line = []); + }, + point: function(λ, φ) { + line.push([ λ, φ ]); + }, + lineEnd: d3_noop, + buffer: function() { + var buffer = lines; + lines = []; + line = null; + return buffer; + }, + rejoin: function() { + if (lines.length > 1) lines.push(lines.pop().concat(lines.shift())); + } + }; + } + function d3_geo_clipSort(a, b) { + return ((a = a.x)[0] < 0 ? a[1] - halfπ - ε : halfπ - a[1]) - ((b = b.x)[0] < 0 ? b[1] - halfπ - ε : halfπ - b[1]); + } + var d3_geo_clipAntimeridian = d3_geo_clip(d3_true, d3_geo_clipAntimeridianLine, d3_geo_clipAntimeridianInterpolate, [ -π, -π / 2 ]); + function d3_geo_clipAntimeridianLine(listener) { + var λ0 = NaN, φ0 = NaN, sλ0 = NaN, clean; + return { + lineStart: function() { + listener.lineStart(); + clean = 1; + }, + point: function(λ1, φ1) { + var sλ1 = λ1 > 0 ? π : -π, dλ = abs(λ1 - λ0); + if (abs(dλ - π) < ε) { + listener.point(λ0, φ0 = (φ0 + φ1) / 2 > 0 ? halfπ : -halfπ); + listener.point(sλ0, φ0); + listener.lineEnd(); + listener.lineStart(); + listener.point(sλ1, φ0); + listener.point(λ1, φ0); + clean = 0; + } else if (sλ0 !== sλ1 && dλ >= π) { + if (abs(λ0 - sλ0) < ε) λ0 -= sλ0 * ε; + if (abs(λ1 - sλ1) < ε) λ1 -= sλ1 * ε; + φ0 = d3_geo_clipAntimeridianIntersect(λ0, φ0, λ1, φ1); + listener.point(sλ0, φ0); + listener.lineEnd(); + listener.lineStart(); + listener.point(sλ1, φ0); + clean = 0; + } + listener.point(λ0 = λ1, φ0 = φ1); + sλ0 = sλ1; + }, + lineEnd: function() { + listener.lineEnd(); + λ0 = φ0 = NaN; + }, + clean: function() { + return 2 - clean; + } + }; + } + function d3_geo_clipAntimeridianIntersect(λ0, φ0, λ1, φ1) { + var cosφ0, cosφ1, sinλ0_λ1 = Math.sin(λ0 - λ1); + return abs(sinλ0_λ1) > ε ? Math.atan((Math.sin(φ0) * (cosφ1 = Math.cos(φ1)) * Math.sin(λ1) - Math.sin(φ1) * (cosφ0 = Math.cos(φ0)) * Math.sin(λ0)) / (cosφ0 * cosφ1 * sinλ0_λ1)) : (φ0 + φ1) / 2; + } + function d3_geo_clipAntimeridianInterpolate(from, to, direction, listener) { + var φ; + if (from == null) { + φ = direction * halfπ; + listener.point(-π, φ); + listener.point(0, φ); + listener.point(π, φ); + listener.point(π, 0); + listener.point(π, -φ); + listener.point(0, -φ); + listener.point(-π, -φ); + listener.point(-π, 0); + listener.point(-π, φ); + } else if (abs(from[0] - to[0]) > ε) { + var s = from[0] < to[0] ? π : -π; + φ = direction * s / 2; + listener.point(-s, φ); + listener.point(0, φ); + listener.point(s, φ); + } else { + listener.point(to[0], to[1]); + } + } + function d3_geo_pointInPolygon(point, polygon) { + var meridian = point[0], parallel = point[1], meridianNormal = [ Math.sin(meridian), -Math.cos(meridian), 0 ], polarAngle = 0, winding = 0; + d3_geo_areaRingSum.reset(); + for (var i = 0, n = polygon.length; i < n; ++i) { + var ring = polygon[i], m = ring.length; + if (!m) continue; + var point0 = ring[0], λ0 = point0[0], φ0 = point0[1] / 2 + π / 4, sinφ0 = Math.sin(φ0), cosφ0 = Math.cos(φ0), j = 1; + while (true) { + if (j === m) j = 0; + point = ring[j]; + var λ = point[0], φ = point[1] / 2 + π / 4, sinφ = Math.sin(φ), cosφ = Math.cos(φ), dλ = λ - λ0, sdλ = dλ >= 0 ? 1 : -1, adλ = sdλ * dλ, antimeridian = adλ > π, k = sinφ0 * sinφ; + d3_geo_areaRingSum.add(Math.atan2(k * sdλ * Math.sin(adλ), cosφ0 * cosφ + k * Math.cos(adλ))); + polarAngle += antimeridian ? dλ + sdλ * τ : dλ; + if (antimeridian ^ λ0 >= meridian ^ λ >= meridian) { + var arc = d3_geo_cartesianCross(d3_geo_cartesian(point0), d3_geo_cartesian(point)); + d3_geo_cartesianNormalize(arc); + var intersection = d3_geo_cartesianCross(meridianNormal, arc); + d3_geo_cartesianNormalize(intersection); + var φarc = (antimeridian ^ dλ >= 0 ? -1 : 1) * d3_asin(intersection[2]); + if (parallel > φarc || parallel === φarc && (arc[0] || arc[1])) { + winding += antimeridian ^ dλ >= 0 ? 1 : -1; + } + } + if (!j++) break; + λ0 = λ, sinφ0 = sinφ, cosφ0 = cosφ, point0 = point; + } + } + return (polarAngle < -ε || polarAngle < ε && d3_geo_areaRingSum < 0) ^ winding & 1; + } + function d3_geo_clipCircle(radius) { + var cr = Math.cos(radius), smallRadius = cr > 0, notHemisphere = abs(cr) > ε, interpolate = d3_geo_circleInterpolate(radius, 6 * d3_radians); + return d3_geo_clip(visible, clipLine, interpolate, smallRadius ? [ 0, -radius ] : [ -π, radius - π ]); + function visible(λ, φ) { + return Math.cos(λ) * Math.cos(φ) > cr; + } + function clipLine(listener) { + var point0, c0, v0, v00, clean; + return { + lineStart: function() { + v00 = v0 = false; + clean = 1; + }, + point: function(λ, φ) { + var point1 = [ λ, φ ], point2, v = visible(λ, φ), c = smallRadius ? v ? 0 : code(λ, φ) : v ? code(λ + (λ < 0 ? π : -π), φ) : 0; + if (!point0 && (v00 = v0 = v)) listener.lineStart(); + if (v !== v0) { + point2 = intersect(point0, point1); + if (d3_geo_sphericalEqual(point0, point2) || d3_geo_sphericalEqual(point1, point2)) { + point1[0] += ε; + point1[1] += ε; + v = visible(point1[0], point1[1]); + } + } + if (v !== v0) { + clean = 0; + if (v) { + listener.lineStart(); + point2 = intersect(point1, point0); + listener.point(point2[0], point2[1]); + } else { + point2 = intersect(point0, point1); + listener.point(point2[0], point2[1]); + listener.lineEnd(); + } + point0 = point2; + } else if (notHemisphere && point0 && smallRadius ^ v) { + var t; + if (!(c & c0) && (t = intersect(point1, point0, true))) { + clean = 0; + if (smallRadius) { + listener.lineStart(); + listener.point(t[0][0], t[0][1]); + listener.point(t[1][0], t[1][1]); + listener.lineEnd(); + } else { + listener.point(t[1][0], t[1][1]); + listener.lineEnd(); + listener.lineStart(); + listener.point(t[0][0], t[0][1]); + } + } + } + if (v && (!point0 || !d3_geo_sphericalEqual(point0, point1))) { + listener.point(point1[0], point1[1]); + } + point0 = point1, v0 = v, c0 = c; + }, + lineEnd: function() { + if (v0) listener.lineEnd(); + point0 = null; + }, + clean: function() { + return clean | (v00 && v0) << 1; + } + }; + } + function intersect(a, b, two) { + var pa = d3_geo_cartesian(a), pb = d3_geo_cartesian(b); + var n1 = [ 1, 0, 0 ], n2 = d3_geo_cartesianCross(pa, pb), n2n2 = d3_geo_cartesianDot(n2, n2), n1n2 = n2[0], determinant = n2n2 - n1n2 * n1n2; + if (!determinant) return !two && a; + var c1 = cr * n2n2 / determinant, c2 = -cr * n1n2 / determinant, n1xn2 = d3_geo_cartesianCross(n1, n2), A = d3_geo_cartesianScale(n1, c1), B = d3_geo_cartesianScale(n2, c2); + d3_geo_cartesianAdd(A, B); + var u = n1xn2, w = d3_geo_cartesianDot(A, u), uu = d3_geo_cartesianDot(u, u), t2 = w * w - uu * (d3_geo_cartesianDot(A, A) - 1); + if (t2 < 0) return; + var t = Math.sqrt(t2), q = d3_geo_cartesianScale(u, (-w - t) / uu); + d3_geo_cartesianAdd(q, A); + q = d3_geo_spherical(q); + if (!two) return q; + var λ0 = a[0], λ1 = b[0], φ0 = a[1], φ1 = b[1], z; + if (λ1 < λ0) z = λ0, λ0 = λ1, λ1 = z; + var δλ = λ1 - λ0, polar = abs(δλ - π) < ε, meridian = polar || δλ < ε; + if (!polar && φ1 < φ0) z = φ0, φ0 = φ1, φ1 = z; + if (meridian ? polar ? φ0 + φ1 > 0 ^ q[1] < (abs(q[0] - λ0) < ε ? φ0 : φ1) : φ0 <= q[1] && q[1] <= φ1 : δλ > π ^ (λ0 <= q[0] && q[0] <= λ1)) { + var q1 = d3_geo_cartesianScale(u, (-w + t) / uu); + d3_geo_cartesianAdd(q1, A); + return [ q, d3_geo_spherical(q1) ]; + } + } + function code(λ, φ) { + var r = smallRadius ? radius : π - radius, code = 0; + if (λ < -r) code |= 1; else if (λ > r) code |= 2; + if (φ < -r) code |= 4; else if (φ > r) code |= 8; + return code; + } + } + function d3_geom_clipLine(x0, y0, x1, y1) { + return function(line) { + var a = line.a, b = line.b, ax = a.x, ay = a.y, bx = b.x, by = b.y, t0 = 0, t1 = 1, dx = bx - ax, dy = by - ay, r; + r = x0 - ax; + if (!dx && r > 0) return; + r /= dx; + if (dx < 0) { + if (r < t0) return; + if (r < t1) t1 = r; + } else if (dx > 0) { + if (r > t1) return; + if (r > t0) t0 = r; + } + r = x1 - ax; + if (!dx && r < 0) return; + r /= dx; + if (dx < 0) { + if (r > t1) return; + if (r > t0) t0 = r; + } else if (dx > 0) { + if (r < t0) return; + if (r < t1) t1 = r; + } + r = y0 - ay; + if (!dy && r > 0) return; + r /= dy; + if (dy < 0) { + if (r < t0) return; + if (r < t1) t1 = r; + } else if (dy > 0) { + if (r > t1) return; + if (r > t0) t0 = r; + } + r = y1 - ay; + if (!dy && r < 0) return; + r /= dy; + if (dy < 0) { + if (r > t1) return; + if (r > t0) t0 = r; + } else if (dy > 0) { + if (r < t0) return; + if (r < t1) t1 = r; + } + if (t0 > 0) line.a = { + x: ax + t0 * dx, + y: ay + t0 * dy + }; + if (t1 < 1) line.b = { + x: ax + t1 * dx, + y: ay + t1 * dy + }; + return line; + }; + } + var d3_geo_clipExtentMAX = 1e9; + d3.geo.clipExtent = function() { + var x0, y0, x1, y1, stream, clip, clipExtent = { + stream: function(output) { + if (stream) stream.valid = false; + stream = clip(output); + stream.valid = true; + return stream; + }, + extent: function(_) { + if (!arguments.length) return [ [ x0, y0 ], [ x1, y1 ] ]; + clip = d3_geo_clipExtent(x0 = +_[0][0], y0 = +_[0][1], x1 = +_[1][0], y1 = +_[1][1]); + if (stream) stream.valid = false, stream = null; + return clipExtent; + } + }; + return clipExtent.extent([ [ 0, 0 ], [ 960, 500 ] ]); + }; + function d3_geo_clipExtent(x0, y0, x1, y1) { + return function(listener) { + var listener_ = listener, bufferListener = d3_geo_clipBufferListener(), clipLine = d3_geom_clipLine(x0, y0, x1, y1), segments, polygon, ring; + var clip = { + point: point, + lineStart: lineStart, + lineEnd: lineEnd, + polygonStart: function() { + listener = bufferListener; + segments = []; + polygon = []; + clean = true; + }, + polygonEnd: function() { + listener = listener_; + segments = d3.merge(segments); + var clipStartInside = insidePolygon([ x0, y1 ]), inside = clean && clipStartInside, visible = segments.length; + if (inside || visible) { + listener.polygonStart(); + if (inside) { + listener.lineStart(); + interpolate(null, null, 1, listener); + listener.lineEnd(); + } + if (visible) { + d3_geo_clipPolygon(segments, compare, clipStartInside, interpolate, listener); + } + listener.polygonEnd(); + } + segments = polygon = ring = null; + } + }; + function insidePolygon(p) { + var wn = 0, n = polygon.length, y = p[1]; + for (var i = 0; i < n; ++i) { + for (var j = 1, v = polygon[i], m = v.length, a = v[0], b; j < m; ++j) { + b = v[j]; + if (a[1] <= y) { + if (b[1] > y && d3_cross2d(a, b, p) > 0) ++wn; + } else { + if (b[1] <= y && d3_cross2d(a, b, p) < 0) --wn; + } + a = b; + } + } + return wn !== 0; + } + function interpolate(from, to, direction, listener) { + var a = 0, a1 = 0; + if (from == null || (a = corner(from, direction)) !== (a1 = corner(to, direction)) || comparePoints(from, to) < 0 ^ direction > 0) { + do { + listener.point(a === 0 || a === 3 ? x0 : x1, a > 1 ? y1 : y0); + } while ((a = (a + direction + 4) % 4) !== a1); + } else { + listener.point(to[0], to[1]); + } + } + function pointVisible(x, y) { + return x0 <= x && x <= x1 && y0 <= y && y <= y1; + } + function point(x, y) { + if (pointVisible(x, y)) listener.point(x, y); + } + var x__, y__, v__, x_, y_, v_, first, clean; + function lineStart() { + clip.point = linePoint; + if (polygon) polygon.push(ring = []); + first = true; + v_ = false; + x_ = y_ = NaN; + } + function lineEnd() { + if (segments) { + linePoint(x__, y__); + if (v__ && v_) bufferListener.rejoin(); + segments.push(bufferListener.buffer()); + } + clip.point = point; + if (v_) listener.lineEnd(); + } + function linePoint(x, y) { + x = Math.max(-d3_geo_clipExtentMAX, Math.min(d3_geo_clipExtentMAX, x)); + y = Math.max(-d3_geo_clipExtentMAX, Math.min(d3_geo_clipExtentMAX, y)); + var v = pointVisible(x, y); + if (polygon) ring.push([ x, y ]); + if (first) { + x__ = x, y__ = y, v__ = v; + first = false; + if (v) { + listener.lineStart(); + listener.point(x, y); + } + } else { + if (v && v_) listener.point(x, y); else { + var l = { + a: { + x: x_, + y: y_ + }, + b: { + x: x, + y: y + } + }; + if (clipLine(l)) { + if (!v_) { + listener.lineStart(); + listener.point(l.a.x, l.a.y); + } + listener.point(l.b.x, l.b.y); + if (!v) listener.lineEnd(); + clean = false; + } else if (v) { + listener.lineStart(); + listener.point(x, y); + clean = false; + } + } + } + x_ = x, y_ = y, v_ = v; + } + return clip; + }; + function corner(p, direction) { + return abs(p[0] - x0) < ε ? direction > 0 ? 0 : 3 : abs(p[0] - x1) < ε ? direction > 0 ? 2 : 1 : abs(p[1] - y0) < ε ? direction > 0 ? 1 : 0 : direction > 0 ? 3 : 2; + } + function compare(a, b) { + return comparePoints(a.x, b.x); + } + function comparePoints(a, b) { + var ca = corner(a, 1), cb = corner(b, 1); + return ca !== cb ? ca - cb : ca === 0 ? b[1] - a[1] : ca === 1 ? a[0] - b[0] : ca === 2 ? a[1] - b[1] : b[0] - a[0]; + } + } + function d3_geo_conic(projectAt) { + var φ0 = 0, φ1 = π / 3, m = d3_geo_projectionMutator(projectAt), p = m(φ0, φ1); + p.parallels = function(_) { + if (!arguments.length) return [ φ0 / π * 180, φ1 / π * 180 ]; + return m(φ0 = _[0] * π / 180, φ1 = _[1] * π / 180); + }; + return p; + } + function d3_geo_conicEqualArea(φ0, φ1) { + var sinφ0 = Math.sin(φ0), n = (sinφ0 + Math.sin(φ1)) / 2, C = 1 + sinφ0 * (2 * n - sinφ0), ρ0 = Math.sqrt(C) / n; + function forward(λ, φ) { + var ρ = Math.sqrt(C - 2 * n * Math.sin(φ)) / n; + return [ ρ * Math.sin(λ *= n), ρ0 - ρ * Math.cos(λ) ]; + } + forward.invert = function(x, y) { + var ρ0_y = ρ0 - y; + return [ Math.atan2(x, ρ0_y) / n, d3_asin((C - (x * x + ρ0_y * ρ0_y) * n * n) / (2 * n)) ]; + }; + return forward; + } + (d3.geo.conicEqualArea = function() { + return d3_geo_conic(d3_geo_conicEqualArea); + }).raw = d3_geo_conicEqualArea; + d3.geo.albers = function() { + return d3.geo.conicEqualArea().rotate([ 96, 0 ]).center([ -.6, 38.7 ]).parallels([ 29.5, 45.5 ]).scale(1070); + }; + d3.geo.albersUsa = function() { + var lower48 = d3.geo.albers(); + var alaska = d3.geo.conicEqualArea().rotate([ 154, 0 ]).center([ -2, 58.5 ]).parallels([ 55, 65 ]); + var hawaii = d3.geo.conicEqualArea().rotate([ 157, 0 ]).center([ -3, 19.9 ]).parallels([ 8, 18 ]); + var point, pointStream = { + point: function(x, y) { + point = [ x, y ]; + } + }, lower48Point, alaskaPoint, hawaiiPoint; + function albersUsa(coordinates) { + var x = coordinates[0], y = coordinates[1]; + point = null; + (lower48Point(x, y), point) || (alaskaPoint(x, y), point) || hawaiiPoint(x, y); + return point; + } + albersUsa.invert = function(coordinates) { + var k = lower48.scale(), t = lower48.translate(), x = (coordinates[0] - t[0]) / k, y = (coordinates[1] - t[1]) / k; + return (y >= .12 && y < .234 && x >= -.425 && x < -.214 ? alaska : y >= .166 && y < .234 && x >= -.214 && x < -.115 ? hawaii : lower48).invert(coordinates); + }; + albersUsa.stream = function(stream) { + var lower48Stream = lower48.stream(stream), alaskaStream = alaska.stream(stream), hawaiiStream = hawaii.stream(stream); + return { + point: function(x, y) { + lower48Stream.point(x, y); + alaskaStream.point(x, y); + hawaiiStream.point(x, y); + }, + sphere: function() { + lower48Stream.sphere(); + alaskaStream.sphere(); + hawaiiStream.sphere(); + }, + lineStart: function() { + lower48Stream.lineStart(); + alaskaStream.lineStart(); + hawaiiStream.lineStart(); + }, + lineEnd: function() { + lower48Stream.lineEnd(); + alaskaStream.lineEnd(); + hawaiiStream.lineEnd(); + }, + polygonStart: function() { + lower48Stream.polygonStart(); + alaskaStream.polygonStart(); + hawaiiStream.polygonStart(); + }, + polygonEnd: function() { + lower48Stream.polygonEnd(); + alaskaStream.polygonEnd(); + hawaiiStream.polygonEnd(); + } + }; + }; + albersUsa.precision = function(_) { + if (!arguments.length) return lower48.precision(); + lower48.precision(_); + alaska.precision(_); + hawaii.precision(_); + return albersUsa; + }; + albersUsa.scale = function(_) { + if (!arguments.length) return lower48.scale(); + lower48.scale(_); + alaska.scale(_ * .35); + hawaii.scale(_); + return albersUsa.translate(lower48.translate()); + }; + albersUsa.translate = function(_) { + if (!arguments.length) return lower48.translate(); + var k = lower48.scale(), x = +_[0], y = +_[1]; + lower48Point = lower48.translate(_).clipExtent([ [ x - .455 * k, y - .238 * k ], [ x + .455 * k, y + .238 * k ] ]).stream(pointStream).point; + alaskaPoint = alaska.translate([ x - .307 * k, y + .201 * k ]).clipExtent([ [ x - .425 * k + ε, y + .12 * k + ε ], [ x - .214 * k - ε, y + .234 * k - ε ] ]).stream(pointStream).point; + hawaiiPoint = hawaii.translate([ x - .205 * k, y + .212 * k ]).clipExtent([ [ x - .214 * k + ε, y + .166 * k + ε ], [ x - .115 * k - ε, y + .234 * k - ε ] ]).stream(pointStream).point; + return albersUsa; + }; + return albersUsa.scale(1070); + }; + var d3_geo_pathAreaSum, d3_geo_pathAreaPolygon, d3_geo_pathArea = { + point: d3_noop, + lineStart: d3_noop, + lineEnd: d3_noop, + polygonStart: function() { + d3_geo_pathAreaPolygon = 0; + d3_geo_pathArea.lineStart = d3_geo_pathAreaRingStart; + }, + polygonEnd: function() { + d3_geo_pathArea.lineStart = d3_geo_pathArea.lineEnd = d3_geo_pathArea.point = d3_noop; + d3_geo_pathAreaSum += abs(d3_geo_pathAreaPolygon / 2); + } + }; + function d3_geo_pathAreaRingStart() { + var x00, y00, x0, y0; + d3_geo_pathArea.point = function(x, y) { + d3_geo_pathArea.point = nextPoint; + x00 = x0 = x, y00 = y0 = y; + }; + function nextPoint(x, y) { + d3_geo_pathAreaPolygon += y0 * x - x0 * y; + x0 = x, y0 = y; + } + d3_geo_pathArea.lineEnd = function() { + nextPoint(x00, y00); + }; + } + var d3_geo_pathBoundsX0, d3_geo_pathBoundsY0, d3_geo_pathBoundsX1, d3_geo_pathBoundsY1; + var d3_geo_pathBounds = { + point: d3_geo_pathBoundsPoint, + lineStart: d3_noop, + lineEnd: d3_noop, + polygonStart: d3_noop, + polygonEnd: d3_noop + }; + function d3_geo_pathBoundsPoint(x, y) { + if (x < d3_geo_pathBoundsX0) d3_geo_pathBoundsX0 = x; + if (x > d3_geo_pathBoundsX1) d3_geo_pathBoundsX1 = x; + if (y < d3_geo_pathBoundsY0) d3_geo_pathBoundsY0 = y; + if (y > d3_geo_pathBoundsY1) d3_geo_pathBoundsY1 = y; + } + function d3_geo_pathBuffer() { + var pointCircle = d3_geo_pathBufferCircle(4.5), buffer = []; + var stream = { + point: point, + lineStart: function() { + stream.point = pointLineStart; + }, + lineEnd: lineEnd, + polygonStart: function() { + stream.lineEnd = lineEndPolygon; + }, + polygonEnd: function() { + stream.lineEnd = lineEnd; + stream.point = point; + }, + pointRadius: function(_) { + pointCircle = d3_geo_pathBufferCircle(_); + return stream; + }, + result: function() { + if (buffer.length) { + var result = buffer.join(""); + buffer = []; + return result; + } + } + }; + function point(x, y) { + buffer.push("M", x, ",", y, pointCircle); + } + function pointLineStart(x, y) { + buffer.push("M", x, ",", y); + stream.point = pointLine; + } + function pointLine(x, y) { + buffer.push("L", x, ",", y); + } + function lineEnd() { + stream.point = point; + } + function lineEndPolygon() { + buffer.push("Z"); + } + return stream; + } + function d3_geo_pathBufferCircle(radius) { + return "m0," + radius + "a" + radius + "," + radius + " 0 1,1 0," + -2 * radius + "a" + radius + "," + radius + " 0 1,1 0," + 2 * radius + "z"; + } + var d3_geo_pathCentroid = { + point: d3_geo_pathCentroidPoint, + lineStart: d3_geo_pathCentroidLineStart, + lineEnd: d3_geo_pathCentroidLineEnd, + polygonStart: function() { + d3_geo_pathCentroid.lineStart = d3_geo_pathCentroidRingStart; + }, + polygonEnd: function() { + d3_geo_pathCentroid.point = d3_geo_pathCentroidPoint; + d3_geo_pathCentroid.lineStart = d3_geo_pathCentroidLineStart; + d3_geo_pathCentroid.lineEnd = d3_geo_pathCentroidLineEnd; + } + }; + function d3_geo_pathCentroidPoint(x, y) { + d3_geo_centroidX0 += x; + d3_geo_centroidY0 += y; + ++d3_geo_centroidZ0; + } + function d3_geo_pathCentroidLineStart() { + var x0, y0; + d3_geo_pathCentroid.point = function(x, y) { + d3_geo_pathCentroid.point = nextPoint; + d3_geo_pathCentroidPoint(x0 = x, y0 = y); + }; + function nextPoint(x, y) { + var dx = x - x0, dy = y - y0, z = Math.sqrt(dx * dx + dy * dy); + d3_geo_centroidX1 += z * (x0 + x) / 2; + d3_geo_centroidY1 += z * (y0 + y) / 2; + d3_geo_centroidZ1 += z; + d3_geo_pathCentroidPoint(x0 = x, y0 = y); + } + } + function d3_geo_pathCentroidLineEnd() { + d3_geo_pathCentroid.point = d3_geo_pathCentroidPoint; + } + function d3_geo_pathCentroidRingStart() { + var x00, y00, x0, y0; + d3_geo_pathCentroid.point = function(x, y) { + d3_geo_pathCentroid.point = nextPoint; + d3_geo_pathCentroidPoint(x00 = x0 = x, y00 = y0 = y); + }; + function nextPoint(x, y) { + var dx = x - x0, dy = y - y0, z = Math.sqrt(dx * dx + dy * dy); + d3_geo_centroidX1 += z * (x0 + x) / 2; + d3_geo_centroidY1 += z * (y0 + y) / 2; + d3_geo_centroidZ1 += z; + z = y0 * x - x0 * y; + d3_geo_centroidX2 += z * (x0 + x); + d3_geo_centroidY2 += z * (y0 + y); + d3_geo_centroidZ2 += z * 3; + d3_geo_pathCentroidPoint(x0 = x, y0 = y); + } + d3_geo_pathCentroid.lineEnd = function() { + nextPoint(x00, y00); + }; + } + function d3_geo_pathContext(context) { + var pointRadius = 4.5; + var stream = { + point: point, + lineStart: function() { + stream.point = pointLineStart; + }, + lineEnd: lineEnd, + polygonStart: function() { + stream.lineEnd = lineEndPolygon; + }, + polygonEnd: function() { + stream.lineEnd = lineEnd; + stream.point = point; + }, + pointRadius: function(_) { + pointRadius = _; + return stream; + }, + result: d3_noop + }; + function point(x, y) { + context.moveTo(x + pointRadius, y); + context.arc(x, y, pointRadius, 0, τ); + } + function pointLineStart(x, y) { + context.moveTo(x, y); + stream.point = pointLine; + } + function pointLine(x, y) { + context.lineTo(x, y); + } + function lineEnd() { + stream.point = point; + } + function lineEndPolygon() { + context.closePath(); + } + return stream; + } + function d3_geo_resample(project) { + var δ2 = .5, cosMinDistance = Math.cos(30 * d3_radians), maxDepth = 16; + function resample(stream) { + return (maxDepth ? resampleRecursive : resampleNone)(stream); + } + function resampleNone(stream) { + return d3_geo_transformPoint(stream, function(x, y) { + x = project(x, y); + stream.point(x[0], x[1]); + }); + } + function resampleRecursive(stream) { + var λ00, φ00, x00, y00, a00, b00, c00, λ0, x0, y0, a0, b0, c0; + var resample = { + point: point, + lineStart: lineStart, + lineEnd: lineEnd, + polygonStart: function() { + stream.polygonStart(); + resample.lineStart = ringStart; + }, + polygonEnd: function() { + stream.polygonEnd(); + resample.lineStart = lineStart; + } + }; + function point(x, y) { + x = project(x, y); + stream.point(x[0], x[1]); + } + function lineStart() { + x0 = NaN; + resample.point = linePoint; + stream.lineStart(); + } + function linePoint(λ, φ) { + var c = d3_geo_cartesian([ λ, φ ]), p = project(λ, φ); + resampleLineTo(x0, y0, λ0, a0, b0, c0, x0 = p[0], y0 = p[1], λ0 = λ, a0 = c[0], b0 = c[1], c0 = c[2], maxDepth, stream); + stream.point(x0, y0); + } + function lineEnd() { + resample.point = point; + stream.lineEnd(); + } + function ringStart() { + lineStart(); + resample.point = ringPoint; + resample.lineEnd = ringEnd; + } + function ringPoint(λ, φ) { + linePoint(λ00 = λ, φ00 = φ), x00 = x0, y00 = y0, a00 = a0, b00 = b0, c00 = c0; + resample.point = linePoint; + } + function ringEnd() { + resampleLineTo(x0, y0, λ0, a0, b0, c0, x00, y00, λ00, a00, b00, c00, maxDepth, stream); + resample.lineEnd = lineEnd; + lineEnd(); + } + return resample; + } + function resampleLineTo(x0, y0, λ0, a0, b0, c0, x1, y1, λ1, a1, b1, c1, depth, stream) { + var dx = x1 - x0, dy = y1 - y0, d2 = dx * dx + dy * dy; + if (d2 > 4 * δ2 && depth--) { + var a = a0 + a1, b = b0 + b1, c = c0 + c1, m = Math.sqrt(a * a + b * b + c * c), φ2 = Math.asin(c /= m), λ2 = abs(abs(c) - 1) < ε || abs(λ0 - λ1) < ε ? (λ0 + λ1) / 2 : Math.atan2(b, a), p = project(λ2, φ2), x2 = p[0], y2 = p[1], dx2 = x2 - x0, dy2 = y2 - y0, dz = dy * dx2 - dx * dy2; + if (dz * dz / d2 > δ2 || abs((dx * dx2 + dy * dy2) / d2 - .5) > .3 || a0 * a1 + b0 * b1 + c0 * c1 < cosMinDistance) { + resampleLineTo(x0, y0, λ0, a0, b0, c0, x2, y2, λ2, a /= m, b /= m, c, depth, stream); + stream.point(x2, y2); + resampleLineTo(x2, y2, λ2, a, b, c, x1, y1, λ1, a1, b1, c1, depth, stream); + } + } + } + resample.precision = function(_) { + if (!arguments.length) return Math.sqrt(δ2); + maxDepth = (δ2 = _ * _) > 0 && 16; + return resample; + }; + return resample; + } + d3.geo.path = function() { + var pointRadius = 4.5, projection, context, projectStream, contextStream, cacheStream; + function path(object) { + if (object) { + if (typeof pointRadius === "function") contextStream.pointRadius(+pointRadius.apply(this, arguments)); + if (!cacheStream || !cacheStream.valid) cacheStream = projectStream(contextStream); + d3.geo.stream(object, cacheStream); + } + return contextStream.result(); + } + path.area = function(object) { + d3_geo_pathAreaSum = 0; + d3.geo.stream(object, projectStream(d3_geo_pathArea)); + return d3_geo_pathAreaSum; + }; + path.centroid = function(object) { + d3_geo_centroidX0 = d3_geo_centroidY0 = d3_geo_centroidZ0 = d3_geo_centroidX1 = d3_geo_centroidY1 = d3_geo_centroidZ1 = d3_geo_centroidX2 = d3_geo_centroidY2 = d3_geo_centroidZ2 = 0; + d3.geo.stream(object, projectStream(d3_geo_pathCentroid)); + return d3_geo_centroidZ2 ? [ d3_geo_centroidX2 / d3_geo_centroidZ2, d3_geo_centroidY2 / d3_geo_centroidZ2 ] : d3_geo_centroidZ1 ? [ d3_geo_centroidX1 / d3_geo_centroidZ1, d3_geo_centroidY1 / d3_geo_centroidZ1 ] : d3_geo_centroidZ0 ? [ d3_geo_centroidX0 / d3_geo_centroidZ0, d3_geo_centroidY0 / d3_geo_centroidZ0 ] : [ NaN, NaN ]; + }; + path.bounds = function(object) { + d3_geo_pathBoundsX1 = d3_geo_pathBoundsY1 = -(d3_geo_pathBoundsX0 = d3_geo_pathBoundsY0 = Infinity); + d3.geo.stream(object, projectStream(d3_geo_pathBounds)); + return [ [ d3_geo_pathBoundsX0, d3_geo_pathBoundsY0 ], [ d3_geo_pathBoundsX1, d3_geo_pathBoundsY1 ] ]; + }; + path.projection = function(_) { + if (!arguments.length) return projection; + projectStream = (projection = _) ? _.stream || d3_geo_pathProjectStream(_) : d3_identity; + return reset(); + }; + path.context = function(_) { + if (!arguments.length) return context; + contextStream = (context = _) == null ? new d3_geo_pathBuffer() : new d3_geo_pathContext(_); + if (typeof pointRadius !== "function") contextStream.pointRadius(pointRadius); + return reset(); + }; + path.pointRadius = function(_) { + if (!arguments.length) return pointRadius; + pointRadius = typeof _ === "function" ? _ : (contextStream.pointRadius(+_), +_); + return path; + }; + function reset() { + cacheStream = null; + return path; + } + return path.projection(d3.geo.albersUsa()).context(null); + }; + function d3_geo_pathProjectStream(project) { + var resample = d3_geo_resample(function(x, y) { + return project([ x * d3_degrees, y * d3_degrees ]); + }); + return function(stream) { + return d3_geo_projectionRadians(resample(stream)); + }; + } + d3.geo.transform = function(methods) { + return { + stream: function(stream) { + var transform = new d3_geo_transform(stream); + for (var k in methods) transform[k] = methods[k]; + return transform; + } + }; + }; + function d3_geo_transform(stream) { + this.stream = stream; + } + d3_geo_transform.prototype = { + point: function(x, y) { + this.stream.point(x, y); + }, + sphere: function() { + this.stream.sphere(); + }, + lineStart: function() { + this.stream.lineStart(); + }, + lineEnd: function() { + this.stream.lineEnd(); + }, + polygonStart: function() { + this.stream.polygonStart(); + }, + polygonEnd: function() { + this.stream.polygonEnd(); + } + }; + function d3_geo_transformPoint(stream, point) { + return { + point: point, + sphere: function() { + stream.sphere(); + }, + lineStart: function() { + stream.lineStart(); + }, + lineEnd: function() { + stream.lineEnd(); + }, + polygonStart: function() { + stream.polygonStart(); + }, + polygonEnd: function() { + stream.polygonEnd(); + } + }; + } + d3.geo.projection = d3_geo_projection; + d3.geo.projectionMutator = d3_geo_projectionMutator; + function d3_geo_projection(project) { + return d3_geo_projectionMutator(function() { + return project; + })(); + } + function d3_geo_projectionMutator(projectAt) { + var project, rotate, projectRotate, projectResample = d3_geo_resample(function(x, y) { + x = project(x, y); + return [ x[0] * k + δx, δy - x[1] * k ]; + }), k = 150, x = 480, y = 250, λ = 0, φ = 0, δλ = 0, δφ = 0, δγ = 0, δx, δy, preclip = d3_geo_clipAntimeridian, postclip = d3_identity, clipAngle = null, clipExtent = null, stream; + function projection(point) { + point = projectRotate(point[0] * d3_radians, point[1] * d3_radians); + return [ point[0] * k + δx, δy - point[1] * k ]; + } + function invert(point) { + point = projectRotate.invert((point[0] - δx) / k, (δy - point[1]) / k); + return point && [ point[0] * d3_degrees, point[1] * d3_degrees ]; + } + projection.stream = function(output) { + if (stream) stream.valid = false; + stream = d3_geo_projectionRadians(preclip(rotate, projectResample(postclip(output)))); + stream.valid = true; + return stream; + }; + projection.clipAngle = function(_) { + if (!arguments.length) return clipAngle; + preclip = _ == null ? (clipAngle = _, d3_geo_clipAntimeridian) : d3_geo_clipCircle((clipAngle = +_) * d3_radians); + return invalidate(); + }; + projection.clipExtent = function(_) { + if (!arguments.length) return clipExtent; + clipExtent = _; + postclip = _ ? d3_geo_clipExtent(_[0][0], _[0][1], _[1][0], _[1][1]) : d3_identity; + return invalidate(); + }; + projection.scale = function(_) { + if (!arguments.length) return k; + k = +_; + return reset(); + }; + projection.translate = function(_) { + if (!arguments.length) return [ x, y ]; + x = +_[0]; + y = +_[1]; + return reset(); + }; + projection.center = function(_) { + if (!arguments.length) return [ λ * d3_degrees, φ * d3_degrees ]; + λ = _[0] % 360 * d3_radians; + φ = _[1] % 360 * d3_radians; + return reset(); + }; + projection.rotate = function(_) { + if (!arguments.length) return [ δλ * d3_degrees, δφ * d3_degrees, δγ * d3_degrees ]; + δλ = _[0] % 360 * d3_radians; + δφ = _[1] % 360 * d3_radians; + δγ = _.length > 2 ? _[2] % 360 * d3_radians : 0; + return reset(); + }; + d3.rebind(projection, projectResample, "precision"); + function reset() { + projectRotate = d3_geo_compose(rotate = d3_geo_rotation(δλ, δφ, δγ), project); + var center = project(λ, φ); + δx = x - center[0] * k; + δy = y + center[1] * k; + return invalidate(); + } + function invalidate() { + if (stream) stream.valid = false, stream = null; + return projection; + } + return function() { + project = projectAt.apply(this, arguments); + projection.invert = project.invert && invert; + return reset(); + }; + } + function d3_geo_projectionRadians(stream) { + return d3_geo_transformPoint(stream, function(x, y) { + stream.point(x * d3_radians, y * d3_radians); + }); + } + function d3_geo_equirectangular(λ, φ) { + return [ λ, φ ]; + } + (d3.geo.equirectangular = function() { + return d3_geo_projection(d3_geo_equirectangular); + }).raw = d3_geo_equirectangular.invert = d3_geo_equirectangular; + d3.geo.rotation = function(rotate) { + rotate = d3_geo_rotation(rotate[0] % 360 * d3_radians, rotate[1] * d3_radians, rotate.length > 2 ? rotate[2] * d3_radians : 0); + function forward(coordinates) { + coordinates = rotate(coordinates[0] * d3_radians, coordinates[1] * d3_radians); + return coordinates[0] *= d3_degrees, coordinates[1] *= d3_degrees, coordinates; + } + forward.invert = function(coordinates) { + coordinates = rotate.invert(coordinates[0] * d3_radians, coordinates[1] * d3_radians); + return coordinates[0] *= d3_degrees, coordinates[1] *= d3_degrees, coordinates; + }; + return forward; + }; + function d3_geo_identityRotation(λ, φ) { + return [ λ > π ? λ - τ : λ < -π ? λ + τ : λ, φ ]; + } + d3_geo_identityRotation.invert = d3_geo_equirectangular; + function d3_geo_rotation(δλ, δφ, δγ) { + return δλ ? δφ || δγ ? d3_geo_compose(d3_geo_rotationλ(δλ), d3_geo_rotationφγ(δφ, δγ)) : d3_geo_rotationλ(δλ) : δφ || δγ ? d3_geo_rotationφγ(δφ, δγ) : d3_geo_identityRotation; + } + function d3_geo_forwardRotationλ(δλ) { + return function(λ, φ) { + return λ += δλ, [ λ > π ? λ - τ : λ < -π ? λ + τ : λ, φ ]; + }; + } + function d3_geo_rotationλ(δλ) { + var rotation = d3_geo_forwardRotationλ(δλ); + rotation.invert = d3_geo_forwardRotationλ(-δλ); + return rotation; + } + function d3_geo_rotationφγ(δφ, δγ) { + var cosδφ = Math.cos(δφ), sinδφ = Math.sin(δφ), cosδγ = Math.cos(δγ), sinδγ = Math.sin(δγ); + function rotation(λ, φ) { + var cosφ = Math.cos(φ), x = Math.cos(λ) * cosφ, y = Math.sin(λ) * cosφ, z = Math.sin(φ), k = z * cosδφ + x * sinδφ; + return [ Math.atan2(y * cosδγ - k * sinδγ, x * cosδφ - z * sinδφ), d3_asin(k * cosδγ + y * sinδγ) ]; + } + rotation.invert = function(λ, φ) { + var cosφ = Math.cos(φ), x = Math.cos(λ) * cosφ, y = Math.sin(λ) * cosφ, z = Math.sin(φ), k = z * cosδγ - y * sinδγ; + return [ Math.atan2(y * cosδγ + z * sinδγ, x * cosδφ + k * sinδφ), d3_asin(k * cosδφ - x * sinδφ) ]; + }; + return rotation; + } + d3.geo.circle = function() { + var origin = [ 0, 0 ], angle, precision = 6, interpolate; + function circle() { + var center = typeof origin === "function" ? origin.apply(this, arguments) : origin, rotate = d3_geo_rotation(-center[0] * d3_radians, -center[1] * d3_radians, 0).invert, ring = []; + interpolate(null, null, 1, { + point: function(x, y) { + ring.push(x = rotate(x, y)); + x[0] *= d3_degrees, x[1] *= d3_degrees; + } + }); + return { + type: "Polygon", + coordinates: [ ring ] + }; + } + circle.origin = function(x) { + if (!arguments.length) return origin; + origin = x; + return circle; + }; + circle.angle = function(x) { + if (!arguments.length) return angle; + interpolate = d3_geo_circleInterpolate((angle = +x) * d3_radians, precision * d3_radians); + return circle; + }; + circle.precision = function(_) { + if (!arguments.length) return precision; + interpolate = d3_geo_circleInterpolate(angle * d3_radians, (precision = +_) * d3_radians); + return circle; + }; + return circle.angle(90); + }; + function d3_geo_circleInterpolate(radius, precision) { + var cr = Math.cos(radius), sr = Math.sin(radius); + return function(from, to, direction, listener) { + var step = direction * precision; + if (from != null) { + from = d3_geo_circleAngle(cr, from); + to = d3_geo_circleAngle(cr, to); + if (direction > 0 ? from < to : from > to) from += direction * τ; + } else { + from = radius + direction * τ; + to = radius - .5 * step; + } + for (var point, t = from; direction > 0 ? t > to : t < to; t -= step) { + listener.point((point = d3_geo_spherical([ cr, -sr * Math.cos(t), -sr * Math.sin(t) ]))[0], point[1]); + } + }; + } + function d3_geo_circleAngle(cr, point) { + var a = d3_geo_cartesian(point); + a[0] -= cr; + d3_geo_cartesianNormalize(a); + var angle = d3_acos(-a[1]); + return ((-a[2] < 0 ? -angle : angle) + 2 * Math.PI - ε) % (2 * Math.PI); + } + d3.geo.distance = function(a, b) { + var Δλ = (b[0] - a[0]) * d3_radians, φ0 = a[1] * d3_radians, φ1 = b[1] * d3_radians, sinΔλ = Math.sin(Δλ), cosΔλ = Math.cos(Δλ), sinφ0 = Math.sin(φ0), cosφ0 = Math.cos(φ0), sinφ1 = Math.sin(φ1), cosφ1 = Math.cos(φ1), t; + return Math.atan2(Math.sqrt((t = cosφ1 * sinΔλ) * t + (t = cosφ0 * sinφ1 - sinφ0 * cosφ1 * cosΔλ) * t), sinφ0 * sinφ1 + cosφ0 * cosφ1 * cosΔλ); + }; + d3.geo.graticule = function() { + var x1, x0, X1, X0, y1, y0, Y1, Y0, dx = 10, dy = dx, DX = 90, DY = 360, x, y, X, Y, precision = 2.5; + function graticule() { + return { + type: "MultiLineString", + coordinates: lines() + }; + } + function lines() { + return d3.range(Math.ceil(X0 / DX) * DX, X1, DX).map(X).concat(d3.range(Math.ceil(Y0 / DY) * DY, Y1, DY).map(Y)).concat(d3.range(Math.ceil(x0 / dx) * dx, x1, dx).filter(function(x) { + return abs(x % DX) > ε; + }).map(x)).concat(d3.range(Math.ceil(y0 / dy) * dy, y1, dy).filter(function(y) { + return abs(y % DY) > ε; + }).map(y)); + } + graticule.lines = function() { + return lines().map(function(coordinates) { + return { + type: "LineString", + coordinates: coordinates + }; + }); + }; + graticule.outline = function() { + return { + type: "Polygon", + coordinates: [ X(X0).concat(Y(Y1).slice(1), X(X1).reverse().slice(1), Y(Y0).reverse().slice(1)) ] + }; + }; + graticule.extent = function(_) { + if (!arguments.length) return graticule.minorExtent(); + return graticule.majorExtent(_).minorExtent(_); + }; + graticule.majorExtent = function(_) { + if (!arguments.length) return [ [ X0, Y0 ], [ X1, Y1 ] ]; + X0 = +_[0][0], X1 = +_[1][0]; + Y0 = +_[0][1], Y1 = +_[1][1]; + if (X0 > X1) _ = X0, X0 = X1, X1 = _; + if (Y0 > Y1) _ = Y0, Y0 = Y1, Y1 = _; + return graticule.precision(precision); + }; + graticule.minorExtent = function(_) { + if (!arguments.length) return [ [ x0, y0 ], [ x1, y1 ] ]; + x0 = +_[0][0], x1 = +_[1][0]; + y0 = +_[0][1], y1 = +_[1][1]; + if (x0 > x1) _ = x0, x0 = x1, x1 = _; + if (y0 > y1) _ = y0, y0 = y1, y1 = _; + return graticule.precision(precision); + }; + graticule.step = function(_) { + if (!arguments.length) return graticule.minorStep(); + return graticule.majorStep(_).minorStep(_); + }; + graticule.majorStep = function(_) { + if (!arguments.length) return [ DX, DY ]; + DX = +_[0], DY = +_[1]; + return graticule; + }; + graticule.minorStep = function(_) { + if (!arguments.length) return [ dx, dy ]; + dx = +_[0], dy = +_[1]; + return graticule; + }; + graticule.precision = function(_) { + if (!arguments.length) return precision; + precision = +_; + x = d3_geo_graticuleX(y0, y1, 90); + y = d3_geo_graticuleY(x0, x1, precision); + X = d3_geo_graticuleX(Y0, Y1, 90); + Y = d3_geo_graticuleY(X0, X1, precision); + return graticule; + }; + return graticule.majorExtent([ [ -180, -90 + ε ], [ 180, 90 - ε ] ]).minorExtent([ [ -180, -80 - ε ], [ 180, 80 + ε ] ]); + }; + function d3_geo_graticuleX(y0, y1, dy) { + var y = d3.range(y0, y1 - ε, dy).concat(y1); + return function(x) { + return y.map(function(y) { + return [ x, y ]; + }); + }; + } + function d3_geo_graticuleY(x0, x1, dx) { + var x = d3.range(x0, x1 - ε, dx).concat(x1); + return function(y) { + return x.map(function(x) { + return [ x, y ]; + }); + }; + } + function d3_source(d) { + return d.source; + } + function d3_target(d) { + return d.target; + } + d3.geo.greatArc = function() { + var source = d3_source, source_, target = d3_target, target_; + function greatArc() { + return { + type: "LineString", + coordinates: [ source_ || source.apply(this, arguments), target_ || target.apply(this, arguments) ] + }; + } + greatArc.distance = function() { + return d3.geo.distance(source_ || source.apply(this, arguments), target_ || target.apply(this, arguments)); + }; + greatArc.source = function(_) { + if (!arguments.length) return source; + source = _, source_ = typeof _ === "function" ? null : _; + return greatArc; + }; + greatArc.target = function(_) { + if (!arguments.length) return target; + target = _, target_ = typeof _ === "function" ? null : _; + return greatArc; + }; + greatArc.precision = function() { + return arguments.length ? greatArc : 0; + }; + return greatArc; + }; + d3.geo.interpolate = function(source, target) { + return d3_geo_interpolate(source[0] * d3_radians, source[1] * d3_radians, target[0] * d3_radians, target[1] * d3_radians); + }; + function d3_geo_interpolate(x0, y0, x1, y1) { + var cy0 = Math.cos(y0), sy0 = Math.sin(y0), cy1 = Math.cos(y1), sy1 = Math.sin(y1), kx0 = cy0 * Math.cos(x0), ky0 = cy0 * Math.sin(x0), kx1 = cy1 * Math.cos(x1), ky1 = cy1 * Math.sin(x1), d = 2 * Math.asin(Math.sqrt(d3_haversin(y1 - y0) + cy0 * cy1 * d3_haversin(x1 - x0))), k = 1 / Math.sin(d); + var interpolate = d ? function(t) { + var B = Math.sin(t *= d) * k, A = Math.sin(d - t) * k, x = A * kx0 + B * kx1, y = A * ky0 + B * ky1, z = A * sy0 + B * sy1; + return [ Math.atan2(y, x) * d3_degrees, Math.atan2(z, Math.sqrt(x * x + y * y)) * d3_degrees ]; + } : function() { + return [ x0 * d3_degrees, y0 * d3_degrees ]; + }; + interpolate.distance = d; + return interpolate; + } + d3.geo.length = function(object) { + d3_geo_lengthSum = 0; + d3.geo.stream(object, d3_geo_length); + return d3_geo_lengthSum; + }; + var d3_geo_lengthSum; + var d3_geo_length = { + sphere: d3_noop, + point: d3_noop, + lineStart: d3_geo_lengthLineStart, + lineEnd: d3_noop, + polygonStart: d3_noop, + polygonEnd: d3_noop + }; + function d3_geo_lengthLineStart() { + var λ0, sinφ0, cosφ0; + d3_geo_length.point = function(λ, φ) { + λ0 = λ * d3_radians, sinφ0 = Math.sin(φ *= d3_radians), cosφ0 = Math.cos(φ); + d3_geo_length.point = nextPoint; + }; + d3_geo_length.lineEnd = function() { + d3_geo_length.point = d3_geo_length.lineEnd = d3_noop; + }; + function nextPoint(λ, φ) { + var sinφ = Math.sin(φ *= d3_radians), cosφ = Math.cos(φ), t = abs((λ *= d3_radians) - λ0), cosΔλ = Math.cos(t); + d3_geo_lengthSum += Math.atan2(Math.sqrt((t = cosφ * Math.sin(t)) * t + (t = cosφ0 * sinφ - sinφ0 * cosφ * cosΔλ) * t), sinφ0 * sinφ + cosφ0 * cosφ * cosΔλ); + λ0 = λ, sinφ0 = sinφ, cosφ0 = cosφ; + } + } + function d3_geo_azimuthal(scale, angle) { + function azimuthal(λ, φ) { + var cosλ = Math.cos(λ), cosφ = Math.cos(φ), k = scale(cosλ * cosφ); + return [ k * cosφ * Math.sin(λ), k * Math.sin(φ) ]; + } + azimuthal.invert = function(x, y) { + var ρ = Math.sqrt(x * x + y * y), c = angle(ρ), sinc = Math.sin(c), cosc = Math.cos(c); + return [ Math.atan2(x * sinc, ρ * cosc), Math.asin(ρ && y * sinc / ρ) ]; + }; + return azimuthal; + } + var d3_geo_azimuthalEqualArea = d3_geo_azimuthal(function(cosλcosφ) { + return Math.sqrt(2 / (1 + cosλcosφ)); + }, function(ρ) { + return 2 * Math.asin(ρ / 2); + }); + (d3.geo.azimuthalEqualArea = function() { + return d3_geo_projection(d3_geo_azimuthalEqualArea); + }).raw = d3_geo_azimuthalEqualArea; + var d3_geo_azimuthalEquidistant = d3_geo_azimuthal(function(cosλcosφ) { + var c = Math.acos(cosλcosφ); + return c && c / Math.sin(c); + }, d3_identity); + (d3.geo.azimuthalEquidistant = function() { + return d3_geo_projection(d3_geo_azimuthalEquidistant); + }).raw = d3_geo_azimuthalEquidistant; + function d3_geo_conicConformal(φ0, φ1) { + var cosφ0 = Math.cos(φ0), t = function(φ) { + return Math.tan(π / 4 + φ / 2); + }, n = φ0 === φ1 ? Math.sin(φ0) : Math.log(cosφ0 / Math.cos(φ1)) / Math.log(t(φ1) / t(φ0)), F = cosφ0 * Math.pow(t(φ0), n) / n; + if (!n) return d3_geo_mercator; + function forward(λ, φ) { + if (F > 0) { + if (φ < -halfπ + ε) φ = -halfπ + ε; + } else { + if (φ > halfπ - ε) φ = halfπ - ε; + } + var ρ = F / Math.pow(t(φ), n); + return [ ρ * Math.sin(n * λ), F - ρ * Math.cos(n * λ) ]; + } + forward.invert = function(x, y) { + var ρ0_y = F - y, ρ = d3_sgn(n) * Math.sqrt(x * x + ρ0_y * ρ0_y); + return [ Math.atan2(x, ρ0_y) / n, 2 * Math.atan(Math.pow(F / ρ, 1 / n)) - halfπ ]; + }; + return forward; + } + (d3.geo.conicConformal = function() { + return d3_geo_conic(d3_geo_conicConformal); + }).raw = d3_geo_conicConformal; + function d3_geo_conicEquidistant(φ0, φ1) { + var cosφ0 = Math.cos(φ0), n = φ0 === φ1 ? Math.sin(φ0) : (cosφ0 - Math.cos(φ1)) / (φ1 - φ0), G = cosφ0 / n + φ0; + if (abs(n) < ε) return d3_geo_equirectangular; + function forward(λ, φ) { + var ρ = G - φ; + return [ ρ * Math.sin(n * λ), G - ρ * Math.cos(n * λ) ]; + } + forward.invert = function(x, y) { + var ρ0_y = G - y; + return [ Math.atan2(x, ρ0_y) / n, G - d3_sgn(n) * Math.sqrt(x * x + ρ0_y * ρ0_y) ]; + }; + return forward; + } + (d3.geo.conicEquidistant = function() { + return d3_geo_conic(d3_geo_conicEquidistant); + }).raw = d3_geo_conicEquidistant; + var d3_geo_gnomonic = d3_geo_azimuthal(function(cosλcosφ) { + return 1 / cosλcosφ; + }, Math.atan); + (d3.geo.gnomonic = function() { + return d3_geo_projection(d3_geo_gnomonic); + }).raw = d3_geo_gnomonic; + function d3_geo_mercator(λ, φ) { + return [ λ, Math.log(Math.tan(π / 4 + φ / 2)) ]; + } + d3_geo_mercator.invert = function(x, y) { + return [ x, 2 * Math.atan(Math.exp(y)) - halfπ ]; + }; + function d3_geo_mercatorProjection(project) { + var m = d3_geo_projection(project), scale = m.scale, translate = m.translate, clipExtent = m.clipExtent, clipAuto; + m.scale = function() { + var v = scale.apply(m, arguments); + return v === m ? clipAuto ? m.clipExtent(null) : m : v; + }; + m.translate = function() { + var v = translate.apply(m, arguments); + return v === m ? clipAuto ? m.clipExtent(null) : m : v; + }; + m.clipExtent = function(_) { + var v = clipExtent.apply(m, arguments); + if (v === m) { + if (clipAuto = _ == null) { + var k = π * scale(), t = translate(); + clipExtent([ [ t[0] - k, t[1] - k ], [ t[0] + k, t[1] + k ] ]); + } + } else if (clipAuto) { + v = null; + } + return v; + }; + return m.clipExtent(null); + } + (d3.geo.mercator = function() { + return d3_geo_mercatorProjection(d3_geo_mercator); + }).raw = d3_geo_mercator; + var d3_geo_orthographic = d3_geo_azimuthal(function() { + return 1; + }, Math.asin); + (d3.geo.orthographic = function() { + return d3_geo_projection(d3_geo_orthographic); + }).raw = d3_geo_orthographic; + var d3_geo_stereographic = d3_geo_azimuthal(function(cosλcosφ) { + return 1 / (1 + cosλcosφ); + }, function(ρ) { + return 2 * Math.atan(ρ); + }); + (d3.geo.stereographic = function() { + return d3_geo_projection(d3_geo_stereographic); + }).raw = d3_geo_stereographic; + function d3_geo_transverseMercator(λ, φ) { + return [ Math.log(Math.tan(π / 4 + φ / 2)), -λ ]; + } + d3_geo_transverseMercator.invert = function(x, y) { + return [ -y, 2 * Math.atan(Math.exp(x)) - halfπ ]; + }; + (d3.geo.transverseMercator = function() { + var projection = d3_geo_mercatorProjection(d3_geo_transverseMercator), center = projection.center, rotate = projection.rotate; + projection.center = function(_) { + return _ ? center([ -_[1], _[0] ]) : (_ = center(), [ _[1], -_[0] ]); + }; + projection.rotate = function(_) { + return _ ? rotate([ _[0], _[1], _.length > 2 ? _[2] + 90 : 90 ]) : (_ = rotate(), + [ _[0], _[1], _[2] - 90 ]); + }; + return rotate([ 0, 0, 90 ]); + }).raw = d3_geo_transverseMercator; + d3.geom = {}; + function d3_geom_pointX(d) { + return d[0]; + } + function d3_geom_pointY(d) { + return d[1]; + } + d3.geom.hull = function(vertices) { + var x = d3_geom_pointX, y = d3_geom_pointY; + if (arguments.length) return hull(vertices); + function hull(data) { + if (data.length < 3) return []; + var fx = d3_functor(x), fy = d3_functor(y), i, n = data.length, points = [], flippedPoints = []; + for (i = 0; i < n; i++) { + points.push([ +fx.call(this, data[i], i), +fy.call(this, data[i], i), i ]); + } + points.sort(d3_geom_hullOrder); + for (i = 0; i < n; i++) flippedPoints.push([ points[i][0], -points[i][1] ]); + var upper = d3_geom_hullUpper(points), lower = d3_geom_hullUpper(flippedPoints); + var skipLeft = lower[0] === upper[0], skipRight = lower[lower.length - 1] === upper[upper.length - 1], polygon = []; + for (i = upper.length - 1; i >= 0; --i) polygon.push(data[points[upper[i]][2]]); + for (i = +skipLeft; i < lower.length - skipRight; ++i) polygon.push(data[points[lower[i]][2]]); + return polygon; + } + hull.x = function(_) { + return arguments.length ? (x = _, hull) : x; + }; + hull.y = function(_) { + return arguments.length ? (y = _, hull) : y; + }; + return hull; + }; + function d3_geom_hullUpper(points) { + var n = points.length, hull = [ 0, 1 ], hs = 2; + for (var i = 2; i < n; i++) { + while (hs > 1 && d3_cross2d(points[hull[hs - 2]], points[hull[hs - 1]], points[i]) <= 0) --hs; + hull[hs++] = i; + } + return hull.slice(0, hs); + } + function d3_geom_hullOrder(a, b) { + return a[0] - b[0] || a[1] - b[1]; + } + d3.geom.polygon = function(coordinates) { + d3_subclass(coordinates, d3_geom_polygonPrototype); + return coordinates; + }; + var d3_geom_polygonPrototype = d3.geom.polygon.prototype = []; + d3_geom_polygonPrototype.area = function() { + var i = -1, n = this.length, a, b = this[n - 1], area = 0; + while (++i < n) { + a = b; + b = this[i]; + area += a[1] * b[0] - a[0] * b[1]; + } + return area * .5; + }; + d3_geom_polygonPrototype.centroid = function(k) { + var i = -1, n = this.length, x = 0, y = 0, a, b = this[n - 1], c; + if (!arguments.length) k = -1 / (6 * this.area()); + while (++i < n) { + a = b; + b = this[i]; + c = a[0] * b[1] - b[0] * a[1]; + x += (a[0] + b[0]) * c; + y += (a[1] + b[1]) * c; + } + return [ x * k, y * k ]; + }; + d3_geom_polygonPrototype.clip = function(subject) { + var input, closed = d3_geom_polygonClosed(subject), i = -1, n = this.length - d3_geom_polygonClosed(this), j, m, a = this[n - 1], b, c, d; + while (++i < n) { + input = subject.slice(); + subject.length = 0; + b = this[i]; + c = input[(m = input.length - closed) - 1]; + j = -1; + while (++j < m) { + d = input[j]; + if (d3_geom_polygonInside(d, a, b)) { + if (!d3_geom_polygonInside(c, a, b)) { + subject.push(d3_geom_polygonIntersect(c, d, a, b)); + } + subject.push(d); + } else if (d3_geom_polygonInside(c, a, b)) { + subject.push(d3_geom_polygonIntersect(c, d, a, b)); + } + c = d; + } + if (closed) subject.push(subject[0]); + a = b; + } + return subject; + }; + function d3_geom_polygonInside(p, a, b) { + return (b[0] - a[0]) * (p[1] - a[1]) < (b[1] - a[1]) * (p[0] - a[0]); + } + function d3_geom_polygonIntersect(c, d, a, b) { + var x1 = c[0], x3 = a[0], x21 = d[0] - x1, x43 = b[0] - x3, y1 = c[1], y3 = a[1], y21 = d[1] - y1, y43 = b[1] - y3, ua = (x43 * (y1 - y3) - y43 * (x1 - x3)) / (y43 * x21 - x43 * y21); + return [ x1 + ua * x21, y1 + ua * y21 ]; + } + function d3_geom_polygonClosed(coordinates) { + var a = coordinates[0], b = coordinates[coordinates.length - 1]; + return !(a[0] - b[0] || a[1] - b[1]); + } + var d3_geom_voronoiEdges, d3_geom_voronoiCells, d3_geom_voronoiBeaches, d3_geom_voronoiBeachPool = [], d3_geom_voronoiFirstCircle, d3_geom_voronoiCircles, d3_geom_voronoiCirclePool = []; + function d3_geom_voronoiBeach() { + d3_geom_voronoiRedBlackNode(this); + this.edge = this.site = this.circle = null; + } + function d3_geom_voronoiCreateBeach(site) { + var beach = d3_geom_voronoiBeachPool.pop() || new d3_geom_voronoiBeach(); + beach.site = site; + return beach; + } + function d3_geom_voronoiDetachBeach(beach) { + d3_geom_voronoiDetachCircle(beach); + d3_geom_voronoiBeaches.remove(beach); + d3_geom_voronoiBeachPool.push(beach); + d3_geom_voronoiRedBlackNode(beach); + } + function d3_geom_voronoiRemoveBeach(beach) { + var circle = beach.circle, x = circle.x, y = circle.cy, vertex = { + x: x, + y: y + }, previous = beach.P, next = beach.N, disappearing = [ beach ]; + d3_geom_voronoiDetachBeach(beach); + var lArc = previous; + while (lArc.circle && abs(x - lArc.circle.x) < ε && abs(y - lArc.circle.cy) < ε) { + previous = lArc.P; + disappearing.unshift(lArc); + d3_geom_voronoiDetachBeach(lArc); + lArc = previous; + } + disappearing.unshift(lArc); + d3_geom_voronoiDetachCircle(lArc); + var rArc = next; + while (rArc.circle && abs(x - rArc.circle.x) < ε && abs(y - rArc.circle.cy) < ε) { + next = rArc.N; + disappearing.push(rArc); + d3_geom_voronoiDetachBeach(rArc); + rArc = next; + } + disappearing.push(rArc); + d3_geom_voronoiDetachCircle(rArc); + var nArcs = disappearing.length, iArc; + for (iArc = 1; iArc < nArcs; ++iArc) { + rArc = disappearing[iArc]; + lArc = disappearing[iArc - 1]; + d3_geom_voronoiSetEdgeEnd(rArc.edge, lArc.site, rArc.site, vertex); + } + lArc = disappearing[0]; + rArc = disappearing[nArcs - 1]; + rArc.edge = d3_geom_voronoiCreateEdge(lArc.site, rArc.site, null, vertex); + d3_geom_voronoiAttachCircle(lArc); + d3_geom_voronoiAttachCircle(rArc); + } + function d3_geom_voronoiAddBeach(site) { + var x = site.x, directrix = site.y, lArc, rArc, dxl, dxr, node = d3_geom_voronoiBeaches._; + while (node) { + dxl = d3_geom_voronoiLeftBreakPoint(node, directrix) - x; + if (dxl > ε) node = node.L; else { + dxr = x - d3_geom_voronoiRightBreakPoint(node, directrix); + if (dxr > ε) { + if (!node.R) { + lArc = node; + break; + } + node = node.R; + } else { + if (dxl > -ε) { + lArc = node.P; + rArc = node; + } else if (dxr > -ε) { + lArc = node; + rArc = node.N; + } else { + lArc = rArc = node; + } + break; + } + } + } + var newArc = d3_geom_voronoiCreateBeach(site); + d3_geom_voronoiBeaches.insert(lArc, newArc); + if (!lArc && !rArc) return; + if (lArc === rArc) { + d3_geom_voronoiDetachCircle(lArc); + rArc = d3_geom_voronoiCreateBeach(lArc.site); + d3_geom_voronoiBeaches.insert(newArc, rArc); + newArc.edge = rArc.edge = d3_geom_voronoiCreateEdge(lArc.site, newArc.site); + d3_geom_voronoiAttachCircle(lArc); + d3_geom_voronoiAttachCircle(rArc); + return; + } + if (!rArc) { + newArc.edge = d3_geom_voronoiCreateEdge(lArc.site, newArc.site); + return; + } + d3_geom_voronoiDetachCircle(lArc); + d3_geom_voronoiDetachCircle(rArc); + var lSite = lArc.site, ax = lSite.x, ay = lSite.y, bx = site.x - ax, by = site.y - ay, rSite = rArc.site, cx = rSite.x - ax, cy = rSite.y - ay, d = 2 * (bx * cy - by * cx), hb = bx * bx + by * by, hc = cx * cx + cy * cy, vertex = { + x: (cy * hb - by * hc) / d + ax, + y: (bx * hc - cx * hb) / d + ay + }; + d3_geom_voronoiSetEdgeEnd(rArc.edge, lSite, rSite, vertex); + newArc.edge = d3_geom_voronoiCreateEdge(lSite, site, null, vertex); + rArc.edge = d3_geom_voronoiCreateEdge(site, rSite, null, vertex); + d3_geom_voronoiAttachCircle(lArc); + d3_geom_voronoiAttachCircle(rArc); + } + function d3_geom_voronoiLeftBreakPoint(arc, directrix) { + var site = arc.site, rfocx = site.x, rfocy = site.y, pby2 = rfocy - directrix; + if (!pby2) return rfocx; + var lArc = arc.P; + if (!lArc) return -Infinity; + site = lArc.site; + var lfocx = site.x, lfocy = site.y, plby2 = lfocy - directrix; + if (!plby2) return lfocx; + var hl = lfocx - rfocx, aby2 = 1 / pby2 - 1 / plby2, b = hl / plby2; + if (aby2) return (-b + Math.sqrt(b * b - 2 * aby2 * (hl * hl / (-2 * plby2) - lfocy + plby2 / 2 + rfocy - pby2 / 2))) / aby2 + rfocx; + return (rfocx + lfocx) / 2; + } + function d3_geom_voronoiRightBreakPoint(arc, directrix) { + var rArc = arc.N; + if (rArc) return d3_geom_voronoiLeftBreakPoint(rArc, directrix); + var site = arc.site; + return site.y === directrix ? site.x : Infinity; + } + function d3_geom_voronoiCell(site) { + this.site = site; + this.edges = []; + } + d3_geom_voronoiCell.prototype.prepare = function() { + var halfEdges = this.edges, iHalfEdge = halfEdges.length, edge; + while (iHalfEdge--) { + edge = halfEdges[iHalfEdge].edge; + if (!edge.b || !edge.a) halfEdges.splice(iHalfEdge, 1); + } + halfEdges.sort(d3_geom_voronoiHalfEdgeOrder); + return halfEdges.length; + }; + function d3_geom_voronoiCloseCells(extent) { + var x0 = extent[0][0], x1 = extent[1][0], y0 = extent[0][1], y1 = extent[1][1], x2, y2, x3, y3, cells = d3_geom_voronoiCells, iCell = cells.length, cell, iHalfEdge, halfEdges, nHalfEdges, start, end; + while (iCell--) { + cell = cells[iCell]; + if (!cell || !cell.prepare()) continue; + halfEdges = cell.edges; + nHalfEdges = halfEdges.length; + iHalfEdge = 0; + while (iHalfEdge < nHalfEdges) { + end = halfEdges[iHalfEdge].end(), x3 = end.x, y3 = end.y; + start = halfEdges[++iHalfEdge % nHalfEdges].start(), x2 = start.x, y2 = start.y; + if (abs(x3 - x2) > ε || abs(y3 - y2) > ε) { + halfEdges.splice(iHalfEdge, 0, new d3_geom_voronoiHalfEdge(d3_geom_voronoiCreateBorderEdge(cell.site, end, abs(x3 - x0) < ε && y1 - y3 > ε ? { + x: x0, + y: abs(x2 - x0) < ε ? y2 : y1 + } : abs(y3 - y1) < ε && x1 - x3 > ε ? { + x: abs(y2 - y1) < ε ? x2 : x1, + y: y1 + } : abs(x3 - x1) < ε && y3 - y0 > ε ? { + x: x1, + y: abs(x2 - x1) < ε ? y2 : y0 + } : abs(y3 - y0) < ε && x3 - x0 > ε ? { + x: abs(y2 - y0) < ε ? x2 : x0, + y: y0 + } : null), cell.site, null)); + ++nHalfEdges; + } + } + } + } + function d3_geom_voronoiHalfEdgeOrder(a, b) { + return b.angle - a.angle; + } + function d3_geom_voronoiCircle() { + d3_geom_voronoiRedBlackNode(this); + this.x = this.y = this.arc = this.site = this.cy = null; + } + function d3_geom_voronoiAttachCircle(arc) { + var lArc = arc.P, rArc = arc.N; + if (!lArc || !rArc) return; + var lSite = lArc.site, cSite = arc.site, rSite = rArc.site; + if (lSite === rSite) return; + var bx = cSite.x, by = cSite.y, ax = lSite.x - bx, ay = lSite.y - by, cx = rSite.x - bx, cy = rSite.y - by; + var d = 2 * (ax * cy - ay * cx); + if (d >= -ε2) return; + var ha = ax * ax + ay * ay, hc = cx * cx + cy * cy, x = (cy * ha - ay * hc) / d, y = (ax * hc - cx * ha) / d, cy = y + by; + var circle = d3_geom_voronoiCirclePool.pop() || new d3_geom_voronoiCircle(); + circle.arc = arc; + circle.site = cSite; + circle.x = x + bx; + circle.y = cy + Math.sqrt(x * x + y * y); + circle.cy = cy; + arc.circle = circle; + var before = null, node = d3_geom_voronoiCircles._; + while (node) { + if (circle.y < node.y || circle.y === node.y && circle.x <= node.x) { + if (node.L) node = node.L; else { + before = node.P; + break; + } + } else { + if (node.R) node = node.R; else { + before = node; + break; + } + } + } + d3_geom_voronoiCircles.insert(before, circle); + if (!before) d3_geom_voronoiFirstCircle = circle; + } + function d3_geom_voronoiDetachCircle(arc) { + var circle = arc.circle; + if (circle) { + if (!circle.P) d3_geom_voronoiFirstCircle = circle.N; + d3_geom_voronoiCircles.remove(circle); + d3_geom_voronoiCirclePool.push(circle); + d3_geom_voronoiRedBlackNode(circle); + arc.circle = null; + } + } + function d3_geom_voronoiClipEdges(extent) { + var edges = d3_geom_voronoiEdges, clip = d3_geom_clipLine(extent[0][0], extent[0][1], extent[1][0], extent[1][1]), i = edges.length, e; + while (i--) { + e = edges[i]; + if (!d3_geom_voronoiConnectEdge(e, extent) || !clip(e) || abs(e.a.x - e.b.x) < ε && abs(e.a.y - e.b.y) < ε) { + e.a = e.b = null; + edges.splice(i, 1); + } + } + } + function d3_geom_voronoiConnectEdge(edge, extent) { + var vb = edge.b; + if (vb) return true; + var va = edge.a, x0 = extent[0][0], x1 = extent[1][0], y0 = extent[0][1], y1 = extent[1][1], lSite = edge.l, rSite = edge.r, lx = lSite.x, ly = lSite.y, rx = rSite.x, ry = rSite.y, fx = (lx + rx) / 2, fy = (ly + ry) / 2, fm, fb; + if (ry === ly) { + if (fx < x0 || fx >= x1) return; + if (lx > rx) { + if (!va) va = { + x: fx, + y: y0 + }; else if (va.y >= y1) return; + vb = { + x: fx, + y: y1 + }; + } else { + if (!va) va = { + x: fx, + y: y1 + }; else if (va.y < y0) return; + vb = { + x: fx, + y: y0 + }; + } + } else { + fm = (lx - rx) / (ry - ly); + fb = fy - fm * fx; + if (fm < -1 || fm > 1) { + if (lx > rx) { + if (!va) va = { + x: (y0 - fb) / fm, + y: y0 + }; else if (va.y >= y1) return; + vb = { + x: (y1 - fb) / fm, + y: y1 + }; + } else { + if (!va) va = { + x: (y1 - fb) / fm, + y: y1 + }; else if (va.y < y0) return; + vb = { + x: (y0 - fb) / fm, + y: y0 + }; + } + } else { + if (ly < ry) { + if (!va) va = { + x: x0, + y: fm * x0 + fb + }; else if (va.x >= x1) return; + vb = { + x: x1, + y: fm * x1 + fb + }; + } else { + if (!va) va = { + x: x1, + y: fm * x1 + fb + }; else if (va.x < x0) return; + vb = { + x: x0, + y: fm * x0 + fb + }; + } + } + } + edge.a = va; + edge.b = vb; + return true; + } + function d3_geom_voronoiEdge(lSite, rSite) { + this.l = lSite; + this.r = rSite; + this.a = this.b = null; + } + function d3_geom_voronoiCreateEdge(lSite, rSite, va, vb) { + var edge = new d3_geom_voronoiEdge(lSite, rSite); + d3_geom_voronoiEdges.push(edge); + if (va) d3_geom_voronoiSetEdgeEnd(edge, lSite, rSite, va); + if (vb) d3_geom_voronoiSetEdgeEnd(edge, rSite, lSite, vb); + d3_geom_voronoiCells[lSite.i].edges.push(new d3_geom_voronoiHalfEdge(edge, lSite, rSite)); + d3_geom_voronoiCells[rSite.i].edges.push(new d3_geom_voronoiHalfEdge(edge, rSite, lSite)); + return edge; + } + function d3_geom_voronoiCreateBorderEdge(lSite, va, vb) { + var edge = new d3_geom_voronoiEdge(lSite, null); + edge.a = va; + edge.b = vb; + d3_geom_voronoiEdges.push(edge); + return edge; + } + function d3_geom_voronoiSetEdgeEnd(edge, lSite, rSite, vertex) { + if (!edge.a && !edge.b) { + edge.a = vertex; + edge.l = lSite; + edge.r = rSite; + } else if (edge.l === rSite) { + edge.b = vertex; + } else { + edge.a = vertex; + } + } + function d3_geom_voronoiHalfEdge(edge, lSite, rSite) { + var va = edge.a, vb = edge.b; + this.edge = edge; + this.site = lSite; + this.angle = rSite ? Math.atan2(rSite.y - lSite.y, rSite.x - lSite.x) : edge.l === lSite ? Math.atan2(vb.x - va.x, va.y - vb.y) : Math.atan2(va.x - vb.x, vb.y - va.y); + } + d3_geom_voronoiHalfEdge.prototype = { + start: function() { + return this.edge.l === this.site ? this.edge.a : this.edge.b; + }, + end: function() { + return this.edge.l === this.site ? this.edge.b : this.edge.a; + } + }; + function d3_geom_voronoiRedBlackTree() { + this._ = null; + } + function d3_geom_voronoiRedBlackNode(node) { + node.U = node.C = node.L = node.R = node.P = node.N = null; + } + d3_geom_voronoiRedBlackTree.prototype = { + insert: function(after, node) { + var parent, grandpa, uncle; + if (after) { + node.P = after; + node.N = after.N; + if (after.N) after.N.P = node; + after.N = node; + if (after.R) { + after = after.R; + while (after.L) after = after.L; + after.L = node; + } else { + after.R = node; + } + parent = after; + } else if (this._) { + after = d3_geom_voronoiRedBlackFirst(this._); + node.P = null; + node.N = after; + after.P = after.L = node; + parent = after; + } else { + node.P = node.N = null; + this._ = node; + parent = null; + } + node.L = node.R = null; + node.U = parent; + node.C = true; + after = node; + while (parent && parent.C) { + grandpa = parent.U; + if (parent === grandpa.L) { + uncle = grandpa.R; + if (uncle && uncle.C) { + parent.C = uncle.C = false; + grandpa.C = true; + after = grandpa; + } else { + if (after === parent.R) { + d3_geom_voronoiRedBlackRotateLeft(this, parent); + after = parent; + parent = after.U; + } + parent.C = false; + grandpa.C = true; + d3_geom_voronoiRedBlackRotateRight(this, grandpa); + } + } else { + uncle = grandpa.L; + if (uncle && uncle.C) { + parent.C = uncle.C = false; + grandpa.C = true; + after = grandpa; + } else { + if (after === parent.L) { + d3_geom_voronoiRedBlackRotateRight(this, parent); + after = parent; + parent = after.U; + } + parent.C = false; + grandpa.C = true; + d3_geom_voronoiRedBlackRotateLeft(this, grandpa); + } + } + parent = after.U; + } + this._.C = false; + }, + remove: function(node) { + if (node.N) node.N.P = node.P; + if (node.P) node.P.N = node.N; + node.N = node.P = null; + var parent = node.U, sibling, left = node.L, right = node.R, next, red; + if (!left) next = right; else if (!right) next = left; else next = d3_geom_voronoiRedBlackFirst(right); + if (parent) { + if (parent.L === node) parent.L = next; else parent.R = next; + } else { + this._ = next; + } + if (left && right) { + red = next.C; + next.C = node.C; + next.L = left; + left.U = next; + if (next !== right) { + parent = next.U; + next.U = node.U; + node = next.R; + parent.L = node; + next.R = right; + right.U = next; + } else { + next.U = parent; + parent = next; + node = next.R; + } + } else { + red = node.C; + node = next; + } + if (node) node.U = parent; + if (red) return; + if (node && node.C) { + node.C = false; + return; + } + do { + if (node === this._) break; + if (node === parent.L) { + sibling = parent.R; + if (sibling.C) { + sibling.C = false; + parent.C = true; + d3_geom_voronoiRedBlackRotateLeft(this, parent); + sibling = parent.R; + } + if (sibling.L && sibling.L.C || sibling.R && sibling.R.C) { + if (!sibling.R || !sibling.R.C) { + sibling.L.C = false; + sibling.C = true; + d3_geom_voronoiRedBlackRotateRight(this, sibling); + sibling = parent.R; + } + sibling.C = parent.C; + parent.C = sibling.R.C = false; + d3_geom_voronoiRedBlackRotateLeft(this, parent); + node = this._; + break; + } + } else { + sibling = parent.L; + if (sibling.C) { + sibling.C = false; + parent.C = true; + d3_geom_voronoiRedBlackRotateRight(this, parent); + sibling = parent.L; + } + if (sibling.L && sibling.L.C || sibling.R && sibling.R.C) { + if (!sibling.L || !sibling.L.C) { + sibling.R.C = false; + sibling.C = true; + d3_geom_voronoiRedBlackRotateLeft(this, sibling); + sibling = parent.L; + } + sibling.C = parent.C; + parent.C = sibling.L.C = false; + d3_geom_voronoiRedBlackRotateRight(this, parent); + node = this._; + break; + } + } + sibling.C = true; + node = parent; + parent = parent.U; + } while (!node.C); + if (node) node.C = false; + } + }; + function d3_geom_voronoiRedBlackRotateLeft(tree, node) { + var p = node, q = node.R, parent = p.U; + if (parent) { + if (parent.L === p) parent.L = q; else parent.R = q; + } else { + tree._ = q; + } + q.U = parent; + p.U = q; + p.R = q.L; + if (p.R) p.R.U = p; + q.L = p; + } + function d3_geom_voronoiRedBlackRotateRight(tree, node) { + var p = node, q = node.L, parent = p.U; + if (parent) { + if (parent.L === p) parent.L = q; else parent.R = q; + } else { + tree._ = q; + } + q.U = parent; + p.U = q; + p.L = q.R; + if (p.L) p.L.U = p; + q.R = p; + } + function d3_geom_voronoiRedBlackFirst(node) { + while (node.L) node = node.L; + return node; + } + function d3_geom_voronoi(sites, bbox) { + var site = sites.sort(d3_geom_voronoiVertexOrder).pop(), x0, y0, circle; + d3_geom_voronoiEdges = []; + d3_geom_voronoiCells = new Array(sites.length); + d3_geom_voronoiBeaches = new d3_geom_voronoiRedBlackTree(); + d3_geom_voronoiCircles = new d3_geom_voronoiRedBlackTree(); + while (true) { + circle = d3_geom_voronoiFirstCircle; + if (site && (!circle || site.y < circle.y || site.y === circle.y && site.x < circle.x)) { + if (site.x !== x0 || site.y !== y0) { + d3_geom_voronoiCells[site.i] = new d3_geom_voronoiCell(site); + d3_geom_voronoiAddBeach(site); + x0 = site.x, y0 = site.y; + } + site = sites.pop(); + } else if (circle) { + d3_geom_voronoiRemoveBeach(circle.arc); + } else { + break; + } + } + if (bbox) d3_geom_voronoiClipEdges(bbox), d3_geom_voronoiCloseCells(bbox); + var diagram = { + cells: d3_geom_voronoiCells, + edges: d3_geom_voronoiEdges + }; + d3_geom_voronoiBeaches = d3_geom_voronoiCircles = d3_geom_voronoiEdges = d3_geom_voronoiCells = null; + return diagram; + } + function d3_geom_voronoiVertexOrder(a, b) { + return b.y - a.y || b.x - a.x; + } + d3.geom.voronoi = function(points) { + var x = d3_geom_pointX, y = d3_geom_pointY, fx = x, fy = y, clipExtent = d3_geom_voronoiClipExtent; + if (points) return voronoi(points); + function voronoi(data) { + var polygons = new Array(data.length), x0 = clipExtent[0][0], y0 = clipExtent[0][1], x1 = clipExtent[1][0], y1 = clipExtent[1][1]; + d3_geom_voronoi(sites(data), clipExtent).cells.forEach(function(cell, i) { + var edges = cell.edges, site = cell.site, polygon = polygons[i] = edges.length ? edges.map(function(e) { + var s = e.start(); + return [ s.x, s.y ]; + }) : site.x >= x0 && site.x <= x1 && site.y >= y0 && site.y <= y1 ? [ [ x0, y1 ], [ x1, y1 ], [ x1, y0 ], [ x0, y0 ] ] : []; + polygon.point = data[i]; + }); + return polygons; + } + function sites(data) { + return data.map(function(d, i) { + return { + x: Math.round(fx(d, i) / ε) * ε, + y: Math.round(fy(d, i) / ε) * ε, + i: i + }; + }); + } + voronoi.links = function(data) { + return d3_geom_voronoi(sites(data)).edges.filter(function(edge) { + return edge.l && edge.r; + }).map(function(edge) { + return { + source: data[edge.l.i], + target: data[edge.r.i] + }; + }); + }; + voronoi.triangles = function(data) { + var triangles = []; + d3_geom_voronoi(sites(data)).cells.forEach(function(cell, i) { + var site = cell.site, edges = cell.edges.sort(d3_geom_voronoiHalfEdgeOrder), j = -1, m = edges.length, e0, s0, e1 = edges[m - 1].edge, s1 = e1.l === site ? e1.r : e1.l; + while (++j < m) { + e0 = e1; + s0 = s1; + e1 = edges[j].edge; + s1 = e1.l === site ? e1.r : e1.l; + if (i < s0.i && i < s1.i && d3_geom_voronoiTriangleArea(site, s0, s1) < 0) { + triangles.push([ data[i], data[s0.i], data[s1.i] ]); + } + } + }); + return triangles; + }; + voronoi.x = function(_) { + return arguments.length ? (fx = d3_functor(x = _), voronoi) : x; + }; + voronoi.y = function(_) { + return arguments.length ? (fy = d3_functor(y = _), voronoi) : y; + }; + voronoi.clipExtent = function(_) { + if (!arguments.length) return clipExtent === d3_geom_voronoiClipExtent ? null : clipExtent; + clipExtent = _ == null ? d3_geom_voronoiClipExtent : _; + return voronoi; + }; + voronoi.size = function(_) { + if (!arguments.length) return clipExtent === d3_geom_voronoiClipExtent ? null : clipExtent && clipExtent[1]; + return voronoi.clipExtent(_ && [ [ 0, 0 ], _ ]); + }; + return voronoi; + }; + var d3_geom_voronoiClipExtent = [ [ -1e6, -1e6 ], [ 1e6, 1e6 ] ]; + function d3_geom_voronoiTriangleArea(a, b, c) { + return (a.x - c.x) * (b.y - a.y) - (a.x - b.x) * (c.y - a.y); + } + d3.geom.delaunay = function(vertices) { + return d3.geom.voronoi().triangles(vertices); + }; + d3.geom.quadtree = function(points, x1, y1, x2, y2) { + var x = d3_geom_pointX, y = d3_geom_pointY, compat; + if (compat = arguments.length) { + x = d3_geom_quadtreeCompatX; + y = d3_geom_quadtreeCompatY; + if (compat === 3) { + y2 = y1; + x2 = x1; + y1 = x1 = 0; + } + return quadtree(points); + } + function quadtree(data) { + var d, fx = d3_functor(x), fy = d3_functor(y), xs, ys, i, n, x1_, y1_, x2_, y2_; + if (x1 != null) { + x1_ = x1, y1_ = y1, x2_ = x2, y2_ = y2; + } else { + x2_ = y2_ = -(x1_ = y1_ = Infinity); + xs = [], ys = []; + n = data.length; + if (compat) for (i = 0; i < n; ++i) { + d = data[i]; + if (d.x < x1_) x1_ = d.x; + if (d.y < y1_) y1_ = d.y; + if (d.x > x2_) x2_ = d.x; + if (d.y > y2_) y2_ = d.y; + xs.push(d.x); + ys.push(d.y); + } else for (i = 0; i < n; ++i) { + var x_ = +fx(d = data[i], i), y_ = +fy(d, i); + if (x_ < x1_) x1_ = x_; + if (y_ < y1_) y1_ = y_; + if (x_ > x2_) x2_ = x_; + if (y_ > y2_) y2_ = y_; + xs.push(x_); + ys.push(y_); + } + } + var dx = x2_ - x1_, dy = y2_ - y1_; + if (dx > dy) y2_ = y1_ + dx; else x2_ = x1_ + dy; + function insert(n, d, x, y, x1, y1, x2, y2) { + if (isNaN(x) || isNaN(y)) return; + if (n.leaf) { + var nx = n.x, ny = n.y; + if (nx != null) { + if (abs(nx - x) + abs(ny - y) < .01) { + insertChild(n, d, x, y, x1, y1, x2, y2); + } else { + var nPoint = n.point; + n.x = n.y = n.point = null; + insertChild(n, nPoint, nx, ny, x1, y1, x2, y2); + insertChild(n, d, x, y, x1, y1, x2, y2); + } + } else { + n.x = x, n.y = y, n.point = d; + } + } else { + insertChild(n, d, x, y, x1, y1, x2, y2); + } + } + function insertChild(n, d, x, y, x1, y1, x2, y2) { + var xm = (x1 + x2) * .5, ym = (y1 + y2) * .5, right = x >= xm, below = y >= ym, i = below << 1 | right; + n.leaf = false; + n = n.nodes[i] || (n.nodes[i] = d3_geom_quadtreeNode()); + if (right) x1 = xm; else x2 = xm; + if (below) y1 = ym; else y2 = ym; + insert(n, d, x, y, x1, y1, x2, y2); + } + var root = d3_geom_quadtreeNode(); + root.add = function(d) { + insert(root, d, +fx(d, ++i), +fy(d, i), x1_, y1_, x2_, y2_); + }; + root.visit = function(f) { + d3_geom_quadtreeVisit(f, root, x1_, y1_, x2_, y2_); + }; + root.find = function(point) { + return d3_geom_quadtreeFind(root, point[0], point[1], x1_, y1_, x2_, y2_); + }; + i = -1; + if (x1 == null) { + while (++i < n) { + insert(root, data[i], xs[i], ys[i], x1_, y1_, x2_, y2_); + } + --i; + } else data.forEach(root.add); + xs = ys = data = d = null; + return root; + } + quadtree.x = function(_) { + return arguments.length ? (x = _, quadtree) : x; + }; + quadtree.y = function(_) { + return arguments.length ? (y = _, quadtree) : y; + }; + quadtree.extent = function(_) { + if (!arguments.length) return x1 == null ? null : [ [ x1, y1 ], [ x2, y2 ] ]; + if (_ == null) x1 = y1 = x2 = y2 = null; else x1 = +_[0][0], y1 = +_[0][1], x2 = +_[1][0], + y2 = +_[1][1]; + return quadtree; + }; + quadtree.size = function(_) { + if (!arguments.length) return x1 == null ? null : [ x2 - x1, y2 - y1 ]; + if (_ == null) x1 = y1 = x2 = y2 = null; else x1 = y1 = 0, x2 = +_[0], y2 = +_[1]; + return quadtree; + }; + return quadtree; + }; + function d3_geom_quadtreeCompatX(d) { + return d.x; + } + function d3_geom_quadtreeCompatY(d) { + return d.y; + } + function d3_geom_quadtreeNode() { + return { + leaf: true, + nodes: [], + point: null, + x: null, + y: null + }; + } + function d3_geom_quadtreeVisit(f, node, x1, y1, x2, y2) { + if (!f(node, x1, y1, x2, y2)) { + var sx = (x1 + x2) * .5, sy = (y1 + y2) * .5, children = node.nodes; + if (children[0]) d3_geom_quadtreeVisit(f, children[0], x1, y1, sx, sy); + if (children[1]) d3_geom_quadtreeVisit(f, children[1], sx, y1, x2, sy); + if (children[2]) d3_geom_quadtreeVisit(f, children[2], x1, sy, sx, y2); + if (children[3]) d3_geom_quadtreeVisit(f, children[3], sx, sy, x2, y2); + } + } + function d3_geom_quadtreeFind(root, x, y, x0, y0, x3, y3) { + var minDistance2 = Infinity, closestPoint; + (function find(node, x1, y1, x2, y2) { + if (x1 > x3 || y1 > y3 || x2 < x0 || y2 < y0) return; + if (point = node.point) { + var point, dx = x - node.x, dy = y - node.y, distance2 = dx * dx + dy * dy; + if (distance2 < minDistance2) { + var distance = Math.sqrt(minDistance2 = distance2); + x0 = x - distance, y0 = y - distance; + x3 = x + distance, y3 = y + distance; + closestPoint = point; + } + } + var children = node.nodes, xm = (x1 + x2) * .5, ym = (y1 + y2) * .5, right = x >= xm, below = y >= ym; + for (var i = below << 1 | right, j = i + 4; i < j; ++i) { + if (node = children[i & 3]) switch (i & 3) { + case 0: + find(node, x1, y1, xm, ym); + break; + + case 1: + find(node, xm, y1, x2, ym); + break; + + case 2: + find(node, x1, ym, xm, y2); + break; + + case 3: + find(node, xm, ym, x2, y2); + break; + } + } + })(root, x0, y0, x3, y3); + return closestPoint; + } + d3.interpolateRgb = d3_interpolateRgb; + function d3_interpolateRgb(a, b) { + a = d3.rgb(a); + b = d3.rgb(b); + var ar = a.r, ag = a.g, ab = a.b, br = b.r - ar, bg = b.g - ag, bb = b.b - ab; + return function(t) { + return "#" + d3_rgb_hex(Math.round(ar + br * t)) + d3_rgb_hex(Math.round(ag + bg * t)) + d3_rgb_hex(Math.round(ab + bb * t)); + }; + } + d3.interpolateObject = d3_interpolateObject; + function d3_interpolateObject(a, b) { + var i = {}, c = {}, k; + for (k in a) { + if (k in b) { + i[k] = d3_interpolate(a[k], b[k]); + } else { + c[k] = a[k]; + } + } + for (k in b) { + if (!(k in a)) { + c[k] = b[k]; + } + } + return function(t) { + for (k in i) c[k] = i[k](t); + return c; + }; + } + d3.interpolateNumber = d3_interpolateNumber; + function d3_interpolateNumber(a, b) { + a = +a, b = +b; + return function(t) { + return a * (1 - t) + b * t; + }; + } + d3.interpolateString = d3_interpolateString; + function d3_interpolateString(a, b) { + var bi = d3_interpolate_numberA.lastIndex = d3_interpolate_numberB.lastIndex = 0, am, bm, bs, i = -1, s = [], q = []; + a = a + "", b = b + ""; + while ((am = d3_interpolate_numberA.exec(a)) && (bm = d3_interpolate_numberB.exec(b))) { + if ((bs = bm.index) > bi) { + bs = b.slice(bi, bs); + if (s[i]) s[i] += bs; else s[++i] = bs; + } + if ((am = am[0]) === (bm = bm[0])) { + if (s[i]) s[i] += bm; else s[++i] = bm; + } else { + s[++i] = null; + q.push({ + i: i, + x: d3_interpolateNumber(am, bm) + }); + } + bi = d3_interpolate_numberB.lastIndex; + } + if (bi < b.length) { + bs = b.slice(bi); + if (s[i]) s[i] += bs; else s[++i] = bs; + } + return s.length < 2 ? q[0] ? (b = q[0].x, function(t) { + return b(t) + ""; + }) : function() { + return b; + } : (b = q.length, function(t) { + for (var i = 0, o; i < b; ++i) s[(o = q[i]).i] = o.x(t); + return s.join(""); + }); + } + var d3_interpolate_numberA = /[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g, d3_interpolate_numberB = new RegExp(d3_interpolate_numberA.source, "g"); + d3.interpolate = d3_interpolate; + function d3_interpolate(a, b) { + var i = d3.interpolators.length, f; + while (--i >= 0 && !(f = d3.interpolators[i](a, b))) ; + return f; + } + d3.interpolators = [ function(a, b) { + var t = typeof b; + return (t === "string" ? d3_rgb_names.has(b) || /^(#|rgb\(|hsl\()/.test(b) ? d3_interpolateRgb : d3_interpolateString : b instanceof d3_color ? d3_interpolateRgb : Array.isArray(b) ? d3_interpolateArray : t === "object" && isNaN(b) ? d3_interpolateObject : d3_interpolateNumber)(a, b); + } ]; + d3.interpolateArray = d3_interpolateArray; + function d3_interpolateArray(a, b) { + var x = [], c = [], na = a.length, nb = b.length, n0 = Math.min(a.length, b.length), i; + for (i = 0; i < n0; ++i) x.push(d3_interpolate(a[i], b[i])); + for (;i < na; ++i) c[i] = a[i]; + for (;i < nb; ++i) c[i] = b[i]; + return function(t) { + for (i = 0; i < n0; ++i) c[i] = x[i](t); + return c; + }; + } + var d3_ease_default = function() { + return d3_identity; + }; + var d3_ease = d3.map({ + linear: d3_ease_default, + poly: d3_ease_poly, + quad: function() { + return d3_ease_quad; + }, + cubic: function() { + return d3_ease_cubic; + }, + sin: function() { + return d3_ease_sin; + }, + exp: function() { + return d3_ease_exp; + }, + circle: function() { + return d3_ease_circle; + }, + elastic: d3_ease_elastic, + back: d3_ease_back, + bounce: function() { + return d3_ease_bounce; + } + }); + var d3_ease_mode = d3.map({ + "in": d3_identity, + out: d3_ease_reverse, + "in-out": d3_ease_reflect, + "out-in": function(f) { + return d3_ease_reflect(d3_ease_reverse(f)); + } + }); + d3.ease = function(name) { + var i = name.indexOf("-"), t = i >= 0 ? name.slice(0, i) : name, m = i >= 0 ? name.slice(i + 1) : "in"; + t = d3_ease.get(t) || d3_ease_default; + m = d3_ease_mode.get(m) || d3_identity; + return d3_ease_clamp(m(t.apply(null, d3_arraySlice.call(arguments, 1)))); + }; + function d3_ease_clamp(f) { + return function(t) { + return t <= 0 ? 0 : t >= 1 ? 1 : f(t); + }; + } + function d3_ease_reverse(f) { + return function(t) { + return 1 - f(1 - t); + }; + } + function d3_ease_reflect(f) { + return function(t) { + return .5 * (t < .5 ? f(2 * t) : 2 - f(2 - 2 * t)); + }; + } + function d3_ease_quad(t) { + return t * t; + } + function d3_ease_cubic(t) { + return t * t * t; + } + function d3_ease_cubicInOut(t) { + if (t <= 0) return 0; + if (t >= 1) return 1; + var t2 = t * t, t3 = t2 * t; + return 4 * (t < .5 ? t3 : 3 * (t - t2) + t3 - .75); + } + function d3_ease_poly(e) { + return function(t) { + return Math.pow(t, e); + }; + } + function d3_ease_sin(t) { + return 1 - Math.cos(t * halfπ); + } + function d3_ease_exp(t) { + return Math.pow(2, 10 * (t - 1)); + } + function d3_ease_circle(t) { + return 1 - Math.sqrt(1 - t * t); + } + function d3_ease_elastic(a, p) { + var s; + if (arguments.length < 2) p = .45; + if (arguments.length) s = p / τ * Math.asin(1 / a); else a = 1, s = p / 4; + return function(t) { + return 1 + a * Math.pow(2, -10 * t) * Math.sin((t - s) * τ / p); + }; + } + function d3_ease_back(s) { + if (!s) s = 1.70158; + return function(t) { + return t * t * ((s + 1) * t - s); + }; + } + function d3_ease_bounce(t) { + return t < 1 / 2.75 ? 7.5625 * t * t : t < 2 / 2.75 ? 7.5625 * (t -= 1.5 / 2.75) * t + .75 : t < 2.5 / 2.75 ? 7.5625 * (t -= 2.25 / 2.75) * t + .9375 : 7.5625 * (t -= 2.625 / 2.75) * t + .984375; + } + d3.interpolateHcl = d3_interpolateHcl; + function d3_interpolateHcl(a, b) { + a = d3.hcl(a); + b = d3.hcl(b); + var ah = a.h, ac = a.c, al = a.l, bh = b.h - ah, bc = b.c - ac, bl = b.l - al; + if (isNaN(bc)) bc = 0, ac = isNaN(ac) ? b.c : ac; + if (isNaN(bh)) bh = 0, ah = isNaN(ah) ? b.h : ah; else if (bh > 180) bh -= 360; else if (bh < -180) bh += 360; + return function(t) { + return d3_hcl_lab(ah + bh * t, ac + bc * t, al + bl * t) + ""; + }; + } + d3.interpolateHsl = d3_interpolateHsl; + function d3_interpolateHsl(a, b) { + a = d3.hsl(a); + b = d3.hsl(b); + var ah = a.h, as = a.s, al = a.l, bh = b.h - ah, bs = b.s - as, bl = b.l - al; + if (isNaN(bs)) bs = 0, as = isNaN(as) ? b.s : as; + if (isNaN(bh)) bh = 0, ah = isNaN(ah) ? b.h : ah; else if (bh > 180) bh -= 360; else if (bh < -180) bh += 360; + return function(t) { + return d3_hsl_rgb(ah + bh * t, as + bs * t, al + bl * t) + ""; + }; + } + d3.interpolateLab = d3_interpolateLab; + function d3_interpolateLab(a, b) { + a = d3.lab(a); + b = d3.lab(b); + var al = a.l, aa = a.a, ab = a.b, bl = b.l - al, ba = b.a - aa, bb = b.b - ab; + return function(t) { + return d3_lab_rgb(al + bl * t, aa + ba * t, ab + bb * t) + ""; + }; + } + d3.interpolateRound = d3_interpolateRound; + function d3_interpolateRound(a, b) { + b -= a; + return function(t) { + return Math.round(a + b * t); + }; + } + d3.transform = function(string) { + var g = d3_document.createElementNS(d3.ns.prefix.svg, "g"); + return (d3.transform = function(string) { + if (string != null) { + g.setAttribute("transform", string); + var t = g.transform.baseVal.consolidate(); + } + return new d3_transform(t ? t.matrix : d3_transformIdentity); + })(string); + }; + function d3_transform(m) { + var r0 = [ m.a, m.b ], r1 = [ m.c, m.d ], kx = d3_transformNormalize(r0), kz = d3_transformDot(r0, r1), ky = d3_transformNormalize(d3_transformCombine(r1, r0, -kz)) || 0; + if (r0[0] * r1[1] < r1[0] * r0[1]) { + r0[0] *= -1; + r0[1] *= -1; + kx *= -1; + kz *= -1; + } + this.rotate = (kx ? Math.atan2(r0[1], r0[0]) : Math.atan2(-r1[0], r1[1])) * d3_degrees; + this.translate = [ m.e, m.f ]; + this.scale = [ kx, ky ]; + this.skew = ky ? Math.atan2(kz, ky) * d3_degrees : 0; + } + d3_transform.prototype.toString = function() { + return "translate(" + this.translate + ")rotate(" + this.rotate + ")skewX(" + this.skew + ")scale(" + this.scale + ")"; + }; + function d3_transformDot(a, b) { + return a[0] * b[0] + a[1] * b[1]; + } + function d3_transformNormalize(a) { + var k = Math.sqrt(d3_transformDot(a, a)); + if (k) { + a[0] /= k; + a[1] /= k; + } + return k; + } + function d3_transformCombine(a, b, k) { + a[0] += k * b[0]; + a[1] += k * b[1]; + return a; + } + var d3_transformIdentity = { + a: 1, + b: 0, + c: 0, + d: 1, + e: 0, + f: 0 + }; + d3.interpolateTransform = d3_interpolateTransform; + function d3_interpolateTransform(a, b) { + var s = [], q = [], n, A = d3.transform(a), B = d3.transform(b), ta = A.translate, tb = B.translate, ra = A.rotate, rb = B.rotate, wa = A.skew, wb = B.skew, ka = A.scale, kb = B.scale; + if (ta[0] != tb[0] || ta[1] != tb[1]) { + s.push("translate(", null, ",", null, ")"); + q.push({ + i: 1, + x: d3_interpolateNumber(ta[0], tb[0]) + }, { + i: 3, + x: d3_interpolateNumber(ta[1], tb[1]) + }); + } else if (tb[0] || tb[1]) { + s.push("translate(" + tb + ")"); + } else { + s.push(""); + } + if (ra != rb) { + if (ra - rb > 180) rb += 360; else if (rb - ra > 180) ra += 360; + q.push({ + i: s.push(s.pop() + "rotate(", null, ")") - 2, + x: d3_interpolateNumber(ra, rb) + }); + } else if (rb) { + s.push(s.pop() + "rotate(" + rb + ")"); + } + if (wa != wb) { + q.push({ + i: s.push(s.pop() + "skewX(", null, ")") - 2, + x: d3_interpolateNumber(wa, wb) + }); + } else if (wb) { + s.push(s.pop() + "skewX(" + wb + ")"); + } + if (ka[0] != kb[0] || ka[1] != kb[1]) { + n = s.push(s.pop() + "scale(", null, ",", null, ")"); + q.push({ + i: n - 4, + x: d3_interpolateNumber(ka[0], kb[0]) + }, { + i: n - 2, + x: d3_interpolateNumber(ka[1], kb[1]) + }); + } else if (kb[0] != 1 || kb[1] != 1) { + s.push(s.pop() + "scale(" + kb + ")"); + } + n = q.length; + return function(t) { + var i = -1, o; + while (++i < n) s[(o = q[i]).i] = o.x(t); + return s.join(""); + }; + } + function d3_uninterpolateNumber(a, b) { + b = (b -= a = +a) || 1 / b; + return function(x) { + return (x - a) / b; + }; + } + function d3_uninterpolateClamp(a, b) { + b = (b -= a = +a) || 1 / b; + return function(x) { + return Math.max(0, Math.min(1, (x - a) / b)); + }; + } + d3.layout = {}; + d3.layout.bundle = function() { + return function(links) { + var paths = [], i = -1, n = links.length; + while (++i < n) paths.push(d3_layout_bundlePath(links[i])); + return paths; + }; + }; + function d3_layout_bundlePath(link) { + var start = link.source, end = link.target, lca = d3_layout_bundleLeastCommonAncestor(start, end), points = [ start ]; + while (start !== lca) { + start = start.parent; + points.push(start); + } + var k = points.length; + while (end !== lca) { + points.splice(k, 0, end); + end = end.parent; + } + return points; + } + function d3_layout_bundleAncestors(node) { + var ancestors = [], parent = node.parent; + while (parent != null) { + ancestors.push(node); + node = parent; + parent = parent.parent; + } + ancestors.push(node); + return ancestors; + } + function d3_layout_bundleLeastCommonAncestor(a, b) { + if (a === b) return a; + var aNodes = d3_layout_bundleAncestors(a), bNodes = d3_layout_bundleAncestors(b), aNode = aNodes.pop(), bNode = bNodes.pop(), sharedNode = null; + while (aNode === bNode) { + sharedNode = aNode; + aNode = aNodes.pop(); + bNode = bNodes.pop(); + } + return sharedNode; + } + d3.layout.chord = function() { + var chord = {}, chords, groups, matrix, n, padding = 0, sortGroups, sortSubgroups, sortChords; + function relayout() { + var subgroups = {}, groupSums = [], groupIndex = d3.range(n), subgroupIndex = [], k, x, x0, i, j; + chords = []; + groups = []; + k = 0, i = -1; + while (++i < n) { + x = 0, j = -1; + while (++j < n) { + x += matrix[i][j]; + } + groupSums.push(x); + subgroupIndex.push(d3.range(n)); + k += x; + } + if (sortGroups) { + groupIndex.sort(function(a, b) { + return sortGroups(groupSums[a], groupSums[b]); + }); + } + if (sortSubgroups) { + subgroupIndex.forEach(function(d, i) { + d.sort(function(a, b) { + return sortSubgroups(matrix[i][a], matrix[i][b]); + }); + }); + } + k = (τ - padding * n) / k; + x = 0, i = -1; + while (++i < n) { + x0 = x, j = -1; + while (++j < n) { + var di = groupIndex[i], dj = subgroupIndex[di][j], v = matrix[di][dj], a0 = x, a1 = x += v * k; + subgroups[di + "-" + dj] = { + index: di, + subindex: dj, + startAngle: a0, + endAngle: a1, + value: v + }; + } + groups[di] = { + index: di, + startAngle: x0, + endAngle: x, + value: (x - x0) / k + }; + x += padding; + } + i = -1; + while (++i < n) { + j = i - 1; + while (++j < n) { + var source = subgroups[i + "-" + j], target = subgroups[j + "-" + i]; + if (source.value || target.value) { + chords.push(source.value < target.value ? { + source: target, + target: source + } : { + source: source, + target: target + }); + } + } + } + if (sortChords) resort(); + } + function resort() { + chords.sort(function(a, b) { + return sortChords((a.source.value + a.target.value) / 2, (b.source.value + b.target.value) / 2); + }); + } + chord.matrix = function(x) { + if (!arguments.length) return matrix; + n = (matrix = x) && matrix.length; + chords = groups = null; + return chord; + }; + chord.padding = function(x) { + if (!arguments.length) return padding; + padding = x; + chords = groups = null; + return chord; + }; + chord.sortGroups = function(x) { + if (!arguments.length) return sortGroups; + sortGroups = x; + chords = groups = null; + return chord; + }; + chord.sortSubgroups = function(x) { + if (!arguments.length) return sortSubgroups; + sortSubgroups = x; + chords = null; + return chord; + }; + chord.sortChords = function(x) { + if (!arguments.length) return sortChords; + sortChords = x; + if (chords) resort(); + return chord; + }; + chord.chords = function() { + if (!chords) relayout(); + return chords; + }; + chord.groups = function() { + if (!groups) relayout(); + return groups; + }; + return chord; + }; + d3.layout.force = function() { + var force = {}, event = d3.dispatch("start", "tick", "end"), size = [ 1, 1 ], drag, alpha, friction = .9, linkDistance = d3_layout_forceLinkDistance, linkStrength = d3_layout_forceLinkStrength, charge = -30, chargeDistance2 = d3_layout_forceChargeDistance2, gravity = .1, theta2 = .64, nodes = [], links = [], distances, strengths, charges; + function repulse(node) { + return function(quad, x1, _, x2) { + if (quad.point !== node) { + var dx = quad.cx - node.x, dy = quad.cy - node.y, dw = x2 - x1, dn = dx * dx + dy * dy; + if (dw * dw / theta2 < dn) { + if (dn < chargeDistance2) { + var k = quad.charge / dn; + node.px -= dx * k; + node.py -= dy * k; + } + return true; + } + if (quad.point && dn && dn < chargeDistance2) { + var k = quad.pointCharge / dn; + node.px -= dx * k; + node.py -= dy * k; + } + } + return !quad.charge; + }; + } + force.tick = function() { + if ((alpha *= .99) < .005) { + event.end({ + type: "end", + alpha: alpha = 0 + }); + return true; + } + var n = nodes.length, m = links.length, q, i, o, s, t, l, k, x, y; + for (i = 0; i < m; ++i) { + o = links[i]; + s = o.source; + t = o.target; + x = t.x - s.x; + y = t.y - s.y; + if (l = x * x + y * y) { + l = alpha * strengths[i] * ((l = Math.sqrt(l)) - distances[i]) / l; + x *= l; + y *= l; + t.x -= x * (k = s.weight / (t.weight + s.weight)); + t.y -= y * k; + s.x += x * (k = 1 - k); + s.y += y * k; + } + } + if (k = alpha * gravity) { + x = size[0] / 2; + y = size[1] / 2; + i = -1; + if (k) while (++i < n) { + o = nodes[i]; + o.x += (x - o.x) * k; + o.y += (y - o.y) * k; + } + } + if (charge) { + d3_layout_forceAccumulate(q = d3.geom.quadtree(nodes), alpha, charges); + i = -1; + while (++i < n) { + if (!(o = nodes[i]).fixed) { + q.visit(repulse(o)); + } + } + } + i = -1; + while (++i < n) { + o = nodes[i]; + if (o.fixed) { + o.x = o.px; + o.y = o.py; + } else { + o.x -= (o.px - (o.px = o.x)) * friction; + o.y -= (o.py - (o.py = o.y)) * friction; + } + } + event.tick({ + type: "tick", + alpha: alpha + }); + }; + force.nodes = function(x) { + if (!arguments.length) return nodes; + nodes = x; + return force; + }; + force.links = function(x) { + if (!arguments.length) return links; + links = x; + return force; + }; + force.size = function(x) { + if (!arguments.length) return size; + size = x; + return force; + }; + force.linkDistance = function(x) { + if (!arguments.length) return linkDistance; + linkDistance = typeof x === "function" ? x : +x; + return force; + }; + force.distance = force.linkDistance; + force.linkStrength = function(x) { + if (!arguments.length) return linkStrength; + linkStrength = typeof x === "function" ? x : +x; + return force; + }; + force.friction = function(x) { + if (!arguments.length) return friction; + friction = +x; + return force; + }; + force.charge = function(x) { + if (!arguments.length) return charge; + charge = typeof x === "function" ? x : +x; + return force; + }; + force.chargeDistance = function(x) { + if (!arguments.length) return Math.sqrt(chargeDistance2); + chargeDistance2 = x * x; + return force; + }; + force.gravity = function(x) { + if (!arguments.length) return gravity; + gravity = +x; + return force; + }; + force.theta = function(x) { + if (!arguments.length) return Math.sqrt(theta2); + theta2 = x * x; + return force; + }; + force.alpha = function(x) { + if (!arguments.length) return alpha; + x = +x; + if (alpha) { + if (x > 0) alpha = x; else alpha = 0; + } else if (x > 0) { + event.start({ + type: "start", + alpha: alpha = x + }); + d3.timer(force.tick); + } + return force; + }; + force.start = function() { + var i, n = nodes.length, m = links.length, w = size[0], h = size[1], neighbors, o; + for (i = 0; i < n; ++i) { + (o = nodes[i]).index = i; + o.weight = 0; + } + for (i = 0; i < m; ++i) { + o = links[i]; + if (typeof o.source == "number") o.source = nodes[o.source]; + if (typeof o.target == "number") o.target = nodes[o.target]; + ++o.source.weight; + ++o.target.weight; + } + for (i = 0; i < n; ++i) { + o = nodes[i]; + if (isNaN(o.x)) o.x = position("x", w); + if (isNaN(o.y)) o.y = position("y", h); + if (isNaN(o.px)) o.px = o.x; + if (isNaN(o.py)) o.py = o.y; + } + distances = []; + if (typeof linkDistance === "function") for (i = 0; i < m; ++i) distances[i] = +linkDistance.call(this, links[i], i); else for (i = 0; i < m; ++i) distances[i] = linkDistance; + strengths = []; + if (typeof linkStrength === "function") for (i = 0; i < m; ++i) strengths[i] = +linkStrength.call(this, links[i], i); else for (i = 0; i < m; ++i) strengths[i] = linkStrength; + charges = []; + if (typeof charge === "function") for (i = 0; i < n; ++i) charges[i] = +charge.call(this, nodes[i], i); else for (i = 0; i < n; ++i) charges[i] = charge; + function position(dimension, size) { + if (!neighbors) { + neighbors = new Array(n); + for (j = 0; j < n; ++j) { + neighbors[j] = []; + } + for (j = 0; j < m; ++j) { + var o = links[j]; + neighbors[o.source.index].push(o.target); + neighbors[o.target.index].push(o.source); + } + } + var candidates = neighbors[i], j = -1, l = candidates.length, x; + while (++j < l) if (!isNaN(x = candidates[j][dimension])) return x; + return Math.random() * size; + } + return force.resume(); + }; + force.resume = function() { + return force.alpha(.1); + }; + force.stop = function() { + return force.alpha(0); + }; + force.drag = function() { + if (!drag) drag = d3.behavior.drag().origin(d3_identity).on("dragstart.force", d3_layout_forceDragstart).on("drag.force", dragmove).on("dragend.force", d3_layout_forceDragend); + if (!arguments.length) return drag; + this.on("mouseover.force", d3_layout_forceMouseover).on("mouseout.force", d3_layout_forceMouseout).call(drag); + }; + function dragmove(d) { + d.px = d3.event.x, d.py = d3.event.y; + force.resume(); + } + return d3.rebind(force, event, "on"); + }; + function d3_layout_forceDragstart(d) { + d.fixed |= 2; + } + function d3_layout_forceDragend(d) { + d.fixed &= ~6; + } + function d3_layout_forceMouseover(d) { + d.fixed |= 4; + d.px = d.x, d.py = d.y; + } + function d3_layout_forceMouseout(d) { + d.fixed &= ~4; + } + function d3_layout_forceAccumulate(quad, alpha, charges) { + var cx = 0, cy = 0; + quad.charge = 0; + if (!quad.leaf) { + var nodes = quad.nodes, n = nodes.length, i = -1, c; + while (++i < n) { + c = nodes[i]; + if (c == null) continue; + d3_layout_forceAccumulate(c, alpha, charges); + quad.charge += c.charge; + cx += c.charge * c.cx; + cy += c.charge * c.cy; + } + } + if (quad.point) { + if (!quad.leaf) { + quad.point.x += Math.random() - .5; + quad.point.y += Math.random() - .5; + } + var k = alpha * charges[quad.point.index]; + quad.charge += quad.pointCharge = k; + cx += k * quad.point.x; + cy += k * quad.point.y; + } + quad.cx = cx / quad.charge; + quad.cy = cy / quad.charge; + } + var d3_layout_forceLinkDistance = 20, d3_layout_forceLinkStrength = 1, d3_layout_forceChargeDistance2 = Infinity; + d3.layout.hierarchy = function() { + var sort = d3_layout_hierarchySort, children = d3_layout_hierarchyChildren, value = d3_layout_hierarchyValue; + function hierarchy(root) { + var stack = [ root ], nodes = [], node; + root.depth = 0; + while ((node = stack.pop()) != null) { + nodes.push(node); + if ((childs = children.call(hierarchy, node, node.depth)) && (n = childs.length)) { + var n, childs, child; + while (--n >= 0) { + stack.push(child = childs[n]); + child.parent = node; + child.depth = node.depth + 1; + } + if (value) node.value = 0; + node.children = childs; + } else { + if (value) node.value = +value.call(hierarchy, node, node.depth) || 0; + delete node.children; + } + } + d3_layout_hierarchyVisitAfter(root, function(node) { + var childs, parent; + if (sort && (childs = node.children)) childs.sort(sort); + if (value && (parent = node.parent)) parent.value += node.value; + }); + return nodes; + } + hierarchy.sort = function(x) { + if (!arguments.length) return sort; + sort = x; + return hierarchy; + }; + hierarchy.children = function(x) { + if (!arguments.length) return children; + children = x; + return hierarchy; + }; + hierarchy.value = function(x) { + if (!arguments.length) return value; + value = x; + return hierarchy; + }; + hierarchy.revalue = function(root) { + if (value) { + d3_layout_hierarchyVisitBefore(root, function(node) { + if (node.children) node.value = 0; + }); + d3_layout_hierarchyVisitAfter(root, function(node) { + var parent; + if (!node.children) node.value = +value.call(hierarchy, node, node.depth) || 0; + if (parent = node.parent) parent.value += node.value; + }); + } + return root; + }; + return hierarchy; + }; + function d3_layout_hierarchyRebind(object, hierarchy) { + d3.rebind(object, hierarchy, "sort", "children", "value"); + object.nodes = object; + object.links = d3_layout_hierarchyLinks; + return object; + } + function d3_layout_hierarchyVisitBefore(node, callback) { + var nodes = [ node ]; + while ((node = nodes.pop()) != null) { + callback(node); + if ((children = node.children) && (n = children.length)) { + var n, children; + while (--n >= 0) nodes.push(children[n]); + } + } + } + function d3_layout_hierarchyVisitAfter(node, callback) { + var nodes = [ node ], nodes2 = []; + while ((node = nodes.pop()) != null) { + nodes2.push(node); + if ((children = node.children) && (n = children.length)) { + var i = -1, n, children; + while (++i < n) nodes.push(children[i]); + } + } + while ((node = nodes2.pop()) != null) { + callback(node); + } + } + function d3_layout_hierarchyChildren(d) { + return d.children; + } + function d3_layout_hierarchyValue(d) { + return d.value; + } + function d3_layout_hierarchySort(a, b) { + return b.value - a.value; + } + function d3_layout_hierarchyLinks(nodes) { + return d3.merge(nodes.map(function(parent) { + return (parent.children || []).map(function(child) { + return { + source: parent, + target: child + }; + }); + })); + } + d3.layout.partition = function() { + var hierarchy = d3.layout.hierarchy(), size = [ 1, 1 ]; + function position(node, x, dx, dy) { + var children = node.children; + node.x = x; + node.y = node.depth * dy; + node.dx = dx; + node.dy = dy; + if (children && (n = children.length)) { + var i = -1, n, c, d; + dx = node.value ? dx / node.value : 0; + while (++i < n) { + position(c = children[i], x, d = c.value * dx, dy); + x += d; + } + } + } + function depth(node) { + var children = node.children, d = 0; + if (children && (n = children.length)) { + var i = -1, n; + while (++i < n) d = Math.max(d, depth(children[i])); + } + return 1 + d; + } + function partition(d, i) { + var nodes = hierarchy.call(this, d, i); + position(nodes[0], 0, size[0], size[1] / depth(nodes[0])); + return nodes; + } + partition.size = function(x) { + if (!arguments.length) return size; + size = x; + return partition; + }; + return d3_layout_hierarchyRebind(partition, hierarchy); + }; + d3.layout.pie = function() { + var value = Number, sort = d3_layout_pieSortByValue, startAngle = 0, endAngle = τ, padAngle = 0; + function pie(data) { + var n = data.length, values = data.map(function(d, i) { + return +value.call(pie, d, i); + }), a = +(typeof startAngle === "function" ? startAngle.apply(this, arguments) : startAngle), da = (typeof endAngle === "function" ? endAngle.apply(this, arguments) : endAngle) - a, p = Math.min(Math.abs(da) / n, +(typeof padAngle === "function" ? padAngle.apply(this, arguments) : padAngle)), pa = p * (da < 0 ? -1 : 1), k = (da - n * pa) / d3.sum(values), index = d3.range(n), arcs = [], v; + if (sort != null) index.sort(sort === d3_layout_pieSortByValue ? function(i, j) { + return values[j] - values[i]; + } : function(i, j) { + return sort(data[i], data[j]); + }); + index.forEach(function(i) { + arcs[i] = { + data: data[i], + value: v = values[i], + startAngle: a, + endAngle: a += v * k + pa, + padAngle: p + }; + }); + return arcs; + } + pie.value = function(_) { + if (!arguments.length) return value; + value = _; + return pie; + }; + pie.sort = function(_) { + if (!arguments.length) return sort; + sort = _; + return pie; + }; + pie.startAngle = function(_) { + if (!arguments.length) return startAngle; + startAngle = _; + return pie; + }; + pie.endAngle = function(_) { + if (!arguments.length) return endAngle; + endAngle = _; + return pie; + }; + pie.padAngle = function(_) { + if (!arguments.length) return padAngle; + padAngle = _; + return pie; + }; + return pie; + }; + var d3_layout_pieSortByValue = {}; + d3.layout.stack = function() { + var values = d3_identity, order = d3_layout_stackOrderDefault, offset = d3_layout_stackOffsetZero, out = d3_layout_stackOut, x = d3_layout_stackX, y = d3_layout_stackY; + function stack(data, index) { + if (!(n = data.length)) return data; + var series = data.map(function(d, i) { + return values.call(stack, d, i); + }); + var points = series.map(function(d) { + return d.map(function(v, i) { + return [ x.call(stack, v, i), y.call(stack, v, i) ]; + }); + }); + var orders = order.call(stack, points, index); + series = d3.permute(series, orders); + points = d3.permute(points, orders); + var offsets = offset.call(stack, points, index); + var m = series[0].length, n, i, j, o; + for (j = 0; j < m; ++j) { + out.call(stack, series[0][j], o = offsets[j], points[0][j][1]); + for (i = 1; i < n; ++i) { + out.call(stack, series[i][j], o += points[i - 1][j][1], points[i][j][1]); + } + } + return data; + } + stack.values = function(x) { + if (!arguments.length) return values; + values = x; + return stack; + }; + stack.order = function(x) { + if (!arguments.length) return order; + order = typeof x === "function" ? x : d3_layout_stackOrders.get(x) || d3_layout_stackOrderDefault; + return stack; + }; + stack.offset = function(x) { + if (!arguments.length) return offset; + offset = typeof x === "function" ? x : d3_layout_stackOffsets.get(x) || d3_layout_stackOffsetZero; + return stack; + }; + stack.x = function(z) { + if (!arguments.length) return x; + x = z; + return stack; + }; + stack.y = function(z) { + if (!arguments.length) return y; + y = z; + return stack; + }; + stack.out = function(z) { + if (!arguments.length) return out; + out = z; + return stack; + }; + return stack; + }; + function d3_layout_stackX(d) { + return d.x; + } + function d3_layout_stackY(d) { + return d.y; + } + function d3_layout_stackOut(d, y0, y) { + d.y0 = y0; + d.y = y; + } + var d3_layout_stackOrders = d3.map({ + "inside-out": function(data) { + var n = data.length, i, j, max = data.map(d3_layout_stackMaxIndex), sums = data.map(d3_layout_stackReduceSum), index = d3.range(n).sort(function(a, b) { + return max[a] - max[b]; + }), top = 0, bottom = 0, tops = [], bottoms = []; + for (i = 0; i < n; ++i) { + j = index[i]; + if (top < bottom) { + top += sums[j]; + tops.push(j); + } else { + bottom += sums[j]; + bottoms.push(j); + } + } + return bottoms.reverse().concat(tops); + }, + reverse: function(data) { + return d3.range(data.length).reverse(); + }, + "default": d3_layout_stackOrderDefault + }); + var d3_layout_stackOffsets = d3.map({ + silhouette: function(data) { + var n = data.length, m = data[0].length, sums = [], max = 0, i, j, o, y0 = []; + for (j = 0; j < m; ++j) { + for (i = 0, o = 0; i < n; i++) o += data[i][j][1]; + if (o > max) max = o; + sums.push(o); + } + for (j = 0; j < m; ++j) { + y0[j] = (max - sums[j]) / 2; + } + return y0; + }, + wiggle: function(data) { + var n = data.length, x = data[0], m = x.length, i, j, k, s1, s2, s3, dx, o, o0, y0 = []; + y0[0] = o = o0 = 0; + for (j = 1; j < m; ++j) { + for (i = 0, s1 = 0; i < n; ++i) s1 += data[i][j][1]; + for (i = 0, s2 = 0, dx = x[j][0] - x[j - 1][0]; i < n; ++i) { + for (k = 0, s3 = (data[i][j][1] - data[i][j - 1][1]) / (2 * dx); k < i; ++k) { + s3 += (data[k][j][1] - data[k][j - 1][1]) / dx; + } + s2 += s3 * data[i][j][1]; + } + y0[j] = o -= s1 ? s2 / s1 * dx : 0; + if (o < o0) o0 = o; + } + for (j = 0; j < m; ++j) y0[j] -= o0; + return y0; + }, + expand: function(data) { + var n = data.length, m = data[0].length, k = 1 / n, i, j, o, y0 = []; + for (j = 0; j < m; ++j) { + for (i = 0, o = 0; i < n; i++) o += data[i][j][1]; + if (o) for (i = 0; i < n; i++) data[i][j][1] /= o; else for (i = 0; i < n; i++) data[i][j][1] = k; + } + for (j = 0; j < m; ++j) y0[j] = 0; + return y0; + }, + zero: d3_layout_stackOffsetZero + }); + function d3_layout_stackOrderDefault(data) { + return d3.range(data.length); + } + function d3_layout_stackOffsetZero(data) { + var j = -1, m = data[0].length, y0 = []; + while (++j < m) y0[j] = 0; + return y0; + } + function d3_layout_stackMaxIndex(array) { + var i = 1, j = 0, v = array[0][1], k, n = array.length; + for (;i < n; ++i) { + if ((k = array[i][1]) > v) { + j = i; + v = k; + } + } + return j; + } + function d3_layout_stackReduceSum(d) { + return d.reduce(d3_layout_stackSum, 0); + } + function d3_layout_stackSum(p, d) { + return p + d[1]; + } + d3.layout.histogram = function() { + var frequency = true, valuer = Number, ranger = d3_layout_histogramRange, binner = d3_layout_histogramBinSturges; + function histogram(data, i) { + var bins = [], values = data.map(valuer, this), range = ranger.call(this, values, i), thresholds = binner.call(this, range, values, i), bin, i = -1, n = values.length, m = thresholds.length - 1, k = frequency ? 1 : 1 / n, x; + while (++i < m) { + bin = bins[i] = []; + bin.dx = thresholds[i + 1] - (bin.x = thresholds[i]); + bin.y = 0; + } + if (m > 0) { + i = -1; + while (++i < n) { + x = values[i]; + if (x >= range[0] && x <= range[1]) { + bin = bins[d3.bisect(thresholds, x, 1, m) - 1]; + bin.y += k; + bin.push(data[i]); + } + } + } + return bins; + } + histogram.value = function(x) { + if (!arguments.length) return valuer; + valuer = x; + return histogram; + }; + histogram.range = function(x) { + if (!arguments.length) return ranger; + ranger = d3_functor(x); + return histogram; + }; + histogram.bins = function(x) { + if (!arguments.length) return binner; + binner = typeof x === "number" ? function(range) { + return d3_layout_histogramBinFixed(range, x); + } : d3_functor(x); + return histogram; + }; + histogram.frequency = function(x) { + if (!arguments.length) return frequency; + frequency = !!x; + return histogram; + }; + return histogram; + }; + function d3_layout_histogramBinSturges(range, values) { + return d3_layout_histogramBinFixed(range, Math.ceil(Math.log(values.length) / Math.LN2 + 1)); + } + function d3_layout_histogramBinFixed(range, n) { + var x = -1, b = +range[0], m = (range[1] - b) / n, f = []; + while (++x <= n) f[x] = m * x + b; + return f; + } + function d3_layout_histogramRange(values) { + return [ d3.min(values), d3.max(values) ]; + } + d3.layout.pack = function() { + var hierarchy = d3.layout.hierarchy().sort(d3_layout_packSort), padding = 0, size = [ 1, 1 ], radius; + function pack(d, i) { + var nodes = hierarchy.call(this, d, i), root = nodes[0], w = size[0], h = size[1], r = radius == null ? Math.sqrt : typeof radius === "function" ? radius : function() { + return radius; + }; + root.x = root.y = 0; + d3_layout_hierarchyVisitAfter(root, function(d) { + d.r = +r(d.value); + }); + d3_layout_hierarchyVisitAfter(root, d3_layout_packSiblings); + if (padding) { + var dr = padding * (radius ? 1 : Math.max(2 * root.r / w, 2 * root.r / h)) / 2; + d3_layout_hierarchyVisitAfter(root, function(d) { + d.r += dr; + }); + d3_layout_hierarchyVisitAfter(root, d3_layout_packSiblings); + d3_layout_hierarchyVisitAfter(root, function(d) { + d.r -= dr; + }); + } + d3_layout_packTransform(root, w / 2, h / 2, radius ? 1 : 1 / Math.max(2 * root.r / w, 2 * root.r / h)); + return nodes; + } + pack.size = function(_) { + if (!arguments.length) return size; + size = _; + return pack; + }; + pack.radius = function(_) { + if (!arguments.length) return radius; + radius = _ == null || typeof _ === "function" ? _ : +_; + return pack; + }; + pack.padding = function(_) { + if (!arguments.length) return padding; + padding = +_; + return pack; + }; + return d3_layout_hierarchyRebind(pack, hierarchy); + }; + function d3_layout_packSort(a, b) { + return a.value - b.value; + } + function d3_layout_packInsert(a, b) { + var c = a._pack_next; + a._pack_next = b; + b._pack_prev = a; + b._pack_next = c; + c._pack_prev = b; + } + function d3_layout_packSplice(a, b) { + a._pack_next = b; + b._pack_prev = a; + } + function d3_layout_packIntersects(a, b) { + var dx = b.x - a.x, dy = b.y - a.y, dr = a.r + b.r; + return .999 * dr * dr > dx * dx + dy * dy; + } + function d3_layout_packSiblings(node) { + if (!(nodes = node.children) || !(n = nodes.length)) return; + var nodes, xMin = Infinity, xMax = -Infinity, yMin = Infinity, yMax = -Infinity, a, b, c, i, j, k, n; + function bound(node) { + xMin = Math.min(node.x - node.r, xMin); + xMax = Math.max(node.x + node.r, xMax); + yMin = Math.min(node.y - node.r, yMin); + yMax = Math.max(node.y + node.r, yMax); + } + nodes.forEach(d3_layout_packLink); + a = nodes[0]; + a.x = -a.r; + a.y = 0; + bound(a); + if (n > 1) { + b = nodes[1]; + b.x = b.r; + b.y = 0; + bound(b); + if (n > 2) { + c = nodes[2]; + d3_layout_packPlace(a, b, c); + bound(c); + d3_layout_packInsert(a, c); + a._pack_prev = c; + d3_layout_packInsert(c, b); + b = a._pack_next; + for (i = 3; i < n; i++) { + d3_layout_packPlace(a, b, c = nodes[i]); + var isect = 0, s1 = 1, s2 = 1; + for (j = b._pack_next; j !== b; j = j._pack_next, s1++) { + if (d3_layout_packIntersects(j, c)) { + isect = 1; + break; + } + } + if (isect == 1) { + for (k = a._pack_prev; k !== j._pack_prev; k = k._pack_prev, s2++) { + if (d3_layout_packIntersects(k, c)) { + break; + } + } + } + if (isect) { + if (s1 < s2 || s1 == s2 && b.r < a.r) d3_layout_packSplice(a, b = j); else d3_layout_packSplice(a = k, b); + i--; + } else { + d3_layout_packInsert(a, c); + b = c; + bound(c); + } + } + } + } + var cx = (xMin + xMax) / 2, cy = (yMin + yMax) / 2, cr = 0; + for (i = 0; i < n; i++) { + c = nodes[i]; + c.x -= cx; + c.y -= cy; + cr = Math.max(cr, c.r + Math.sqrt(c.x * c.x + c.y * c.y)); + } + node.r = cr; + nodes.forEach(d3_layout_packUnlink); + } + function d3_layout_packLink(node) { + node._pack_next = node._pack_prev = node; + } + function d3_layout_packUnlink(node) { + delete node._pack_next; + delete node._pack_prev; + } + function d3_layout_packTransform(node, x, y, k) { + var children = node.children; + node.x = x += k * node.x; + node.y = y += k * node.y; + node.r *= k; + if (children) { + var i = -1, n = children.length; + while (++i < n) d3_layout_packTransform(children[i], x, y, k); + } + } + function d3_layout_packPlace(a, b, c) { + var db = a.r + c.r, dx = b.x - a.x, dy = b.y - a.y; + if (db && (dx || dy)) { + var da = b.r + c.r, dc = dx * dx + dy * dy; + da *= da; + db *= db; + var x = .5 + (db - da) / (2 * dc), y = Math.sqrt(Math.max(0, 2 * da * (db + dc) - (db -= dc) * db - da * da)) / (2 * dc); + c.x = a.x + x * dx + y * dy; + c.y = a.y + x * dy - y * dx; + } else { + c.x = a.x + db; + c.y = a.y; + } + } + d3.layout.tree = function() { + var hierarchy = d3.layout.hierarchy().sort(null).value(null), separation = d3_layout_treeSeparation, size = [ 1, 1 ], nodeSize = null; + function tree(d, i) { + var nodes = hierarchy.call(this, d, i), root0 = nodes[0], root1 = wrapTree(root0); + d3_layout_hierarchyVisitAfter(root1, firstWalk), root1.parent.m = -root1.z; + d3_layout_hierarchyVisitBefore(root1, secondWalk); + if (nodeSize) d3_layout_hierarchyVisitBefore(root0, sizeNode); else { + var left = root0, right = root0, bottom = root0; + d3_layout_hierarchyVisitBefore(root0, function(node) { + if (node.x < left.x) left = node; + if (node.x > right.x) right = node; + if (node.depth > bottom.depth) bottom = node; + }); + var tx = separation(left, right) / 2 - left.x, kx = size[0] / (right.x + separation(right, left) / 2 + tx), ky = size[1] / (bottom.depth || 1); + d3_layout_hierarchyVisitBefore(root0, function(node) { + node.x = (node.x + tx) * kx; + node.y = node.depth * ky; + }); + } + return nodes; + } + function wrapTree(root0) { + var root1 = { + A: null, + children: [ root0 ] + }, queue = [ root1 ], node1; + while ((node1 = queue.pop()) != null) { + for (var children = node1.children, child, i = 0, n = children.length; i < n; ++i) { + queue.push((children[i] = child = { + _: children[i], + parent: node1, + children: (child = children[i].children) && child.slice() || [], + A: null, + a: null, + z: 0, + m: 0, + c: 0, + s: 0, + t: null, + i: i + }).a = child); + } + } + return root1.children[0]; + } + function firstWalk(v) { + var children = v.children, siblings = v.parent.children, w = v.i ? siblings[v.i - 1] : null; + if (children.length) { + d3_layout_treeShift(v); + var midpoint = (children[0].z + children[children.length - 1].z) / 2; + if (w) { + v.z = w.z + separation(v._, w._); + v.m = v.z - midpoint; + } else { + v.z = midpoint; + } + } else if (w) { + v.z = w.z + separation(v._, w._); + } + v.parent.A = apportion(v, w, v.parent.A || siblings[0]); + } + function secondWalk(v) { + v._.x = v.z + v.parent.m; + v.m += v.parent.m; + } + function apportion(v, w, ancestor) { + if (w) { + var vip = v, vop = v, vim = w, vom = vip.parent.children[0], sip = vip.m, sop = vop.m, sim = vim.m, som = vom.m, shift; + while (vim = d3_layout_treeRight(vim), vip = d3_layout_treeLeft(vip), vim && vip) { + vom = d3_layout_treeLeft(vom); + vop = d3_layout_treeRight(vop); + vop.a = v; + shift = vim.z + sim - vip.z - sip + separation(vim._, vip._); + if (shift > 0) { + d3_layout_treeMove(d3_layout_treeAncestor(vim, v, ancestor), v, shift); + sip += shift; + sop += shift; + } + sim += vim.m; + sip += vip.m; + som += vom.m; + sop += vop.m; + } + if (vim && !d3_layout_treeRight(vop)) { + vop.t = vim; + vop.m += sim - sop; + } + if (vip && !d3_layout_treeLeft(vom)) { + vom.t = vip; + vom.m += sip - som; + ancestor = v; + } + } + return ancestor; + } + function sizeNode(node) { + node.x *= size[0]; + node.y = node.depth * size[1]; + } + tree.separation = function(x) { + if (!arguments.length) return separation; + separation = x; + return tree; + }; + tree.size = function(x) { + if (!arguments.length) return nodeSize ? null : size; + nodeSize = (size = x) == null ? sizeNode : null; + return tree; + }; + tree.nodeSize = function(x) { + if (!arguments.length) return nodeSize ? size : null; + nodeSize = (size = x) == null ? null : sizeNode; + return tree; + }; + return d3_layout_hierarchyRebind(tree, hierarchy); + }; + function d3_layout_treeSeparation(a, b) { + return a.parent == b.parent ? 1 : 2; + } + function d3_layout_treeLeft(v) { + var children = v.children; + return children.length ? children[0] : v.t; + } + function d3_layout_treeRight(v) { + var children = v.children, n; + return (n = children.length) ? children[n - 1] : v.t; + } + function d3_layout_treeMove(wm, wp, shift) { + var change = shift / (wp.i - wm.i); + wp.c -= change; + wp.s += shift; + wm.c += change; + wp.z += shift; + wp.m += shift; + } + function d3_layout_treeShift(v) { + var shift = 0, change = 0, children = v.children, i = children.length, w; + while (--i >= 0) { + w = children[i]; + w.z += shift; + w.m += shift; + shift += w.s + (change += w.c); + } + } + function d3_layout_treeAncestor(vim, v, ancestor) { + return vim.a.parent === v.parent ? vim.a : ancestor; + } + d3.layout.cluster = function() { + var hierarchy = d3.layout.hierarchy().sort(null).value(null), separation = d3_layout_treeSeparation, size = [ 1, 1 ], nodeSize = false; + function cluster(d, i) { + var nodes = hierarchy.call(this, d, i), root = nodes[0], previousNode, x = 0; + d3_layout_hierarchyVisitAfter(root, function(node) { + var children = node.children; + if (children && children.length) { + node.x = d3_layout_clusterX(children); + node.y = d3_layout_clusterY(children); + } else { + node.x = previousNode ? x += separation(node, previousNode) : 0; + node.y = 0; + previousNode = node; + } + }); + var left = d3_layout_clusterLeft(root), right = d3_layout_clusterRight(root), x0 = left.x - separation(left, right) / 2, x1 = right.x + separation(right, left) / 2; + d3_layout_hierarchyVisitAfter(root, nodeSize ? function(node) { + node.x = (node.x - root.x) * size[0]; + node.y = (root.y - node.y) * size[1]; + } : function(node) { + node.x = (node.x - x0) / (x1 - x0) * size[0]; + node.y = (1 - (root.y ? node.y / root.y : 1)) * size[1]; + }); + return nodes; + } + cluster.separation = function(x) { + if (!arguments.length) return separation; + separation = x; + return cluster; + }; + cluster.size = function(x) { + if (!arguments.length) return nodeSize ? null : size; + nodeSize = (size = x) == null; + return cluster; + }; + cluster.nodeSize = function(x) { + if (!arguments.length) return nodeSize ? size : null; + nodeSize = (size = x) != null; + return cluster; + }; + return d3_layout_hierarchyRebind(cluster, hierarchy); + }; + function d3_layout_clusterY(children) { + return 1 + d3.max(children, function(child) { + return child.y; + }); + } + function d3_layout_clusterX(children) { + return children.reduce(function(x, child) { + return x + child.x; + }, 0) / children.length; + } + function d3_layout_clusterLeft(node) { + var children = node.children; + return children && children.length ? d3_layout_clusterLeft(children[0]) : node; + } + function d3_layout_clusterRight(node) { + var children = node.children, n; + return children && (n = children.length) ? d3_layout_clusterRight(children[n - 1]) : node; + } + d3.layout.treemap = function() { + var hierarchy = d3.layout.hierarchy(), round = Math.round, size = [ 1, 1 ], padding = null, pad = d3_layout_treemapPadNull, sticky = false, stickies, mode = "squarify", ratio = .5 * (1 + Math.sqrt(5)); + function scale(children, k) { + var i = -1, n = children.length, child, area; + while (++i < n) { + area = (child = children[i]).value * (k < 0 ? 0 : k); + child.area = isNaN(area) || area <= 0 ? 0 : area; + } + } + function squarify(node) { + var children = node.children; + if (children && children.length) { + var rect = pad(node), row = [], remaining = children.slice(), child, best = Infinity, score, u = mode === "slice" ? rect.dx : mode === "dice" ? rect.dy : mode === "slice-dice" ? node.depth & 1 ? rect.dy : rect.dx : Math.min(rect.dx, rect.dy), n; + scale(remaining, rect.dx * rect.dy / node.value); + row.area = 0; + while ((n = remaining.length) > 0) { + row.push(child = remaining[n - 1]); + row.area += child.area; + if (mode !== "squarify" || (score = worst(row, u)) <= best) { + remaining.pop(); + best = score; + } else { + row.area -= row.pop().area; + position(row, u, rect, false); + u = Math.min(rect.dx, rect.dy); + row.length = row.area = 0; + best = Infinity; + } + } + if (row.length) { + position(row, u, rect, true); + row.length = row.area = 0; + } + children.forEach(squarify); + } + } + function stickify(node) { + var children = node.children; + if (children && children.length) { + var rect = pad(node), remaining = children.slice(), child, row = []; + scale(remaining, rect.dx * rect.dy / node.value); + row.area = 0; + while (child = remaining.pop()) { + row.push(child); + row.area += child.area; + if (child.z != null) { + position(row, child.z ? rect.dx : rect.dy, rect, !remaining.length); + row.length = row.area = 0; + } + } + children.forEach(stickify); + } + } + function worst(row, u) { + var s = row.area, r, rmax = 0, rmin = Infinity, i = -1, n = row.length; + while (++i < n) { + if (!(r = row[i].area)) continue; + if (r < rmin) rmin = r; + if (r > rmax) rmax = r; + } + s *= s; + u *= u; + return s ? Math.max(u * rmax * ratio / s, s / (u * rmin * ratio)) : Infinity; + } + function position(row, u, rect, flush) { + var i = -1, n = row.length, x = rect.x, y = rect.y, v = u ? round(row.area / u) : 0, o; + if (u == rect.dx) { + if (flush || v > rect.dy) v = rect.dy; + while (++i < n) { + o = row[i]; + o.x = x; + o.y = y; + o.dy = v; + x += o.dx = Math.min(rect.x + rect.dx - x, v ? round(o.area / v) : 0); + } + o.z = true; + o.dx += rect.x + rect.dx - x; + rect.y += v; + rect.dy -= v; + } else { + if (flush || v > rect.dx) v = rect.dx; + while (++i < n) { + o = row[i]; + o.x = x; + o.y = y; + o.dx = v; + y += o.dy = Math.min(rect.y + rect.dy - y, v ? round(o.area / v) : 0); + } + o.z = false; + o.dy += rect.y + rect.dy - y; + rect.x += v; + rect.dx -= v; + } + } + function treemap(d) { + var nodes = stickies || hierarchy(d), root = nodes[0]; + root.x = 0; + root.y = 0; + root.dx = size[0]; + root.dy = size[1]; + if (stickies) hierarchy.revalue(root); + scale([ root ], root.dx * root.dy / root.value); + (stickies ? stickify : squarify)(root); + if (sticky) stickies = nodes; + return nodes; + } + treemap.size = function(x) { + if (!arguments.length) return size; + size = x; + return treemap; + }; + treemap.padding = function(x) { + if (!arguments.length) return padding; + function padFunction(node) { + var p = x.call(treemap, node, node.depth); + return p == null ? d3_layout_treemapPadNull(node) : d3_layout_treemapPad(node, typeof p === "number" ? [ p, p, p, p ] : p); + } + function padConstant(node) { + return d3_layout_treemapPad(node, x); + } + var type; + pad = (padding = x) == null ? d3_layout_treemapPadNull : (type = typeof x) === "function" ? padFunction : type === "number" ? (x = [ x, x, x, x ], + padConstant) : padConstant; + return treemap; + }; + treemap.round = function(x) { + if (!arguments.length) return round != Number; + round = x ? Math.round : Number; + return treemap; + }; + treemap.sticky = function(x) { + if (!arguments.length) return sticky; + sticky = x; + stickies = null; + return treemap; + }; + treemap.ratio = function(x) { + if (!arguments.length) return ratio; + ratio = x; + return treemap; + }; + treemap.mode = function(x) { + if (!arguments.length) return mode; + mode = x + ""; + return treemap; + }; + return d3_layout_hierarchyRebind(treemap, hierarchy); + }; + function d3_layout_treemapPadNull(node) { + return { + x: node.x, + y: node.y, + dx: node.dx, + dy: node.dy + }; + } + function d3_layout_treemapPad(node, padding) { + var x = node.x + padding[3], y = node.y + padding[0], dx = node.dx - padding[1] - padding[3], dy = node.dy - padding[0] - padding[2]; + if (dx < 0) { + x += dx / 2; + dx = 0; + } + if (dy < 0) { + y += dy / 2; + dy = 0; + } + return { + x: x, + y: y, + dx: dx, + dy: dy + }; + } + d3.random = { + normal: function(µ, σ) { + var n = arguments.length; + if (n < 2) σ = 1; + if (n < 1) µ = 0; + return function() { + var x, y, r; + do { + x = Math.random() * 2 - 1; + y = Math.random() * 2 - 1; + r = x * x + y * y; + } while (!r || r > 1); + return µ + σ * x * Math.sqrt(-2 * Math.log(r) / r); + }; + }, + logNormal: function() { + var random = d3.random.normal.apply(d3, arguments); + return function() { + return Math.exp(random()); + }; + }, + bates: function(m) { + var random = d3.random.irwinHall(m); + return function() { + return random() / m; + }; + }, + irwinHall: function(m) { + return function() { + for (var s = 0, j = 0; j < m; j++) s += Math.random(); + return s; + }; + } + }; + d3.scale = {}; + function d3_scaleExtent(domain) { + var start = domain[0], stop = domain[domain.length - 1]; + return start < stop ? [ start, stop ] : [ stop, start ]; + } + function d3_scaleRange(scale) { + return scale.rangeExtent ? scale.rangeExtent() : d3_scaleExtent(scale.range()); + } + function d3_scale_bilinear(domain, range, uninterpolate, interpolate) { + var u = uninterpolate(domain[0], domain[1]), i = interpolate(range[0], range[1]); + return function(x) { + return i(u(x)); + }; + } + function d3_scale_nice(domain, nice) { + var i0 = 0, i1 = domain.length - 1, x0 = domain[i0], x1 = domain[i1], dx; + if (x1 < x0) { + dx = i0, i0 = i1, i1 = dx; + dx = x0, x0 = x1, x1 = dx; + } + domain[i0] = nice.floor(x0); + domain[i1] = nice.ceil(x1); + return domain; + } + function d3_scale_niceStep(step) { + return step ? { + floor: function(x) { + return Math.floor(x / step) * step; + }, + ceil: function(x) { + return Math.ceil(x / step) * step; + } + } : d3_scale_niceIdentity; + } + var d3_scale_niceIdentity = { + floor: d3_identity, + ceil: d3_identity + }; + function d3_scale_polylinear(domain, range, uninterpolate, interpolate) { + var u = [], i = [], j = 0, k = Math.min(domain.length, range.length) - 1; + if (domain[k] < domain[0]) { + domain = domain.slice().reverse(); + range = range.slice().reverse(); + } + while (++j <= k) { + u.push(uninterpolate(domain[j - 1], domain[j])); + i.push(interpolate(range[j - 1], range[j])); + } + return function(x) { + var j = d3.bisect(domain, x, 1, k) - 1; + return i[j](u[j](x)); + }; + } + d3.scale.linear = function() { + return d3_scale_linear([ 0, 1 ], [ 0, 1 ], d3_interpolate, false); + }; + function d3_scale_linear(domain, range, interpolate, clamp) { + var output, input; + function rescale() { + var linear = Math.min(domain.length, range.length) > 2 ? d3_scale_polylinear : d3_scale_bilinear, uninterpolate = clamp ? d3_uninterpolateClamp : d3_uninterpolateNumber; + output = linear(domain, range, uninterpolate, interpolate); + input = linear(range, domain, uninterpolate, d3_interpolate); + return scale; + } + function scale(x) { + return output(x); + } + scale.invert = function(y) { + return input(y); + }; + scale.domain = function(x) { + if (!arguments.length) return domain; + domain = x.map(Number); + return rescale(); + }; + scale.range = function(x) { + if (!arguments.length) return range; + range = x; + return rescale(); + }; + scale.rangeRound = function(x) { + return scale.range(x).interpolate(d3_interpolateRound); + }; + scale.clamp = function(x) { + if (!arguments.length) return clamp; + clamp = x; + return rescale(); + }; + scale.interpolate = function(x) { + if (!arguments.length) return interpolate; + interpolate = x; + return rescale(); + }; + scale.ticks = function(m) { + return d3_scale_linearTicks(domain, m); + }; + scale.tickFormat = function(m, format) { + return d3_scale_linearTickFormat(domain, m, format); + }; + scale.nice = function(m) { + d3_scale_linearNice(domain, m); + return rescale(); + }; + scale.copy = function() { + return d3_scale_linear(domain, range, interpolate, clamp); + }; + return rescale(); + } + function d3_scale_linearRebind(scale, linear) { + return d3.rebind(scale, linear, "range", "rangeRound", "interpolate", "clamp"); + } + function d3_scale_linearNice(domain, m) { + return d3_scale_nice(domain, d3_scale_niceStep(d3_scale_linearTickRange(domain, m)[2])); + } + function d3_scale_linearTickRange(domain, m) { + if (m == null) m = 10; + var extent = d3_scaleExtent(domain), span = extent[1] - extent[0], step = Math.pow(10, Math.floor(Math.log(span / m) / Math.LN10)), err = m / span * step; + if (err <= .15) step *= 10; else if (err <= .35) step *= 5; else if (err <= .75) step *= 2; + extent[0] = Math.ceil(extent[0] / step) * step; + extent[1] = Math.floor(extent[1] / step) * step + step * .5; + extent[2] = step; + return extent; + } + function d3_scale_linearTicks(domain, m) { + return d3.range.apply(d3, d3_scale_linearTickRange(domain, m)); + } + function d3_scale_linearTickFormat(domain, m, format) { + var range = d3_scale_linearTickRange(domain, m); + if (format) { + var match = d3_format_re.exec(format); + match.shift(); + if (match[8] === "s") { + var prefix = d3.formatPrefix(Math.max(abs(range[0]), abs(range[1]))); + if (!match[7]) match[7] = "." + d3_scale_linearPrecision(prefix.scale(range[2])); + match[8] = "f"; + format = d3.format(match.join("")); + return function(d) { + return format(prefix.scale(d)) + prefix.symbol; + }; + } + if (!match[7]) match[7] = "." + d3_scale_linearFormatPrecision(match[8], range); + format = match.join(""); + } else { + format = ",." + d3_scale_linearPrecision(range[2]) + "f"; + } + return d3.format(format); + } + var d3_scale_linearFormatSignificant = { + s: 1, + g: 1, + p: 1, + r: 1, + e: 1 + }; + function d3_scale_linearPrecision(value) { + return -Math.floor(Math.log(value) / Math.LN10 + .01); + } + function d3_scale_linearFormatPrecision(type, range) { + var p = d3_scale_linearPrecision(range[2]); + return type in d3_scale_linearFormatSignificant ? Math.abs(p - d3_scale_linearPrecision(Math.max(abs(range[0]), abs(range[1])))) + +(type !== "e") : p - (type === "%") * 2; + } + d3.scale.log = function() { + return d3_scale_log(d3.scale.linear().domain([ 0, 1 ]), 10, true, [ 1, 10 ]); + }; + function d3_scale_log(linear, base, positive, domain) { + function log(x) { + return (positive ? Math.log(x < 0 ? 0 : x) : -Math.log(x > 0 ? 0 : -x)) / Math.log(base); + } + function pow(x) { + return positive ? Math.pow(base, x) : -Math.pow(base, -x); + } + function scale(x) { + return linear(log(x)); + } + scale.invert = function(x) { + return pow(linear.invert(x)); + }; + scale.domain = function(x) { + if (!arguments.length) return domain; + positive = x[0] >= 0; + linear.domain((domain = x.map(Number)).map(log)); + return scale; + }; + scale.base = function(_) { + if (!arguments.length) return base; + base = +_; + linear.domain(domain.map(log)); + return scale; + }; + scale.nice = function() { + var niced = d3_scale_nice(domain.map(log), positive ? Math : d3_scale_logNiceNegative); + linear.domain(niced); + domain = niced.map(pow); + return scale; + }; + scale.ticks = function() { + var extent = d3_scaleExtent(domain), ticks = [], u = extent[0], v = extent[1], i = Math.floor(log(u)), j = Math.ceil(log(v)), n = base % 1 ? 2 : base; + if (isFinite(j - i)) { + if (positive) { + for (;i < j; i++) for (var k = 1; k < n; k++) ticks.push(pow(i) * k); + ticks.push(pow(i)); + } else { + ticks.push(pow(i)); + for (;i++ < j; ) for (var k = n - 1; k > 0; k--) ticks.push(pow(i) * k); + } + for (i = 0; ticks[i] < u; i++) {} + for (j = ticks.length; ticks[j - 1] > v; j--) {} + ticks = ticks.slice(i, j); + } + return ticks; + }; + scale.tickFormat = function(n, format) { + if (!arguments.length) return d3_scale_logFormat; + if (arguments.length < 2) format = d3_scale_logFormat; else if (typeof format !== "function") format = d3.format(format); + var k = Math.max(.1, n / scale.ticks().length), f = positive ? (e = 1e-12, Math.ceil) : (e = -1e-12, + Math.floor), e; + return function(d) { + return d / pow(f(log(d) + e)) <= k ? format(d) : ""; + }; + }; + scale.copy = function() { + return d3_scale_log(linear.copy(), base, positive, domain); + }; + return d3_scale_linearRebind(scale, linear); + } + var d3_scale_logFormat = d3.format(".0e"), d3_scale_logNiceNegative = { + floor: function(x) { + return -Math.ceil(-x); + }, + ceil: function(x) { + return -Math.floor(-x); + } + }; + d3.scale.pow = function() { + return d3_scale_pow(d3.scale.linear(), 1, [ 0, 1 ]); + }; + function d3_scale_pow(linear, exponent, domain) { + var powp = d3_scale_powPow(exponent), powb = d3_scale_powPow(1 / exponent); + function scale(x) { + return linear(powp(x)); + } + scale.invert = function(x) { + return powb(linear.invert(x)); + }; + scale.domain = function(x) { + if (!arguments.length) return domain; + linear.domain((domain = x.map(Number)).map(powp)); + return scale; + }; + scale.ticks = function(m) { + return d3_scale_linearTicks(domain, m); + }; + scale.tickFormat = function(m, format) { + return d3_scale_linearTickFormat(domain, m, format); + }; + scale.nice = function(m) { + return scale.domain(d3_scale_linearNice(domain, m)); + }; + scale.exponent = function(x) { + if (!arguments.length) return exponent; + powp = d3_scale_powPow(exponent = x); + powb = d3_scale_powPow(1 / exponent); + linear.domain(domain.map(powp)); + return scale; + }; + scale.copy = function() { + return d3_scale_pow(linear.copy(), exponent, domain); + }; + return d3_scale_linearRebind(scale, linear); + } + function d3_scale_powPow(e) { + return function(x) { + return x < 0 ? -Math.pow(-x, e) : Math.pow(x, e); + }; + } + d3.scale.sqrt = function() { + return d3.scale.pow().exponent(.5); + }; + d3.scale.ordinal = function() { + return d3_scale_ordinal([], { + t: "range", + a: [ [] ] + }); + }; + function d3_scale_ordinal(domain, ranger) { + var index, range, rangeBand; + function scale(x) { + return range[((index.get(x) || (ranger.t === "range" ? index.set(x, domain.push(x)) : NaN)) - 1) % range.length]; + } + function steps(start, step) { + return d3.range(domain.length).map(function(i) { + return start + step * i; + }); + } + scale.domain = function(x) { + if (!arguments.length) return domain; + domain = []; + index = new d3_Map(); + var i = -1, n = x.length, xi; + while (++i < n) if (!index.has(xi = x[i])) index.set(xi, domain.push(xi)); + return scale[ranger.t].apply(scale, ranger.a); + }; + scale.range = function(x) { + if (!arguments.length) return range; + range = x; + rangeBand = 0; + ranger = { + t: "range", + a: arguments + }; + return scale; + }; + scale.rangePoints = function(x, padding) { + if (arguments.length < 2) padding = 0; + var start = x[0], stop = x[1], step = domain.length < 2 ? (start = (start + stop) / 2, + 0) : (stop - start) / (domain.length - 1 + padding); + range = steps(start + step * padding / 2, step); + rangeBand = 0; + ranger = { + t: "rangePoints", + a: arguments + }; + return scale; + }; + scale.rangeRoundPoints = function(x, padding) { + if (arguments.length < 2) padding = 0; + var start = x[0], stop = x[1], step = domain.length < 2 ? (start = stop = Math.round((start + stop) / 2), + 0) : (stop - start) / (domain.length - 1 + padding) | 0; + range = steps(start + Math.round(step * padding / 2 + (stop - start - (domain.length - 1 + padding) * step) / 2), step); + rangeBand = 0; + ranger = { + t: "rangeRoundPoints", + a: arguments + }; + return scale; + }; + scale.rangeBands = function(x, padding, outerPadding) { + if (arguments.length < 2) padding = 0; + if (arguments.length < 3) outerPadding = padding; + var reverse = x[1] < x[0], start = x[reverse - 0], stop = x[1 - reverse], step = (stop - start) / (domain.length - padding + 2 * outerPadding); + range = steps(start + step * outerPadding, step); + if (reverse) range.reverse(); + rangeBand = step * (1 - padding); + ranger = { + t: "rangeBands", + a: arguments + }; + return scale; + }; + scale.rangeRoundBands = function(x, padding, outerPadding) { + if (arguments.length < 2) padding = 0; + if (arguments.length < 3) outerPadding = padding; + var reverse = x[1] < x[0], start = x[reverse - 0], stop = x[1 - reverse], step = Math.floor((stop - start) / (domain.length - padding + 2 * outerPadding)); + range = steps(start + Math.round((stop - start - (domain.length - padding) * step) / 2), step); + if (reverse) range.reverse(); + rangeBand = Math.round(step * (1 - padding)); + ranger = { + t: "rangeRoundBands", + a: arguments + }; + return scale; + }; + scale.rangeBand = function() { + return rangeBand; + }; + scale.rangeExtent = function() { + return d3_scaleExtent(ranger.a[0]); + }; + scale.copy = function() { + return d3_scale_ordinal(domain, ranger); + }; + return scale.domain(domain); + } + d3.scale.category10 = function() { + return d3.scale.ordinal().range(d3_category10); + }; + d3.scale.category20 = function() { + return d3.scale.ordinal().range(d3_category20); + }; + d3.scale.category20b = function() { + return d3.scale.ordinal().range(d3_category20b); + }; + d3.scale.category20c = function() { + return d3.scale.ordinal().range(d3_category20c); + }; + var d3_category10 = [ 2062260, 16744206, 2924588, 14034728, 9725885, 9197131, 14907330, 8355711, 12369186, 1556175 ].map(d3_rgbString); + var d3_category20 = [ 2062260, 11454440, 16744206, 16759672, 2924588, 10018698, 14034728, 16750742, 9725885, 12955861, 9197131, 12885140, 14907330, 16234194, 8355711, 13092807, 12369186, 14408589, 1556175, 10410725 ].map(d3_rgbString); + var d3_category20b = [ 3750777, 5395619, 7040719, 10264286, 6519097, 9216594, 11915115, 13556636, 9202993, 12426809, 15186514, 15190932, 8666169, 11356490, 14049643, 15177372, 8077683, 10834324, 13528509, 14589654 ].map(d3_rgbString); + var d3_category20c = [ 3244733, 7057110, 10406625, 13032431, 15095053, 16616764, 16625259, 16634018, 3253076, 7652470, 10607003, 13101504, 7695281, 10394312, 12369372, 14342891, 6513507, 9868950, 12434877, 14277081 ].map(d3_rgbString); + d3.scale.quantile = function() { + return d3_scale_quantile([], []); + }; + function d3_scale_quantile(domain, range) { + var thresholds; + function rescale() { + var k = 0, q = range.length; + thresholds = []; + while (++k < q) thresholds[k - 1] = d3.quantile(domain, k / q); + return scale; + } + function scale(x) { + if (!isNaN(x = +x)) return range[d3.bisect(thresholds, x)]; + } + scale.domain = function(x) { + if (!arguments.length) return domain; + domain = x.map(d3_number).filter(d3_numeric).sort(d3_ascending); + return rescale(); + }; + scale.range = function(x) { + if (!arguments.length) return range; + range = x; + return rescale(); + }; + scale.quantiles = function() { + return thresholds; + }; + scale.invertExtent = function(y) { + y = range.indexOf(y); + return y < 0 ? [ NaN, NaN ] : [ y > 0 ? thresholds[y - 1] : domain[0], y < thresholds.length ? thresholds[y] : domain[domain.length - 1] ]; + }; + scale.copy = function() { + return d3_scale_quantile(domain, range); + }; + return rescale(); + } + d3.scale.quantize = function() { + return d3_scale_quantize(0, 1, [ 0, 1 ]); + }; + function d3_scale_quantize(x0, x1, range) { + var kx, i; + function scale(x) { + return range[Math.max(0, Math.min(i, Math.floor(kx * (x - x0))))]; + } + function rescale() { + kx = range.length / (x1 - x0); + i = range.length - 1; + return scale; + } + scale.domain = function(x) { + if (!arguments.length) return [ x0, x1 ]; + x0 = +x[0]; + x1 = +x[x.length - 1]; + return rescale(); + }; + scale.range = function(x) { + if (!arguments.length) return range; + range = x; + return rescale(); + }; + scale.invertExtent = function(y) { + y = range.indexOf(y); + y = y < 0 ? NaN : y / kx + x0; + return [ y, y + 1 / kx ]; + }; + scale.copy = function() { + return d3_scale_quantize(x0, x1, range); + }; + return rescale(); + } + d3.scale.threshold = function() { + return d3_scale_threshold([ .5 ], [ 0, 1 ]); + }; + function d3_scale_threshold(domain, range) { + function scale(x) { + if (x <= x) return range[d3.bisect(domain, x)]; + } + scale.domain = function(_) { + if (!arguments.length) return domain; + domain = _; + return scale; + }; + scale.range = function(_) { + if (!arguments.length) return range; + range = _; + return scale; + }; + scale.invertExtent = function(y) { + y = range.indexOf(y); + return [ domain[y - 1], domain[y] ]; + }; + scale.copy = function() { + return d3_scale_threshold(domain, range); + }; + return scale; + } + d3.scale.identity = function() { + return d3_scale_identity([ 0, 1 ]); + }; + function d3_scale_identity(domain) { + function identity(x) { + return +x; + } + identity.invert = identity; + identity.domain = identity.range = function(x) { + if (!arguments.length) return domain; + domain = x.map(identity); + return identity; + }; + identity.ticks = function(m) { + return d3_scale_linearTicks(domain, m); + }; + identity.tickFormat = function(m, format) { + return d3_scale_linearTickFormat(domain, m, format); + }; + identity.copy = function() { + return d3_scale_identity(domain); + }; + return identity; + } + d3.svg = {}; + function d3_zero() { + return 0; + } + d3.svg.arc = function() { + var innerRadius = d3_svg_arcInnerRadius, outerRadius = d3_svg_arcOuterRadius, cornerRadius = d3_zero, padRadius = d3_svg_arcAuto, startAngle = d3_svg_arcStartAngle, endAngle = d3_svg_arcEndAngle, padAngle = d3_svg_arcPadAngle; + function arc() { + var r0 = Math.max(0, +innerRadius.apply(this, arguments)), r1 = Math.max(0, +outerRadius.apply(this, arguments)), a0 = startAngle.apply(this, arguments) - halfπ, a1 = endAngle.apply(this, arguments) - halfπ, da = Math.abs(a1 - a0), cw = a0 > a1 ? 0 : 1; + if (r1 < r0) rc = r1, r1 = r0, r0 = rc; + if (da >= τε) return circleSegment(r1, cw) + (r0 ? circleSegment(r0, 1 - cw) : "") + "Z"; + var rc, cr, rp, ap, p0 = 0, p1 = 0, x0, y0, x1, y1, x2, y2, x3, y3, path = []; + if (ap = (+padAngle.apply(this, arguments) || 0) / 2) { + rp = padRadius === d3_svg_arcAuto ? Math.sqrt(r0 * r0 + r1 * r1) : +padRadius.apply(this, arguments); + if (!cw) p1 *= -1; + if (r1) p1 = d3_asin(rp / r1 * Math.sin(ap)); + if (r0) p0 = d3_asin(rp / r0 * Math.sin(ap)); + } + if (r1) { + x0 = r1 * Math.cos(a0 + p1); + y0 = r1 * Math.sin(a0 + p1); + x1 = r1 * Math.cos(a1 - p1); + y1 = r1 * Math.sin(a1 - p1); + var l1 = Math.abs(a1 - a0 - 2 * p1) <= π ? 0 : 1; + if (p1 && d3_svg_arcSweep(x0, y0, x1, y1) === cw ^ l1) { + var h1 = (a0 + a1) / 2; + x0 = r1 * Math.cos(h1); + y0 = r1 * Math.sin(h1); + x1 = y1 = null; + } + } else { + x0 = y0 = 0; + } + if (r0) { + x2 = r0 * Math.cos(a1 - p0); + y2 = r0 * Math.sin(a1 - p0); + x3 = r0 * Math.cos(a0 + p0); + y3 = r0 * Math.sin(a0 + p0); + var l0 = Math.abs(a0 - a1 + 2 * p0) <= π ? 0 : 1; + if (p0 && d3_svg_arcSweep(x2, y2, x3, y3) === 1 - cw ^ l0) { + var h0 = (a0 + a1) / 2; + x2 = r0 * Math.cos(h0); + y2 = r0 * Math.sin(h0); + x3 = y3 = null; + } + } else { + x2 = y2 = 0; + } + if ((rc = Math.min(Math.abs(r1 - r0) / 2, +cornerRadius.apply(this, arguments))) > .001) { + cr = r0 < r1 ^ cw ? 0 : 1; + var oc = x3 == null ? [ x2, y2 ] : x1 == null ? [ x0, y0 ] : d3_geom_polygonIntersect([ x0, y0 ], [ x3, y3 ], [ x1, y1 ], [ x2, y2 ]), ax = x0 - oc[0], ay = y0 - oc[1], bx = x1 - oc[0], by = y1 - oc[1], kc = 1 / Math.sin(Math.acos((ax * bx + ay * by) / (Math.sqrt(ax * ax + ay * ay) * Math.sqrt(bx * bx + by * by))) / 2), lc = Math.sqrt(oc[0] * oc[0] + oc[1] * oc[1]); + if (x1 != null) { + var rc1 = Math.min(rc, (r1 - lc) / (kc + 1)), t30 = d3_svg_arcCornerTangents(x3 == null ? [ x2, y2 ] : [ x3, y3 ], [ x0, y0 ], r1, rc1, cw), t12 = d3_svg_arcCornerTangents([ x1, y1 ], [ x2, y2 ], r1, rc1, cw); + if (rc === rc1) { + path.push("M", t30[0], "A", rc1, ",", rc1, " 0 0,", cr, " ", t30[1], "A", r1, ",", r1, " 0 ", 1 - cw ^ d3_svg_arcSweep(t30[1][0], t30[1][1], t12[1][0], t12[1][1]), ",", cw, " ", t12[1], "A", rc1, ",", rc1, " 0 0,", cr, " ", t12[0]); + } else { + path.push("M", t30[0], "A", rc1, ",", rc1, " 0 1,", cr, " ", t12[0]); + } + } else { + path.push("M", x0, ",", y0); + } + if (x3 != null) { + var rc0 = Math.min(rc, (r0 - lc) / (kc - 1)), t03 = d3_svg_arcCornerTangents([ x0, y0 ], [ x3, y3 ], r0, -rc0, cw), t21 = d3_svg_arcCornerTangents([ x2, y2 ], x1 == null ? [ x0, y0 ] : [ x1, y1 ], r0, -rc0, cw); + if (rc === rc0) { + path.push("L", t21[0], "A", rc0, ",", rc0, " 0 0,", cr, " ", t21[1], "A", r0, ",", r0, " 0 ", cw ^ d3_svg_arcSweep(t21[1][0], t21[1][1], t03[1][0], t03[1][1]), ",", 1 - cw, " ", t03[1], "A", rc0, ",", rc0, " 0 0,", cr, " ", t03[0]); + } else { + path.push("L", t21[0], "A", rc0, ",", rc0, " 0 0,", cr, " ", t03[0]); + } + } else { + path.push("L", x2, ",", y2); + } + } else { + path.push("M", x0, ",", y0); + if (x1 != null) path.push("A", r1, ",", r1, " 0 ", l1, ",", cw, " ", x1, ",", y1); + path.push("L", x2, ",", y2); + if (x3 != null) path.push("A", r0, ",", r0, " 0 ", l0, ",", 1 - cw, " ", x3, ",", y3); + } + path.push("Z"); + return path.join(""); + } + function circleSegment(r1, cw) { + return "M0," + r1 + "A" + r1 + "," + r1 + " 0 1," + cw + " 0," + -r1 + "A" + r1 + "," + r1 + " 0 1," + cw + " 0," + r1; + } + arc.innerRadius = function(v) { + if (!arguments.length) return innerRadius; + innerRadius = d3_functor(v); + return arc; + }; + arc.outerRadius = function(v) { + if (!arguments.length) return outerRadius; + outerRadius = d3_functor(v); + return arc; + }; + arc.cornerRadius = function(v) { + if (!arguments.length) return cornerRadius; + cornerRadius = d3_functor(v); + return arc; + }; + arc.padRadius = function(v) { + if (!arguments.length) return padRadius; + padRadius = v == d3_svg_arcAuto ? d3_svg_arcAuto : d3_functor(v); + return arc; + }; + arc.startAngle = function(v) { + if (!arguments.length) return startAngle; + startAngle = d3_functor(v); + return arc; + }; + arc.endAngle = function(v) { + if (!arguments.length) return endAngle; + endAngle = d3_functor(v); + return arc; + }; + arc.padAngle = function(v) { + if (!arguments.length) return padAngle; + padAngle = d3_functor(v); + return arc; + }; + arc.centroid = function() { + var r = (+innerRadius.apply(this, arguments) + +outerRadius.apply(this, arguments)) / 2, a = (+startAngle.apply(this, arguments) + +endAngle.apply(this, arguments)) / 2 - halfπ; + return [ Math.cos(a) * r, Math.sin(a) * r ]; + }; + return arc; + }; + var d3_svg_arcAuto = "auto"; + function d3_svg_arcInnerRadius(d) { + return d.innerRadius; + } + function d3_svg_arcOuterRadius(d) { + return d.outerRadius; + } + function d3_svg_arcStartAngle(d) { + return d.startAngle; + } + function d3_svg_arcEndAngle(d) { + return d.endAngle; + } + function d3_svg_arcPadAngle(d) { + return d && d.padAngle; + } + function d3_svg_arcSweep(x0, y0, x1, y1) { + return (x0 - x1) * y0 - (y0 - y1) * x0 > 0 ? 0 : 1; + } + function d3_svg_arcCornerTangents(p0, p1, r1, rc, cw) { + var x01 = p0[0] - p1[0], y01 = p0[1] - p1[1], lo = (cw ? rc : -rc) / Math.sqrt(x01 * x01 + y01 * y01), ox = lo * y01, oy = -lo * x01, x1 = p0[0] + ox, y1 = p0[1] + oy, x2 = p1[0] + ox, y2 = p1[1] + oy, x3 = (x1 + x2) / 2, y3 = (y1 + y2) / 2, dx = x2 - x1, dy = y2 - y1, d2 = dx * dx + dy * dy, r = r1 - rc, D = x1 * y2 - x2 * y1, d = (dy < 0 ? -1 : 1) * Math.sqrt(r * r * d2 - D * D), cx0 = (D * dy - dx * d) / d2, cy0 = (-D * dx - dy * d) / d2, cx1 = (D * dy + dx * d) / d2, cy1 = (-D * dx + dy * d) / d2, dx0 = cx0 - x3, dy0 = cy0 - y3, dx1 = cx1 - x3, dy1 = cy1 - y3; + if (dx0 * dx0 + dy0 * dy0 > dx1 * dx1 + dy1 * dy1) cx0 = cx1, cy0 = cy1; + return [ [ cx0 - ox, cy0 - oy ], [ cx0 * r1 / r, cy0 * r1 / r ] ]; + } + function d3_svg_line(projection) { + var x = d3_geom_pointX, y = d3_geom_pointY, defined = d3_true, interpolate = d3_svg_lineLinear, interpolateKey = interpolate.key, tension = .7; + function line(data) { + var segments = [], points = [], i = -1, n = data.length, d, fx = d3_functor(x), fy = d3_functor(y); + function segment() { + segments.push("M", interpolate(projection(points), tension)); + } + while (++i < n) { + if (defined.call(this, d = data[i], i)) { + points.push([ +fx.call(this, d, i), +fy.call(this, d, i) ]); + } else if (points.length) { + segment(); + points = []; + } + } + if (points.length) segment(); + return segments.length ? segments.join("") : null; + } + line.x = function(_) { + if (!arguments.length) return x; + x = _; + return line; + }; + line.y = function(_) { + if (!arguments.length) return y; + y = _; + return line; + }; + line.defined = function(_) { + if (!arguments.length) return defined; + defined = _; + return line; + }; + line.interpolate = function(_) { + if (!arguments.length) return interpolateKey; + if (typeof _ === "function") interpolateKey = interpolate = _; else interpolateKey = (interpolate = d3_svg_lineInterpolators.get(_) || d3_svg_lineLinear).key; + return line; + }; + line.tension = function(_) { + if (!arguments.length) return tension; + tension = _; + return line; + }; + return line; + } + d3.svg.line = function() { + return d3_svg_line(d3_identity); + }; + var d3_svg_lineInterpolators = d3.map({ + linear: d3_svg_lineLinear, + "linear-closed": d3_svg_lineLinearClosed, + step: d3_svg_lineStep, + "step-before": d3_svg_lineStepBefore, + "step-after": d3_svg_lineStepAfter, + basis: d3_svg_lineBasis, + "basis-open": d3_svg_lineBasisOpen, + "basis-closed": d3_svg_lineBasisClosed, + bundle: d3_svg_lineBundle, + cardinal: d3_svg_lineCardinal, + "cardinal-open": d3_svg_lineCardinalOpen, + "cardinal-closed": d3_svg_lineCardinalClosed, + monotone: d3_svg_lineMonotone + }); + d3_svg_lineInterpolators.forEach(function(key, value) { + value.key = key; + value.closed = /-closed$/.test(key); + }); + function d3_svg_lineLinear(points) { + return points.join("L"); + } + function d3_svg_lineLinearClosed(points) { + return d3_svg_lineLinear(points) + "Z"; + } + function d3_svg_lineStep(points) { + var i = 0, n = points.length, p = points[0], path = [ p[0], ",", p[1] ]; + while (++i < n) path.push("H", (p[0] + (p = points[i])[0]) / 2, "V", p[1]); + if (n > 1) path.push("H", p[0]); + return path.join(""); + } + function d3_svg_lineStepBefore(points) { + var i = 0, n = points.length, p = points[0], path = [ p[0], ",", p[1] ]; + while (++i < n) path.push("V", (p = points[i])[1], "H", p[0]); + return path.join(""); + } + function d3_svg_lineStepAfter(points) { + var i = 0, n = points.length, p = points[0], path = [ p[0], ",", p[1] ]; + while (++i < n) path.push("H", (p = points[i])[0], "V", p[1]); + return path.join(""); + } + function d3_svg_lineCardinalOpen(points, tension) { + return points.length < 4 ? d3_svg_lineLinear(points) : points[1] + d3_svg_lineHermite(points.slice(1, -1), d3_svg_lineCardinalTangents(points, tension)); + } + function d3_svg_lineCardinalClosed(points, tension) { + return points.length < 3 ? d3_svg_lineLinear(points) : points[0] + d3_svg_lineHermite((points.push(points[0]), + points), d3_svg_lineCardinalTangents([ points[points.length - 2] ].concat(points, [ points[1] ]), tension)); + } + function d3_svg_lineCardinal(points, tension) { + return points.length < 3 ? d3_svg_lineLinear(points) : points[0] + d3_svg_lineHermite(points, d3_svg_lineCardinalTangents(points, tension)); + } + function d3_svg_lineHermite(points, tangents) { + if (tangents.length < 1 || points.length != tangents.length && points.length != tangents.length + 2) { + return d3_svg_lineLinear(points); + } + var quad = points.length != tangents.length, path = "", p0 = points[0], p = points[1], t0 = tangents[0], t = t0, pi = 1; + if (quad) { + path += "Q" + (p[0] - t0[0] * 2 / 3) + "," + (p[1] - t0[1] * 2 / 3) + "," + p[0] + "," + p[1]; + p0 = points[1]; + pi = 2; + } + if (tangents.length > 1) { + t = tangents[1]; + p = points[pi]; + pi++; + path += "C" + (p0[0] + t0[0]) + "," + (p0[1] + t0[1]) + "," + (p[0] - t[0]) + "," + (p[1] - t[1]) + "," + p[0] + "," + p[1]; + for (var i = 2; i < tangents.length; i++, pi++) { + p = points[pi]; + t = tangents[i]; + path += "S" + (p[0] - t[0]) + "," + (p[1] - t[1]) + "," + p[0] + "," + p[1]; + } + } + if (quad) { + var lp = points[pi]; + path += "Q" + (p[0] + t[0] * 2 / 3) + "," + (p[1] + t[1] * 2 / 3) + "," + lp[0] + "," + lp[1]; + } + return path; + } + function d3_svg_lineCardinalTangents(points, tension) { + var tangents = [], a = (1 - tension) / 2, p0, p1 = points[0], p2 = points[1], i = 1, n = points.length; + while (++i < n) { + p0 = p1; + p1 = p2; + p2 = points[i]; + tangents.push([ a * (p2[0] - p0[0]), a * (p2[1] - p0[1]) ]); + } + return tangents; + } + function d3_svg_lineBasis(points) { + if (points.length < 3) return d3_svg_lineLinear(points); + var i = 1, n = points.length, pi = points[0], x0 = pi[0], y0 = pi[1], px = [ x0, x0, x0, (pi = points[1])[0] ], py = [ y0, y0, y0, pi[1] ], path = [ x0, ",", y0, "L", d3_svg_lineDot4(d3_svg_lineBasisBezier3, px), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier3, py) ]; + points.push(points[n - 1]); + while (++i <= n) { + pi = points[i]; + px.shift(); + px.push(pi[0]); + py.shift(); + py.push(pi[1]); + d3_svg_lineBasisBezier(path, px, py); + } + points.pop(); + path.push("L", pi); + return path.join(""); + } + function d3_svg_lineBasisOpen(points) { + if (points.length < 4) return d3_svg_lineLinear(points); + var path = [], i = -1, n = points.length, pi, px = [ 0 ], py = [ 0 ]; + while (++i < 3) { + pi = points[i]; + px.push(pi[0]); + py.push(pi[1]); + } + path.push(d3_svg_lineDot4(d3_svg_lineBasisBezier3, px) + "," + d3_svg_lineDot4(d3_svg_lineBasisBezier3, py)); + --i; + while (++i < n) { + pi = points[i]; + px.shift(); + px.push(pi[0]); + py.shift(); + py.push(pi[1]); + d3_svg_lineBasisBezier(path, px, py); + } + return path.join(""); + } + function d3_svg_lineBasisClosed(points) { + var path, i = -1, n = points.length, m = n + 4, pi, px = [], py = []; + while (++i < 4) { + pi = points[i % n]; + px.push(pi[0]); + py.push(pi[1]); + } + path = [ d3_svg_lineDot4(d3_svg_lineBasisBezier3, px), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier3, py) ]; + --i; + while (++i < m) { + pi = points[i % n]; + px.shift(); + px.push(pi[0]); + py.shift(); + py.push(pi[1]); + d3_svg_lineBasisBezier(path, px, py); + } + return path.join(""); + } + function d3_svg_lineBundle(points, tension) { + var n = points.length - 1; + if (n) { + var x0 = points[0][0], y0 = points[0][1], dx = points[n][0] - x0, dy = points[n][1] - y0, i = -1, p, t; + while (++i <= n) { + p = points[i]; + t = i / n; + p[0] = tension * p[0] + (1 - tension) * (x0 + t * dx); + p[1] = tension * p[1] + (1 - tension) * (y0 + t * dy); + } + } + return d3_svg_lineBasis(points); + } + function d3_svg_lineDot4(a, b) { + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3]; + } + var d3_svg_lineBasisBezier1 = [ 0, 2 / 3, 1 / 3, 0 ], d3_svg_lineBasisBezier2 = [ 0, 1 / 3, 2 / 3, 0 ], d3_svg_lineBasisBezier3 = [ 0, 1 / 6, 2 / 3, 1 / 6 ]; + function d3_svg_lineBasisBezier(path, x, y) { + path.push("C", d3_svg_lineDot4(d3_svg_lineBasisBezier1, x), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier1, y), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier2, x), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier2, y), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier3, x), ",", d3_svg_lineDot4(d3_svg_lineBasisBezier3, y)); + } + function d3_svg_lineSlope(p0, p1) { + return (p1[1] - p0[1]) / (p1[0] - p0[0]); + } + function d3_svg_lineFiniteDifferences(points) { + var i = 0, j = points.length - 1, m = [], p0 = points[0], p1 = points[1], d = m[0] = d3_svg_lineSlope(p0, p1); + while (++i < j) { + m[i] = (d + (d = d3_svg_lineSlope(p0 = p1, p1 = points[i + 1]))) / 2; + } + m[i] = d; + return m; + } + function d3_svg_lineMonotoneTangents(points) { + var tangents = [], d, a, b, s, m = d3_svg_lineFiniteDifferences(points), i = -1, j = points.length - 1; + while (++i < j) { + d = d3_svg_lineSlope(points[i], points[i + 1]); + if (abs(d) < ε) { + m[i] = m[i + 1] = 0; + } else { + a = m[i] / d; + b = m[i + 1] / d; + s = a * a + b * b; + if (s > 9) { + s = d * 3 / Math.sqrt(s); + m[i] = s * a; + m[i + 1] = s * b; + } + } + } + i = -1; + while (++i <= j) { + s = (points[Math.min(j, i + 1)][0] - points[Math.max(0, i - 1)][0]) / (6 * (1 + m[i] * m[i])); + tangents.push([ s || 0, m[i] * s || 0 ]); + } + return tangents; + } + function d3_svg_lineMonotone(points) { + return points.length < 3 ? d3_svg_lineLinear(points) : points[0] + d3_svg_lineHermite(points, d3_svg_lineMonotoneTangents(points)); + } + d3.svg.line.radial = function() { + var line = d3_svg_line(d3_svg_lineRadial); + line.radius = line.x, delete line.x; + line.angle = line.y, delete line.y; + return line; + }; + function d3_svg_lineRadial(points) { + var point, i = -1, n = points.length, r, a; + while (++i < n) { + point = points[i]; + r = point[0]; + a = point[1] - halfπ; + point[0] = r * Math.cos(a); + point[1] = r * Math.sin(a); + } + return points; + } + function d3_svg_area(projection) { + var x0 = d3_geom_pointX, x1 = d3_geom_pointX, y0 = 0, y1 = d3_geom_pointY, defined = d3_true, interpolate = d3_svg_lineLinear, interpolateKey = interpolate.key, interpolateReverse = interpolate, L = "L", tension = .7; + function area(data) { + var segments = [], points0 = [], points1 = [], i = -1, n = data.length, d, fx0 = d3_functor(x0), fy0 = d3_functor(y0), fx1 = x0 === x1 ? function() { + return x; + } : d3_functor(x1), fy1 = y0 === y1 ? function() { + return y; + } : d3_functor(y1), x, y; + function segment() { + segments.push("M", interpolate(projection(points1), tension), L, interpolateReverse(projection(points0.reverse()), tension), "Z"); + } + while (++i < n) { + if (defined.call(this, d = data[i], i)) { + points0.push([ x = +fx0.call(this, d, i), y = +fy0.call(this, d, i) ]); + points1.push([ +fx1.call(this, d, i), +fy1.call(this, d, i) ]); + } else if (points0.length) { + segment(); + points0 = []; + points1 = []; + } + } + if (points0.length) segment(); + return segments.length ? segments.join("") : null; + } + area.x = function(_) { + if (!arguments.length) return x1; + x0 = x1 = _; + return area; + }; + area.x0 = function(_) { + if (!arguments.length) return x0; + x0 = _; + return area; + }; + area.x1 = function(_) { + if (!arguments.length) return x1; + x1 = _; + return area; + }; + area.y = function(_) { + if (!arguments.length) return y1; + y0 = y1 = _; + return area; + }; + area.y0 = function(_) { + if (!arguments.length) return y0; + y0 = _; + return area; + }; + area.y1 = function(_) { + if (!arguments.length) return y1; + y1 = _; + return area; + }; + area.defined = function(_) { + if (!arguments.length) return defined; + defined = _; + return area; + }; + area.interpolate = function(_) { + if (!arguments.length) return interpolateKey; + if (typeof _ === "function") interpolateKey = interpolate = _; else interpolateKey = (interpolate = d3_svg_lineInterpolators.get(_) || d3_svg_lineLinear).key; + interpolateReverse = interpolate.reverse || interpolate; + L = interpolate.closed ? "M" : "L"; + return area; + }; + area.tension = function(_) { + if (!arguments.length) return tension; + tension = _; + return area; + }; + return area; + } + d3_svg_lineStepBefore.reverse = d3_svg_lineStepAfter; + d3_svg_lineStepAfter.reverse = d3_svg_lineStepBefore; + d3.svg.area = function() { + return d3_svg_area(d3_identity); + }; + d3.svg.area.radial = function() { + var area = d3_svg_area(d3_svg_lineRadial); + area.radius = area.x, delete area.x; + area.innerRadius = area.x0, delete area.x0; + area.outerRadius = area.x1, delete area.x1; + area.angle = area.y, delete area.y; + area.startAngle = area.y0, delete area.y0; + area.endAngle = area.y1, delete area.y1; + return area; + }; + d3.svg.chord = function() { + var source = d3_source, target = d3_target, radius = d3_svg_chordRadius, startAngle = d3_svg_arcStartAngle, endAngle = d3_svg_arcEndAngle; + function chord(d, i) { + var s = subgroup(this, source, d, i), t = subgroup(this, target, d, i); + return "M" + s.p0 + arc(s.r, s.p1, s.a1 - s.a0) + (equals(s, t) ? curve(s.r, s.p1, s.r, s.p0) : curve(s.r, s.p1, t.r, t.p0) + arc(t.r, t.p1, t.a1 - t.a0) + curve(t.r, t.p1, s.r, s.p0)) + "Z"; + } + function subgroup(self, f, d, i) { + var subgroup = f.call(self, d, i), r = radius.call(self, subgroup, i), a0 = startAngle.call(self, subgroup, i) - halfπ, a1 = endAngle.call(self, subgroup, i) - halfπ; + return { + r: r, + a0: a0, + a1: a1, + p0: [ r * Math.cos(a0), r * Math.sin(a0) ], + p1: [ r * Math.cos(a1), r * Math.sin(a1) ] + }; + } + function equals(a, b) { + return a.a0 == b.a0 && a.a1 == b.a1; + } + function arc(r, p, a) { + return "A" + r + "," + r + " 0 " + +(a > π) + ",1 " + p; + } + function curve(r0, p0, r1, p1) { + return "Q 0,0 " + p1; + } + chord.radius = function(v) { + if (!arguments.length) return radius; + radius = d3_functor(v); + return chord; + }; + chord.source = function(v) { + if (!arguments.length) return source; + source = d3_functor(v); + return chord; + }; + chord.target = function(v) { + if (!arguments.length) return target; + target = d3_functor(v); + return chord; + }; + chord.startAngle = function(v) { + if (!arguments.length) return startAngle; + startAngle = d3_functor(v); + return chord; + }; + chord.endAngle = function(v) { + if (!arguments.length) return endAngle; + endAngle = d3_functor(v); + return chord; + }; + return chord; + }; + function d3_svg_chordRadius(d) { + return d.radius; + } + d3.svg.diagonal = function() { + var source = d3_source, target = d3_target, projection = d3_svg_diagonalProjection; + function diagonal(d, i) { + var p0 = source.call(this, d, i), p3 = target.call(this, d, i), m = (p0.y + p3.y) / 2, p = [ p0, { + x: p0.x, + y: m + }, { + x: p3.x, + y: m + }, p3 ]; + p = p.map(projection); + return "M" + p[0] + "C" + p[1] + " " + p[2] + " " + p[3]; + } + diagonal.source = function(x) { + if (!arguments.length) return source; + source = d3_functor(x); + return diagonal; + }; + diagonal.target = function(x) { + if (!arguments.length) return target; + target = d3_functor(x); + return diagonal; + }; + diagonal.projection = function(x) { + if (!arguments.length) return projection; + projection = x; + return diagonal; + }; + return diagonal; + }; + function d3_svg_diagonalProjection(d) { + return [ d.x, d.y ]; + } + d3.svg.diagonal.radial = function() { + var diagonal = d3.svg.diagonal(), projection = d3_svg_diagonalProjection, projection_ = diagonal.projection; + diagonal.projection = function(x) { + return arguments.length ? projection_(d3_svg_diagonalRadialProjection(projection = x)) : projection; + }; + return diagonal; + }; + function d3_svg_diagonalRadialProjection(projection) { + return function() { + var d = projection.apply(this, arguments), r = d[0], a = d[1] - halfπ; + return [ r * Math.cos(a), r * Math.sin(a) ]; + }; + } + d3.svg.symbol = function() { + var type = d3_svg_symbolType, size = d3_svg_symbolSize; + function symbol(d, i) { + return (d3_svg_symbols.get(type.call(this, d, i)) || d3_svg_symbolCircle)(size.call(this, d, i)); + } + symbol.type = function(x) { + if (!arguments.length) return type; + type = d3_functor(x); + return symbol; + }; + symbol.size = function(x) { + if (!arguments.length) return size; + size = d3_functor(x); + return symbol; + }; + return symbol; + }; + function d3_svg_symbolSize() { + return 64; + } + function d3_svg_symbolType() { + return "circle"; + } + function d3_svg_symbolCircle(size) { + var r = Math.sqrt(size / π); + return "M0," + r + "A" + r + "," + r + " 0 1,1 0," + -r + "A" + r + "," + r + " 0 1,1 0," + r + "Z"; + } + var d3_svg_symbols = d3.map({ + circle: d3_svg_symbolCircle, + cross: function(size) { + var r = Math.sqrt(size / 5) / 2; + return "M" + -3 * r + "," + -r + "H" + -r + "V" + -3 * r + "H" + r + "V" + -r + "H" + 3 * r + "V" + r + "H" + r + "V" + 3 * r + "H" + -r + "V" + r + "H" + -3 * r + "Z"; + }, + diamond: function(size) { + var ry = Math.sqrt(size / (2 * d3_svg_symbolTan30)), rx = ry * d3_svg_symbolTan30; + return "M0," + -ry + "L" + rx + ",0" + " 0," + ry + " " + -rx + ",0" + "Z"; + }, + square: function(size) { + var r = Math.sqrt(size) / 2; + return "M" + -r + "," + -r + "L" + r + "," + -r + " " + r + "," + r + " " + -r + "," + r + "Z"; + }, + "triangle-down": function(size) { + var rx = Math.sqrt(size / d3_svg_symbolSqrt3), ry = rx * d3_svg_symbolSqrt3 / 2; + return "M0," + ry + "L" + rx + "," + -ry + " " + -rx + "," + -ry + "Z"; + }, + "triangle-up": function(size) { + var rx = Math.sqrt(size / d3_svg_symbolSqrt3), ry = rx * d3_svg_symbolSqrt3 / 2; + return "M0," + -ry + "L" + rx + "," + ry + " " + -rx + "," + ry + "Z"; + } + }); + d3.svg.symbolTypes = d3_svg_symbols.keys(); + var d3_svg_symbolSqrt3 = Math.sqrt(3), d3_svg_symbolTan30 = Math.tan(30 * d3_radians); + d3_selectionPrototype.transition = function(name) { + var id = d3_transitionInheritId || ++d3_transitionId, ns = d3_transitionNamespace(name), subgroups = [], subgroup, node, transition = d3_transitionInherit || { + time: Date.now(), + ease: d3_ease_cubicInOut, + delay: 0, + duration: 250 + }; + for (var j = -1, m = this.length; ++j < m; ) { + subgroups.push(subgroup = []); + for (var group = this[j], i = -1, n = group.length; ++i < n; ) { + if (node = group[i]) d3_transitionNode(node, i, ns, id, transition); + subgroup.push(node); + } + } + return d3_transition(subgroups, ns, id); + }; + d3_selectionPrototype.interrupt = function(name) { + return this.each(name == null ? d3_selection_interrupt : d3_selection_interruptNS(d3_transitionNamespace(name))); + }; + var d3_selection_interrupt = d3_selection_interruptNS(d3_transitionNamespace()); + function d3_selection_interruptNS(ns) { + return function() { + var lock, active; + if ((lock = this[ns]) && (active = lock[lock.active])) { + if (--lock.count) delete lock[lock.active]; else delete this[ns]; + lock.active += .5; + active.event && active.event.interrupt.call(this, this.__data__, active.index); + } + }; + } + function d3_transition(groups, ns, id) { + d3_subclass(groups, d3_transitionPrototype); + groups.namespace = ns; + groups.id = id; + return groups; + } + var d3_transitionPrototype = [], d3_transitionId = 0, d3_transitionInheritId, d3_transitionInherit; + d3_transitionPrototype.call = d3_selectionPrototype.call; + d3_transitionPrototype.empty = d3_selectionPrototype.empty; + d3_transitionPrototype.node = d3_selectionPrototype.node; + d3_transitionPrototype.size = d3_selectionPrototype.size; + d3.transition = function(selection, name) { + return selection && selection.transition ? d3_transitionInheritId ? selection.transition(name) : selection : d3.selection().transition(selection); + }; + d3.transition.prototype = d3_transitionPrototype; + d3_transitionPrototype.select = function(selector) { + var id = this.id, ns = this.namespace, subgroups = [], subgroup, subnode, node; + selector = d3_selection_selector(selector); + for (var j = -1, m = this.length; ++j < m; ) { + subgroups.push(subgroup = []); + for (var group = this[j], i = -1, n = group.length; ++i < n; ) { + if ((node = group[i]) && (subnode = selector.call(node, node.__data__, i, j))) { + if ("__data__" in node) subnode.__data__ = node.__data__; + d3_transitionNode(subnode, i, ns, id, node[ns][id]); + subgroup.push(subnode); + } else { + subgroup.push(null); + } + } + } + return d3_transition(subgroups, ns, id); + }; + d3_transitionPrototype.selectAll = function(selector) { + var id = this.id, ns = this.namespace, subgroups = [], subgroup, subnodes, node, subnode, transition; + selector = d3_selection_selectorAll(selector); + for (var j = -1, m = this.length; ++j < m; ) { + for (var group = this[j], i = -1, n = group.length; ++i < n; ) { + if (node = group[i]) { + transition = node[ns][id]; + subnodes = selector.call(node, node.__data__, i, j); + subgroups.push(subgroup = []); + for (var k = -1, o = subnodes.length; ++k < o; ) { + if (subnode = subnodes[k]) d3_transitionNode(subnode, k, ns, id, transition); + subgroup.push(subnode); + } + } + } + } + return d3_transition(subgroups, ns, id); + }; + d3_transitionPrototype.filter = function(filter) { + var subgroups = [], subgroup, group, node; + if (typeof filter !== "function") filter = d3_selection_filter(filter); + for (var j = 0, m = this.length; j < m; j++) { + subgroups.push(subgroup = []); + for (var group = this[j], i = 0, n = group.length; i < n; i++) { + if ((node = group[i]) && filter.call(node, node.__data__, i, j)) { + subgroup.push(node); + } + } + } + return d3_transition(subgroups, this.namespace, this.id); + }; + d3_transitionPrototype.tween = function(name, tween) { + var id = this.id, ns = this.namespace; + if (arguments.length < 2) return this.node()[ns][id].tween.get(name); + return d3_selection_each(this, tween == null ? function(node) { + node[ns][id].tween.remove(name); + } : function(node) { + node[ns][id].tween.set(name, tween); + }); + }; + function d3_transition_tween(groups, name, value, tween) { + var id = groups.id, ns = groups.namespace; + return d3_selection_each(groups, typeof value === "function" ? function(node, i, j) { + node[ns][id].tween.set(name, tween(value.call(node, node.__data__, i, j))); + } : (value = tween(value), function(node) { + node[ns][id].tween.set(name, value); + })); + } + d3_transitionPrototype.attr = function(nameNS, value) { + if (arguments.length < 2) { + for (value in nameNS) this.attr(value, nameNS[value]); + return this; + } + var interpolate = nameNS == "transform" ? d3_interpolateTransform : d3_interpolate, name = d3.ns.qualify(nameNS); + function attrNull() { + this.removeAttribute(name); + } + function attrNullNS() { + this.removeAttributeNS(name.space, name.local); + } + function attrTween(b) { + return b == null ? attrNull : (b += "", function() { + var a = this.getAttribute(name), i; + return a !== b && (i = interpolate(a, b), function(t) { + this.setAttribute(name, i(t)); + }); + }); + } + function attrTweenNS(b) { + return b == null ? attrNullNS : (b += "", function() { + var a = this.getAttributeNS(name.space, name.local), i; + return a !== b && (i = interpolate(a, b), function(t) { + this.setAttributeNS(name.space, name.local, i(t)); + }); + }); + } + return d3_transition_tween(this, "attr." + nameNS, value, name.local ? attrTweenNS : attrTween); + }; + d3_transitionPrototype.attrTween = function(nameNS, tween) { + var name = d3.ns.qualify(nameNS); + function attrTween(d, i) { + var f = tween.call(this, d, i, this.getAttribute(name)); + return f && function(t) { + this.setAttribute(name, f(t)); + }; + } + function attrTweenNS(d, i) { + var f = tween.call(this, d, i, this.getAttributeNS(name.space, name.local)); + return f && function(t) { + this.setAttributeNS(name.space, name.local, f(t)); + }; + } + return this.tween("attr." + nameNS, name.local ? attrTweenNS : attrTween); + }; + d3_transitionPrototype.style = function(name, value, priority) { + var n = arguments.length; + if (n < 3) { + if (typeof name !== "string") { + if (n < 2) value = ""; + for (priority in name) this.style(priority, name[priority], value); + return this; + } + priority = ""; + } + function styleNull() { + this.style.removeProperty(name); + } + function styleString(b) { + return b == null ? styleNull : (b += "", function() { + var a = d3_window(this).getComputedStyle(this, null).getPropertyValue(name), i; + return a !== b && (i = d3_interpolate(a, b), function(t) { + this.style.setProperty(name, i(t), priority); + }); + }); + } + return d3_transition_tween(this, "style." + name, value, styleString); + }; + d3_transitionPrototype.styleTween = function(name, tween, priority) { + if (arguments.length < 3) priority = ""; + function styleTween(d, i) { + var f = tween.call(this, d, i, d3_window(this).getComputedStyle(this, null).getPropertyValue(name)); + return f && function(t) { + this.style.setProperty(name, f(t), priority); + }; + } + return this.tween("style." + name, styleTween); + }; + d3_transitionPrototype.text = function(value) { + return d3_transition_tween(this, "text", value, d3_transition_text); + }; + function d3_transition_text(b) { + if (b == null) b = ""; + return function() { + this.textContent = b; + }; + } + d3_transitionPrototype.remove = function() { + var ns = this.namespace; + return this.each("end.transition", function() { + var p; + if (this[ns].count < 2 && (p = this.parentNode)) p.removeChild(this); + }); + }; + d3_transitionPrototype.ease = function(value) { + var id = this.id, ns = this.namespace; + if (arguments.length < 1) return this.node()[ns][id].ease; + if (typeof value !== "function") value = d3.ease.apply(d3, arguments); + return d3_selection_each(this, function(node) { + node[ns][id].ease = value; + }); + }; + d3_transitionPrototype.delay = function(value) { + var id = this.id, ns = this.namespace; + if (arguments.length < 1) return this.node()[ns][id].delay; + return d3_selection_each(this, typeof value === "function" ? function(node, i, j) { + node[ns][id].delay = +value.call(node, node.__data__, i, j); + } : (value = +value, function(node) { + node[ns][id].delay = value; + })); + }; + d3_transitionPrototype.duration = function(value) { + var id = this.id, ns = this.namespace; + if (arguments.length < 1) return this.node()[ns][id].duration; + return d3_selection_each(this, typeof value === "function" ? function(node, i, j) { + node[ns][id].duration = Math.max(1, value.call(node, node.__data__, i, j)); + } : (value = Math.max(1, value), function(node) { + node[ns][id].duration = value; + })); + }; + d3_transitionPrototype.each = function(type, listener) { + var id = this.id, ns = this.namespace; + if (arguments.length < 2) { + var inherit = d3_transitionInherit, inheritId = d3_transitionInheritId; + try { + d3_transitionInheritId = id; + d3_selection_each(this, function(node, i, j) { + d3_transitionInherit = node[ns][id]; + type.call(node, node.__data__, i, j); + }); + } finally { + d3_transitionInherit = inherit; + d3_transitionInheritId = inheritId; + } + } else { + d3_selection_each(this, function(node) { + var transition = node[ns][id]; + (transition.event || (transition.event = d3.dispatch("start", "end", "interrupt"))).on(type, listener); + }); + } + return this; + }; + d3_transitionPrototype.transition = function() { + var id0 = this.id, id1 = ++d3_transitionId, ns = this.namespace, subgroups = [], subgroup, group, node, transition; + for (var j = 0, m = this.length; j < m; j++) { + subgroups.push(subgroup = []); + for (var group = this[j], i = 0, n = group.length; i < n; i++) { + if (node = group[i]) { + transition = node[ns][id0]; + d3_transitionNode(node, i, ns, id1, { + time: transition.time, + ease: transition.ease, + delay: transition.delay + transition.duration, + duration: transition.duration + }); + } + subgroup.push(node); + } + } + return d3_transition(subgroups, ns, id1); + }; + function d3_transitionNamespace(name) { + return name == null ? "__transition__" : "__transition_" + name + "__"; + } + function d3_transitionNode(node, i, ns, id, inherit) { + var lock = node[ns] || (node[ns] = { + active: 0, + count: 0 + }), transition = lock[id]; + if (!transition) { + var time = inherit.time; + transition = lock[id] = { + tween: new d3_Map(), + time: time, + delay: inherit.delay, + duration: inherit.duration, + ease: inherit.ease, + index: i + }; + inherit = null; + ++lock.count; + d3.timer(function(elapsed) { + var delay = transition.delay, duration, ease, timer = d3_timer_active, tweened = []; + timer.t = delay + time; + if (delay <= elapsed) return start(elapsed - delay); + timer.c = start; + function start(elapsed) { + if (lock.active > id) return stop(); + var active = lock[lock.active]; + if (active) { + --lock.count; + delete lock[lock.active]; + active.event && active.event.interrupt.call(node, node.__data__, active.index); + } + lock.active = id; + transition.event && transition.event.start.call(node, node.__data__, i); + transition.tween.forEach(function(key, value) { + if (value = value.call(node, node.__data__, i)) { + tweened.push(value); + } + }); + ease = transition.ease; + duration = transition.duration; + d3.timer(function() { + timer.c = tick(elapsed || 1) ? d3_true : tick; + return 1; + }, 0, time); + } + function tick(elapsed) { + if (lock.active !== id) return 1; + var t = elapsed / duration, e = ease(t), n = tweened.length; + while (n > 0) { + tweened[--n].call(node, e); + } + if (t >= 1) { + transition.event && transition.event.end.call(node, node.__data__, i); + return stop(); + } + } + function stop() { + if (--lock.count) delete lock[id]; else delete node[ns]; + return 1; + } + }, 0, time); + } + } + d3.svg.axis = function() { + var scale = d3.scale.linear(), orient = d3_svg_axisDefaultOrient, innerTickSize = 6, outerTickSize = 6, tickPadding = 3, tickArguments_ = [ 10 ], tickValues = null, tickFormat_; + function axis(g) { + g.each(function() { + var g = d3.select(this); + var scale0 = this.__chart__ || scale, scale1 = this.__chart__ = scale.copy(); + var ticks = tickValues == null ? scale1.ticks ? scale1.ticks.apply(scale1, tickArguments_) : scale1.domain() : tickValues, tickFormat = tickFormat_ == null ? scale1.tickFormat ? scale1.tickFormat.apply(scale1, tickArguments_) : d3_identity : tickFormat_, tick = g.selectAll(".tick").data(ticks, scale1), tickEnter = tick.enter().insert("g", ".domain").attr("class", "tick").style("opacity", ε), tickExit = d3.transition(tick.exit()).style("opacity", ε).remove(), tickUpdate = d3.transition(tick.order()).style("opacity", 1), tickSpacing = Math.max(innerTickSize, 0) + tickPadding, tickTransform; + var range = d3_scaleRange(scale1), path = g.selectAll(".domain").data([ 0 ]), pathUpdate = (path.enter().append("path").attr("class", "domain"), + d3.transition(path)); + tickEnter.append("line"); + tickEnter.append("text"); + var lineEnter = tickEnter.select("line"), lineUpdate = tickUpdate.select("line"), text = tick.select("text").text(tickFormat), textEnter = tickEnter.select("text"), textUpdate = tickUpdate.select("text"), sign = orient === "top" || orient === "left" ? -1 : 1, x1, x2, y1, y2; + if (orient === "bottom" || orient === "top") { + tickTransform = d3_svg_axisX, x1 = "x", y1 = "y", x2 = "x2", y2 = "y2"; + text.attr("dy", sign < 0 ? "0em" : ".71em").style("text-anchor", "middle"); + pathUpdate.attr("d", "M" + range[0] + "," + sign * outerTickSize + "V0H" + range[1] + "V" + sign * outerTickSize); + } else { + tickTransform = d3_svg_axisY, x1 = "y", y1 = "x", x2 = "y2", y2 = "x2"; + text.attr("dy", ".32em").style("text-anchor", sign < 0 ? "end" : "start"); + pathUpdate.attr("d", "M" + sign * outerTickSize + "," + range[0] + "H0V" + range[1] + "H" + sign * outerTickSize); + } + lineEnter.attr(y2, sign * innerTickSize); + textEnter.attr(y1, sign * tickSpacing); + lineUpdate.attr(x2, 0).attr(y2, sign * innerTickSize); + textUpdate.attr(x1, 0).attr(y1, sign * tickSpacing); + if (scale1.rangeBand) { + var x = scale1, dx = x.rangeBand() / 2; + scale0 = scale1 = function(d) { + return x(d) + dx; + }; + } else if (scale0.rangeBand) { + scale0 = scale1; + } else { + tickExit.call(tickTransform, scale1, scale0); + } + tickEnter.call(tickTransform, scale0, scale1); + tickUpdate.call(tickTransform, scale1, scale1); + }); + } + axis.scale = function(x) { + if (!arguments.length) return scale; + scale = x; + return axis; + }; + axis.orient = function(x) { + if (!arguments.length) return orient; + orient = x in d3_svg_axisOrients ? x + "" : d3_svg_axisDefaultOrient; + return axis; + }; + axis.ticks = function() { + if (!arguments.length) return tickArguments_; + tickArguments_ = arguments; + return axis; + }; + axis.tickValues = function(x) { + if (!arguments.length) return tickValues; + tickValues = x; + return axis; + }; + axis.tickFormat = function(x) { + if (!arguments.length) return tickFormat_; + tickFormat_ = x; + return axis; + }; + axis.tickSize = function(x) { + var n = arguments.length; + if (!n) return innerTickSize; + innerTickSize = +x; + outerTickSize = +arguments[n - 1]; + return axis; + }; + axis.innerTickSize = function(x) { + if (!arguments.length) return innerTickSize; + innerTickSize = +x; + return axis; + }; + axis.outerTickSize = function(x) { + if (!arguments.length) return outerTickSize; + outerTickSize = +x; + return axis; + }; + axis.tickPadding = function(x) { + if (!arguments.length) return tickPadding; + tickPadding = +x; + return axis; + }; + axis.tickSubdivide = function() { + return arguments.length && axis; + }; + return axis; + }; + var d3_svg_axisDefaultOrient = "bottom", d3_svg_axisOrients = { + top: 1, + right: 1, + bottom: 1, + left: 1 + }; + function d3_svg_axisX(selection, x0, x1) { + selection.attr("transform", function(d) { + var v0 = x0(d); + return "translate(" + (isFinite(v0) ? v0 : x1(d)) + ",0)"; + }); + } + function d3_svg_axisY(selection, y0, y1) { + selection.attr("transform", function(d) { + var v0 = y0(d); + return "translate(0," + (isFinite(v0) ? v0 : y1(d)) + ")"; + }); + } + d3.svg.brush = function() { + var event = d3_eventDispatch(brush, "brushstart", "brush", "brushend"), x = null, y = null, xExtent = [ 0, 0 ], yExtent = [ 0, 0 ], xExtentDomain, yExtentDomain, xClamp = true, yClamp = true, resizes = d3_svg_brushResizes[0]; + function brush(g) { + g.each(function() { + var g = d3.select(this).style("pointer-events", "all").style("-webkit-tap-highlight-color", "rgba(0,0,0,0)").on("mousedown.brush", brushstart).on("touchstart.brush", brushstart); + var background = g.selectAll(".background").data([ 0 ]); + background.enter().append("rect").attr("class", "background").style("visibility", "hidden").style("cursor", "crosshair"); + g.selectAll(".extent").data([ 0 ]).enter().append("rect").attr("class", "extent").style("cursor", "move"); + var resize = g.selectAll(".resize").data(resizes, d3_identity); + resize.exit().remove(); + resize.enter().append("g").attr("class", function(d) { + return "resize " + d; + }).style("cursor", function(d) { + return d3_svg_brushCursor[d]; + }).append("rect").attr("x", function(d) { + return /[ew]$/.test(d) ? -3 : null; + }).attr("y", function(d) { + return /^[ns]/.test(d) ? -3 : null; + }).attr("width", 6).attr("height", 6).style("visibility", "hidden"); + resize.style("display", brush.empty() ? "none" : null); + var gUpdate = d3.transition(g), backgroundUpdate = d3.transition(background), range; + if (x) { + range = d3_scaleRange(x); + backgroundUpdate.attr("x", range[0]).attr("width", range[1] - range[0]); + redrawX(gUpdate); + } + if (y) { + range = d3_scaleRange(y); + backgroundUpdate.attr("y", range[0]).attr("height", range[1] - range[0]); + redrawY(gUpdate); + } + redraw(gUpdate); + }); + } + brush.event = function(g) { + g.each(function() { + var event_ = event.of(this, arguments), extent1 = { + x: xExtent, + y: yExtent, + i: xExtentDomain, + j: yExtentDomain + }, extent0 = this.__chart__ || extent1; + this.__chart__ = extent1; + if (d3_transitionInheritId) { + d3.select(this).transition().each("start.brush", function() { + xExtentDomain = extent0.i; + yExtentDomain = extent0.j; + xExtent = extent0.x; + yExtent = extent0.y; + event_({ + type: "brushstart" + }); + }).tween("brush:brush", function() { + var xi = d3_interpolateArray(xExtent, extent1.x), yi = d3_interpolateArray(yExtent, extent1.y); + xExtentDomain = yExtentDomain = null; + return function(t) { + xExtent = extent1.x = xi(t); + yExtent = extent1.y = yi(t); + event_({ + type: "brush", + mode: "resize" + }); + }; + }).each("end.brush", function() { + xExtentDomain = extent1.i; + yExtentDomain = extent1.j; + event_({ + type: "brush", + mode: "resize" + }); + event_({ + type: "brushend" + }); + }); + } else { + event_({ + type: "brushstart" + }); + event_({ + type: "brush", + mode: "resize" + }); + event_({ + type: "brushend" + }); + } + }); + }; + function redraw(g) { + g.selectAll(".resize").attr("transform", function(d) { + return "translate(" + xExtent[+/e$/.test(d)] + "," + yExtent[+/^s/.test(d)] + ")"; + }); + } + function redrawX(g) { + g.select(".extent").attr("x", xExtent[0]); + g.selectAll(".extent,.n>rect,.s>rect").attr("width", xExtent[1] - xExtent[0]); + } + function redrawY(g) { + g.select(".extent").attr("y", yExtent[0]); + g.selectAll(".extent,.e>rect,.w>rect").attr("height", yExtent[1] - yExtent[0]); + } + function brushstart() { + var target = this, eventTarget = d3.select(d3.event.target), event_ = event.of(target, arguments), g = d3.select(target), resizing = eventTarget.datum(), resizingX = !/^(n|s)$/.test(resizing) && x, resizingY = !/^(e|w)$/.test(resizing) && y, dragging = eventTarget.classed("extent"), dragRestore = d3_event_dragSuppress(target), center, origin = d3.mouse(target), offset; + var w = d3.select(d3_window(target)).on("keydown.brush", keydown).on("keyup.brush", keyup); + if (d3.event.changedTouches) { + w.on("touchmove.brush", brushmove).on("touchend.brush", brushend); + } else { + w.on("mousemove.brush", brushmove).on("mouseup.brush", brushend); + } + g.interrupt().selectAll("*").interrupt(); + if (dragging) { + origin[0] = xExtent[0] - origin[0]; + origin[1] = yExtent[0] - origin[1]; + } else if (resizing) { + var ex = +/w$/.test(resizing), ey = +/^n/.test(resizing); + offset = [ xExtent[1 - ex] - origin[0], yExtent[1 - ey] - origin[1] ]; + origin[0] = xExtent[ex]; + origin[1] = yExtent[ey]; + } else if (d3.event.altKey) center = origin.slice(); + g.style("pointer-events", "none").selectAll(".resize").style("display", null); + d3.select("body").style("cursor", eventTarget.style("cursor")); + event_({ + type: "brushstart" + }); + brushmove(); + function keydown() { + if (d3.event.keyCode == 32) { + if (!dragging) { + center = null; + origin[0] -= xExtent[1]; + origin[1] -= yExtent[1]; + dragging = 2; + } + d3_eventPreventDefault(); + } + } + function keyup() { + if (d3.event.keyCode == 32 && dragging == 2) { + origin[0] += xExtent[1]; + origin[1] += yExtent[1]; + dragging = 0; + d3_eventPreventDefault(); + } + } + function brushmove() { + var point = d3.mouse(target), moved = false; + if (offset) { + point[0] += offset[0]; + point[1] += offset[1]; + } + if (!dragging) { + if (d3.event.altKey) { + if (!center) center = [ (xExtent[0] + xExtent[1]) / 2, (yExtent[0] + yExtent[1]) / 2 ]; + origin[0] = xExtent[+(point[0] < center[0])]; + origin[1] = yExtent[+(point[1] < center[1])]; + } else center = null; + } + if (resizingX && move1(point, x, 0)) { + redrawX(g); + moved = true; + } + if (resizingY && move1(point, y, 1)) { + redrawY(g); + moved = true; + } + if (moved) { + redraw(g); + event_({ + type: "brush", + mode: dragging ? "move" : "resize" + }); + } + } + function move1(point, scale, i) { + var range = d3_scaleRange(scale), r0 = range[0], r1 = range[1], position = origin[i], extent = i ? yExtent : xExtent, size = extent[1] - extent[0], min, max; + if (dragging) { + r0 -= position; + r1 -= size + position; + } + min = (i ? yClamp : xClamp) ? Math.max(r0, Math.min(r1, point[i])) : point[i]; + if (dragging) { + max = (min += position) + size; + } else { + if (center) position = Math.max(r0, Math.min(r1, 2 * center[i] - min)); + if (position < min) { + max = min; + min = position; + } else { + max = position; + } + } + if (extent[0] != min || extent[1] != max) { + if (i) yExtentDomain = null; else xExtentDomain = null; + extent[0] = min; + extent[1] = max; + return true; + } + } + function brushend() { + brushmove(); + g.style("pointer-events", "all").selectAll(".resize").style("display", brush.empty() ? "none" : null); + d3.select("body").style("cursor", null); + w.on("mousemove.brush", null).on("mouseup.brush", null).on("touchmove.brush", null).on("touchend.brush", null).on("keydown.brush", null).on("keyup.brush", null); + dragRestore(); + event_({ + type: "brushend" + }); + } + } + brush.x = function(z) { + if (!arguments.length) return x; + x = z; + resizes = d3_svg_brushResizes[!x << 1 | !y]; + return brush; + }; + brush.y = function(z) { + if (!arguments.length) return y; + y = z; + resizes = d3_svg_brushResizes[!x << 1 | !y]; + return brush; + }; + brush.clamp = function(z) { + if (!arguments.length) return x && y ? [ xClamp, yClamp ] : x ? xClamp : y ? yClamp : null; + if (x && y) xClamp = !!z[0], yClamp = !!z[1]; else if (x) xClamp = !!z; else if (y) yClamp = !!z; + return brush; + }; + brush.extent = function(z) { + var x0, x1, y0, y1, t; + if (!arguments.length) { + if (x) { + if (xExtentDomain) { + x0 = xExtentDomain[0], x1 = xExtentDomain[1]; + } else { + x0 = xExtent[0], x1 = xExtent[1]; + if (x.invert) x0 = x.invert(x0), x1 = x.invert(x1); + if (x1 < x0) t = x0, x0 = x1, x1 = t; + } + } + if (y) { + if (yExtentDomain) { + y0 = yExtentDomain[0], y1 = yExtentDomain[1]; + } else { + y0 = yExtent[0], y1 = yExtent[1]; + if (y.invert) y0 = y.invert(y0), y1 = y.invert(y1); + if (y1 < y0) t = y0, y0 = y1, y1 = t; + } + } + return x && y ? [ [ x0, y0 ], [ x1, y1 ] ] : x ? [ x0, x1 ] : y && [ y0, y1 ]; + } + if (x) { + x0 = z[0], x1 = z[1]; + if (y) x0 = x0[0], x1 = x1[0]; + xExtentDomain = [ x0, x1 ]; + if (x.invert) x0 = x(x0), x1 = x(x1); + if (x1 < x0) t = x0, x0 = x1, x1 = t; + if (x0 != xExtent[0] || x1 != xExtent[1]) xExtent = [ x0, x1 ]; + } + if (y) { + y0 = z[0], y1 = z[1]; + if (x) y0 = y0[1], y1 = y1[1]; + yExtentDomain = [ y0, y1 ]; + if (y.invert) y0 = y(y0), y1 = y(y1); + if (y1 < y0) t = y0, y0 = y1, y1 = t; + if (y0 != yExtent[0] || y1 != yExtent[1]) yExtent = [ y0, y1 ]; + } + return brush; + }; + brush.clear = function() { + if (!brush.empty()) { + xExtent = [ 0, 0 ], yExtent = [ 0, 0 ]; + xExtentDomain = yExtentDomain = null; + } + return brush; + }; + brush.empty = function() { + return !!x && xExtent[0] == xExtent[1] || !!y && yExtent[0] == yExtent[1]; + }; + return d3.rebind(brush, event, "on"); + }; + var d3_svg_brushCursor = { + n: "ns-resize", + e: "ew-resize", + s: "ns-resize", + w: "ew-resize", + nw: "nwse-resize", + ne: "nesw-resize", + se: "nwse-resize", + sw: "nesw-resize" + }; + var d3_svg_brushResizes = [ [ "n", "e", "s", "w", "nw", "ne", "se", "sw" ], [ "e", "w" ], [ "n", "s" ], [] ]; + var d3_time_format = d3_time.format = d3_locale_enUS.timeFormat; + var d3_time_formatUtc = d3_time_format.utc; + var d3_time_formatIso = d3_time_formatUtc("%Y-%m-%dT%H:%M:%S.%LZ"); + d3_time_format.iso = Date.prototype.toISOString && +new Date("2000-01-01T00:00:00.000Z") ? d3_time_formatIsoNative : d3_time_formatIso; + function d3_time_formatIsoNative(date) { + return date.toISOString(); + } + d3_time_formatIsoNative.parse = function(string) { + var date = new Date(string); + return isNaN(date) ? null : date; + }; + d3_time_formatIsoNative.toString = d3_time_formatIso.toString; + d3_time.second = d3_time_interval(function(date) { + return new d3_date(Math.floor(date / 1e3) * 1e3); + }, function(date, offset) { + date.setTime(date.getTime() + Math.floor(offset) * 1e3); + }, function(date) { + return date.getSeconds(); + }); + d3_time.seconds = d3_time.second.range; + d3_time.seconds.utc = d3_time.second.utc.range; + d3_time.minute = d3_time_interval(function(date) { + return new d3_date(Math.floor(date / 6e4) * 6e4); + }, function(date, offset) { + date.setTime(date.getTime() + Math.floor(offset) * 6e4); + }, function(date) { + return date.getMinutes(); + }); + d3_time.minutes = d3_time.minute.range; + d3_time.minutes.utc = d3_time.minute.utc.range; + d3_time.hour = d3_time_interval(function(date) { + var timezone = date.getTimezoneOffset() / 60; + return new d3_date((Math.floor(date / 36e5 - timezone) + timezone) * 36e5); + }, function(date, offset) { + date.setTime(date.getTime() + Math.floor(offset) * 36e5); + }, function(date) { + return date.getHours(); + }); + d3_time.hours = d3_time.hour.range; + d3_time.hours.utc = d3_time.hour.utc.range; + d3_time.month = d3_time_interval(function(date) { + date = d3_time.day(date); + date.setDate(1); + return date; + }, function(date, offset) { + date.setMonth(date.getMonth() + offset); + }, function(date) { + return date.getMonth(); + }); + d3_time.months = d3_time.month.range; + d3_time.months.utc = d3_time.month.utc.range; + function d3_time_scale(linear, methods, format) { + function scale(x) { + return linear(x); + } + scale.invert = function(x) { + return d3_time_scaleDate(linear.invert(x)); + }; + scale.domain = function(x) { + if (!arguments.length) return linear.domain().map(d3_time_scaleDate); + linear.domain(x); + return scale; + }; + function tickMethod(extent, count) { + var span = extent[1] - extent[0], target = span / count, i = d3.bisect(d3_time_scaleSteps, target); + return i == d3_time_scaleSteps.length ? [ methods.year, d3_scale_linearTickRange(extent.map(function(d) { + return d / 31536e6; + }), count)[2] ] : !i ? [ d3_time_scaleMilliseconds, d3_scale_linearTickRange(extent, count)[2] ] : methods[target / d3_time_scaleSteps[i - 1] < d3_time_scaleSteps[i] / target ? i - 1 : i]; + } + scale.nice = function(interval, skip) { + var domain = scale.domain(), extent = d3_scaleExtent(domain), method = interval == null ? tickMethod(extent, 10) : typeof interval === "number" && tickMethod(extent, interval); + if (method) interval = method[0], skip = method[1]; + function skipped(date) { + return !isNaN(date) && !interval.range(date, d3_time_scaleDate(+date + 1), skip).length; + } + return scale.domain(d3_scale_nice(domain, skip > 1 ? { + floor: function(date) { + while (skipped(date = interval.floor(date))) date = d3_time_scaleDate(date - 1); + return date; + }, + ceil: function(date) { + while (skipped(date = interval.ceil(date))) date = d3_time_scaleDate(+date + 1); + return date; + } + } : interval)); + }; + scale.ticks = function(interval, skip) { + var extent = d3_scaleExtent(scale.domain()), method = interval == null ? tickMethod(extent, 10) : typeof interval === "number" ? tickMethod(extent, interval) : !interval.range && [ { + range: interval + }, skip ]; + if (method) interval = method[0], skip = method[1]; + return interval.range(extent[0], d3_time_scaleDate(+extent[1] + 1), skip < 1 ? 1 : skip); + }; + scale.tickFormat = function() { + return format; + }; + scale.copy = function() { + return d3_time_scale(linear.copy(), methods, format); + }; + return d3_scale_linearRebind(scale, linear); + } + function d3_time_scaleDate(t) { + return new Date(t); + } + var d3_time_scaleSteps = [ 1e3, 5e3, 15e3, 3e4, 6e4, 3e5, 9e5, 18e5, 36e5, 108e5, 216e5, 432e5, 864e5, 1728e5, 6048e5, 2592e6, 7776e6, 31536e6 ]; + var d3_time_scaleLocalMethods = [ [ d3_time.second, 1 ], [ d3_time.second, 5 ], [ d3_time.second, 15 ], [ d3_time.second, 30 ], [ d3_time.minute, 1 ], [ d3_time.minute, 5 ], [ d3_time.minute, 15 ], [ d3_time.minute, 30 ], [ d3_time.hour, 1 ], [ d3_time.hour, 3 ], [ d3_time.hour, 6 ], [ d3_time.hour, 12 ], [ d3_time.day, 1 ], [ d3_time.day, 2 ], [ d3_time.week, 1 ], [ d3_time.month, 1 ], [ d3_time.month, 3 ], [ d3_time.year, 1 ] ]; + var d3_time_scaleLocalFormat = d3_time_format.multi([ [ ".%L", function(d) { + return d.getMilliseconds(); + } ], [ ":%S", function(d) { + return d.getSeconds(); + } ], [ "%I:%M", function(d) { + return d.getMinutes(); + } ], [ "%I %p", function(d) { + return d.getHours(); + } ], [ "%a %d", function(d) { + return d.getDay() && d.getDate() != 1; + } ], [ "%b %d", function(d) { + return d.getDate() != 1; + } ], [ "%B", function(d) { + return d.getMonth(); + } ], [ "%Y", d3_true ] ]); + var d3_time_scaleMilliseconds = { + range: function(start, stop, step) { + return d3.range(Math.ceil(start / step) * step, +stop, step).map(d3_time_scaleDate); + }, + floor: d3_identity, + ceil: d3_identity + }; + d3_time_scaleLocalMethods.year = d3_time.year; + d3_time.scale = function() { + return d3_time_scale(d3.scale.linear(), d3_time_scaleLocalMethods, d3_time_scaleLocalFormat); + }; + var d3_time_scaleUtcMethods = d3_time_scaleLocalMethods.map(function(m) { + return [ m[0].utc, m[1] ]; + }); + var d3_time_scaleUtcFormat = d3_time_formatUtc.multi([ [ ".%L", function(d) { + return d.getUTCMilliseconds(); + } ], [ ":%S", function(d) { + return d.getUTCSeconds(); + } ], [ "%I:%M", function(d) { + return d.getUTCMinutes(); + } ], [ "%I %p", function(d) { + return d.getUTCHours(); + } ], [ "%a %d", function(d) { + return d.getUTCDay() && d.getUTCDate() != 1; + } ], [ "%b %d", function(d) { + return d.getUTCDate() != 1; + } ], [ "%B", function(d) { + return d.getUTCMonth(); + } ], [ "%Y", d3_true ] ]); + d3_time_scaleUtcMethods.year = d3_time.year.utc; + d3_time.scale.utc = function() { + return d3_time_scale(d3.scale.linear(), d3_time_scaleUtcMethods, d3_time_scaleUtcFormat); + }; + d3.text = d3_xhrType(function(request) { + return request.responseText; + }); + d3.json = function(url, callback) { + return d3_xhr(url, "application/json", d3_json, callback); + }; + function d3_json(request) { + return JSON.parse(request.responseText); + } + d3.html = function(url, callback) { + return d3_xhr(url, "text/html", d3_html, callback); + }; + function d3_html(request) { + var range = d3_document.createRange(); + range.selectNode(d3_document.body); + return range.createContextualFragment(request.responseText); + } + d3.xml = d3_xhrType(function(request) { + return request.responseXML; + }); + if (typeof define === "function" && define.amd) define(d3); else if (typeof module === "object" && module.exports) module.exports = d3; + this.d3 = d3; +}(); \ No newline at end of file diff --git a/play/js/d3/d3.min.js b/play/js/d3/d3.min.js new file mode 100644 index 00000000..34d5513e --- /dev/null +++ b/play/js/d3/d3.min.js @@ -0,0 +1,5 @@ +!function(){function n(n){return n&&(n.ownerDocument||n.document||n).documentElement}function t(n){return n&&(n.ownerDocument&&n.ownerDocument.defaultView||n.document&&n||n.defaultView)}function e(n,t){return t>n?-1:n>t?1:n>=t?0:0/0}function r(n){return null===n?0/0:+n}function u(n){return!isNaN(n)}function i(n){return{left:function(t,e,r,u){for(arguments.length<3&&(r=0),arguments.length<4&&(u=t.length);u>r;){var i=r+u>>>1;n(t[i],e)<0?r=i+1:u=i}return r},right:function(t,e,r,u){for(arguments.length<3&&(r=0),arguments.length<4&&(u=t.length);u>r;){var i=r+u>>>1;n(t[i],e)>0?u=i:r=i+1}return r}}}function o(n){return n.length}function a(n){for(var t=1;n*t%1;)t*=10;return t}function c(n,t){for(var e in t)Object.defineProperty(n.prototype,e,{value:t[e],enumerable:!1})}function l(){this._=Object.create(null)}function s(n){return(n+="")===pa||n[0]===va?va+n:n}function f(n){return(n+="")[0]===va?n.slice(1):n}function h(n){return s(n)in this._}function g(n){return(n=s(n))in this._&&delete this._[n]}function p(){var n=[];for(var t in this._)n.push(f(t));return n}function v(){var n=0;for(var t in this._)++n;return n}function d(){for(var n in this._)return!1;return!0}function m(){this._=Object.create(null)}function y(n){return n}function M(n,t,e){return function(){var r=e.apply(t,arguments);return r===t?n:r}}function x(n,t){if(t in n)return t;t=t.charAt(0).toUpperCase()+t.slice(1);for(var e=0,r=da.length;r>e;++e){var u=da[e]+t;if(u in n)return u}}function b(){}function _(){}function w(n){function t(){for(var t,r=e,u=-1,i=r.length;++u<i;)(t=r[u].on)&&t.apply(this,arguments);return n}var e=[],r=new l;return t.on=function(t,u){var i,o=r.get(t);return arguments.length<2?o&&o.on:(o&&(o.on=null,e=e.slice(0,i=e.indexOf(o)).concat(e.slice(i+1)),r.remove(t)),u&&e.push(r.set(t,{on:u})),n)},t}function S(){ta.event.preventDefault()}function k(){for(var n,t=ta.event;n=t.sourceEvent;)t=n;return t}function E(n){for(var t=new _,e=0,r=arguments.length;++e<r;)t[arguments[e]]=w(t);return t.of=function(e,r){return function(u){try{var i=u.sourceEvent=ta.event;u.target=n,ta.event=u,t[u.type].apply(e,r)}finally{ta.event=i}}},t}function A(n){return ya(n,_a),n}function N(n){return"function"==typeof n?n:function(){return Ma(n,this)}}function C(n){return"function"==typeof n?n:function(){return xa(n,this)}}function z(n,t){function e(){this.removeAttribute(n)}function r(){this.removeAttributeNS(n.space,n.local)}function u(){this.setAttribute(n,t)}function i(){this.setAttributeNS(n.space,n.local,t)}function o(){var e=t.apply(this,arguments);null==e?this.removeAttribute(n):this.setAttribute(n,e)}function a(){var e=t.apply(this,arguments);null==e?this.removeAttributeNS(n.space,n.local):this.setAttributeNS(n.space,n.local,e)}return n=ta.ns.qualify(n),null==t?n.local?r:e:"function"==typeof t?n.local?a:o:n.local?i:u}function q(n){return n.trim().replace(/\s+/g," ")}function L(n){return new RegExp("(?:^|\\s+)"+ta.requote(n)+"(?:\\s+|$)","g")}function T(n){return(n+"").trim().split(/^|\s+/)}function R(n,t){function e(){for(var e=-1;++e<u;)n[e](this,t)}function r(){for(var e=-1,r=t.apply(this,arguments);++e<u;)n[e](this,r)}n=T(n).map(D);var u=n.length;return"function"==typeof t?r:e}function D(n){var t=L(n);return function(e,r){if(u=e.classList)return r?u.add(n):u.remove(n);var u=e.getAttribute("class")||"";r?(t.lastIndex=0,t.test(u)||e.setAttribute("class",q(u+" "+n))):e.setAttribute("class",q(u.replace(t," ")))}}function P(n,t,e){function r(){this.style.removeProperty(n)}function u(){this.style.setProperty(n,t,e)}function i(){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?i:u}function U(n,t){function e(){delete this[n]}function r(){this[n]=t}function u(){var e=t.apply(this,arguments);null==e?delete this[n]:this[n]=e}return null==t?e:"function"==typeof t?u:r}function j(n){function t(){var t=this.ownerDocument,e=this.namespaceURI;return e?t.createElementNS(e,n):t.createElement(n)}function e(){return this.ownerDocument.createElementNS(n.space,n.local)}return"function"==typeof n?n:(n=ta.ns.qualify(n)).local?e:t}function F(){var n=this.parentNode;n&&n.removeChild(this)}function H(n){return{__data__:n}}function O(n){return function(){return ba(this,n)}}function I(n){return arguments.length||(n=e),function(t,e){return t&&e?n(t.__data__,e.__data__):!t-!e}}function Y(n,t){for(var e=0,r=n.length;r>e;e++)for(var u,i=n[e],o=0,a=i.length;a>o;o++)(u=i[o])&&t(u,o,e);return n}function Z(n){return ya(n,Sa),n}function V(n){var t,e;return function(r,u,i){var o,a=n[i].update,c=a.length;for(i!=e&&(e=i,t=0),u>=t&&(t=u+1);!(o=a[t])&&++t<c;);return o}}function X(n,t,e){function r(){var t=this[o];t&&(this.removeEventListener(n,t,t.$),delete this[o])}function u(){var u=c(t,ra(arguments));r.call(this),this.addEventListener(n,this[o]=u,u.$=e),u._=t}function i(){var t,e=new RegExp("^__on([^.]+)"+ta.requote(n)+"$");for(var r in this)if(t=r.match(e)){var u=this[r];this.removeEventListener(t[1],u,u.$),delete this[r]}}var o="__on"+n,a=n.indexOf("."),c=$;a>0&&(n=n.slice(0,a));var l=ka.get(n);return l&&(n=l,c=B),a?t?u:r:t?b:i}function $(n,t){return function(e){var r=ta.event;ta.event=e,t[0]=this.__data__;try{n.apply(this,t)}finally{ta.event=r}}}function B(n,t){var e=$(n,t);return function(n){var t=this,r=n.relatedTarget;r&&(r===t||8&r.compareDocumentPosition(t))||e.call(t,n)}}function W(e){var r=".dragsuppress-"+ ++Aa,u="click"+r,i=ta.select(t(e)).on("touchmove"+r,S).on("dragstart"+r,S).on("selectstart"+r,S);if(null==Ea&&(Ea="onselectstart"in e?!1:x(e.style,"userSelect")),Ea){var o=n(e).style,a=o[Ea];o[Ea]="none"}return function(n){if(i.on(r,null),Ea&&(o[Ea]=a),n){var t=function(){i.on(u,null)};i.on(u,function(){S(),t()},!0),setTimeout(t,0)}}}function J(n,e){e.changedTouches&&(e=e.changedTouches[0]);var r=n.ownerSVGElement||n;if(r.createSVGPoint){var u=r.createSVGPoint();if(0>Na){var i=t(n);if(i.scrollX||i.scrollY){r=ta.select("body").append("svg").style({position:"absolute",top:0,left:0,margin:0,padding:0,border:"none"},"important");var o=r[0][0].getScreenCTM();Na=!(o.f||o.e),r.remove()}}return Na?(u.x=e.pageX,u.y=e.pageY):(u.x=e.clientX,u.y=e.clientY),u=u.matrixTransform(n.getScreenCTM().inverse()),[u.x,u.y]}var a=n.getBoundingClientRect();return[e.clientX-a.left-n.clientLeft,e.clientY-a.top-n.clientTop]}function G(){return ta.event.changedTouches[0].identifier}function K(n){return n>0?1:0>n?-1:0}function Q(n,t,e){return(t[0]-n[0])*(e[1]-n[1])-(t[1]-n[1])*(e[0]-n[0])}function nt(n){return n>1?0:-1>n?qa:Math.acos(n)}function tt(n){return n>1?Ra:-1>n?-Ra:Math.asin(n)}function et(n){return((n=Math.exp(n))-1/n)/2}function rt(n){return((n=Math.exp(n))+1/n)/2}function ut(n){return((n=Math.exp(2*n))-1)/(n+1)}function it(n){return(n=Math.sin(n/2))*n}function ot(){}function at(n,t,e){return this instanceof at?(this.h=+n,this.s=+t,void(this.l=+e)):arguments.length<2?n instanceof at?new at(n.h,n.s,n.l):bt(""+n,_t,at):new at(n,t,e)}function ct(n,t,e){function r(n){return n>360?n-=360:0>n&&(n+=360),60>n?i+(o-i)*n/60:180>n?o:240>n?i+(o-i)*(240-n)/60:i}function u(n){return Math.round(255*r(n))}var i,o;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,o=.5>=e?e*(1+t):e+t-e*t,i=2*e-o,new mt(u(n+120),u(n),u(n-120))}function lt(n,t,e){return this instanceof lt?(this.h=+n,this.c=+t,void(this.l=+e)):arguments.length<2?n instanceof lt?new lt(n.h,n.c,n.l):n instanceof ft?gt(n.l,n.a,n.b):gt((n=wt((n=ta.rgb(n)).r,n.g,n.b)).l,n.a,n.b):new lt(n,t,e)}function st(n,t,e){return isNaN(n)&&(n=0),isNaN(t)&&(t=0),new ft(e,Math.cos(n*=Da)*t,Math.sin(n)*t)}function ft(n,t,e){return this instanceof ft?(this.l=+n,this.a=+t,void(this.b=+e)):arguments.length<2?n instanceof ft?new ft(n.l,n.a,n.b):n instanceof lt?st(n.h,n.c,n.l):wt((n=mt(n)).r,n.g,n.b):new ft(n,t,e)}function ht(n,t,e){var r=(n+16)/116,u=r+t/500,i=r-e/200;return u=pt(u)*Xa,r=pt(r)*$a,i=pt(i)*Ba,new mt(dt(3.2404542*u-1.5371385*r-.4985314*i),dt(-.969266*u+1.8760108*r+.041556*i),dt(.0556434*u-.2040259*r+1.0572252*i))}function gt(n,t,e){return n>0?new lt(Math.atan2(e,t)*Pa,Math.sqrt(t*t+e*e),n):new lt(0/0,0/0,n)}function pt(n){return n>.206893034?n*n*n:(n-4/29)/7.787037}function vt(n){return n>.008856?Math.pow(n,1/3):7.787037*n+4/29}function dt(n){return Math.round(255*(.00304>=n?12.92*n:1.055*Math.pow(n,1/2.4)-.055))}function mt(n,t,e){return this instanceof mt?(this.r=~~n,this.g=~~t,void(this.b=~~e)):arguments.length<2?n instanceof mt?new mt(n.r,n.g,n.b):bt(""+n,mt,ct):new mt(n,t,e)}function yt(n){return new mt(n>>16,n>>8&255,255&n)}function Mt(n){return yt(n)+""}function xt(n){return 16>n?"0"+Math.max(0,n).toString(16):Math.min(255,n).toString(16)}function bt(n,t,e){var r,u,i,o=0,a=0,c=0;if(r=/([a-z]+)\((.*)\)/i.exec(n))switch(u=r[2].split(","),r[1]){case"hsl":return e(parseFloat(u[0]),parseFloat(u[1])/100,parseFloat(u[2])/100);case"rgb":return t(kt(u[0]),kt(u[1]),kt(u[2]))}return(i=Ga.get(n.toLowerCase()))?t(i.r,i.g,i.b):(null==n||"#"!==n.charAt(0)||isNaN(i=parseInt(n.slice(1),16))||(4===n.length?(o=(3840&i)>>4,o=o>>4|o,a=240&i,a=a>>4|a,c=15&i,c=c<<4|c):7===n.length&&(o=(16711680&i)>>16,a=(65280&i)>>8,c=255&i)),t(o,a,c))}function _t(n,t,e){var r,u,i=Math.min(n/=255,t/=255,e/=255),o=Math.max(n,t,e),a=o-i,c=(o+i)/2;return a?(u=.5>c?a/(o+i):a/(2-o-i),r=n==o?(t-e)/a+(e>t?6:0):t==o?(e-n)/a+2:(n-t)/a+4,r*=60):(r=0/0,u=c>0&&1>c?0:r),new at(r,u,c)}function wt(n,t,e){n=St(n),t=St(t),e=St(e);var r=vt((.4124564*n+.3575761*t+.1804375*e)/Xa),u=vt((.2126729*n+.7151522*t+.072175*e)/$a),i=vt((.0193339*n+.119192*t+.9503041*e)/Ba);return ft(116*u-16,500*(r-u),200*(u-i))}function St(n){return(n/=255)<=.04045?n/12.92:Math.pow((n+.055)/1.055,2.4)}function kt(n){var t=parseFloat(n);return"%"===n.charAt(n.length-1)?Math.round(2.55*t):t}function Et(n){return"function"==typeof n?n:function(){return n}}function At(n){return function(t,e,r){return 2===arguments.length&&"function"==typeof e&&(r=e,e=null),Nt(t,e,n,r)}}function Nt(n,t,e,r){function u(){var n,t=c.status;if(!t&&zt(c)||t>=200&&300>t||304===t){try{n=e.call(i,c)}catch(r){return void o.error.call(i,r)}o.load.call(i,n)}else o.error.call(i,c)}var i={},o=ta.dispatch("beforesend","progress","load","error"),a={},c=new XMLHttpRequest,l=null;return!this.XDomainRequest||"withCredentials"in c||!/^(http(s)?:)?\/\//.test(n)||(c=new XDomainRequest),"onload"in c?c.onload=c.onerror=u:c.onreadystatechange=function(){c.readyState>3&&u()},c.onprogress=function(n){var t=ta.event;ta.event=n;try{o.progress.call(i,c)}finally{ta.event=t}},i.header=function(n,t){return n=(n+"").toLowerCase(),arguments.length<2?a[n]:(null==t?delete a[n]:a[n]=t+"",i)},i.mimeType=function(n){return arguments.length?(t=null==n?null:n+"",i):t},i.responseType=function(n){return arguments.length?(l=n,i):l},i.response=function(n){return e=n,i},["get","post"].forEach(function(n){i[n]=function(){return i.send.apply(i,[n].concat(ra(arguments)))}}),i.send=function(e,r,u){if(2===arguments.length&&"function"==typeof r&&(u=r,r=null),c.open(e,n,!0),null==t||"accept"in a||(a.accept=t+",*/*"),c.setRequestHeader)for(var s in a)c.setRequestHeader(s,a[s]);return null!=t&&c.overrideMimeType&&c.overrideMimeType(t),null!=l&&(c.responseType=l),null!=u&&i.on("error",u).on("load",function(n){u(null,n)}),o.beforesend.call(i,c),c.send(null==r?null:r),i},i.abort=function(){return c.abort(),i},ta.rebind(i,o,"on"),null==r?i:i.get(Ct(r))}function Ct(n){return 1===n.length?function(t,e){n(null==t?e:null)}:n}function zt(n){var t=n.responseType;return t&&"text"!==t?n.response:n.responseText}function qt(){var n=Lt(),t=Tt()-n;t>24?(isFinite(t)&&(clearTimeout(tc),tc=setTimeout(qt,t)),nc=0):(nc=1,rc(qt))}function Lt(){var n=Date.now();for(ec=Ka;ec;)n>=ec.t&&(ec.f=ec.c(n-ec.t)),ec=ec.n;return n}function Tt(){for(var n,t=Ka,e=1/0;t;)t.f?t=n?n.n=t.n:Ka=t.n:(t.t<e&&(e=t.t),t=(n=t).n);return Qa=n,e}function Rt(n,t){return t-(n?Math.ceil(Math.log(n)/Math.LN10):1)}function Dt(n,t){var e=Math.pow(10,3*ga(8-t));return{scale:t>8?function(n){return n/e}:function(n){return n*e},symbol:n}}function Pt(n){var t=n.decimal,e=n.thousands,r=n.grouping,u=n.currency,i=r&&e?function(n,t){for(var u=n.length,i=[],o=0,a=r[0],c=0;u>0&&a>0&&(c+a+1>t&&(a=Math.max(1,t-c)),i.push(n.substring(u-=a,u+a)),!((c+=a+1)>t));)a=r[o=(o+1)%r.length];return i.reverse().join(e)}:y;return function(n){var e=ic.exec(n),r=e[1]||" ",o=e[2]||">",a=e[3]||"-",c=e[4]||"",l=e[5],s=+e[6],f=e[7],h=e[8],g=e[9],p=1,v="",d="",m=!1,y=!0;switch(h&&(h=+h.substring(1)),(l||"0"===r&&"="===o)&&(l=r="0",o="="),g){case"n":f=!0,g="g";break;case"%":p=100,d="%",g="f";break;case"p":p=100,d="%",g="r";break;case"b":case"o":case"x":case"X":"#"===c&&(v="0"+g.toLowerCase());case"c":y=!1;case"d":m=!0,h=0;break;case"s":p=-1,g="r"}"$"===c&&(v=u[0],d=u[1]),"r"!=g||h||(g="g"),null!=h&&("g"==g?h=Math.max(1,Math.min(21,h)):("e"==g||"f"==g)&&(h=Math.max(0,Math.min(20,h)))),g=oc.get(g)||Ut;var M=l&&f;return function(n){var e=d;if(m&&n%1)return"";var u=0>n||0===n&&0>1/n?(n=-n,"-"):"-"===a?"":a;if(0>p){var c=ta.formatPrefix(n,h);n=c.scale(n),e=c.symbol+d}else n*=p;n=g(n,h);var x,b,_=n.lastIndexOf(".");if(0>_){var w=y?n.lastIndexOf("e"):-1;0>w?(x=n,b=""):(x=n.substring(0,w),b=n.substring(w))}else x=n.substring(0,_),b=t+n.substring(_+1);!l&&f&&(x=i(x,1/0));var S=v.length+x.length+b.length+(M?0:u.length),k=s>S?new Array(S=s-S+1).join(r):"";return M&&(x=i(k+x,k.length?s-b.length:1/0)),u+=v,n=x+b,("<"===o?u+n+k:">"===o?k+u+n:"^"===o?k.substring(0,S>>=1)+u+n+k.substring(S):u+(M?n:k+n))+e}}}function Ut(n){return n+""}function jt(){this._=new Date(arguments.length>1?Date.UTC.apply(this,arguments):arguments[0])}function Ft(n,t,e){function r(t){var e=n(t),r=i(e,1);return r-t>t-e?e:r}function u(e){return t(e=n(new cc(e-1)),1),e}function i(n,e){return t(n=new cc(+n),e),n}function o(n,r,i){var o=u(n),a=[];if(i>1)for(;r>o;)e(o)%i||a.push(new Date(+o)),t(o,1);else for(;r>o;)a.push(new Date(+o)),t(o,1);return a}function a(n,t,e){try{cc=jt;var r=new jt;return r._=n,o(r,t,e)}finally{cc=Date}}n.floor=n,n.round=r,n.ceil=u,n.offset=i,n.range=o;var c=n.utc=Ht(n);return c.floor=c,c.round=Ht(r),c.ceil=Ht(u),c.offset=Ht(i),c.range=a,n}function Ht(n){return function(t,e){try{cc=jt;var r=new jt;return r._=t,n(r,e)._}finally{cc=Date}}}function Ot(n){function t(n){function t(t){for(var e,u,i,o=[],a=-1,c=0;++a<r;)37===n.charCodeAt(a)&&(o.push(n.slice(c,a)),null!=(u=sc[e=n.charAt(++a)])&&(e=n.charAt(++a)),(i=N[e])&&(e=i(t,null==u?"e"===e?" ":"0":u)),o.push(e),c=a+1);return o.push(n.slice(c,a)),o.join("")}var r=n.length;return t.parse=function(t){var r={y:1900,m:0,d:1,H:0,M:0,S:0,L:0,Z:null},u=e(r,n,t,0);if(u!=t.length)return null;"p"in r&&(r.H=r.H%12+12*r.p);var i=null!=r.Z&&cc!==jt,o=new(i?jt:cc);return"j"in r?o.setFullYear(r.y,0,r.j):"w"in r&&("W"in r||"U"in r)?(o.setFullYear(r.y,0,1),o.setFullYear(r.y,0,"W"in r?(r.w+6)%7+7*r.W-(o.getDay()+5)%7:r.w+7*r.U-(o.getDay()+6)%7)):o.setFullYear(r.y,r.m,r.d),o.setHours(r.H+(r.Z/100|0),r.M+r.Z%100,r.S,r.L),i?o._:o},t.toString=function(){return n},t}function e(n,t,e,r){for(var u,i,o,a=0,c=t.length,l=e.length;c>a;){if(r>=l)return-1;if(u=t.charCodeAt(a++),37===u){if(o=t.charAt(a++),i=C[o in sc?t.charAt(a++):o],!i||(r=i(n,e,r))<0)return-1}else if(u!=e.charCodeAt(r++))return-1}return r}function r(n,t,e){_.lastIndex=0;var r=_.exec(t.slice(e));return r?(n.w=w.get(r[0].toLowerCase()),e+r[0].length):-1}function u(n,t,e){x.lastIndex=0;var r=x.exec(t.slice(e));return r?(n.w=b.get(r[0].toLowerCase()),e+r[0].length):-1}function i(n,t,e){E.lastIndex=0;var r=E.exec(t.slice(e));return r?(n.m=A.get(r[0].toLowerCase()),e+r[0].length):-1}function o(n,t,e){S.lastIndex=0;var r=S.exec(t.slice(e));return r?(n.m=k.get(r[0].toLowerCase()),e+r[0].length):-1}function a(n,t,r){return e(n,N.c.toString(),t,r)}function c(n,t,r){return e(n,N.x.toString(),t,r)}function l(n,t,r){return e(n,N.X.toString(),t,r)}function s(n,t,e){var r=M.get(t.slice(e,e+=2).toLowerCase());return null==r?-1:(n.p=r,e)}var f=n.dateTime,h=n.date,g=n.time,p=n.periods,v=n.days,d=n.shortDays,m=n.months,y=n.shortMonths;t.utc=function(n){function e(n){try{cc=jt;var t=new cc;return t._=n,r(t)}finally{cc=Date}}var r=t(n);return e.parse=function(n){try{cc=jt;var t=r.parse(n);return t&&t._}finally{cc=Date}},e.toString=r.toString,e},t.multi=t.utc.multi=ae;var M=ta.map(),x=Yt(v),b=Zt(v),_=Yt(d),w=Zt(d),S=Yt(m),k=Zt(m),E=Yt(y),A=Zt(y);p.forEach(function(n,t){M.set(n.toLowerCase(),t)});var N={a:function(n){return d[n.getDay()]},A:function(n){return v[n.getDay()]},b:function(n){return y[n.getMonth()]},B:function(n){return m[n.getMonth()]},c:t(f),d:function(n,t){return It(n.getDate(),t,2)},e:function(n,t){return It(n.getDate(),t,2)},H:function(n,t){return It(n.getHours(),t,2)},I:function(n,t){return It(n.getHours()%12||12,t,2)},j:function(n,t){return It(1+ac.dayOfYear(n),t,3)},L:function(n,t){return It(n.getMilliseconds(),t,3)},m:function(n,t){return It(n.getMonth()+1,t,2)},M:function(n,t){return It(n.getMinutes(),t,2)},p:function(n){return p[+(n.getHours()>=12)]},S:function(n,t){return It(n.getSeconds(),t,2)},U:function(n,t){return It(ac.sundayOfYear(n),t,2)},w:function(n){return n.getDay()},W:function(n,t){return It(ac.mondayOfYear(n),t,2)},x:t(h),X:t(g),y:function(n,t){return It(n.getFullYear()%100,t,2)},Y:function(n,t){return It(n.getFullYear()%1e4,t,4)},Z:ie,"%":function(){return"%"}},C={a:r,A:u,b:i,B:o,c:a,d:Qt,e:Qt,H:te,I:te,j:ne,L:ue,m:Kt,M:ee,p:s,S:re,U:Xt,w:Vt,W:$t,x:c,X:l,y:Wt,Y:Bt,Z:Jt,"%":oe};return t}function It(n,t,e){var r=0>n?"-":"",u=(r?-n:n)+"",i=u.length;return r+(e>i?new Array(e-i+1).join(t)+u:u)}function Yt(n){return new RegExp("^(?:"+n.map(ta.requote).join("|")+")","i")}function Zt(n){for(var t=new l,e=-1,r=n.length;++e<r;)t.set(n[e].toLowerCase(),e);return t}function Vt(n,t,e){fc.lastIndex=0;var r=fc.exec(t.slice(e,e+1));return r?(n.w=+r[0],e+r[0].length):-1}function Xt(n,t,e){fc.lastIndex=0;var r=fc.exec(t.slice(e));return r?(n.U=+r[0],e+r[0].length):-1}function $t(n,t,e){fc.lastIndex=0;var r=fc.exec(t.slice(e));return r?(n.W=+r[0],e+r[0].length):-1}function Bt(n,t,e){fc.lastIndex=0;var r=fc.exec(t.slice(e,e+4));return r?(n.y=+r[0],e+r[0].length):-1}function Wt(n,t,e){fc.lastIndex=0;var r=fc.exec(t.slice(e,e+2));return r?(n.y=Gt(+r[0]),e+r[0].length):-1}function Jt(n,t,e){return/^[+-]\d{4}$/.test(t=t.slice(e,e+5))?(n.Z=-t,e+5):-1}function Gt(n){return n+(n>68?1900:2e3)}function Kt(n,t,e){fc.lastIndex=0;var r=fc.exec(t.slice(e,e+2));return r?(n.m=r[0]-1,e+r[0].length):-1}function Qt(n,t,e){fc.lastIndex=0;var r=fc.exec(t.slice(e,e+2));return r?(n.d=+r[0],e+r[0].length):-1}function ne(n,t,e){fc.lastIndex=0;var r=fc.exec(t.slice(e,e+3));return r?(n.j=+r[0],e+r[0].length):-1}function te(n,t,e){fc.lastIndex=0;var r=fc.exec(t.slice(e,e+2));return r?(n.H=+r[0],e+r[0].length):-1}function ee(n,t,e){fc.lastIndex=0;var r=fc.exec(t.slice(e,e+2));return r?(n.M=+r[0],e+r[0].length):-1}function re(n,t,e){fc.lastIndex=0;var r=fc.exec(t.slice(e,e+2));return r?(n.S=+r[0],e+r[0].length):-1}function ue(n,t,e){fc.lastIndex=0;var r=fc.exec(t.slice(e,e+3));return r?(n.L=+r[0],e+r[0].length):-1}function ie(n){var t=n.getTimezoneOffset(),e=t>0?"-":"+",r=ga(t)/60|0,u=ga(t)%60;return e+It(r,"0",2)+It(u,"0",2)}function oe(n,t,e){hc.lastIndex=0;var r=hc.exec(t.slice(e,e+1));return r?e+r[0].length:-1}function ae(n){for(var t=n.length,e=-1;++e<t;)n[e][0]=this(n[e][0]);return function(t){for(var e=0,r=n[e];!r[1](t);)r=n[++e];return r[0](t)}}function ce(){}function le(n,t,e){var r=e.s=n+t,u=r-n,i=r-u;e.t=n-i+(t-u)}function se(n,t){n&&dc.hasOwnProperty(n.type)&&dc[n.type](n,t)}function fe(n,t,e){var r,u=-1,i=n.length-e;for(t.lineStart();++u<i;)r=n[u],t.point(r[0],r[1],r[2]);t.lineEnd()}function he(n,t){var e=-1,r=n.length;for(t.polygonStart();++e<r;)fe(n[e],t,1);t.polygonEnd()}function ge(){function n(n,t){n*=Da,t=t*Da/2+qa/4;var e=n-r,o=e>=0?1:-1,a=o*e,c=Math.cos(t),l=Math.sin(t),s=i*l,f=u*c+s*Math.cos(a),h=s*o*Math.sin(a);yc.add(Math.atan2(h,f)),r=n,u=c,i=l}var t,e,r,u,i;Mc.point=function(o,a){Mc.point=n,r=(t=o)*Da,u=Math.cos(a=(e=a)*Da/2+qa/4),i=Math.sin(a)},Mc.lineEnd=function(){n(t,e)}}function pe(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 ve(n,t){return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]}function de(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 me(n,t){n[0]+=t[0],n[1]+=t[1],n[2]+=t[2]}function ye(n,t){return[n[0]*t,n[1]*t,n[2]*t]}function Me(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 xe(n){return[Math.atan2(n[1],n[0]),tt(n[2])]}function be(n,t){return ga(n[0]-t[0])<Ca&&ga(n[1]-t[1])<Ca}function _e(n,t){n*=Da;var e=Math.cos(t*=Da);we(e*Math.cos(n),e*Math.sin(n),Math.sin(t))}function we(n,t,e){++xc,_c+=(n-_c)/xc,wc+=(t-wc)/xc,Sc+=(e-Sc)/xc}function Se(){function n(n,u){n*=Da;var i=Math.cos(u*=Da),o=i*Math.cos(n),a=i*Math.sin(n),c=Math.sin(u),l=Math.atan2(Math.sqrt((l=e*c-r*a)*l+(l=r*o-t*c)*l+(l=t*a-e*o)*l),t*o+e*a+r*c);bc+=l,kc+=l*(t+(t=o)),Ec+=l*(e+(e=a)),Ac+=l*(r+(r=c)),we(t,e,r)}var t,e,r;qc.point=function(u,i){u*=Da;var o=Math.cos(i*=Da);t=o*Math.cos(u),e=o*Math.sin(u),r=Math.sin(i),qc.point=n,we(t,e,r)}}function ke(){qc.point=_e}function Ee(){function n(n,t){n*=Da;var e=Math.cos(t*=Da),o=e*Math.cos(n),a=e*Math.sin(n),c=Math.sin(t),l=u*c-i*a,s=i*o-r*c,f=r*a-u*o,h=Math.sqrt(l*l+s*s+f*f),g=r*o+u*a+i*c,p=h&&-nt(g)/h,v=Math.atan2(h,g);Nc+=p*l,Cc+=p*s,zc+=p*f,bc+=v,kc+=v*(r+(r=o)),Ec+=v*(u+(u=a)),Ac+=v*(i+(i=c)),we(r,u,i)}var t,e,r,u,i;qc.point=function(o,a){t=o,e=a,qc.point=n,o*=Da;var c=Math.cos(a*=Da);r=c*Math.cos(o),u=c*Math.sin(o),i=Math.sin(a),we(r,u,i)},qc.lineEnd=function(){n(t,e),qc.lineEnd=ke,qc.point=_e}}function Ae(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 Ne(){return!0}function Ce(n,t,e,r,u){var i=[],o=[];if(n.forEach(function(n){if(!((t=n.length-1)<=0)){var t,e=n[0],r=n[t];if(be(e,r)){u.lineStart();for(var a=0;t>a;++a)u.point((e=n[a])[0],e[1]);return void u.lineEnd()}var c=new qe(e,n,null,!0),l=new qe(e,null,c,!1);c.o=l,i.push(c),o.push(l),c=new qe(r,n,null,!1),l=new qe(r,null,c,!0),c.o=l,i.push(c),o.push(l)}}),o.sort(t),ze(i),ze(o),i.length){for(var a=0,c=e,l=o.length;l>a;++a)o[a].e=c=!c;for(var s,f,h=i[0];;){for(var g=h,p=!0;g.v;)if((g=g.n)===h)return;s=g.z,u.lineStart();do{if(g.v=g.o.v=!0,g.e){if(p)for(var a=0,l=s.length;l>a;++a)u.point((f=s[a])[0],f[1]);else r(g.x,g.n.x,1,u);g=g.n}else{if(p){s=g.p.z;for(var a=s.length-1;a>=0;--a)u.point((f=s[a])[0],f[1])}else r(g.x,g.p.x,-1,u);g=g.p}g=g.o,s=g.z,p=!p}while(!g.v);u.lineEnd()}}}function ze(n){if(t=n.length){for(var t,e,r=0,u=n[0];++r<t;)u.n=e=n[r],e.p=u,u=e;u.n=e=n[0],e.p=u}}function qe(n,t,e,r){this.x=n,this.z=t,this.o=e,this.e=r,this.v=!1,this.n=this.p=null}function Le(n,t,e,r){return function(u,i){function o(t,e){var r=u(t,e);n(t=r[0],e=r[1])&&i.point(t,e)}function a(n,t){var e=u(n,t);d.point(e[0],e[1])}function c(){y.point=a,d.lineStart()}function l(){y.point=o,d.lineEnd()}function s(n,t){v.push([n,t]);var e=u(n,t);x.point(e[0],e[1])}function f(){x.lineStart(),v=[]}function h(){s(v[0][0],v[0][1]),x.lineEnd();var n,t=x.clean(),e=M.buffer(),r=e.length;if(v.pop(),p.push(v),v=null,r)if(1&t){n=e[0];var u,r=n.length-1,o=-1;if(r>0){for(b||(i.polygonStart(),b=!0),i.lineStart();++o<r;)i.point((u=n[o])[0],u[1]);i.lineEnd()}}else r>1&&2&t&&e.push(e.pop().concat(e.shift())),g.push(e.filter(Te))}var g,p,v,d=t(i),m=u.invert(r[0],r[1]),y={point:o,lineStart:c,lineEnd:l,polygonStart:function(){y.point=s,y.lineStart=f,y.lineEnd=h,g=[],p=[]},polygonEnd:function(){y.point=o,y.lineStart=c,y.lineEnd=l,g=ta.merge(g);var n=Fe(m,p);g.length?(b||(i.polygonStart(),b=!0),Ce(g,De,n,e,i)):n&&(b||(i.polygonStart(),b=!0),i.lineStart(),e(null,null,1,i),i.lineEnd()),b&&(i.polygonEnd(),b=!1),g=p=null},sphere:function(){i.polygonStart(),i.lineStart(),e(null,null,1,i),i.lineEnd(),i.polygonEnd()}},M=Re(),x=t(M),b=!1;return y}}function Te(n){return n.length>1}function Re(){var n,t=[];return{lineStart:function(){t.push(n=[])},point:function(t,e){n.push([t,e])},lineEnd:b,buffer:function(){var e=t;return t=[],n=null,e},rejoin:function(){t.length>1&&t.push(t.pop().concat(t.shift()))}}}function De(n,t){return((n=n.x)[0]<0?n[1]-Ra-Ca:Ra-n[1])-((t=t.x)[0]<0?t[1]-Ra-Ca:Ra-t[1])}function Pe(n){var t,e=0/0,r=0/0,u=0/0;return{lineStart:function(){n.lineStart(),t=1},point:function(i,o){var a=i>0?qa:-qa,c=ga(i-e);ga(c-qa)<Ca?(n.point(e,r=(r+o)/2>0?Ra:-Ra),n.point(u,r),n.lineEnd(),n.lineStart(),n.point(a,r),n.point(i,r),t=0):u!==a&&c>=qa&&(ga(e-u)<Ca&&(e-=u*Ca),ga(i-a)<Ca&&(i-=a*Ca),r=Ue(e,r,i,o),n.point(u,r),n.lineEnd(),n.lineStart(),n.point(a,r),t=0),n.point(e=i,r=o),u=a},lineEnd:function(){n.lineEnd(),e=r=0/0},clean:function(){return 2-t}}}function Ue(n,t,e,r){var u,i,o=Math.sin(n-e);return ga(o)>Ca?Math.atan((Math.sin(t)*(i=Math.cos(r))*Math.sin(e)-Math.sin(r)*(u=Math.cos(t))*Math.sin(n))/(u*i*o)):(t+r)/2}function je(n,t,e,r){var u;if(null==n)u=e*Ra,r.point(-qa,u),r.point(0,u),r.point(qa,u),r.point(qa,0),r.point(qa,-u),r.point(0,-u),r.point(-qa,-u),r.point(-qa,0),r.point(-qa,u);else if(ga(n[0]-t[0])>Ca){var i=n[0]<t[0]?qa:-qa;u=e*i/2,r.point(-i,u),r.point(0,u),r.point(i,u)}else r.point(t[0],t[1])}function Fe(n,t){var e=n[0],r=n[1],u=[Math.sin(e),-Math.cos(e),0],i=0,o=0;yc.reset();for(var a=0,c=t.length;c>a;++a){var l=t[a],s=l.length;if(s)for(var f=l[0],h=f[0],g=f[1]/2+qa/4,p=Math.sin(g),v=Math.cos(g),d=1;;){d===s&&(d=0),n=l[d];var m=n[0],y=n[1]/2+qa/4,M=Math.sin(y),x=Math.cos(y),b=m-h,_=b>=0?1:-1,w=_*b,S=w>qa,k=p*M;if(yc.add(Math.atan2(k*_*Math.sin(w),v*x+k*Math.cos(w))),i+=S?b+_*La:b,S^h>=e^m>=e){var E=de(pe(f),pe(n));Me(E);var A=de(u,E);Me(A);var N=(S^b>=0?-1:1)*tt(A[2]);(r>N||r===N&&(E[0]||E[1]))&&(o+=S^b>=0?1:-1)}if(!d++)break;h=m,p=M,v=x,f=n}}return(-Ca>i||Ca>i&&0>yc)^1&o}function He(n){function t(n,t){return Math.cos(n)*Math.cos(t)>i}function e(n){var e,i,c,l,s;return{lineStart:function(){l=c=!1,s=1},point:function(f,h){var g,p=[f,h],v=t(f,h),d=o?v?0:u(f,h):v?u(f+(0>f?qa:-qa),h):0;if(!e&&(l=c=v)&&n.lineStart(),v!==c&&(g=r(e,p),(be(e,g)||be(p,g))&&(p[0]+=Ca,p[1]+=Ca,v=t(p[0],p[1]))),v!==c)s=0,v?(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(a&&e&&o^v){var m;d&i||!(m=r(p,e,!0))||(s=0,o?(n.lineStart(),n.point(m[0][0],m[0][1]),n.point(m[1][0],m[1][1]),n.lineEnd()):(n.point(m[1][0],m[1][1]),n.lineEnd(),n.lineStart(),n.point(m[0][0],m[0][1])))}!v||e&&be(e,p)||n.point(p[0],p[1]),e=p,c=v,i=d},lineEnd:function(){c&&n.lineEnd(),e=null},clean:function(){return s|(l&&c)<<1}}}function r(n,t,e){var r=pe(n),u=pe(t),o=[1,0,0],a=de(r,u),c=ve(a,a),l=a[0],s=c-l*l;if(!s)return!e&&n;var f=i*c/s,h=-i*l/s,g=de(o,a),p=ye(o,f),v=ye(a,h);me(p,v);var d=g,m=ve(p,d),y=ve(d,d),M=m*m-y*(ve(p,p)-1);if(!(0>M)){var x=Math.sqrt(M),b=ye(d,(-m-x)/y);if(me(b,p),b=xe(b),!e)return b;var _,w=n[0],S=t[0],k=n[1],E=t[1];w>S&&(_=w,w=S,S=_);var A=S-w,N=ga(A-qa)<Ca,C=N||Ca>A;if(!N&&k>E&&(_=k,k=E,E=_),C?N?k+E>0^b[1]<(ga(b[0]-w)<Ca?k:E):k<=b[1]&&b[1]<=E:A>qa^(w<=b[0]&&b[0]<=S)){var z=ye(d,(-m+x)/y);return me(z,p),[b,xe(z)]}}}function u(t,e){var r=o?n:qa-n,u=0;return-r>t?u|=1:t>r&&(u|=2),-r>e?u|=4:e>r&&(u|=8),u}var i=Math.cos(n),o=i>0,a=ga(i)>Ca,c=gr(n,6*Da);return Le(t,e,c,o?[0,-n]:[-qa,n-qa])}function Oe(n,t,e,r){return function(u){var i,o=u.a,a=u.b,c=o.x,l=o.y,s=a.x,f=a.y,h=0,g=1,p=s-c,v=f-l;if(i=n-c,p||!(i>0)){if(i/=p,0>p){if(h>i)return;g>i&&(g=i)}else if(p>0){if(i>g)return;i>h&&(h=i)}if(i=e-c,p||!(0>i)){if(i/=p,0>p){if(i>g)return;i>h&&(h=i)}else if(p>0){if(h>i)return;g>i&&(g=i)}if(i=t-l,v||!(i>0)){if(i/=v,0>v){if(h>i)return;g>i&&(g=i)}else if(v>0){if(i>g)return;i>h&&(h=i)}if(i=r-l,v||!(0>i)){if(i/=v,0>v){if(i>g)return;i>h&&(h=i)}else if(v>0){if(h>i)return;g>i&&(g=i)}return h>0&&(u.a={x:c+h*p,y:l+h*v}),1>g&&(u.b={x:c+g*p,y:l+g*v}),u}}}}}}function Ie(n,t,e,r){function u(r,u){return ga(r[0]-n)<Ca?u>0?0:3:ga(r[0]-e)<Ca?u>0?2:1:ga(r[1]-t)<Ca?u>0?1:0:u>0?3:2}function i(n,t){return o(n.x,t.x)}function o(n,t){var e=u(n,1),r=u(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]}return function(a){function c(n){for(var t=0,e=d.length,r=n[1],u=0;e>u;++u)for(var i,o=1,a=d[u],c=a.length,l=a[0];c>o;++o)i=a[o],l[1]<=r?i[1]>r&&Q(l,i,n)>0&&++t:i[1]<=r&&Q(l,i,n)<0&&--t,l=i;return 0!==t}function l(i,a,c,l){var s=0,f=0;if(null==i||(s=u(i,c))!==(f=u(a,c))||o(i,a)<0^c>0){do l.point(0===s||3===s?n:e,s>1?r:t);while((s=(s+c+4)%4)!==f)}else l.point(a[0],a[1])}function s(u,i){return u>=n&&e>=u&&i>=t&&r>=i}function f(n,t){s(n,t)&&a.point(n,t)}function h(){C.point=p,d&&d.push(m=[]),S=!0,w=!1,b=_=0/0}function g(){v&&(p(y,M),x&&w&&A.rejoin(),v.push(A.buffer())),C.point=f,w&&a.lineEnd()}function p(n,t){n=Math.max(-Tc,Math.min(Tc,n)),t=Math.max(-Tc,Math.min(Tc,t));var e=s(n,t);if(d&&m.push([n,t]),S)y=n,M=t,x=e,S=!1,e&&(a.lineStart(),a.point(n,t));else if(e&&w)a.point(n,t);else{var r={a:{x:b,y:_},b:{x:n,y:t}};N(r)?(w||(a.lineStart(),a.point(r.a.x,r.a.y)),a.point(r.b.x,r.b.y),e||a.lineEnd(),k=!1):e&&(a.lineStart(),a.point(n,t),k=!1)}b=n,_=t,w=e}var v,d,m,y,M,x,b,_,w,S,k,E=a,A=Re(),N=Oe(n,t,e,r),C={point:f,lineStart:h,lineEnd:g,polygonStart:function(){a=A,v=[],d=[],k=!0},polygonEnd:function(){a=E,v=ta.merge(v);var t=c([n,r]),e=k&&t,u=v.length;(e||u)&&(a.polygonStart(),e&&(a.lineStart(),l(null,null,1,a),a.lineEnd()),u&&Ce(v,i,t,l,a),a.polygonEnd()),v=d=m=null}};return C}}function Ye(n){var t=0,e=qa/3,r=ir(n),u=r(t,e);return u.parallels=function(n){return arguments.length?r(t=n[0]*qa/180,e=n[1]*qa/180):[t/qa*180,e/qa*180]},u}function Ze(n,t){function e(n,t){var e=Math.sqrt(i-2*u*Math.sin(t))/u;return[e*Math.sin(n*=u),o-e*Math.cos(n)]}var r=Math.sin(n),u=(r+Math.sin(t))/2,i=1+r*(2*u-r),o=Math.sqrt(i)/u;return e.invert=function(n,t){var e=o-t;return[Math.atan2(n,e)/u,tt((i-(n*n+e*e)*u*u)/(2*u))]},e}function Ve(){function n(n,t){Dc+=u*n-r*t,r=n,u=t}var t,e,r,u;Hc.point=function(i,o){Hc.point=n,t=r=i,e=u=o},Hc.lineEnd=function(){n(t,e)}}function Xe(n,t){Pc>n&&(Pc=n),n>jc&&(jc=n),Uc>t&&(Uc=t),t>Fc&&(Fc=t)}function $e(){function n(n,t){o.push("M",n,",",t,i)}function t(n,t){o.push("M",n,",",t),a.point=e}function e(n,t){o.push("L",n,",",t)}function r(){a.point=n}function u(){o.push("Z")}var i=Be(4.5),o=[],a={point:n,lineStart:function(){a.point=t},lineEnd:r,polygonStart:function(){a.lineEnd=u},polygonEnd:function(){a.lineEnd=r,a.point=n},pointRadius:function(n){return i=Be(n),a},result:function(){if(o.length){var n=o.join("");return o=[],n}}};return a}function Be(n){return"m0,"+n+"a"+n+","+n+" 0 1,1 0,"+-2*n+"a"+n+","+n+" 0 1,1 0,"+2*n+"z"}function We(n,t){_c+=n,wc+=t,++Sc}function Je(){function n(n,r){var u=n-t,i=r-e,o=Math.sqrt(u*u+i*i);kc+=o*(t+n)/2,Ec+=o*(e+r)/2,Ac+=o,We(t=n,e=r)}var t,e;Ic.point=function(r,u){Ic.point=n,We(t=r,e=u)}}function Ge(){Ic.point=We}function Ke(){function n(n,t){var e=n-r,i=t-u,o=Math.sqrt(e*e+i*i);kc+=o*(r+n)/2,Ec+=o*(u+t)/2,Ac+=o,o=u*n-r*t,Nc+=o*(r+n),Cc+=o*(u+t),zc+=3*o,We(r=n,u=t)}var t,e,r,u;Ic.point=function(i,o){Ic.point=n,We(t=r=i,e=u=o)},Ic.lineEnd=function(){n(t,e)}}function Qe(n){function t(t,e){n.moveTo(t+o,e),n.arc(t,e,o,0,La)}function e(t,e){n.moveTo(t,e),a.point=r}function r(t,e){n.lineTo(t,e)}function u(){a.point=t}function i(){n.closePath()}var o=4.5,a={point:t,lineStart:function(){a.point=e},lineEnd:u,polygonStart:function(){a.lineEnd=i},polygonEnd:function(){a.lineEnd=u,a.point=t},pointRadius:function(n){return o=n,a},result:b};return a}function nr(n){function t(n){return(a?r:e)(n)}function e(t){return rr(t,function(e,r){e=n(e,r),t.point(e[0],e[1])})}function r(t){function e(e,r){e=n(e,r),t.point(e[0],e[1])}function r(){M=0/0,S.point=i,t.lineStart()}function i(e,r){var i=pe([e,r]),o=n(e,r);u(M,x,y,b,_,w,M=o[0],x=o[1],y=e,b=i[0],_=i[1],w=i[2],a,t),t.point(M,x)}function o(){S.point=e,t.lineEnd()}function c(){r(),S.point=l,S.lineEnd=s}function l(n,t){i(f=n,h=t),g=M,p=x,v=b,d=_,m=w,S.point=i}function s(){u(M,x,y,b,_,w,g,p,f,v,d,m,a,t),S.lineEnd=o,o()}var f,h,g,p,v,d,m,y,M,x,b,_,w,S={point:e,lineStart:r,lineEnd:o,polygonStart:function(){t.polygonStart(),S.lineStart=c +},polygonEnd:function(){t.polygonEnd(),S.lineStart=r}};return S}function u(t,e,r,a,c,l,s,f,h,g,p,v,d,m){var y=s-t,M=f-e,x=y*y+M*M;if(x>4*i&&d--){var b=a+g,_=c+p,w=l+v,S=Math.sqrt(b*b+_*_+w*w),k=Math.asin(w/=S),E=ga(ga(w)-1)<Ca||ga(r-h)<Ca?(r+h)/2:Math.atan2(_,b),A=n(E,k),N=A[0],C=A[1],z=N-t,q=C-e,L=M*z-y*q;(L*L/x>i||ga((y*z+M*q)/x-.5)>.3||o>a*g+c*p+l*v)&&(u(t,e,r,a,c,l,N,C,E,b/=S,_/=S,w,d,m),m.point(N,C),u(N,C,E,b,_,w,s,f,h,g,p,v,d,m))}}var i=.5,o=Math.cos(30*Da),a=16;return t.precision=function(n){return arguments.length?(a=(i=n*n)>0&&16,t):Math.sqrt(i)},t}function tr(n){var t=nr(function(t,e){return n([t*Pa,e*Pa])});return function(n){return or(t(n))}}function er(n){this.stream=n}function rr(n,t){return{point:t,sphere:function(){n.sphere()},lineStart:function(){n.lineStart()},lineEnd:function(){n.lineEnd()},polygonStart:function(){n.polygonStart()},polygonEnd:function(){n.polygonEnd()}}}function ur(n){return ir(function(){return n})()}function ir(n){function t(n){return n=a(n[0]*Da,n[1]*Da),[n[0]*h+c,l-n[1]*h]}function e(n){return n=a.invert((n[0]-c)/h,(l-n[1])/h),n&&[n[0]*Pa,n[1]*Pa]}function r(){a=Ae(o=lr(m,M,x),i);var n=i(v,d);return c=g-n[0]*h,l=p+n[1]*h,u()}function u(){return s&&(s.valid=!1,s=null),t}var i,o,a,c,l,s,f=nr(function(n,t){return n=i(n,t),[n[0]*h+c,l-n[1]*h]}),h=150,g=480,p=250,v=0,d=0,m=0,M=0,x=0,b=Lc,_=y,w=null,S=null;return t.stream=function(n){return s&&(s.valid=!1),s=or(b(o,f(_(n)))),s.valid=!0,s},t.clipAngle=function(n){return arguments.length?(b=null==n?(w=n,Lc):He((w=+n)*Da),u()):w},t.clipExtent=function(n){return arguments.length?(S=n,_=n?Ie(n[0][0],n[0][1],n[1][0],n[1][1]):y,u()):S},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?(v=n[0]%360*Da,d=n[1]%360*Da,r()):[v*Pa,d*Pa]},t.rotate=function(n){return arguments.length?(m=n[0]%360*Da,M=n[1]%360*Da,x=n.length>2?n[2]%360*Da:0,r()):[m*Pa,M*Pa,x*Pa]},ta.rebind(t,f,"precision"),function(){return i=n.apply(this,arguments),t.invert=i.invert&&e,r()}}function or(n){return rr(n,function(t,e){n.point(t*Da,e*Da)})}function ar(n,t){return[n,t]}function cr(n,t){return[n>qa?n-La:-qa>n?n+La:n,t]}function lr(n,t,e){return n?t||e?Ae(fr(n),hr(t,e)):fr(n):t||e?hr(t,e):cr}function sr(n){return function(t,e){return t+=n,[t>qa?t-La:-qa>t?t+La:t,e]}}function fr(n){var t=sr(n);return t.invert=sr(-n),t}function hr(n,t){function e(n,t){var e=Math.cos(t),a=Math.cos(n)*e,c=Math.sin(n)*e,l=Math.sin(t),s=l*r+a*u;return[Math.atan2(c*i-s*o,a*r-l*u),tt(s*i+c*o)]}var r=Math.cos(n),u=Math.sin(n),i=Math.cos(t),o=Math.sin(t);return e.invert=function(n,t){var e=Math.cos(t),a=Math.cos(n)*e,c=Math.sin(n)*e,l=Math.sin(t),s=l*i-c*o;return[Math.atan2(c*i+l*o,a*r+s*u),tt(s*r-a*u)]},e}function gr(n,t){var e=Math.cos(n),r=Math.sin(n);return function(u,i,o,a){var c=o*t;null!=u?(u=pr(e,u),i=pr(e,i),(o>0?i>u:u>i)&&(u+=o*La)):(u=n+o*La,i=n-.5*c);for(var l,s=u;o>0?s>i:i>s;s-=c)a.point((l=xe([e,-r*Math.cos(s),-r*Math.sin(s)]))[0],l[1])}}function pr(n,t){var e=pe(t);e[0]-=n,Me(e);var r=nt(-e[1]);return((-e[2]<0?-r:r)+2*Math.PI-Ca)%(2*Math.PI)}function vr(n,t,e){var r=ta.range(n,t-Ca,e).concat(t);return function(n){return r.map(function(t){return[n,t]})}}function dr(n,t,e){var r=ta.range(n,t-Ca,e).concat(t);return function(n){return r.map(function(t){return[t,n]})}}function mr(n){return n.source}function yr(n){return n.target}function Mr(n,t,e,r){var u=Math.cos(t),i=Math.sin(t),o=Math.cos(r),a=Math.sin(r),c=u*Math.cos(n),l=u*Math.sin(n),s=o*Math.cos(e),f=o*Math.sin(e),h=2*Math.asin(Math.sqrt(it(r-t)+u*o*it(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*s,u=e*l+t*f,o=e*i+t*a;return[Math.atan2(u,r)*Pa,Math.atan2(o,Math.sqrt(r*r+u*u))*Pa]}:function(){return[n*Pa,t*Pa]};return p.distance=h,p}function xr(){function n(n,u){var i=Math.sin(u*=Da),o=Math.cos(u),a=ga((n*=Da)-t),c=Math.cos(a);Yc+=Math.atan2(Math.sqrt((a=o*Math.sin(a))*a+(a=r*i-e*o*c)*a),e*i+r*o*c),t=n,e=i,r=o}var t,e,r;Zc.point=function(u,i){t=u*Da,e=Math.sin(i*=Da),r=Math.cos(i),Zc.point=n},Zc.lineEnd=function(){Zc.point=Zc.lineEnd=b}}function br(n,t){function e(t,e){var r=Math.cos(t),u=Math.cos(e),i=n(r*u);return[i*u*Math.sin(t),i*Math.sin(e)]}return e.invert=function(n,e){var r=Math.sqrt(n*n+e*e),u=t(r),i=Math.sin(u),o=Math.cos(u);return[Math.atan2(n*i,r*o),Math.asin(r&&e*i/r)]},e}function _r(n,t){function e(n,t){o>0?-Ra+Ca>t&&(t=-Ra+Ca):t>Ra-Ca&&(t=Ra-Ca);var e=o/Math.pow(u(t),i);return[e*Math.sin(i*n),o-e*Math.cos(i*n)]}var r=Math.cos(n),u=function(n){return Math.tan(qa/4+n/2)},i=n===t?Math.sin(n):Math.log(r/Math.cos(t))/Math.log(u(t)/u(n)),o=r*Math.pow(u(n),i)/i;return i?(e.invert=function(n,t){var e=o-t,r=K(i)*Math.sqrt(n*n+e*e);return[Math.atan2(n,e)/i,2*Math.atan(Math.pow(o/r,1/i))-Ra]},e):Sr}function wr(n,t){function e(n,t){var e=i-t;return[e*Math.sin(u*n),i-e*Math.cos(u*n)]}var r=Math.cos(n),u=n===t?Math.sin(n):(r-Math.cos(t))/(t-n),i=r/u+n;return ga(u)<Ca?ar:(e.invert=function(n,t){var e=i-t;return[Math.atan2(n,e)/u,i-K(u)*Math.sqrt(n*n+e*e)]},e)}function Sr(n,t){return[n,Math.log(Math.tan(qa/4+t/2))]}function kr(n){var t,e=ur(n),r=e.scale,u=e.translate,i=e.clipExtent;return e.scale=function(){var n=r.apply(e,arguments);return n===e?t?e.clipExtent(null):e:n},e.translate=function(){var n=u.apply(e,arguments);return n===e?t?e.clipExtent(null):e:n},e.clipExtent=function(n){var o=i.apply(e,arguments);if(o===e){if(t=null==n){var a=qa*r(),c=u();i([[c[0]-a,c[1]-a],[c[0]+a,c[1]+a]])}}else t&&(o=null);return o},e.clipExtent(null)}function Er(n,t){return[Math.log(Math.tan(qa/4+t/2)),-n]}function Ar(n){return n[0]}function Nr(n){return n[1]}function Cr(n){for(var t=n.length,e=[0,1],r=2,u=2;t>u;u++){for(;r>1&&Q(n[e[r-2]],n[e[r-1]],n[u])<=0;)--r;e[r++]=u}return e.slice(0,r)}function zr(n,t){return n[0]-t[0]||n[1]-t[1]}function qr(n,t,e){return(e[0]-t[0])*(n[1]-t[1])<(e[1]-t[1])*(n[0]-t[0])}function Lr(n,t,e,r){var u=n[0],i=e[0],o=t[0]-u,a=r[0]-i,c=n[1],l=e[1],s=t[1]-c,f=r[1]-l,h=(a*(c-l)-f*(u-i))/(f*o-a*s);return[u+h*o,c+h*s]}function Tr(n){var t=n[0],e=n[n.length-1];return!(t[0]-e[0]||t[1]-e[1])}function Rr(){tu(this),this.edge=this.site=this.circle=null}function Dr(n){var t=el.pop()||new Rr;return t.site=n,t}function Pr(n){Xr(n),Qc.remove(n),el.push(n),tu(n)}function Ur(n){var t=n.circle,e=t.x,r=t.cy,u={x:e,y:r},i=n.P,o=n.N,a=[n];Pr(n);for(var c=i;c.circle&&ga(e-c.circle.x)<Ca&&ga(r-c.circle.cy)<Ca;)i=c.P,a.unshift(c),Pr(c),c=i;a.unshift(c),Xr(c);for(var l=o;l.circle&&ga(e-l.circle.x)<Ca&&ga(r-l.circle.cy)<Ca;)o=l.N,a.push(l),Pr(l),l=o;a.push(l),Xr(l);var s,f=a.length;for(s=1;f>s;++s)l=a[s],c=a[s-1],Kr(l.edge,c.site,l.site,u);c=a[0],l=a[f-1],l.edge=Jr(c.site,l.site,null,u),Vr(c),Vr(l)}function jr(n){for(var t,e,r,u,i=n.x,o=n.y,a=Qc._;a;)if(r=Fr(a,o)-i,r>Ca)a=a.L;else{if(u=i-Hr(a,o),!(u>Ca)){r>-Ca?(t=a.P,e=a):u>-Ca?(t=a,e=a.N):t=e=a;break}if(!a.R){t=a;break}a=a.R}var c=Dr(n);if(Qc.insert(t,c),t||e){if(t===e)return Xr(t),e=Dr(t.site),Qc.insert(c,e),c.edge=e.edge=Jr(t.site,c.site),Vr(t),void Vr(e);if(!e)return void(c.edge=Jr(t.site,c.site));Xr(t),Xr(e);var l=t.site,s=l.x,f=l.y,h=n.x-s,g=n.y-f,p=e.site,v=p.x-s,d=p.y-f,m=2*(h*d-g*v),y=h*h+g*g,M=v*v+d*d,x={x:(d*y-g*M)/m+s,y:(h*M-v*y)/m+f};Kr(e.edge,l,p,x),c.edge=Jr(l,n,null,x),e.edge=Jr(n,p,null,x),Vr(t),Vr(e)}}function Fr(n,t){var e=n.site,r=e.x,u=e.y,i=u-t;if(!i)return r;var o=n.P;if(!o)return-1/0;e=o.site;var a=e.x,c=e.y,l=c-t;if(!l)return a;var s=a-r,f=1/i-1/l,h=s/l;return f?(-h+Math.sqrt(h*h-2*f*(s*s/(-2*l)-c+l/2+u-i/2)))/f+r:(r+a)/2}function Hr(n,t){var e=n.N;if(e)return Fr(e,t);var r=n.site;return r.y===t?r.x:1/0}function Or(n){this.site=n,this.edges=[]}function Ir(n){for(var t,e,r,u,i,o,a,c,l,s,f=n[0][0],h=n[1][0],g=n[0][1],p=n[1][1],v=Kc,d=v.length;d--;)if(i=v[d],i&&i.prepare())for(a=i.edges,c=a.length,o=0;c>o;)s=a[o].end(),r=s.x,u=s.y,l=a[++o%c].start(),t=l.x,e=l.y,(ga(r-t)>Ca||ga(u-e)>Ca)&&(a.splice(o,0,new Qr(Gr(i.site,s,ga(r-f)<Ca&&p-u>Ca?{x:f,y:ga(t-f)<Ca?e:p}:ga(u-p)<Ca&&h-r>Ca?{x:ga(e-p)<Ca?t:h,y:p}:ga(r-h)<Ca&&u-g>Ca?{x:h,y:ga(t-h)<Ca?e:g}:ga(u-g)<Ca&&r-f>Ca?{x:ga(e-g)<Ca?t:f,y:g}:null),i.site,null)),++c)}function Yr(n,t){return t.angle-n.angle}function Zr(){tu(this),this.x=this.y=this.arc=this.site=this.cy=null}function Vr(n){var t=n.P,e=n.N;if(t&&e){var r=t.site,u=n.site,i=e.site;if(r!==i){var o=u.x,a=u.y,c=r.x-o,l=r.y-a,s=i.x-o,f=i.y-a,h=2*(c*f-l*s);if(!(h>=-za)){var g=c*c+l*l,p=s*s+f*f,v=(f*g-l*p)/h,d=(c*p-s*g)/h,f=d+a,m=rl.pop()||new Zr;m.arc=n,m.site=u,m.x=v+o,m.y=f+Math.sqrt(v*v+d*d),m.cy=f,n.circle=m;for(var y=null,M=tl._;M;)if(m.y<M.y||m.y===M.y&&m.x<=M.x){if(!M.L){y=M.P;break}M=M.L}else{if(!M.R){y=M;break}M=M.R}tl.insert(y,m),y||(nl=m)}}}}function Xr(n){var t=n.circle;t&&(t.P||(nl=t.N),tl.remove(t),rl.push(t),tu(t),n.circle=null)}function $r(n){for(var t,e=Gc,r=Oe(n[0][0],n[0][1],n[1][0],n[1][1]),u=e.length;u--;)t=e[u],(!Br(t,n)||!r(t)||ga(t.a.x-t.b.x)<Ca&&ga(t.a.y-t.b.y)<Ca)&&(t.a=t.b=null,e.splice(u,1))}function Br(n,t){var e=n.b;if(e)return!0;var r,u,i=n.a,o=t[0][0],a=t[1][0],c=t[0][1],l=t[1][1],s=n.l,f=n.r,h=s.x,g=s.y,p=f.x,v=f.y,d=(h+p)/2,m=(g+v)/2;if(v===g){if(o>d||d>=a)return;if(h>p){if(i){if(i.y>=l)return}else i={x:d,y:c};e={x:d,y:l}}else{if(i){if(i.y<c)return}else i={x:d,y:l};e={x:d,y:c}}}else if(r=(h-p)/(v-g),u=m-r*d,-1>r||r>1)if(h>p){if(i){if(i.y>=l)return}else i={x:(c-u)/r,y:c};e={x:(l-u)/r,y:l}}else{if(i){if(i.y<c)return}else i={x:(l-u)/r,y:l};e={x:(c-u)/r,y:c}}else if(v>g){if(i){if(i.x>=a)return}else i={x:o,y:r*o+u};e={x:a,y:r*a+u}}else{if(i){if(i.x<o)return}else i={x:a,y:r*a+u};e={x:o,y:r*o+u}}return n.a=i,n.b=e,!0}function Wr(n,t){this.l=n,this.r=t,this.a=this.b=null}function Jr(n,t,e,r){var u=new Wr(n,t);return Gc.push(u),e&&Kr(u,n,t,e),r&&Kr(u,t,n,r),Kc[n.i].edges.push(new Qr(u,n,t)),Kc[t.i].edges.push(new Qr(u,t,n)),u}function Gr(n,t,e){var r=new Wr(n,null);return r.a=t,r.b=e,Gc.push(r),r}function Kr(n,t,e,r){n.a||n.b?n.l===e?n.b=r:n.a=r:(n.a=r,n.l=t,n.r=e)}function Qr(n,t,e){var r=n.a,u=n.b;this.edge=n,this.site=t,this.angle=e?Math.atan2(e.y-t.y,e.x-t.x):n.l===t?Math.atan2(u.x-r.x,r.y-u.y):Math.atan2(r.x-u.x,u.y-r.y)}function nu(){this._=null}function tu(n){n.U=n.C=n.L=n.R=n.P=n.N=null}function eu(n,t){var e=t,r=t.R,u=e.U;u?u.L===e?u.L=r:u.R=r:n._=r,r.U=u,e.U=r,e.R=r.L,e.R&&(e.R.U=e),r.L=e}function ru(n,t){var e=t,r=t.L,u=e.U;u?u.L===e?u.L=r:u.R=r:n._=r,r.U=u,e.U=r,e.L=r.R,e.L&&(e.L.U=e),r.R=e}function uu(n){for(;n.L;)n=n.L;return n}function iu(n,t){var e,r,u,i=n.sort(ou).pop();for(Gc=[],Kc=new Array(n.length),Qc=new nu,tl=new nu;;)if(u=nl,i&&(!u||i.y<u.y||i.y===u.y&&i.x<u.x))(i.x!==e||i.y!==r)&&(Kc[i.i]=new Or(i),jr(i),e=i.x,r=i.y),i=n.pop();else{if(!u)break;Ur(u.arc)}t&&($r(t),Ir(t));var o={cells:Kc,edges:Gc};return Qc=tl=Gc=Kc=null,o}function ou(n,t){return t.y-n.y||t.x-n.x}function au(n,t,e){return(n.x-e.x)*(t.y-n.y)-(n.x-t.x)*(e.y-n.y)}function cu(n){return n.x}function lu(n){return n.y}function su(){return{leaf:!0,nodes:[],point:null,x:null,y:null}}function fu(n,t,e,r,u,i){if(!n(t,e,r,u,i)){var o=.5*(e+u),a=.5*(r+i),c=t.nodes;c[0]&&fu(n,c[0],e,r,o,a),c[1]&&fu(n,c[1],o,r,u,a),c[2]&&fu(n,c[2],e,a,o,i),c[3]&&fu(n,c[3],o,a,u,i)}}function hu(n,t,e,r,u,i,o){var a,c=1/0;return function l(n,s,f,h,g){if(!(s>i||f>o||r>h||u>g)){if(p=n.point){var p,v=t-n.x,d=e-n.y,m=v*v+d*d;if(c>m){var y=Math.sqrt(c=m);r=t-y,u=e-y,i=t+y,o=e+y,a=p}}for(var M=n.nodes,x=.5*(s+h),b=.5*(f+g),_=t>=x,w=e>=b,S=w<<1|_,k=S+4;k>S;++S)if(n=M[3&S])switch(3&S){case 0:l(n,s,f,x,b);break;case 1:l(n,x,f,h,b);break;case 2:l(n,s,b,x,g);break;case 3:l(n,x,b,h,g)}}}(n,r,u,i,o),a}function gu(n,t){n=ta.rgb(n),t=ta.rgb(t);var e=n.r,r=n.g,u=n.b,i=t.r-e,o=t.g-r,a=t.b-u;return function(n){return"#"+xt(Math.round(e+i*n))+xt(Math.round(r+o*n))+xt(Math.round(u+a*n))}}function pu(n,t){var e,r={},u={};for(e in n)e in t?r[e]=mu(n[e],t[e]):u[e]=n[e];for(e in t)e in n||(u[e]=t[e]);return function(n){for(e in r)u[e]=r[e](n);return u}}function vu(n,t){return n=+n,t=+t,function(e){return n*(1-e)+t*e}}function du(n,t){var e,r,u,i=il.lastIndex=ol.lastIndex=0,o=-1,a=[],c=[];for(n+="",t+="";(e=il.exec(n))&&(r=ol.exec(t));)(u=r.index)>i&&(u=t.slice(i,u),a[o]?a[o]+=u:a[++o]=u),(e=e[0])===(r=r[0])?a[o]?a[o]+=r:a[++o]=r:(a[++o]=null,c.push({i:o,x:vu(e,r)})),i=ol.lastIndex;return i<t.length&&(u=t.slice(i),a[o]?a[o]+=u:a[++o]=u),a.length<2?c[0]?(t=c[0].x,function(n){return t(n)+""}):function(){return t}:(t=c.length,function(n){for(var e,r=0;t>r;++r)a[(e=c[r]).i]=e.x(n);return a.join("")})}function mu(n,t){for(var e,r=ta.interpolators.length;--r>=0&&!(e=ta.interpolators[r](n,t)););return e}function yu(n,t){var e,r=[],u=[],i=n.length,o=t.length,a=Math.min(n.length,t.length);for(e=0;a>e;++e)r.push(mu(n[e],t[e]));for(;i>e;++e)u[e]=n[e];for(;o>e;++e)u[e]=t[e];return function(n){for(e=0;a>e;++e)u[e]=r[e](n);return u}}function Mu(n){return function(t){return 0>=t?0:t>=1?1:n(t)}}function xu(n){return function(t){return 1-n(1-t)}}function bu(n){return function(t){return.5*(.5>t?n(2*t):2-n(2-2*t))}}function _u(n){return n*n}function wu(n){return n*n*n}function Su(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 ku(n){return function(t){return Math.pow(t,n)}}function Eu(n){return 1-Math.cos(n*Ra)}function Au(n){return Math.pow(2,10*(n-1))}function Nu(n){return 1-Math.sqrt(1-n*n)}function Cu(n,t){var e;return arguments.length<2&&(t=.45),arguments.length?e=t/La*Math.asin(1/n):(n=1,e=t/4),function(r){return 1+n*Math.pow(2,-10*r)*Math.sin((r-e)*La/t)}}function zu(n){return n||(n=1.70158),function(t){return t*t*((n+1)*t-n)}}function qu(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 Lu(n,t){n=ta.hcl(n),t=ta.hcl(t);var e=n.h,r=n.c,u=n.l,i=t.h-e,o=t.c-r,a=t.l-u;return isNaN(o)&&(o=0,r=isNaN(r)?t.c:r),isNaN(i)?(i=0,e=isNaN(e)?t.h:e):i>180?i-=360:-180>i&&(i+=360),function(n){return st(e+i*n,r+o*n,u+a*n)+""}}function Tu(n,t){n=ta.hsl(n),t=ta.hsl(t);var e=n.h,r=n.s,u=n.l,i=t.h-e,o=t.s-r,a=t.l-u;return isNaN(o)&&(o=0,r=isNaN(r)?t.s:r),isNaN(i)?(i=0,e=isNaN(e)?t.h:e):i>180?i-=360:-180>i&&(i+=360),function(n){return ct(e+i*n,r+o*n,u+a*n)+""}}function Ru(n,t){n=ta.lab(n),t=ta.lab(t);var e=n.l,r=n.a,u=n.b,i=t.l-e,o=t.a-r,a=t.b-u;return function(n){return ht(e+i*n,r+o*n,u+a*n)+""}}function Du(n,t){return t-=n,function(e){return Math.round(n+t*e)}}function Pu(n){var t=[n.a,n.b],e=[n.c,n.d],r=ju(t),u=Uu(t,e),i=ju(Fu(e,t,-u))||0;t[0]*e[1]<e[0]*t[1]&&(t[0]*=-1,t[1]*=-1,r*=-1,u*=-1),this.rotate=(r?Math.atan2(t[1],t[0]):Math.atan2(-e[0],e[1]))*Pa,this.translate=[n.e,n.f],this.scale=[r,i],this.skew=i?Math.atan2(u,i)*Pa:0}function Uu(n,t){return n[0]*t[0]+n[1]*t[1]}function ju(n){var t=Math.sqrt(Uu(n,n));return t&&(n[0]/=t,n[1]/=t),t}function Fu(n,t,e){return n[0]+=e*t[0],n[1]+=e*t[1],n}function Hu(n,t){var e,r=[],u=[],i=ta.transform(n),o=ta.transform(t),a=i.translate,c=o.translate,l=i.rotate,s=o.rotate,f=i.skew,h=o.skew,g=i.scale,p=o.scale;return a[0]!=c[0]||a[1]!=c[1]?(r.push("translate(",null,",",null,")"),u.push({i:1,x:vu(a[0],c[0])},{i:3,x:vu(a[1],c[1])})):r.push(c[0]||c[1]?"translate("+c+")":""),l!=s?(l-s>180?s+=360:s-l>180&&(l+=360),u.push({i:r.push(r.pop()+"rotate(",null,")")-2,x:vu(l,s)})):s&&r.push(r.pop()+"rotate("+s+")"),f!=h?u.push({i:r.push(r.pop()+"skewX(",null,")")-2,x:vu(f,h)}):h&&r.push(r.pop()+"skewX("+h+")"),g[0]!=p[0]||g[1]!=p[1]?(e=r.push(r.pop()+"scale(",null,",",null,")"),u.push({i:e-4,x:vu(g[0],p[0])},{i:e-2,x:vu(g[1],p[1])})):(1!=p[0]||1!=p[1])&&r.push(r.pop()+"scale("+p+")"),e=u.length,function(n){for(var t,i=-1;++i<e;)r[(t=u[i]).i]=t.x(n);return r.join("")}}function Ou(n,t){return t=(t-=n=+n)||1/t,function(e){return(e-n)/t}}function Iu(n,t){return t=(t-=n=+n)||1/t,function(e){return Math.max(0,Math.min(1,(e-n)/t))}}function Yu(n){for(var t=n.source,e=n.target,r=Vu(t,e),u=[t];t!==r;)t=t.parent,u.push(t);for(var i=u.length;e!==r;)u.splice(i,0,e),e=e.parent;return u}function Zu(n){for(var t=[],e=n.parent;null!=e;)t.push(n),n=e,e=e.parent;return t.push(n),t}function Vu(n,t){if(n===t)return n;for(var e=Zu(n),r=Zu(t),u=e.pop(),i=r.pop(),o=null;u===i;)o=u,u=e.pop(),i=r.pop();return o}function Xu(n){n.fixed|=2}function $u(n){n.fixed&=-7}function Bu(n){n.fixed|=4,n.px=n.x,n.py=n.y}function Wu(n){n.fixed&=-5}function Ju(n,t,e){var r=0,u=0;if(n.charge=0,!n.leaf)for(var i,o=n.nodes,a=o.length,c=-1;++c<a;)i=o[c],null!=i&&(Ju(i,t,e),n.charge+=i.charge,r+=i.charge*i.cx,u+=i.charge*i.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,u+=l*n.point.y}n.cx=r/n.charge,n.cy=u/n.charge}function Gu(n,t){return ta.rebind(n,t,"sort","children","value"),n.nodes=n,n.links=ri,n}function Ku(n,t){for(var e=[n];null!=(n=e.pop());)if(t(n),(u=n.children)&&(r=u.length))for(var r,u;--r>=0;)e.push(u[r])}function Qu(n,t){for(var e=[n],r=[];null!=(n=e.pop());)if(r.push(n),(i=n.children)&&(u=i.length))for(var u,i,o=-1;++o<u;)e.push(i[o]);for(;null!=(n=r.pop());)t(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 ta.merge(n.map(function(n){return(n.children||[]).map(function(t){return{source:n,target:t}})}))}function ui(n){return n.x}function ii(n){return n.y}function oi(n,t,e){n.y0=t,n.y=e}function ai(n){return ta.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,u=n[0][1],i=n.length;i>e;++e)(t=n[e][1])>u&&(r=e,u=t);return r}function si(n){return n.reduce(fi,0)}function fi(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],u=(n[1]-r)/t,i=[];++e<=t;)i[e]=u*e+r;return i}function pi(n){return[ta.min(n),ta.max(n)]}function vi(n,t){return n.value-t.value}function di(n,t){var e=n._pack_next;n._pack_next=t,t._pack_prev=n,t._pack_next=e,e._pack_prev=t}function mi(n,t){n._pack_next=t,t._pack_prev=n}function yi(n,t){var e=t.x-n.x,r=t.y-n.y,u=n.r+t.r;return.999*u*u>e*e+r*r}function Mi(n){function t(n){s=Math.min(n.x-n.r,s),f=Math.max(n.x+n.r,f),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,u,i,o,a,c,l,s=1/0,f=-1/0,h=1/0,g=-1/0;if(e.forEach(xi),r=e[0],r.x=-r.r,r.y=0,t(r),l>1&&(u=e[1],u.x=u.r,u.y=0,t(u),l>2))for(i=e[2],wi(r,u,i),t(i),di(r,i),r._pack_prev=i,di(i,u),u=r._pack_next,o=3;l>o;o++){wi(r,u,i=e[o]);var p=0,v=1,d=1;for(a=u._pack_next;a!==u;a=a._pack_next,v++)if(yi(a,i)){p=1;break}if(1==p)for(c=r._pack_prev;c!==a._pack_prev&&!yi(c,i);c=c._pack_prev,d++);p?(d>v||v==d&&u.r<r.r?mi(r,u=a):mi(r=c,u),o--):(di(r,i),u=i,t(i))}var m=(s+f)/2,y=(h+g)/2,M=0;for(o=0;l>o;o++)i=e[o],i.x-=m,i.y-=y,M=Math.max(M,i.r+Math.sqrt(i.x*i.x+i.y*i.y));n.r=M,e.forEach(bi)}}function xi(n){n._pack_next=n._pack_prev=n}function bi(n){delete n._pack_next,delete n._pack_prev}function _i(n,t,e,r){var u=n.children;if(n.x=t+=r*n.x,n.y=e+=r*n.y,n.r*=r,u)for(var i=-1,o=u.length;++i<o;)_i(u[i],t,e,r)}function wi(n,t,e){var r=n.r+e.r,u=t.x-n.x,i=t.y-n.y;if(r&&(u||i)){var o=t.r+e.r,a=u*u+i*i;o*=o,r*=r;var c=.5+(r-o)/(2*a),l=Math.sqrt(Math.max(0,2*o*(r+a)-(r-=a)*r-o*o))/(2*a);e.x=n.x+c*u+l*i,e.y=n.y+c*i-l*u}else e.x=n.x+r,e.y=n.y}function Si(n,t){return n.parent==t.parent?1:2}function ki(n){var t=n.children;return t.length?t[0]:n.t}function Ei(n){var t,e=n.children;return(t=e.length)?e[t-1]:n.t}function Ai(n,t,e){var r=e/(t.i-n.i);t.c-=r,t.s+=e,n.c+=r,t.z+=e,t.m+=e}function Ni(n){for(var t,e=0,r=0,u=n.children,i=u.length;--i>=0;)t=u[i],t.z+=e,t.m+=e,e+=t.s+(r+=t.c)}function Ci(n,t,e){return n.a.parent===t.parent?n.a:e}function zi(n){return 1+ta.max(n,function(n){return n.y})}function qi(n){return n.reduce(function(n,t){return n+t.x},0)/n.length}function Li(n){var t=n.children;return t&&t.length?Li(t[0]):n}function Ti(n){var t,e=n.children;return e&&(t=e.length)?Ti(e[t-1]):n}function Ri(n){return{x:n.x,y:n.y,dx:n.dx,dy:n.dy}}function Di(n,t){var e=n.x+t[3],r=n.y+t[0],u=n.dx-t[1]-t[3],i=n.dy-t[0]-t[2];return 0>u&&(e+=u/2,u=0),0>i&&(r+=i/2,i=0),{x:e,y:r,dx:u,dy:i}}function Pi(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():Pi(n.range())}function ji(n,t,e,r){var u=e(n[0],n[1]),i=r(t[0],t[1]);return function(n){return i(u(n))}}function Fi(n,t){var e,r=0,u=n.length-1,i=n[r],o=n[u];return i>o&&(e=r,r=u,u=e,e=i,i=o,o=e),n[r]=t.floor(i),n[u]=t.ceil(o),n}function Hi(n){return n?{floor:function(t){return Math.floor(t/n)*n},ceil:function(t){return Math.ceil(t/n)*n}}:ml}function Oi(n,t,e,r){var u=[],i=[],o=0,a=Math.min(n.length,t.length)-1;for(n[a]<n[0]&&(n=n.slice().reverse(),t=t.slice().reverse());++o<=a;)u.push(e(n[o-1],n[o])),i.push(r(t[o-1],t[o]));return function(t){var e=ta.bisect(n,t,1,a)-1;return i[e](u[e](t))}}function Ii(n,t,e,r){function u(){var u=Math.min(n.length,t.length)>2?Oi:ji,c=r?Iu:Ou;return o=u(n,t,c,e),a=u(t,n,c,mu),i}function i(n){return o(n)}var o,a;return i.invert=function(n){return a(n)},i.domain=function(t){return arguments.length?(n=t.map(Number),u()):n},i.range=function(n){return arguments.length?(t=n,u()):t},i.rangeRound=function(n){return i.range(n).interpolate(Du)},i.clamp=function(n){return arguments.length?(r=n,u()):r},i.interpolate=function(n){return arguments.length?(e=n,u()):e},i.ticks=function(t){return Xi(n,t)},i.tickFormat=function(t,e){return $i(n,t,e)},i.nice=function(t){return Zi(n,t),u()},i.copy=function(){return Ii(n,t,e,r)},u()}function Yi(n,t){return ta.rebind(n,t,"range","rangeRound","interpolate","clamp")}function Zi(n,t){return Fi(n,Hi(Vi(n,t)[2]))}function Vi(n,t){null==t&&(t=10);var e=Pi(n),r=e[1]-e[0],u=Math.pow(10,Math.floor(Math.log(r/t)/Math.LN10)),i=t/r*u;return.15>=i?u*=10:.35>=i?u*=5:.75>=i&&(u*=2),e[0]=Math.ceil(e[0]/u)*u,e[1]=Math.floor(e[1]/u)*u+.5*u,e[2]=u,e}function Xi(n,t){return ta.range.apply(ta,Vi(n,t))}function $i(n,t,e){var r=Vi(n,t);if(e){var u=ic.exec(e);if(u.shift(),"s"===u[8]){var i=ta.formatPrefix(Math.max(ga(r[0]),ga(r[1])));return u[7]||(u[7]="."+Bi(i.scale(r[2]))),u[8]="f",e=ta.format(u.join("")),function(n){return e(i.scale(n))+i.symbol}}u[7]||(u[7]="."+Wi(u[8],r)),e=u.join("")}else e=",."+Bi(r[2])+"f";return ta.format(e)}function Bi(n){return-Math.floor(Math.log(n)/Math.LN10+.01)}function Wi(n,t){var e=Bi(t[2]);return n in yl?Math.abs(e-Bi(Math.max(ga(t[0]),ga(t[1]))))+ +("e"!==n):e-2*("%"===n)}function Ji(n,t,e,r){function u(n){return(e?Math.log(0>n?0:n):-Math.log(n>0?0:-n))/Math.log(t)}function i(n){return e?Math.pow(t,n):-Math.pow(t,-n)}function o(t){return n(u(t))}return o.invert=function(t){return i(n.invert(t))},o.domain=function(t){return arguments.length?(e=t[0]>=0,n.domain((r=t.map(Number)).map(u)),o):r},o.base=function(e){return arguments.length?(t=+e,n.domain(r.map(u)),o):t},o.nice=function(){var t=Fi(r.map(u),e?Math:xl);return n.domain(t),r=t.map(i),o},o.ticks=function(){var n=Pi(r),o=[],a=n[0],c=n[1],l=Math.floor(u(a)),s=Math.ceil(u(c)),f=t%1?2:t;if(isFinite(s-l)){if(e){for(;s>l;l++)for(var h=1;f>h;h++)o.push(i(l)*h);o.push(i(l))}else for(o.push(i(l));l++<s;)for(var h=f-1;h>0;h--)o.push(i(l)*h);for(l=0;o[l]<a;l++);for(s=o.length;o[s-1]>c;s--);o=o.slice(l,s)}return o},o.tickFormat=function(n,t){if(!arguments.length)return Ml;arguments.length<2?t=Ml:"function"!=typeof t&&(t=ta.format(t));var r,a=Math.max(.1,n/o.ticks().length),c=e?(r=1e-12,Math.ceil):(r=-1e-12,Math.floor);return function(n){return n/i(c(u(n)+r))<=a?t(n):""}},o.copy=function(){return Ji(n.copy(),t,e,r)},Yi(o,n)}function Gi(n,t,e){function r(t){return n(u(t))}var u=Ki(t),i=Ki(1/t);return r.invert=function(t){return i(n.invert(t))},r.domain=function(t){return arguments.length?(n.domain((e=t.map(Number)).map(u)),r):e},r.ticks=function(n){return Xi(e,n)},r.tickFormat=function(n,t){return $i(e,n,t)},r.nice=function(n){return r.domain(Zi(e,n))},r.exponent=function(o){return arguments.length?(u=Ki(t=o),i=Ki(1/t),n.domain(e.map(u)),r):t},r.copy=function(){return Gi(n.copy(),t,e)},Yi(r,n)}function Ki(n){return function(t){return 0>t?-Math.pow(-t,n):Math.pow(t,n)}}function Qi(n,t){function e(e){return i[((u.get(e)||("range"===t.t?u.set(e,n.push(e)):0/0))-1)%i.length]}function r(t,e){return ta.range(n.length).map(function(n){return t+e*n})}var u,i,o;return e.domain=function(r){if(!arguments.length)return n;n=[],u=new l;for(var i,o=-1,a=r.length;++o<a;)u.has(i=r[o])||u.set(i,n.push(i));return e[t.t].apply(e,t.a)},e.range=function(n){return arguments.length?(i=n,o=0,t={t:"range",a:arguments},e):i},e.rangePoints=function(u,a){arguments.length<2&&(a=0);var c=u[0],l=u[1],s=n.length<2?(c=(c+l)/2,0):(l-c)/(n.length-1+a);return i=r(c+s*a/2,s),o=0,t={t:"rangePoints",a:arguments},e},e.rangeRoundPoints=function(u,a){arguments.length<2&&(a=0);var c=u[0],l=u[1],s=n.length<2?(c=l=Math.round((c+l)/2),0):(l-c)/(n.length-1+a)|0;return i=r(c+Math.round(s*a/2+(l-c-(n.length-1+a)*s)/2),s),o=0,t={t:"rangeRoundPoints",a:arguments},e},e.rangeBands=function(u,a,c){arguments.length<2&&(a=0),arguments.length<3&&(c=a);var l=u[1]<u[0],s=u[l-0],f=u[1-l],h=(f-s)/(n.length-a+2*c);return i=r(s+h*c,h),l&&i.reverse(),o=h*(1-a),t={t:"rangeBands",a:arguments},e},e.rangeRoundBands=function(u,a,c){arguments.length<2&&(a=0),arguments.length<3&&(c=a);var l=u[1]<u[0],s=u[l-0],f=u[1-l],h=Math.floor((f-s)/(n.length-a+2*c));return i=r(s+Math.round((f-s-(n.length-a)*h)/2),h),l&&i.reverse(),o=Math.round(h*(1-a)),t={t:"rangeRoundBands",a:arguments},e},e.rangeBand=function(){return o},e.rangeExtent=function(){return Pi(t.a[0])},e.copy=function(){return Qi(n,t)},e.domain(n)}function no(n,t){function i(){var e=0,r=t.length;for(a=[];++e<r;)a[e-1]=ta.quantile(n,e/r);return o}function o(n){return isNaN(n=+n)?void 0:t[ta.bisect(a,n)]}var a;return o.domain=function(t){return arguments.length?(n=t.map(r).filter(u).sort(e),i()):n},o.range=function(n){return arguments.length?(t=n,i()):t},o.quantiles=function(){return a},o.invertExtent=function(e){return e=t.indexOf(e),0>e?[0/0,0/0]:[e>0?a[e-1]:n[0],e<a.length?a[e]:n[n.length-1]]},o.copy=function(){return no(n,t)},i()}function to(n,t,e){function r(t){return e[Math.max(0,Math.min(o,Math.floor(i*(t-n))))]}function u(){return i=e.length/(t-n),o=e.length-1,r}var i,o;return r.domain=function(e){return arguments.length?(n=+e[0],t=+e[e.length-1],u()):[n,t]},r.range=function(n){return arguments.length?(e=n,u()):e},r.invertExtent=function(t){return t=e.indexOf(t),t=0>t?0/0:t/i+n,[t,t+1/i]},r.copy=function(){return to(n,t,e)},u()}function eo(n,t){function e(e){return e>=e?t[ta.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 eo(n,t)},e}function ro(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 Xi(n,t)},t.tickFormat=function(t,e){return $i(n,t,e)},t.copy=function(){return ro(n)},t}function uo(){return 0}function io(n){return n.innerRadius}function oo(n){return n.outerRadius}function ao(n){return n.startAngle}function co(n){return n.endAngle}function lo(n){return n&&n.padAngle}function so(n,t,e,r){return(n-e)*t-(t-r)*n>0?0:1}function fo(n,t,e,r,u){var i=n[0]-t[0],o=n[1]-t[1],a=(u?r:-r)/Math.sqrt(i*i+o*o),c=a*o,l=-a*i,s=n[0]+c,f=n[1]+l,h=t[0]+c,g=t[1]+l,p=(s+h)/2,v=(f+g)/2,d=h-s,m=g-f,y=d*d+m*m,M=e-r,x=s*g-h*f,b=(0>m?-1:1)*Math.sqrt(M*M*y-x*x),_=(x*m-d*b)/y,w=(-x*d-m*b)/y,S=(x*m+d*b)/y,k=(-x*d+m*b)/y,E=_-p,A=w-v,N=S-p,C=k-v;return E*E+A*A>N*N+C*C&&(_=S,w=k),[[_-c,w-l],[_*e/M,w*e/M]]}function ho(n){function t(t){function o(){l.push("M",i(n(s),a))}for(var c,l=[],s=[],f=-1,h=t.length,g=Et(e),p=Et(r);++f<h;)u.call(this,c=t[f],f)?s.push([+g.call(this,c,f),+p.call(this,c,f)]):s.length&&(o(),s=[]);return s.length&&o(),l.length?l.join(""):null}var e=Ar,r=Nr,u=Ne,i=go,o=i.key,a=.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?(u=n,t):u},t.interpolate=function(n){return arguments.length?(o="function"==typeof n?i=n:(i=El.get(n)||go).key,t):o},t.tension=function(n){return arguments.length?(a=n,t):a},t}function go(n){return n.join("L")}function po(n){return go(n)+"Z"}function vo(n){for(var t=0,e=n.length,r=n[0],u=[r[0],",",r[1]];++t<e;)u.push("H",(r[0]+(r=n[t])[0])/2,"V",r[1]);return e>1&&u.push("H",r[0]),u.join("")}function mo(n){for(var t=0,e=n.length,r=n[0],u=[r[0],",",r[1]];++t<e;)u.push("V",(r=n[t])[1],"H",r[0]);return u.join("")}function yo(n){for(var t=0,e=n.length,r=n[0],u=[r[0],",",r[1]];++t<e;)u.push("H",(r=n[t])[0],"V",r[1]);return u.join("")}function Mo(n,t){return n.length<4?go(n):n[1]+_o(n.slice(1,-1),wo(n,t))}function xo(n,t){return n.length<3?go(n):n[0]+_o((n.push(n[0]),n),wo([n[n.length-2]].concat(n,[n[1]]),t))}function bo(n,t){return n.length<3?go(n):n[0]+_o(n,wo(n,t))}function _o(n,t){if(t.length<1||n.length!=t.length&&n.length!=t.length+2)return go(n);var e=n.length!=t.length,r="",u=n[0],i=n[1],o=t[0],a=o,c=1;if(e&&(r+="Q"+(i[0]-2*o[0]/3)+","+(i[1]-2*o[1]/3)+","+i[0]+","+i[1],u=n[1],c=2),t.length>1){a=t[1],i=n[c],c++,r+="C"+(u[0]+o[0])+","+(u[1]+o[1])+","+(i[0]-a[0])+","+(i[1]-a[1])+","+i[0]+","+i[1];for(var l=2;l<t.length;l++,c++)i=n[c],a=t[l],r+="S"+(i[0]-a[0])+","+(i[1]-a[1])+","+i[0]+","+i[1]}if(e){var s=n[c];r+="Q"+(i[0]+2*a[0]/3)+","+(i[1]+2*a[1]/3)+","+s[0]+","+s[1]}return r}function wo(n,t){for(var e,r=[],u=(1-t)/2,i=n[0],o=n[1],a=1,c=n.length;++a<c;)e=i,i=o,o=n[a],r.push([u*(o[0]-e[0]),u*(o[1]-e[1])]);return r}function So(n){if(n.length<3)return go(n);var t=1,e=n.length,r=n[0],u=r[0],i=r[1],o=[u,u,u,(r=n[1])[0]],a=[i,i,i,r[1]],c=[u,",",i,"L",No(Cl,o),",",No(Cl,a)];for(n.push(n[e-1]);++t<=e;)r=n[t],o.shift(),o.push(r[0]),a.shift(),a.push(r[1]),Co(c,o,a);return n.pop(),c.push("L",r),c.join("")}function ko(n){if(n.length<4)return go(n);for(var t,e=[],r=-1,u=n.length,i=[0],o=[0];++r<3;)t=n[r],i.push(t[0]),o.push(t[1]);for(e.push(No(Cl,i)+","+No(Cl,o)),--r;++r<u;)t=n[r],i.shift(),i.push(t[0]),o.shift(),o.push(t[1]),Co(e,i,o);return e.join("")}function Eo(n){for(var t,e,r=-1,u=n.length,i=u+4,o=[],a=[];++r<4;)e=n[r%u],o.push(e[0]),a.push(e[1]);for(t=[No(Cl,o),",",No(Cl,a)],--r;++r<i;)e=n[r%u],o.shift(),o.push(e[0]),a.shift(),a.push(e[1]),Co(t,o,a);return t.join("")}function Ao(n,t){var e=n.length-1;if(e)for(var r,u,i=n[0][0],o=n[0][1],a=n[e][0]-i,c=n[e][1]-o,l=-1;++l<=e;)r=n[l],u=l/e,r[0]=t*r[0]+(1-t)*(i+u*a),r[1]=t*r[1]+(1-t)*(o+u*c);return So(n)}function No(n,t){return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]+n[3]*t[3]}function Co(n,t,e){n.push("C",No(Al,t),",",No(Al,e),",",No(Nl,t),",",No(Nl,e),",",No(Cl,t),",",No(Cl,e))}function zo(n,t){return(t[1]-n[1])/(t[0]-n[0])}function qo(n){for(var t=0,e=n.length-1,r=[],u=n[0],i=n[1],o=r[0]=zo(u,i);++t<e;)r[t]=(o+(o=zo(u=i,i=n[t+1])))/2;return r[t]=o,r}function Lo(n){for(var t,e,r,u,i=[],o=qo(n),a=-1,c=n.length-1;++a<c;)t=zo(n[a],n[a+1]),ga(t)<Ca?o[a]=o[a+1]=0:(e=o[a]/t,r=o[a+1]/t,u=e*e+r*r,u>9&&(u=3*t/Math.sqrt(u),o[a]=u*e,o[a+1]=u*r));for(a=-1;++a<=c;)u=(n[Math.min(c,a+1)][0]-n[Math.max(0,a-1)][0])/(6*(1+o[a]*o[a])),i.push([u||0,o[a]*u||0]);return i}function To(n){return n.length<3?go(n):n[0]+_o(n,Lo(n))}function Ro(n){for(var t,e,r,u=-1,i=n.length;++u<i;)t=n[u],e=t[0],r=t[1]-Ra,t[0]=e*Math.cos(r),t[1]=e*Math.sin(r);return n}function Do(n){function t(t){function c(){v.push("M",a(n(m),f),s,l(n(d.reverse()),f),"Z")}for(var h,g,p,v=[],d=[],m=[],y=-1,M=t.length,x=Et(e),b=Et(u),_=e===r?function(){return g}:Et(r),w=u===i?function(){return p}:Et(i);++y<M;)o.call(this,h=t[y],y)?(d.push([g=+x.call(this,h,y),p=+b.call(this,h,y)]),m.push([+_.call(this,h,y),+w.call(this,h,y)])):d.length&&(c(),d=[],m=[]);return d.length&&c(),v.length?v.join(""):null}var e=Ar,r=Ar,u=0,i=Nr,o=Ne,a=go,c=a.key,l=a,s="L",f=.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?(u=i=n,t):i},t.y0=function(n){return arguments.length?(u=n,t):u},t.y1=function(n){return arguments.length?(i=n,t):i},t.defined=function(n){return arguments.length?(o=n,t):o},t.interpolate=function(n){return arguments.length?(c="function"==typeof n?a=n:(a=El.get(n)||go).key,l=a.reverse||a,s=a.closed?"M":"L",t):c},t.tension=function(n){return arguments.length?(f=n,t):f},t}function Po(n){return n.radius}function Uo(n){return[n.x,n.y]}function jo(n){return function(){var t=n.apply(this,arguments),e=t[0],r=t[1]-Ra;return[e*Math.cos(r),e*Math.sin(r)]}}function Fo(){return 64}function Ho(){return"circle"}function Oo(n){var t=Math.sqrt(n/qa);return"M0,"+t+"A"+t+","+t+" 0 1,1 0,"+-t+"A"+t+","+t+" 0 1,1 0,"+t+"Z"}function Io(n){return function(){var t,e;(t=this[n])&&(e=t[t.active])&&(--t.count?delete t[t.active]:delete this[n],t.active+=.5,e.event&&e.event.interrupt.call(this,this.__data__,e.index))}}function Yo(n,t,e){return ya(n,Pl),n.namespace=t,n.id=e,n}function Zo(n,t,e,r){var u=n.id,i=n.namespace;return Y(n,"function"==typeof e?function(n,o,a){n[i][u].tween.set(t,r(e.call(n,n.__data__,o,a)))}:(e=r(e),function(n){n[i][u].tween.set(t,e)}))}function Vo(n){return null==n&&(n=""),function(){this.textContent=n}}function Xo(n){return null==n?"__transition__":"__transition_"+n+"__"}function $o(n,t,e,r,u){var i=n[e]||(n[e]={active:0,count:0}),o=i[r];if(!o){var a=u.time;o=i[r]={tween:new l,time:a,delay:u.delay,duration:u.duration,ease:u.ease,index:t},u=null,++i.count,ta.timer(function(u){function c(e){if(i.active>r)return s();var u=i[i.active];u&&(--i.count,delete i[i.active],u.event&&u.event.interrupt.call(n,n.__data__,u.index)),i.active=r,o.event&&o.event.start.call(n,n.__data__,t),o.tween.forEach(function(e,r){(r=r.call(n,n.__data__,t))&&v.push(r)}),h=o.ease,f=o.duration,ta.timer(function(){return p.c=l(e||1)?Ne:l,1},0,a)}function l(e){if(i.active!==r)return 1;for(var u=e/f,a=h(u),c=v.length;c>0;)v[--c].call(n,a);return u>=1?(o.event&&o.event.end.call(n,n.__data__,t),s()):void 0}function s(){return--i.count?delete i[r]:delete n[e],1}var f,h,g=o.delay,p=ec,v=[];return p.t=g+a,u>=g?c(u-g):void(p.c=c)},0,a)}}function Bo(n,t,e){n.attr("transform",function(n){var r=t(n);return"translate("+(isFinite(r)?r:e(n))+",0)"})}function Wo(n,t,e){n.attr("transform",function(n){var r=t(n);return"translate(0,"+(isFinite(r)?r:e(n))+")"})}function Jo(n){return n.toISOString()}function Go(n,t,e){function r(t){return n(t)}function u(n,e){var r=n[1]-n[0],u=r/e,i=ta.bisect(Vl,u);return i==Vl.length?[t.year,Vi(n.map(function(n){return n/31536e6}),e)[2]]:i?t[u/Vl[i-1]<Vl[i]/u?i-1:i]:[Bl,Vi(n,e)[2]]}return r.invert=function(t){return Ko(n.invert(t))},r.domain=function(t){return arguments.length?(n.domain(t),r):n.domain().map(Ko)},r.nice=function(n,t){function e(e){return!isNaN(e)&&!n.range(e,Ko(+e+1),t).length}var i=r.domain(),o=Pi(i),a=null==n?u(o,10):"number"==typeof n&&u(o,n);return a&&(n=a[0],t=a[1]),r.domain(Fi(i,t>1?{floor:function(t){for(;e(t=n.floor(t));)t=Ko(t-1);return t},ceil:function(t){for(;e(t=n.ceil(t));)t=Ko(+t+1);return t}}:n))},r.ticks=function(n,t){var e=Pi(r.domain()),i=null==n?u(e,10):"number"==typeof n?u(e,n):!n.range&&[{range:n},t];return i&&(n=i[0],t=i[1]),n.range(e[0],Ko(+e[1]+1),1>t?1:t)},r.tickFormat=function(){return e},r.copy=function(){return Go(n.copy(),t,e)},Yi(r,n)}function Ko(n){return new Date(n)}function Qo(n){return JSON.parse(n.responseText)}function na(n){var t=ua.createRange();return t.selectNode(ua.body),t.createContextualFragment(n.responseText)}var ta={version:"3.5.5"},ea=[].slice,ra=function(n){return ea.call(n)},ua=this.document;if(ua)try{ra(ua.documentElement.childNodes)[0].nodeType}catch(ia){ra=function(n){for(var t=n.length,e=new Array(t);t--;)e[t]=n[t];return e}}if(Date.now||(Date.now=function(){return+new Date}),ua)try{ua.createElement("DIV").style.setProperty("opacity",0,"")}catch(oa){var aa=this.Element.prototype,ca=aa.setAttribute,la=aa.setAttributeNS,sa=this.CSSStyleDeclaration.prototype,fa=sa.setProperty;aa.setAttribute=function(n,t){ca.call(this,n,t+"")},aa.setAttributeNS=function(n,t,e){la.call(this,n,t,e+"")},sa.setProperty=function(n,t,e){fa.call(this,n,t+"",e)}}ta.ascending=e,ta.descending=function(n,t){return n>t?-1:t>n?1:t>=n?0:0/0},ta.min=function(n,t){var e,r,u=-1,i=n.length;if(1===arguments.length){for(;++u<i;)if(null!=(r=n[u])&&r>=r){e=r;break}for(;++u<i;)null!=(r=n[u])&&e>r&&(e=r)}else{for(;++u<i;)if(null!=(r=t.call(n,n[u],u))&&r>=r){e=r;break}for(;++u<i;)null!=(r=t.call(n,n[u],u))&&e>r&&(e=r)}return e},ta.max=function(n,t){var e,r,u=-1,i=n.length;if(1===arguments.length){for(;++u<i;)if(null!=(r=n[u])&&r>=r){e=r;break}for(;++u<i;)null!=(r=n[u])&&r>e&&(e=r)}else{for(;++u<i;)if(null!=(r=t.call(n,n[u],u))&&r>=r){e=r;break}for(;++u<i;)null!=(r=t.call(n,n[u],u))&&r>e&&(e=r)}return e},ta.extent=function(n,t){var e,r,u,i=-1,o=n.length;if(1===arguments.length){for(;++i<o;)if(null!=(r=n[i])&&r>=r){e=u=r;break}for(;++i<o;)null!=(r=n[i])&&(e>r&&(e=r),r>u&&(u=r))}else{for(;++i<o;)if(null!=(r=t.call(n,n[i],i))&&r>=r){e=u=r;break}for(;++i<o;)null!=(r=t.call(n,n[i],i))&&(e>r&&(e=r),r>u&&(u=r))}return[e,u]},ta.sum=function(n,t){var e,r=0,i=n.length,o=-1;if(1===arguments.length)for(;++o<i;)u(e=+n[o])&&(r+=e);else for(;++o<i;)u(e=+t.call(n,n[o],o))&&(r+=e);return r},ta.mean=function(n,t){var e,i=0,o=n.length,a=-1,c=o;if(1===arguments.length)for(;++a<o;)u(e=r(n[a]))?i+=e:--c;else for(;++a<o;)u(e=r(t.call(n,n[a],a)))?i+=e:--c;return c?i/c:void 0},ta.quantile=function(n,t){var e=(n.length-1)*t+1,r=Math.floor(e),u=+n[r-1],i=e-r;return i?u+i*(n[r]-u):u},ta.median=function(n,t){var i,o=[],a=n.length,c=-1;if(1===arguments.length)for(;++c<a;)u(i=r(n[c]))&&o.push(i);else for(;++c<a;)u(i=r(t.call(n,n[c],c)))&&o.push(i);return o.length?ta.quantile(o.sort(e),.5):void 0},ta.variance=function(n,t){var e,i,o=n.length,a=0,c=0,l=-1,s=0;if(1===arguments.length)for(;++l<o;)u(e=r(n[l]))&&(i=e-a,a+=i/++s,c+=i*(e-a));else for(;++l<o;)u(e=r(t.call(n,n[l],l)))&&(i=e-a,a+=i/++s,c+=i*(e-a));return s>1?c/(s-1):void 0},ta.deviation=function(){var n=ta.variance.apply(this,arguments);return n?Math.sqrt(n):n};var ha=i(e);ta.bisectLeft=ha.left,ta.bisect=ta.bisectRight=ha.right,ta.bisector=function(n){return i(1===n.length?function(t,r){return e(n(t),r)}:n)},ta.shuffle=function(n,t,e){(i=arguments.length)<3&&(e=n.length,2>i&&(t=0));for(var r,u,i=e-t;i;)u=Math.random()*i--|0,r=n[i+t],n[i+t]=n[u+t],n[u+t]=r;return n},ta.permute=function(n,t){for(var e=t.length,r=new Array(e);e--;)r[e]=n[t[e]];return r},ta.pairs=function(n){for(var t,e=0,r=n.length-1,u=n[0],i=new Array(0>r?0:r);r>e;)i[e]=[t=u,u=n[++e]];return i},ta.zip=function(){if(!(r=arguments.length))return[];for(var n=-1,t=ta.min(arguments,o),e=new Array(t);++n<t;)for(var r,u=-1,i=e[n]=new Array(r);++u<r;)i[u]=arguments[u][n];return e},ta.transpose=function(n){return ta.zip.apply(ta,n)},ta.keys=function(n){var t=[];for(var e in n)t.push(e);return t},ta.values=function(n){var t=[];for(var e in n)t.push(n[e]);return t},ta.entries=function(n){var t=[];for(var e in n)t.push({key:e,value:n[e]});return t},ta.merge=function(n){for(var t,e,r,u=n.length,i=-1,o=0;++i<u;)o+=n[i].length;for(e=new Array(o);--u>=0;)for(r=n[u],t=r.length;--t>=0;)e[--o]=r[t];return e};var ga=Math.abs;ta.range=function(n,t,e){if(arguments.length<3&&(e=1,arguments.length<2&&(t=n,n=0)),(t-n)/e===1/0)throw new Error("infinite range");var r,u=[],i=a(ga(e)),o=-1;if(n*=i,t*=i,e*=i,0>e)for(;(r=n+e*++o)>t;)u.push(r/i);else for(;(r=n+e*++o)<t;)u.push(r/i);return u},ta.map=function(n,t){var e=new l;if(n instanceof l)n.forEach(function(n,t){e.set(n,t)});else if(Array.isArray(n)){var r,u=-1,i=n.length;if(1===arguments.length)for(;++u<i;)e.set(u,n[u]);else for(;++u<i;)e.set(t.call(n,r=n[u],u),r)}else for(var o in n)e.set(o,n[o]);return e};var pa="__proto__",va="\x00";c(l,{has:h,get:function(n){return this._[s(n)]},set:function(n,t){return this._[s(n)]=t},remove:g,keys:p,values:function(){var n=[];for(var t in this._)n.push(this._[t]);return n},entries:function(){var n=[];for(var t in this._)n.push({key:f(t),value:this._[t]});return n},size:v,empty:d,forEach:function(n){for(var t in this._)n.call(this,f(t),this._[t])}}),ta.nest=function(){function n(t,o,a){if(a>=i.length)return r?r.call(u,o):e?o.sort(e):o;for(var c,s,f,h,g=-1,p=o.length,v=i[a++],d=new l;++g<p;)(h=d.get(c=v(s=o[g])))?h.push(s):d.set(c,[s]);return t?(s=t(),f=function(e,r){s.set(e,n(t,r,a))}):(s={},f=function(e,r){s[e]=n(t,r,a)}),d.forEach(f),s}function t(n,e){if(e>=i.length)return n;var r=[],u=o[e++];return n.forEach(function(n,u){r.push({key:n,values:t(u,e)})}),u?r.sort(function(n,t){return u(n.key,t.key)}):r}var e,r,u={},i=[],o=[];return u.map=function(t,e){return n(e,t,0)},u.entries=function(e){return t(n(ta.map,e,0),0)},u.key=function(n){return i.push(n),u},u.sortKeys=function(n){return o[i.length-1]=n,u},u.sortValues=function(n){return e=n,u},u.rollup=function(n){return r=n,u},u},ta.set=function(n){var t=new m;if(n)for(var e=0,r=n.length;r>e;++e)t.add(n[e]);return t},c(m,{has:h,add:function(n){return this._[s(n+="")]=!0,n},remove:g,values:p,size:v,empty:d,forEach:function(n){for(var t in this._)n.call(this,f(t))}}),ta.behavior={},ta.rebind=function(n,t){for(var e,r=1,u=arguments.length;++r<u;)n[e=arguments[r]]=M(n,t,t[e]);return n};var da=["webkit","ms","moz","Moz","o","O"];ta.dispatch=function(){for(var n=new _,t=-1,e=arguments.length;++t<e;)n[arguments[t]]=w(n);return n},_.prototype.on=function(n,t){var e=n.indexOf("."),r="";if(e>=0&&(r=n.slice(e+1),n=n.slice(0,e)),n)return arguments.length<2?this[n].on(r):this[n].on(r,t);if(2===arguments.length){if(null==t)for(n in this)this.hasOwnProperty(n)&&this[n].on(r,null);return this}},ta.event=null,ta.requote=function(n){return n.replace(ma,"\\$&")};var ma=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g,ya={}.__proto__?function(n,t){n.__proto__=t}:function(n,t){for(var e in t)n[e]=t[e]},Ma=function(n,t){return t.querySelector(n)},xa=function(n,t){return t.querySelectorAll(n)},ba=function(n,t){var e=n.matches||n[x(n,"matchesSelector")];return(ba=function(n,t){return e.call(n,t)})(n,t)};"function"==typeof Sizzle&&(Ma=function(n,t){return Sizzle(n,t)[0]||null},xa=Sizzle,ba=Sizzle.matchesSelector),ta.selection=function(){return ta.select(ua.documentElement)};var _a=ta.selection.prototype=[];_a.select=function(n){var t,e,r,u,i=[];n=N(n);for(var o=-1,a=this.length;++o<a;){i.push(t=[]),t.parentNode=(r=this[o]).parentNode;for(var c=-1,l=r.length;++c<l;)(u=r[c])?(t.push(e=n.call(u,u.__data__,c,o)),e&&"__data__"in u&&(e.__data__=u.__data__)):t.push(null)}return A(i)},_a.selectAll=function(n){var t,e,r=[];n=C(n);for(var u=-1,i=this.length;++u<i;)for(var o=this[u],a=-1,c=o.length;++a<c;)(e=o[a])&&(r.push(t=ra(n.call(e,e.__data__,a,u))),t.parentNode=e);return A(r)};var wa={svg:"http://www.w3.org/2000/svg",xhtml:"http://www.w3.org/1999/xhtml",xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"};ta.ns={prefix:wa,qualify:function(n){var t=n.indexOf(":"),e=n;return t>=0&&(e=n.slice(0,t),n=n.slice(t+1)),wa.hasOwnProperty(e)?{space:wa[e],local:n}:n}},_a.attr=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node();return n=ta.ns.qualify(n),n.local?e.getAttributeNS(n.space,n.local):e.getAttribute(n)}for(t in n)this.each(z(t,n[t]));return this}return this.each(z(n,t))},_a.classed=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node(),r=(n=T(n)).length,u=-1;if(t=e.classList){for(;++u<r;)if(!t.contains(n[u]))return!1}else for(t=e.getAttribute("class");++u<r;)if(!L(n[u]).test(t))return!1;return!0}for(t in n)this.each(R(t,n[t]));return this}return this.each(R(n,t))},_a.style=function(n,e,r){var u=arguments.length;if(3>u){if("string"!=typeof n){2>u&&(e="");for(r in n)this.each(P(r,n[r],e));return this}if(2>u){var i=this.node();return t(i).getComputedStyle(i,null).getPropertyValue(n)}r=""}return this.each(P(n,e,r))},_a.property=function(n,t){if(arguments.length<2){if("string"==typeof n)return this.node()[n];for(t in n)this.each(U(t,n[t]));return this}return this.each(U(n,t))},_a.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},_a.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},_a.append=function(n){return n=j(n),this.select(function(){return this.appendChild(n.apply(this,arguments))})},_a.insert=function(n,t){return n=j(n),t=N(t),this.select(function(){return this.insertBefore(n.apply(this,arguments),t.apply(this,arguments)||null)})},_a.remove=function(){return this.each(F)},_a.data=function(n,t){function e(n,e){var r,u,i,o=n.length,f=e.length,h=Math.min(o,f),g=new Array(f),p=new Array(f),v=new Array(o);if(t){var d,m=new l,y=new Array(o);for(r=-1;++r<o;)m.has(d=t.call(u=n[r],u.__data__,r))?v[r]=u:m.set(d,u),y[r]=d;for(r=-1;++r<f;)(u=m.get(d=t.call(e,i=e[r],r)))?u!==!0&&(g[r]=u,u.__data__=i):p[r]=H(i),m.set(d,!0);for(r=-1;++r<o;)m.get(y[r])!==!0&&(v[r]=n[r])}else{for(r=-1;++r<h;)u=n[r],i=e[r],u?(u.__data__=i,g[r]=u):p[r]=H(i);for(;f>r;++r)p[r]=H(e[r]);for(;o>r;++r)v[r]=n[r]}p.update=g,p.parentNode=g.parentNode=v.parentNode=n.parentNode,a.push(p),c.push(g),s.push(v)}var r,u,i=-1,o=this.length;if(!arguments.length){for(n=new Array(o=(r=this[0]).length);++i<o;)(u=r[i])&&(n[i]=u.__data__);return n}var a=Z([]),c=A([]),s=A([]);if("function"==typeof n)for(;++i<o;)e(r=this[i],n.call(r,r.parentNode.__data__,i));else for(;++i<o;)e(r=this[i],n);return c.enter=function(){return a},c.exit=function(){return s},c},_a.datum=function(n){return arguments.length?this.property("__data__",n):this.property("__data__")},_a.filter=function(n){var t,e,r,u=[];"function"!=typeof n&&(n=O(n));for(var i=0,o=this.length;o>i;i++){u.push(t=[]),t.parentNode=(e=this[i]).parentNode;for(var a=0,c=e.length;c>a;a++)(r=e[a])&&n.call(r,r.__data__,a,i)&&t.push(r)}return A(u)},_a.order=function(){for(var n=-1,t=this.length;++n<t;)for(var e,r=this[n],u=r.length-1,i=r[u];--u>=0;)(e=r[u])&&(i&&i!==e.nextSibling&&i.parentNode.insertBefore(e,i),i=e);return this},_a.sort=function(n){n=I.apply(this,arguments);for(var t=-1,e=this.length;++t<e;)this[t].sort(n);return this.order()},_a.each=function(n){return Y(this,function(t,e,r){n.call(t,t.__data__,e,r)})},_a.call=function(n){var t=ra(arguments);return n.apply(t[0]=this,t),this},_a.empty=function(){return!this.node()},_a.node=function(){for(var n=0,t=this.length;t>n;n++)for(var e=this[n],r=0,u=e.length;u>r;r++){var i=e[r];if(i)return i}return null},_a.size=function(){var n=0;return Y(this,function(){++n}),n};var Sa=[];ta.selection.enter=Z,ta.selection.enter.prototype=Sa,Sa.append=_a.append,Sa.empty=_a.empty,Sa.node=_a.node,Sa.call=_a.call,Sa.size=_a.size,Sa.select=function(n){for(var t,e,r,u,i,o=[],a=-1,c=this.length;++a<c;){r=(u=this[a]).update,o.push(t=[]),t.parentNode=u.parentNode;for(var l=-1,s=u.length;++l<s;)(i=u[l])?(t.push(r[l]=e=n.call(u.parentNode,i.__data__,l,a)),e.__data__=i.__data__):t.push(null)}return A(o)},Sa.insert=function(n,t){return arguments.length<2&&(t=V(this)),_a.insert.call(this,n,t)},ta.select=function(t){var e;return"string"==typeof t?(e=[Ma(t,ua)],e.parentNode=ua.documentElement):(e=[t],e.parentNode=n(t)),A([e])},ta.selectAll=function(n){var t;return"string"==typeof n?(t=ra(xa(n,ua)),t.parentNode=ua.documentElement):(t=n,t.parentNode=null),A([t])},_a.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(X(e,n[e],t));return this}if(2>r)return(r=this.node()["__on"+n])&&r._;e=!1}return this.each(X(n,t,e))};var ka=ta.map({mouseenter:"mouseover",mouseleave:"mouseout"});ua&&ka.forEach(function(n){"on"+n in ua&&ka.remove(n)});var Ea,Aa=0;ta.mouse=function(n){return J(n,k())};var Na=this.navigator&&/WebKit/.test(this.navigator.userAgent)?-1:0;ta.touch=function(n,t,e){if(arguments.length<3&&(e=t,t=k().changedTouches),t)for(var r,u=0,i=t.length;i>u;++u)if((r=t[u]).identifier===e)return J(n,r)},ta.behavior.drag=function(){function n(){this.on("mousedown.drag",i).on("touchstart.drag",o)}function e(n,t,e,i,o){return function(){function a(){var n,e,r=t(h,v);r&&(n=r[0]-M[0],e=r[1]-M[1],p|=n|e,M=r,g({type:"drag",x:r[0]+l[0],y:r[1]+l[1],dx:n,dy:e}))}function c(){t(h,v)&&(m.on(i+d,null).on(o+d,null),y(p&&ta.event.target===f),g({type:"dragend"}))}var l,s=this,f=ta.event.target,h=s.parentNode,g=r.of(s,arguments),p=0,v=n(),d=".drag"+(null==v?"":"-"+v),m=ta.select(e(f)).on(i+d,a).on(o+d,c),y=W(f),M=t(h,v);u?(l=u.apply(s,arguments),l=[l.x-M[0],l.y-M[1]]):l=[0,0],g({type:"dragstart"})}}var r=E(n,"drag","dragstart","dragend"),u=null,i=e(b,ta.mouse,t,"mousemove","mouseup"),o=e(G,ta.touch,y,"touchmove","touchend");return n.origin=function(t){return arguments.length?(u=t,n):u},ta.rebind(n,r,"on")},ta.touches=function(n,t){return arguments.length<2&&(t=k().touches),t?ra(t).map(function(t){var e=J(n,t);return e.identifier=t.identifier,e}):[]};var Ca=1e-6,za=Ca*Ca,qa=Math.PI,La=2*qa,Ta=La-Ca,Ra=qa/2,Da=qa/180,Pa=180/qa,Ua=Math.SQRT2,ja=2,Fa=4;ta.interpolateZoom=function(n,t){function e(n){var t=n*y;if(m){var e=rt(v),o=i/(ja*h)*(e*ut(Ua*t+v)-et(v));return[r+o*l,u+o*s,i*e/rt(Ua*t+v)]}return[r+n*l,u+n*s,i*Math.exp(Ua*t)]}var r=n[0],u=n[1],i=n[2],o=t[0],a=t[1],c=t[2],l=o-r,s=a-u,f=l*l+s*s,h=Math.sqrt(f),g=(c*c-i*i+Fa*f)/(2*i*ja*h),p=(c*c-i*i-Fa*f)/(2*c*ja*h),v=Math.log(Math.sqrt(g*g+1)-g),d=Math.log(Math.sqrt(p*p+1)-p),m=d-v,y=(m||Math.log(c/i))/Ua;return e.duration=1e3*y,e},ta.behavior.zoom=function(){function n(n){n.on(q,f).on(Oa+".zoom",g).on("dblclick.zoom",p).on(R,h)}function e(n){return[(n[0]-k.x)/k.k,(n[1]-k.y)/k.k]}function r(n){return[n[0]*k.k+k.x,n[1]*k.k+k.y]}function u(n){k.k=Math.max(N[0],Math.min(N[1],n))}function i(n,t){t=r(t),k.x+=n[0]-t[0],k.y+=n[1]-t[1]}function o(t,e,r,o){t.__chart__={x:k.x,y:k.y,k:k.k},u(Math.pow(2,o)),i(d=e,r),t=ta.select(t),C>0&&(t=t.transition().duration(C)),t.call(n.event)}function a(){b&&b.domain(x.range().map(function(n){return(n-k.x)/k.k}).map(x.invert)),w&&w.domain(_.range().map(function(n){return(n-k.y)/k.k}).map(_.invert))}function c(n){z++||n({type:"zoomstart"})}function l(n){a(),n({type:"zoom",scale:k.k,translate:[k.x,k.y]})}function s(n){--z||n({type:"zoomend"}),d=null}function f(){function n(){f=1,i(ta.mouse(u),g),l(a)}function r(){h.on(L,null).on(T,null),p(f&&ta.event.target===o),s(a)}var u=this,o=ta.event.target,a=D.of(u,arguments),f=0,h=ta.select(t(u)).on(L,n).on(T,r),g=e(ta.mouse(u)),p=W(u);Dl.call(u),c(a)}function h(){function n(){var n=ta.touches(p);return g=k.k,n.forEach(function(n){n.identifier in d&&(d[n.identifier]=e(n))}),n}function t(){var t=ta.event.target;ta.select(t).on(x,r).on(b,a),_.push(t);for(var e=ta.event.changedTouches,u=0,i=e.length;i>u;++u)d[e[u].identifier]=null;var c=n(),l=Date.now();if(1===c.length){if(500>l-M){var s=c[0];o(p,s,d[s.identifier],Math.floor(Math.log(k.k)/Math.LN2)+1),S()}M=l}else if(c.length>1){var s=c[0],f=c[1],h=s[0]-f[0],g=s[1]-f[1];m=h*h+g*g}}function r(){var n,t,e,r,o=ta.touches(p);Dl.call(p);for(var a=0,c=o.length;c>a;++a,r=null)if(e=o[a],r=d[e.identifier]){if(t)break;n=e,t=r}if(r){var s=(s=e[0]-n[0])*s+(s=e[1]-n[1])*s,f=m&&Math.sqrt(s/m);n=[(n[0]+e[0])/2,(n[1]+e[1])/2],t=[(t[0]+r[0])/2,(t[1]+r[1])/2],u(f*g)}M=null,i(n,t),l(v)}function a(){if(ta.event.touches.length){for(var t=ta.event.changedTouches,e=0,r=t.length;r>e;++e)delete d[t[e].identifier];for(var u in d)return void n()}ta.selectAll(_).on(y,null),w.on(q,f).on(R,h),E(),s(v)}var g,p=this,v=D.of(p,arguments),d={},m=0,y=".zoom-"+ta.event.changedTouches[0].identifier,x="touchmove"+y,b="touchend"+y,_=[],w=ta.select(p),E=W(p);t(),c(v),w.on(q,null).on(R,t)}function g(){var n=D.of(this,arguments);y?clearTimeout(y):(v=e(d=m||ta.mouse(this)),Dl.call(this),c(n)),y=setTimeout(function(){y=null,s(n)},50),S(),u(Math.pow(2,.002*Ha())*k.k),i(d,v),l(n)}function p(){var n=ta.mouse(this),t=Math.log(k.k)/Math.LN2;o(this,n,e(n),ta.event.shiftKey?Math.ceil(t)-1:Math.floor(t)+1)}var v,d,m,y,M,x,b,_,w,k={x:0,y:0,k:1},A=[960,500],N=Ia,C=250,z=0,q="mousedown.zoom",L="mousemove.zoom",T="mouseup.zoom",R="touchstart.zoom",D=E(n,"zoomstart","zoom","zoomend");return Oa||(Oa="onwheel"in ua?(Ha=function(){return-ta.event.deltaY*(ta.event.deltaMode?120:1)},"wheel"):"onmousewheel"in ua?(Ha=function(){return ta.event.wheelDelta},"mousewheel"):(Ha=function(){return-ta.event.detail},"MozMousePixelScroll")),n.event=function(n){n.each(function(){var n=D.of(this,arguments),t=k;Tl?ta.select(this).transition().each("start.zoom",function(){k=this.__chart__||{x:0,y:0,k:1},c(n)}).tween("zoom:zoom",function(){var e=A[0],r=A[1],u=d?d[0]:e/2,i=d?d[1]:r/2,o=ta.interpolateZoom([(u-k.x)/k.k,(i-k.y)/k.k,e/k.k],[(u-t.x)/t.k,(i-t.y)/t.k,e/t.k]);return function(t){var r=o(t),a=e/r[2];this.__chart__=k={x:u-r[0]*a,y:i-r[1]*a,k:a},l(n)}}).each("interrupt.zoom",function(){s(n)}).each("end.zoom",function(){s(n)}):(this.__chart__=k,c(n),l(n),s(n))})},n.translate=function(t){return arguments.length?(k={x:+t[0],y:+t[1],k:k.k},a(),n):[k.x,k.y]},n.scale=function(t){return arguments.length?(k={x:k.x,y:k.y,k:+t},a(),n):k.k},n.scaleExtent=function(t){return arguments.length?(N=null==t?Ia:[+t[0],+t[1]],n):N},n.center=function(t){return arguments.length?(m=t&&[+t[0],+t[1]],n):m},n.size=function(t){return arguments.length?(A=t&&[+t[0],+t[1]],n):A},n.duration=function(t){return arguments.length?(C=+t,n):C},n.x=function(t){return arguments.length?(b=t,x=t.copy(),k={x:0,y:0,k:1},n):b},n.y=function(t){return arguments.length?(w=t,_=t.copy(),k={x:0,y:0,k:1},n):w},ta.rebind(n,D,"on")};var Ha,Oa,Ia=[0,1/0];ta.color=ot,ot.prototype.toString=function(){return this.rgb()+""},ta.hsl=at;var Ya=at.prototype=new ot;Ya.brighter=function(n){return n=Math.pow(.7,arguments.length?n:1),new at(this.h,this.s,this.l/n)},Ya.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),new at(this.h,this.s,n*this.l)},Ya.rgb=function(){return ct(this.h,this.s,this.l)},ta.hcl=lt;var Za=lt.prototype=new ot;Za.brighter=function(n){return new lt(this.h,this.c,Math.min(100,this.l+Va*(arguments.length?n:1)))},Za.darker=function(n){return new lt(this.h,this.c,Math.max(0,this.l-Va*(arguments.length?n:1)))},Za.rgb=function(){return st(this.h,this.c,this.l).rgb()},ta.lab=ft;var Va=18,Xa=.95047,$a=1,Ba=1.08883,Wa=ft.prototype=new ot;Wa.brighter=function(n){return new ft(Math.min(100,this.l+Va*(arguments.length?n:1)),this.a,this.b)},Wa.darker=function(n){return new ft(Math.max(0,this.l-Va*(arguments.length?n:1)),this.a,this.b)},Wa.rgb=function(){return ht(this.l,this.a,this.b)},ta.rgb=mt;var Ja=mt.prototype=new ot;Ja.brighter=function(n){n=Math.pow(.7,arguments.length?n:1);var t=this.r,e=this.g,r=this.b,u=30;return t||e||r?(t&&u>t&&(t=u),e&&u>e&&(e=u),r&&u>r&&(r=u),new mt(Math.min(255,t/n),Math.min(255,e/n),Math.min(255,r/n))):new mt(u,u,u)},Ja.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),new mt(n*this.r,n*this.g,n*this.b)},Ja.hsl=function(){return _t(this.r,this.g,this.b)},Ja.toString=function(){return"#"+xt(this.r)+xt(this.g)+xt(this.b)};var Ga=ta.map({aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074});Ga.forEach(function(n,t){Ga.set(n,yt(t))}),ta.functor=Et,ta.xhr=At(y),ta.dsv=function(n,t){function e(n,e,i){arguments.length<3&&(i=e,e=null);var o=Nt(n,t,null==e?r:u(e),i);return o.row=function(n){return arguments.length?o.response(null==(e=n)?r:u(n)):e},o}function r(n){return e.parse(n.responseText)}function u(n){return function(t){return e.parse(t.responseText,n)}}function i(t){return t.map(o).join(n)}function o(n){return a.test(n)?'"'+n.replace(/\"/g,'""')+'"':n}var a=new RegExp('["'+n+"\n]"),c=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 u=new Function("d","return {"+n.map(function(n,t){return JSON.stringify(n)+": d["+t+"]"}).join(",")+"}");r=t?function(n,e){return t(u(n),e)}:u})},e.parseRows=function(n,t){function e(){if(s>=l)return o;if(u)return u=!1,i;var t=s;if(34===n.charCodeAt(t)){for(var e=t;e++<l;)if(34===n.charCodeAt(e)){if(34!==n.charCodeAt(e+1))break;++e}s=e+2;var r=n.charCodeAt(e+1);return 13===r?(u=!0,10===n.charCodeAt(e+2)&&++s):10===r&&(u=!0),n.slice(t+1,e).replace(/""/g,'"')}for(;l>s;){var r=n.charCodeAt(s++),a=1;if(10===r)u=!0;else if(13===r)u=!0,10===n.charCodeAt(s)&&(++s,++a);else if(r!==c)continue;return n.slice(t,s-a)}return n.slice(t)}for(var r,u,i={},o={},a=[],l=n.length,s=0,f=0;(r=e())!==o;){for(var h=[];r!==i&&r!==o;)h.push(r),r=e();t&&null==(h=t(h,f++))||a.push(h)}return a},e.format=function(t){if(Array.isArray(t[0]))return e.formatRows(t);var r=new m,u=[];return t.forEach(function(n){for(var t in n)r.has(t)||u.push(r.add(t))}),[u.map(o).join(n)].concat(t.map(function(t){return u.map(function(n){return o(t[n])}).join(n)})).join("\n")},e.formatRows=function(n){return n.map(i).join("\n")},e},ta.csv=ta.dsv(",","text/csv"),ta.tsv=ta.dsv(" ","text/tab-separated-values");var Ka,Qa,nc,tc,ec,rc=this[x(this,"requestAnimationFrame")]||function(n){setTimeout(n,17)};ta.timer=function(n,t,e){var r=arguments.length;2>r&&(t=0),3>r&&(e=Date.now());var u=e+t,i={c:n,t:u,f:!1,n:null};Qa?Qa.n=i:Ka=i,Qa=i,nc||(tc=clearTimeout(tc),nc=1,rc(qt))},ta.timer.flush=function(){Lt(),Tt()},ta.round=function(n,t){return t?Math.round(n*(t=Math.pow(10,t)))/t:Math.round(n)};var uc=["y","z","a","f","p","n","\xb5","m","","k","M","G","T","P","E","Z","Y"].map(Dt);ta.formatPrefix=function(n,t){var e=0;return n&&(0>n&&(n*=-1),t&&(n=ta.round(n,Rt(n,t))),e=1+Math.floor(1e-12+Math.log(n)/Math.LN10),e=Math.max(-24,Math.min(24,3*Math.floor((e-1)/3)))),uc[8+e/3]};var ic=/(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i,oc=ta.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=ta.round(n,Rt(n,t))).toFixed(Math.max(0,Math.min(20,Rt(n*(1+1e-15),t))))}}),ac=ta.time={},cc=Date;jt.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:function(){return this._.getUTCSeconds()},getTime:function(){return this._.getTime()},getTimezoneOffset:function(){return 0},valueOf:function(){return this._.valueOf()},setDate:function(){lc.setUTCDate.apply(this._,arguments)},setDay:function(){lc.setUTCDay.apply(this._,arguments)},setFullYear:function(){lc.setUTCFullYear.apply(this._,arguments)},setHours:function(){lc.setUTCHours.apply(this._,arguments)},setMilliseconds:function(){lc.setUTCMilliseconds.apply(this._,arguments)},setMinutes:function(){lc.setUTCMinutes.apply(this._,arguments)},setMonth:function(){lc.setUTCMonth.apply(this._,arguments)},setSeconds:function(){lc.setUTCSeconds.apply(this._,arguments)},setTime:function(){lc.setTime.apply(this._,arguments)}};var lc=Date.prototype;ac.year=Ft(function(n){return n=ac.day(n),n.setMonth(0,1),n},function(n,t){n.setFullYear(n.getFullYear()+t)},function(n){return n.getFullYear()}),ac.years=ac.year.range,ac.years.utc=ac.year.utc.range,ac.day=Ft(function(n){var t=new cc(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}),ac.days=ac.day.range,ac.days.utc=ac.day.utc.range,ac.dayOfYear=function(n){var t=ac.year(n);return Math.floor((n-t-6e4*(n.getTimezoneOffset()-t.getTimezoneOffset()))/864e5)},["sunday","monday","tuesday","wednesday","thursday","friday","saturday"].forEach(function(n,t){t=7-t;var e=ac[n]=Ft(function(n){return(n=ac.day(n)).setDate(n.getDate()-(n.getDay()+t)%7),n},function(n,t){n.setDate(n.getDate()+7*Math.floor(t))},function(n){var e=ac.year(n).getDay();return Math.floor((ac.dayOfYear(n)+(e+t)%7)/7)-(e!==t)});ac[n+"s"]=e.range,ac[n+"s"].utc=e.utc.range,ac[n+"OfYear"]=function(n){var e=ac.year(n).getDay();return Math.floor((ac.dayOfYear(n)+(e+t)%7)/7)}}),ac.week=ac.sunday,ac.weeks=ac.sunday.range,ac.weeks.utc=ac.sunday.utc.range,ac.weekOfYear=ac.sundayOfYear;var sc={"-":"",_:" ",0:"0"},fc=/^\s*\d+/,hc=/^%/;ta.locale=function(n){return{numberFormat:Pt(n),timeFormat:Ot(n)}};var gc=ta.locale({decimal:".",thousands:",",grouping:[3],currency:["$",""],dateTime:"%a %b %e %X %Y",date:"%m/%d/%Y",time:"%H:%M:%S",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});ta.format=gc.numberFormat,ta.geo={},ce.prototype={s:0,t:0,add:function(n){le(n,this.t,pc),le(pc.s,this.s,this),this.s?this.t+=pc.t:this.s=pc.t +},reset:function(){this.s=this.t=0},valueOf:function(){return this.s}};var pc=new ce;ta.geo.stream=function(n,t){n&&vc.hasOwnProperty(n.type)?vc[n.type](n,t):se(n,t)};var vc={Feature:function(n,t){se(n.geometry,t)},FeatureCollection:function(n,t){for(var e=n.features,r=-1,u=e.length;++r<u;)se(e[r].geometry,t)}},dc={Sphere:function(n,t){t.sphere()},Point:function(n,t){n=n.coordinates,t.point(n[0],n[1],n[2])},MultiPoint:function(n,t){for(var e=n.coordinates,r=-1,u=e.length;++r<u;)n=e[r],t.point(n[0],n[1],n[2])},LineString:function(n,t){fe(n.coordinates,t,0)},MultiLineString:function(n,t){for(var e=n.coordinates,r=-1,u=e.length;++r<u;)fe(e[r],t,0)},Polygon:function(n,t){he(n.coordinates,t)},MultiPolygon:function(n,t){for(var e=n.coordinates,r=-1,u=e.length;++r<u;)he(e[r],t)},GeometryCollection:function(n,t){for(var e=n.geometries,r=-1,u=e.length;++r<u;)se(e[r],t)}};ta.geo.area=function(n){return mc=0,ta.geo.stream(n,Mc),mc};var mc,yc=new ce,Mc={sphere:function(){mc+=4*qa},point:b,lineStart:b,lineEnd:b,polygonStart:function(){yc.reset(),Mc.lineStart=ge},polygonEnd:function(){var n=2*yc;mc+=0>n?4*qa+n:n,Mc.lineStart=Mc.lineEnd=Mc.point=b}};ta.geo.bounds=function(){function n(n,t){M.push(x=[s=n,h=n]),f>t&&(f=t),t>g&&(g=t)}function t(t,e){var r=pe([t*Da,e*Da]);if(m){var u=de(m,r),i=[u[1],-u[0],0],o=de(i,u);Me(o),o=xe(o);var c=t-p,l=c>0?1:-1,v=o[0]*Pa*l,d=ga(c)>180;if(d^(v>l*p&&l*t>v)){var y=o[1]*Pa;y>g&&(g=y)}else if(v=(v+360)%360-180,d^(v>l*p&&l*t>v)){var y=-o[1]*Pa;f>y&&(f=y)}else f>e&&(f=e),e>g&&(g=e);d?p>t?a(s,t)>a(s,h)&&(h=t):a(t,h)>a(s,h)&&(s=t):h>=s?(s>t&&(s=t),t>h&&(h=t)):t>p?a(s,t)>a(s,h)&&(h=t):a(t,h)>a(s,h)&&(s=t)}else n(t,e);m=r,p=t}function e(){b.point=t}function r(){x[0]=s,x[1]=h,b.point=n,m=null}function u(n,e){if(m){var r=n-p;y+=ga(r)>180?r+(r>0?360:-360):r}else v=n,d=e;Mc.point(n,e),t(n,e)}function i(){Mc.lineStart()}function o(){u(v,d),Mc.lineEnd(),ga(y)>Ca&&(s=-(h=180)),x[0]=s,x[1]=h,m=null}function a(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 s,f,h,g,p,v,d,m,y,M,x,b={point:n,lineStart:e,lineEnd:r,polygonStart:function(){b.point=u,b.lineStart=i,b.lineEnd=o,y=0,Mc.polygonStart()},polygonEnd:function(){Mc.polygonEnd(),b.point=n,b.lineStart=e,b.lineEnd=r,0>yc?(s=-(h=180),f=-(g=90)):y>Ca?g=90:-Ca>y&&(f=-90),x[0]=s,x[1]=h}};return function(n){g=h=-(s=f=1/0),M=[],ta.geo.stream(n,b);var t=M.length;if(t){M.sort(c);for(var e,r=1,u=M[0],i=[u];t>r;++r)e=M[r],l(e[0],u)||l(e[1],u)?(a(u[0],e[1])>a(u[0],u[1])&&(u[1]=e[1]),a(e[0],u[1])>a(u[0],u[1])&&(u[0]=e[0])):i.push(u=e);for(var o,e,p=-1/0,t=i.length-1,r=0,u=i[t];t>=r;u=e,++r)e=i[r],(o=a(u[1],e[0]))>p&&(p=o,s=e[0],h=u[1])}return M=x=null,1/0===s||1/0===f?[[0/0,0/0],[0/0,0/0]]:[[s,f],[h,g]]}}(),ta.geo.centroid=function(n){xc=bc=_c=wc=Sc=kc=Ec=Ac=Nc=Cc=zc=0,ta.geo.stream(n,qc);var t=Nc,e=Cc,r=zc,u=t*t+e*e+r*r;return za>u&&(t=kc,e=Ec,r=Ac,Ca>bc&&(t=_c,e=wc,r=Sc),u=t*t+e*e+r*r,za>u)?[0/0,0/0]:[Math.atan2(e,t)*Pa,tt(r/Math.sqrt(u))*Pa]};var xc,bc,_c,wc,Sc,kc,Ec,Ac,Nc,Cc,zc,qc={sphere:b,point:_e,lineStart:Se,lineEnd:ke,polygonStart:function(){qc.lineStart=Ee},polygonEnd:function(){qc.lineStart=Se}},Lc=Le(Ne,Pe,je,[-qa,-qa/2]),Tc=1e9;ta.geo.clipExtent=function(){var n,t,e,r,u,i,o={stream:function(n){return u&&(u.valid=!1),u=i(n),u.valid=!0,u},extent:function(a){return arguments.length?(i=Ie(n=+a[0][0],t=+a[0][1],e=+a[1][0],r=+a[1][1]),u&&(u.valid=!1,u=null),o):[[n,t],[e,r]]}};return o.extent([[0,0],[960,500]])},(ta.geo.conicEqualArea=function(){return Ye(Ze)}).raw=Ze,ta.geo.albers=function(){return ta.geo.conicEqualArea().rotate([96,0]).center([-.6,38.7]).parallels([29.5,45.5]).scale(1070)},ta.geo.albersUsa=function(){function n(n){var i=n[0],o=n[1];return t=null,e(i,o),t||(r(i,o),t)||u(i,o),t}var t,e,r,u,i=ta.geo.albers(),o=ta.geo.conicEqualArea().rotate([154,0]).center([-2,58.5]).parallels([55,65]),a=ta.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=i.scale(),e=i.translate(),r=(n[0]-e[0])/t,u=(n[1]-e[1])/t;return(u>=.12&&.234>u&&r>=-.425&&-.214>r?o:u>=.166&&.234>u&&r>=-.214&&-.115>r?a:i).invert(n)},n.stream=function(n){var t=i.stream(n),e=o.stream(n),r=a.stream(n);return{point:function(n,u){t.point(n,u),e.point(n,u),r.point(n,u)},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?(i.precision(t),o.precision(t),a.precision(t),n):i.precision()},n.scale=function(t){return arguments.length?(i.scale(t),o.scale(.35*t),a.scale(t),n.translate(i.translate())):i.scale()},n.translate=function(t){if(!arguments.length)return i.translate();var l=i.scale(),s=+t[0],f=+t[1];return e=i.translate(t).clipExtent([[s-.455*l,f-.238*l],[s+.455*l,f+.238*l]]).stream(c).point,r=o.translate([s-.307*l,f+.201*l]).clipExtent([[s-.425*l+Ca,f+.12*l+Ca],[s-.214*l-Ca,f+.234*l-Ca]]).stream(c).point,u=a.translate([s-.205*l,f+.212*l]).clipExtent([[s-.214*l+Ca,f+.166*l+Ca],[s-.115*l-Ca,f+.234*l-Ca]]).stream(c).point,n},n.scale(1070)};var Rc,Dc,Pc,Uc,jc,Fc,Hc={point:b,lineStart:b,lineEnd:b,polygonStart:function(){Dc=0,Hc.lineStart=Ve},polygonEnd:function(){Hc.lineStart=Hc.lineEnd=Hc.point=b,Rc+=ga(Dc/2)}},Oc={point:Xe,lineStart:b,lineEnd:b,polygonStart:b,polygonEnd:b},Ic={point:We,lineStart:Je,lineEnd:Ge,polygonStart:function(){Ic.lineStart=Ke},polygonEnd:function(){Ic.point=We,Ic.lineStart=Je,Ic.lineEnd=Ge}};ta.geo.path=function(){function n(n){return n&&("function"==typeof a&&i.pointRadius(+a.apply(this,arguments)),o&&o.valid||(o=u(i)),ta.geo.stream(n,o)),i.result()}function t(){return o=null,n}var e,r,u,i,o,a=4.5;return n.area=function(n){return Rc=0,ta.geo.stream(n,u(Hc)),Rc},n.centroid=function(n){return _c=wc=Sc=kc=Ec=Ac=Nc=Cc=zc=0,ta.geo.stream(n,u(Ic)),zc?[Nc/zc,Cc/zc]:Ac?[kc/Ac,Ec/Ac]:Sc?[_c/Sc,wc/Sc]:[0/0,0/0]},n.bounds=function(n){return jc=Fc=-(Pc=Uc=1/0),ta.geo.stream(n,u(Oc)),[[Pc,Uc],[jc,Fc]]},n.projection=function(n){return arguments.length?(u=(e=n)?n.stream||tr(n):y,t()):e},n.context=function(n){return arguments.length?(i=null==(r=n)?new $e:new Qe(n),"function"!=typeof a&&i.pointRadius(a),t()):r},n.pointRadius=function(t){return arguments.length?(a="function"==typeof t?t:(i.pointRadius(+t),+t),n):a},n.projection(ta.geo.albersUsa()).context(null)},ta.geo.transform=function(n){return{stream:function(t){var e=new er(t);for(var r in n)e[r]=n[r];return e}}},er.prototype={point:function(n,t){this.stream.point(n,t)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}},ta.geo.projection=ur,ta.geo.projectionMutator=ir,(ta.geo.equirectangular=function(){return ur(ar)}).raw=ar.invert=ar,ta.geo.rotation=function(n){function t(t){return t=n(t[0]*Da,t[1]*Da),t[0]*=Pa,t[1]*=Pa,t}return n=lr(n[0]%360*Da,n[1]*Da,n.length>2?n[2]*Da:0),t.invert=function(t){return t=n.invert(t[0]*Da,t[1]*Da),t[0]*=Pa,t[1]*=Pa,t},t},cr.invert=ar,ta.geo.circle=function(){function n(){var n="function"==typeof r?r.apply(this,arguments):r,t=lr(-n[0]*Da,-n[1]*Da,0).invert,u=[];return e(null,null,1,{point:function(n,e){u.push(n=t(n,e)),n[0]*=Pa,n[1]*=Pa}}),{type:"Polygon",coordinates:[u]}}var t,e,r=[0,0],u=6;return n.origin=function(t){return arguments.length?(r=t,n):r},n.angle=function(r){return arguments.length?(e=gr((t=+r)*Da,u*Da),n):t},n.precision=function(r){return arguments.length?(e=gr(t*Da,(u=+r)*Da),n):u},n.angle(90)},ta.geo.distance=function(n,t){var e,r=(t[0]-n[0])*Da,u=n[1]*Da,i=t[1]*Da,o=Math.sin(r),a=Math.cos(r),c=Math.sin(u),l=Math.cos(u),s=Math.sin(i),f=Math.cos(i);return Math.atan2(Math.sqrt((e=f*o)*e+(e=l*s-c*f*a)*e),c*s+l*f*a)},ta.geo.graticule=function(){function n(){return{type:"MultiLineString",coordinates:t()}}function t(){return ta.range(Math.ceil(i/d)*d,u,d).map(h).concat(ta.range(Math.ceil(l/m)*m,c,m).map(g)).concat(ta.range(Math.ceil(r/p)*p,e,p).filter(function(n){return ga(n%d)>Ca}).map(s)).concat(ta.range(Math.ceil(a/v)*v,o,v).filter(function(n){return ga(n%m)>Ca}).map(f))}var e,r,u,i,o,a,c,l,s,f,h,g,p=10,v=p,d=90,m=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(i).concat(g(c).slice(1),h(u).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?(i=+t[0][0],u=+t[1][0],l=+t[0][1],c=+t[1][1],i>u&&(t=i,i=u,u=t),l>c&&(t=l,l=c,c=t),n.precision(y)):[[i,l],[u,c]]},n.minorExtent=function(t){return arguments.length?(r=+t[0][0],e=+t[1][0],a=+t[0][1],o=+t[1][1],r>e&&(t=r,r=e,e=t),a>o&&(t=a,a=o,o=t),n.precision(y)):[[r,a],[e,o]]},n.step=function(t){return arguments.length?n.majorStep(t).minorStep(t):n.minorStep()},n.majorStep=function(t){return arguments.length?(d=+t[0],m=+t[1],n):[d,m]},n.minorStep=function(t){return arguments.length?(p=+t[0],v=+t[1],n):[p,v]},n.precision=function(t){return arguments.length?(y=+t,s=vr(a,o,90),f=dr(r,e,y),h=vr(l,c,90),g=dr(i,u,y),n):y},n.majorExtent([[-180,-90+Ca],[180,90-Ca]]).minorExtent([[-180,-80-Ca],[180,80+Ca]])},ta.geo.greatArc=function(){function n(){return{type:"LineString",coordinates:[t||r.apply(this,arguments),e||u.apply(this,arguments)]}}var t,e,r=mr,u=yr;return n.distance=function(){return ta.geo.distance(t||r.apply(this,arguments),e||u.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?(u=t,e="function"==typeof t?null:t,n):u},n.precision=function(){return arguments.length?n:0},n},ta.geo.interpolate=function(n,t){return Mr(n[0]*Da,n[1]*Da,t[0]*Da,t[1]*Da)},ta.geo.length=function(n){return Yc=0,ta.geo.stream(n,Zc),Yc};var Yc,Zc={sphere:b,point:b,lineStart:xr,lineEnd:b,polygonStart:b,polygonEnd:b},Vc=br(function(n){return Math.sqrt(2/(1+n))},function(n){return 2*Math.asin(n/2)});(ta.geo.azimuthalEqualArea=function(){return ur(Vc)}).raw=Vc;var Xc=br(function(n){var t=Math.acos(n);return t&&t/Math.sin(t)},y);(ta.geo.azimuthalEquidistant=function(){return ur(Xc)}).raw=Xc,(ta.geo.conicConformal=function(){return Ye(_r)}).raw=_r,(ta.geo.conicEquidistant=function(){return Ye(wr)}).raw=wr;var $c=br(function(n){return 1/n},Math.atan);(ta.geo.gnomonic=function(){return ur($c)}).raw=$c,Sr.invert=function(n,t){return[n,2*Math.atan(Math.exp(t))-Ra]},(ta.geo.mercator=function(){return kr(Sr)}).raw=Sr;var Bc=br(function(){return 1},Math.asin);(ta.geo.orthographic=function(){return ur(Bc)}).raw=Bc;var Wc=br(function(n){return 1/(1+n)},function(n){return 2*Math.atan(n)});(ta.geo.stereographic=function(){return ur(Wc)}).raw=Wc,Er.invert=function(n,t){return[-t,2*Math.atan(Math.exp(n))-Ra]},(ta.geo.transverseMercator=function(){var n=kr(Er),t=n.center,e=n.rotate;return n.center=function(n){return n?t([-n[1],n[0]]):(n=t(),[n[1],-n[0]])},n.rotate=function(n){return n?e([n[0],n[1],n.length>2?n[2]+90:90]):(n=e(),[n[0],n[1],n[2]-90])},e([0,0,90])}).raw=Er,ta.geom={},ta.geom.hull=function(n){function t(n){if(n.length<3)return[];var t,u=Et(e),i=Et(r),o=n.length,a=[],c=[];for(t=0;o>t;t++)a.push([+u.call(this,n[t],t),+i.call(this,n[t],t),t]);for(a.sort(zr),t=0;o>t;t++)c.push([a[t][0],-a[t][1]]);var l=Cr(a),s=Cr(c),f=s[0]===l[0],h=s[s.length-1]===l[l.length-1],g=[];for(t=l.length-1;t>=0;--t)g.push(n[a[l[t]][2]]);for(t=+f;t<s.length-h;++t)g.push(n[a[s[t]][2]]);return g}var e=Ar,r=Nr;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)},ta.geom.polygon=function(n){return ya(n,Jc),n};var Jc=ta.geom.polygon.prototype=[];Jc.area=function(){for(var n,t=-1,e=this.length,r=this[e-1],u=0;++t<e;)n=r,r=this[t],u+=n[1]*r[0]-n[0]*r[1];return.5*u},Jc.centroid=function(n){var t,e,r=-1,u=this.length,i=0,o=0,a=this[u-1];for(arguments.length||(n=-1/(6*this.area()));++r<u;)t=a,a=this[r],e=t[0]*a[1]-a[0]*t[1],i+=(t[0]+a[0])*e,o+=(t[1]+a[1])*e;return[i*n,o*n]},Jc.clip=function(n){for(var t,e,r,u,i,o,a=Tr(n),c=-1,l=this.length-Tr(this),s=this[l-1];++c<l;){for(t=n.slice(),n.length=0,u=this[c],i=t[(r=t.length-a)-1],e=-1;++e<r;)o=t[e],qr(o,s,u)?(qr(i,s,u)||n.push(Lr(i,o,s,u)),n.push(o)):qr(i,s,u)&&n.push(Lr(i,o,s,u)),i=o;a&&n.push(n[0]),s=u}return n};var Gc,Kc,Qc,nl,tl,el=[],rl=[];Or.prototype.prepare=function(){for(var n,t=this.edges,e=t.length;e--;)n=t[e].edge,n.b&&n.a||t.splice(e,1);return t.sort(Yr),t.length},Qr.prototype={start:function(){return this.edge.l===this.site?this.edge.a:this.edge.b},end:function(){return this.edge.l===this.site?this.edge.b:this.edge.a}},nu.prototype={insert:function(n,t){var e,r,u;if(n){if(t.P=n,t.N=n.N,n.N&&(n.N.P=t),n.N=t,n.R){for(n=n.R;n.L;)n=n.L;n.L=t}else n.R=t;e=n}else this._?(n=uu(this._),t.P=null,t.N=n,n.P=n.L=t,e=n):(t.P=t.N=null,this._=t,e=null);for(t.L=t.R=null,t.U=e,t.C=!0,n=t;e&&e.C;)r=e.U,e===r.L?(u=r.R,u&&u.C?(e.C=u.C=!1,r.C=!0,n=r):(n===e.R&&(eu(this,e),n=e,e=n.U),e.C=!1,r.C=!0,ru(this,r))):(u=r.L,u&&u.C?(e.C=u.C=!1,r.C=!0,n=r):(n===e.L&&(ru(this,e),n=e,e=n.U),e.C=!1,r.C=!0,eu(this,r))),e=n.U;this._.C=!1},remove:function(n){n.N&&(n.N.P=n.P),n.P&&(n.P.N=n.N),n.N=n.P=null;var t,e,r,u=n.U,i=n.L,o=n.R;if(e=i?o?uu(o):i:o,u?u.L===n?u.L=e:u.R=e:this._=e,i&&o?(r=e.C,e.C=n.C,e.L=i,i.U=e,e!==o?(u=e.U,e.U=n.U,n=e.R,u.L=n,e.R=o,o.U=e):(e.U=u,u=e,n=e.R)):(r=n.C,n=e),n&&(n.U=u),!r){if(n&&n.C)return void(n.C=!1);do{if(n===this._)break;if(n===u.L){if(t=u.R,t.C&&(t.C=!1,u.C=!0,eu(this,u),t=u.R),t.L&&t.L.C||t.R&&t.R.C){t.R&&t.R.C||(t.L.C=!1,t.C=!0,ru(this,t),t=u.R),t.C=u.C,u.C=t.R.C=!1,eu(this,u),n=this._;break}}else if(t=u.L,t.C&&(t.C=!1,u.C=!0,ru(this,u),t=u.L),t.L&&t.L.C||t.R&&t.R.C){t.L&&t.L.C||(t.R.C=!1,t.C=!0,eu(this,t),t=u.L),t.C=u.C,u.C=t.L.C=!1,ru(this,u),n=this._;break}t.C=!0,n=u,u=u.U}while(!n.C);n&&(n.C=!1)}}},ta.geom.voronoi=function(n){function t(n){var t=new Array(n.length),r=a[0][0],u=a[0][1],i=a[1][0],o=a[1][1];return iu(e(n),a).cells.forEach(function(e,a){var c=e.edges,l=e.site,s=t[a]=c.length?c.map(function(n){var t=n.start();return[t.x,t.y]}):l.x>=r&&l.x<=i&&l.y>=u&&l.y<=o?[[r,o],[i,o],[i,u],[r,u]]:[];s.point=n[a]}),t}function e(n){return n.map(function(n,t){return{x:Math.round(i(n,t)/Ca)*Ca,y:Math.round(o(n,t)/Ca)*Ca,i:t}})}var r=Ar,u=Nr,i=r,o=u,a=ul;return n?t(n):(t.links=function(n){return iu(e(n)).edges.filter(function(n){return n.l&&n.r}).map(function(t){return{source:n[t.l.i],target:n[t.r.i]}})},t.triangles=function(n){var t=[];return iu(e(n)).cells.forEach(function(e,r){for(var u,i,o=e.site,a=e.edges.sort(Yr),c=-1,l=a.length,s=a[l-1].edge,f=s.l===o?s.r:s.l;++c<l;)u=s,i=f,s=a[c].edge,f=s.l===o?s.r:s.l,r<i.i&&r<f.i&&au(o,i,f)<0&&t.push([n[r],n[i.i],n[f.i]])}),t},t.x=function(n){return arguments.length?(i=Et(r=n),t):r},t.y=function(n){return arguments.length?(o=Et(u=n),t):u},t.clipExtent=function(n){return arguments.length?(a=null==n?ul:n,t):a===ul?null:a},t.size=function(n){return arguments.length?t.clipExtent(n&&[[0,0],n]):a===ul?null:a&&a[1]},t)};var ul=[[-1e6,-1e6],[1e6,1e6]];ta.geom.delaunay=function(n){return ta.geom.voronoi().triangles(n)},ta.geom.quadtree=function(n,t,e,r,u){function i(n){function i(n,t,e,r,u,i,o,a){if(!isNaN(e)&&!isNaN(r))if(n.leaf){var c=n.x,s=n.y;if(null!=c)if(ga(c-e)+ga(s-r)<.01)l(n,t,e,r,u,i,o,a);else{var f=n.point;n.x=n.y=n.point=null,l(n,f,c,s,u,i,o,a),l(n,t,e,r,u,i,o,a)}else n.x=e,n.y=r,n.point=t}else l(n,t,e,r,u,i,o,a)}function l(n,t,e,r,u,o,a,c){var l=.5*(u+a),s=.5*(o+c),f=e>=l,h=r>=s,g=h<<1|f;n.leaf=!1,n=n.nodes[g]||(n.nodes[g]=su()),f?u=l:a=l,h?o=s:c=s,i(n,t,e,r,u,o,a,c)}var s,f,h,g,p,v,d,m,y,M=Et(a),x=Et(c);if(null!=t)v=t,d=e,m=r,y=u;else if(m=y=-(v=d=1/0),f=[],h=[],p=n.length,o)for(g=0;p>g;++g)s=n[g],s.x<v&&(v=s.x),s.y<d&&(d=s.y),s.x>m&&(m=s.x),s.y>y&&(y=s.y),f.push(s.x),h.push(s.y);else for(g=0;p>g;++g){var b=+M(s=n[g],g),_=+x(s,g);v>b&&(v=b),d>_&&(d=_),b>m&&(m=b),_>y&&(y=_),f.push(b),h.push(_)}var w=m-v,S=y-d;w>S?y=d+w:m=v+S;var k=su();if(k.add=function(n){i(k,n,+M(n,++g),+x(n,g),v,d,m,y)},k.visit=function(n){fu(n,k,v,d,m,y)},k.find=function(n){return hu(k,n[0],n[1],v,d,m,y)},g=-1,null==t){for(;++g<p;)i(k,n[g],f[g],h[g],v,d,m,y);--g}else n.forEach(k.add);return f=h=n=s=null,k}var o,a=Ar,c=Nr;return(o=arguments.length)?(a=cu,c=lu,3===o&&(u=e,r=t,e=t=0),i(n)):(i.x=function(n){return arguments.length?(a=n,i):a},i.y=function(n){return arguments.length?(c=n,i):c},i.extent=function(n){return arguments.length?(null==n?t=e=r=u=null:(t=+n[0][0],e=+n[0][1],r=+n[1][0],u=+n[1][1]),i):null==t?null:[[t,e],[r,u]]},i.size=function(n){return arguments.length?(null==n?t=e=r=u=null:(t=e=0,r=+n[0],u=+n[1]),i):null==t?null:[r-t,u-e]},i)},ta.interpolateRgb=gu,ta.interpolateObject=pu,ta.interpolateNumber=vu,ta.interpolateString=du;var il=/[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g,ol=new RegExp(il.source,"g");ta.interpolate=mu,ta.interpolators=[function(n,t){var e=typeof t;return("string"===e?Ga.has(t)||/^(#|rgb\(|hsl\()/.test(t)?gu:du:t instanceof ot?gu:Array.isArray(t)?yu:"object"===e&&isNaN(t)?pu:vu)(n,t)}],ta.interpolateArray=yu;var al=function(){return y},cl=ta.map({linear:al,poly:ku,quad:function(){return _u},cubic:function(){return wu},sin:function(){return Eu},exp:function(){return Au},circle:function(){return Nu},elastic:Cu,back:zu,bounce:function(){return qu}}),ll=ta.map({"in":y,out:xu,"in-out":bu,"out-in":function(n){return bu(xu(n))}});ta.ease=function(n){var t=n.indexOf("-"),e=t>=0?n.slice(0,t):n,r=t>=0?n.slice(t+1):"in";return e=cl.get(e)||al,r=ll.get(r)||y,Mu(r(e.apply(null,ea.call(arguments,1))))},ta.interpolateHcl=Lu,ta.interpolateHsl=Tu,ta.interpolateLab=Ru,ta.interpolateRound=Du,ta.transform=function(n){var t=ua.createElementNS(ta.ns.prefix.svg,"g");return(ta.transform=function(n){if(null!=n){t.setAttribute("transform",n);var e=t.transform.baseVal.consolidate()}return new Pu(e?e.matrix:sl)})(n)},Pu.prototype.toString=function(){return"translate("+this.translate+")rotate("+this.rotate+")skewX("+this.skew+")scale("+this.scale+")"};var sl={a:1,b:0,c:0,d:1,e:0,f:0};ta.interpolateTransform=Hu,ta.layout={},ta.layout.bundle=function(){return function(n){for(var t=[],e=-1,r=n.length;++e<r;)t.push(Yu(n[e]));return t}},ta.layout.chord=function(){function n(){var n,l,f,h,g,p={},v=[],d=ta.range(i),m=[];for(e=[],r=[],n=0,h=-1;++h<i;){for(l=0,g=-1;++g<i;)l+=u[h][g];v.push(l),m.push(ta.range(i)),n+=l}for(o&&d.sort(function(n,t){return o(v[n],v[t])}),a&&m.forEach(function(n,t){n.sort(function(n,e){return a(u[t][n],u[t][e])})}),n=(La-s*i)/n,l=0,h=-1;++h<i;){for(f=l,g=-1;++g<i;){var y=d[h],M=m[y][g],x=u[y][M],b=l,_=l+=x*n;p[y+"-"+M]={index:y,subindex:M,startAngle:b,endAngle:_,value:x}}r[y]={index:y,startAngle:f,endAngle:l,value:(l-f)/n},l+=s}for(h=-1;++h<i;)for(g=h-1;++g<i;){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,u,i,o,a,c,l={},s=0;return l.matrix=function(n){return arguments.length?(i=(u=n)&&u.length,e=r=null,l):u},l.padding=function(n){return arguments.length?(s=n,e=r=null,l):s},l.sortGroups=function(n){return arguments.length?(o=n,e=r=null,l):o},l.sortSubgroups=function(n){return arguments.length?(a=n,e=null,l):a},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},ta.layout.force=function(){function n(n){return function(t,e,r,u){if(t.point!==n){var i=t.cx-n.x,o=t.cy-n.y,a=u-e,c=i*i+o*o;if(c>a*a/d){if(p>c){var l=t.charge/c;n.px-=i*l,n.py-=o*l}return!0}if(t.point&&c&&p>c){var l=t.pointCharge/c;n.px-=i*l,n.py-=o*l}}return!t.charge}}function t(n){n.px=ta.event.x,n.py=ta.event.y,a.resume()}var e,r,u,i,o,a={},c=ta.dispatch("start","tick","end"),l=[1,1],s=.9,f=fl,h=hl,g=-30,p=gl,v=.1,d=.64,m=[],M=[];return a.tick=function(){if((r*=.99)<.005)return c.end({type:"end",alpha:r=0}),!0;var t,e,a,f,h,p,d,y,x,b=m.length,_=M.length;for(e=0;_>e;++e)a=M[e],f=a.source,h=a.target,y=h.x-f.x,x=h.y-f.y,(p=y*y+x*x)&&(p=r*i[e]*((p=Math.sqrt(p))-u[e])/p,y*=p,x*=p,h.x-=y*(d=f.weight/(h.weight+f.weight)),h.y-=x*d,f.x+=y*(d=1-d),f.y+=x*d);if((d=r*v)&&(y=l[0]/2,x=l[1]/2,e=-1,d))for(;++e<b;)a=m[e],a.x+=(y-a.x)*d,a.y+=(x-a.y)*d;if(g)for(Ju(t=ta.geom.quadtree(m),r,o),e=-1;++e<b;)(a=m[e]).fixed||t.visit(n(a));for(e=-1;++e<b;)a=m[e],a.fixed?(a.x=a.px,a.y=a.py):(a.x-=(a.px-(a.px=a.x))*s,a.y-=(a.py-(a.py=a.y))*s);c.tick({type:"tick",alpha:r})},a.nodes=function(n){return arguments.length?(m=n,a):m},a.links=function(n){return arguments.length?(M=n,a):M},a.size=function(n){return arguments.length?(l=n,a):l},a.linkDistance=function(n){return arguments.length?(f="function"==typeof n?n:+n,a):f},a.distance=a.linkDistance,a.linkStrength=function(n){return arguments.length?(h="function"==typeof n?n:+n,a):h},a.friction=function(n){return arguments.length?(s=+n,a):s},a.charge=function(n){return arguments.length?(g="function"==typeof n?n:+n,a):g},a.chargeDistance=function(n){return arguments.length?(p=n*n,a):Math.sqrt(p)},a.gravity=function(n){return arguments.length?(v=+n,a):v},a.theta=function(n){return arguments.length?(d=n*n,a):Math.sqrt(d)},a.alpha=function(n){return arguments.length?(n=+n,r?r=n>0?n:0:n>0&&(c.start({type:"start",alpha:r=n}),ta.timer(a.tick)),a):r},a.start=function(){function n(n,r){if(!e){for(e=new Array(c),a=0;c>a;++a)e[a]=[];for(a=0;s>a;++a){var u=M[a];e[u.source.index].push(u.target),e[u.target.index].push(u.source)}}for(var i,o=e[t],a=-1,l=o.length;++a<l;)if(!isNaN(i=o[a][n]))return i;return Math.random()*r}var t,e,r,c=m.length,s=M.length,p=l[0],v=l[1];for(t=0;c>t;++t)(r=m[t]).index=t,r.weight=0;for(t=0;s>t;++t)r=M[t],"number"==typeof r.source&&(r.source=m[r.source]),"number"==typeof r.target&&(r.target=m[r.target]),++r.source.weight,++r.target.weight;for(t=0;c>t;++t)r=m[t],isNaN(r.x)&&(r.x=n("x",p)),isNaN(r.y)&&(r.y=n("y",v)),isNaN(r.px)&&(r.px=r.x),isNaN(r.py)&&(r.py=r.y);if(u=[],"function"==typeof f)for(t=0;s>t;++t)u[t]=+f.call(this,M[t],t);else for(t=0;s>t;++t)u[t]=f;if(i=[],"function"==typeof h)for(t=0;s>t;++t)i[t]=+h.call(this,M[t],t);else for(t=0;s>t;++t)i[t]=h;if(o=[],"function"==typeof g)for(t=0;c>t;++t)o[t]=+g.call(this,m[t],t);else for(t=0;c>t;++t)o[t]=g;return a.resume()},a.resume=function(){return a.alpha(.1)},a.stop=function(){return a.alpha(0)},a.drag=function(){return e||(e=ta.behavior.drag().origin(y).on("dragstart.force",Xu).on("drag.force",t).on("dragend.force",$u)),arguments.length?void this.on("mouseover.force",Bu).on("mouseout.force",Wu).call(e):e},ta.rebind(a,c,"on")};var fl=20,hl=1,gl=1/0;ta.layout.hierarchy=function(){function n(u){var i,o=[u],a=[];for(u.depth=0;null!=(i=o.pop());)if(a.push(i),(l=e.call(n,i,i.depth))&&(c=l.length)){for(var c,l,s;--c>=0;)o.push(s=l[c]),s.parent=i,s.depth=i.depth+1;r&&(i.value=0),i.children=l}else r&&(i.value=+r.call(n,i,i.depth)||0),delete i.children;return Qu(u,function(n){var e,u;t&&(e=n.children)&&e.sort(t),r&&(u=n.parent)&&(u.value+=n.value)}),a}var t=ei,e=ni,r=ti;return n.sort=function(e){return arguments.length?(t=e,n):t},n.children=function(t){return arguments.length?(e=t,n):e},n.value=function(t){return arguments.length?(r=t,n):r},n.revalue=function(t){return r&&(Ku(t,function(n){n.children&&(n.value=0)}),Qu(t,function(t){var e;t.children||(t.value=+r.call(n,t,t.depth)||0),(e=t.parent)&&(e.value+=t.value)})),t},n},ta.layout.partition=function(){function n(t,e,r,u){var i=t.children;if(t.x=e,t.y=t.depth*u,t.dx=r,t.dy=u,i&&(o=i.length)){var o,a,c,l=-1;for(r=t.value?r/t.value:0;++l<o;)n(a=i[l],e,c=a.value*r,u),e+=c}}function t(n){var e=n.children,r=0;if(e&&(u=e.length))for(var u,i=-1;++i<u;)r=Math.max(r,t(e[i]));return 1+r}function e(e,i){var o=r.call(this,e,i);return n(o[0],0,u[0],u[1]/t(o[0])),o}var r=ta.layout.hierarchy(),u=[1,1];return e.size=function(n){return arguments.length?(u=n,e):u},Gu(e,r)},ta.layout.pie=function(){function n(o){var a,c=o.length,l=o.map(function(e,r){return+t.call(n,e,r)}),s=+("function"==typeof r?r.apply(this,arguments):r),f=("function"==typeof u?u.apply(this,arguments):u)-s,h=Math.min(Math.abs(f)/c,+("function"==typeof i?i.apply(this,arguments):i)),g=h*(0>f?-1:1),p=(f-c*g)/ta.sum(l),v=ta.range(c),d=[];return null!=e&&v.sort(e===pl?function(n,t){return l[t]-l[n]}:function(n,t){return e(o[n],o[t])}),v.forEach(function(n){d[n]={data:o[n],value:a=l[n],startAngle:s,endAngle:s+=a*p+g,padAngle:h}}),d}var t=Number,e=pl,r=0,u=La,i=0;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?(u=t,n):u},n.padAngle=function(t){return arguments.length?(i=t,n):i},n};var pl={};ta.layout.stack=function(){function n(a,c){if(!(h=a.length))return a;var l=a.map(function(e,r){return t.call(n,e,r)}),s=l.map(function(t){return t.map(function(t,e){return[i.call(n,t,e),o.call(n,t,e)]})}),f=e.call(n,s,c);l=ta.permute(l,f),s=ta.permute(s,f);var h,g,p,v,d=r.call(n,s,c),m=l[0].length;for(p=0;m>p;++p)for(u.call(n,l[0][p],v=d[p],s[0][p][1]),g=1;h>g;++g)u.call(n,l[g][p],v+=s[g-1][p][1],s[g][p][1]);return a}var t=y,e=ai,r=ci,u=oi,i=ui,o=ii;return n.values=function(e){return arguments.length?(t=e,n):t},n.order=function(t){return arguments.length?(e="function"==typeof t?t:vl.get(t)||ai,n):e},n.offset=function(t){return arguments.length?(r="function"==typeof t?t:dl.get(t)||ci,n):r},n.x=function(t){return arguments.length?(i=t,n):i},n.y=function(t){return arguments.length?(o=t,n):o},n.out=function(t){return arguments.length?(u=t,n):u},n};var vl=ta.map({"inside-out":function(n){var t,e,r=n.length,u=n.map(li),i=n.map(si),o=ta.range(r).sort(function(n,t){return u[n]-u[t]}),a=0,c=0,l=[],s=[];for(t=0;r>t;++t)e=o[t],c>a?(a+=i[e],l.push(e)):(c+=i[e],s.push(e));return s.reverse().concat(l)},reverse:function(n){return ta.range(n.length).reverse()},"default":ai}),dl=ta.map({silhouette:function(n){var t,e,r,u=n.length,i=n[0].length,o=[],a=0,c=[];for(e=0;i>e;++e){for(t=0,r=0;u>t;t++)r+=n[t][e][1];r>a&&(a=r),o.push(r)}for(e=0;i>e;++e)c[e]=(a-o[e])/2;return c},wiggle:function(n){var t,e,r,u,i,o,a,c,l,s=n.length,f=n[0],h=f.length,g=[];for(g[0]=c=l=0,e=1;h>e;++e){for(t=0,u=0;s>t;++t)u+=n[t][e][1];for(t=0,i=0,a=f[e][0]-f[e-1][0];s>t;++t){for(r=0,o=(n[t][e][1]-n[t][e-1][1])/(2*a);t>r;++r)o+=(n[r][e][1]-n[r][e-1][1])/a;i+=o*n[t][e][1]}g[e]=c-=u?i/u*a:0,l>c&&(l=c)}for(e=0;h>e;++e)g[e]-=l;return g},expand:function(n){var t,e,r,u=n.length,i=n[0].length,o=1/u,a=[];for(e=0;i>e;++e){for(t=0,r=0;u>t;t++)r+=n[t][e][1];if(r)for(t=0;u>t;t++)n[t][e][1]/=r;else for(t=0;u>t;t++)n[t][e][1]=o}for(e=0;i>e;++e)a[e]=0;return a},zero:ci});ta.layout.histogram=function(){function n(n,i){for(var o,a,c=[],l=n.map(e,this),s=r.call(this,l,i),f=u.call(this,s,l,i),i=-1,h=l.length,g=f.length-1,p=t?1:1/h;++i<g;)o=c[i]=[],o.dx=f[i+1]-(o.x=f[i]),o.y=0;if(g>0)for(i=-1;++i<h;)a=l[i],a>=s[0]&&a<=s[1]&&(o=c[ta.bisect(f,a,1,g)-1],o.y+=p,o.push(n[i]));return c}var t=!0,e=Number,r=pi,u=hi;return n.value=function(t){return arguments.length?(e=t,n):e},n.range=function(t){return arguments.length?(r=Et(t),n):r},n.bins=function(t){return arguments.length?(u="number"==typeof t?function(n){return gi(n,t)}:Et(t),n):u},n.frequency=function(e){return arguments.length?(t=!!e,n):t},n},ta.layout.pack=function(){function n(n,i){var o=e.call(this,n,i),a=o[0],c=u[0],l=u[1],s=null==t?Math.sqrt:"function"==typeof t?t:function(){return t};if(a.x=a.y=0,Qu(a,function(n){n.r=+s(n.value)}),Qu(a,Mi),r){var f=r*(t?1:Math.max(2*a.r/c,2*a.r/l))/2;Qu(a,function(n){n.r+=f}),Qu(a,Mi),Qu(a,function(n){n.r-=f})}return _i(a,c/2,l/2,t?1:1/Math.max(2*a.r/c,2*a.r/l)),o}var t,e=ta.layout.hierarchy().sort(vi),r=0,u=[1,1];return n.size=function(t){return arguments.length?(u=t,n):u},n.radius=function(e){return arguments.length?(t=null==e||"function"==typeof e?e:+e,n):t},n.padding=function(t){return arguments.length?(r=+t,n):r},Gu(n,e)},ta.layout.tree=function(){function n(n,u){var s=o.call(this,n,u),f=s[0],h=t(f);if(Qu(h,e),h.parent.m=-h.z,Ku(h,r),l)Ku(f,i);else{var g=f,p=f,v=f;Ku(f,function(n){n.x<g.x&&(g=n),n.x>p.x&&(p=n),n.depth>v.depth&&(v=n)});var d=a(g,p)/2-g.x,m=c[0]/(p.x+a(p,g)/2+d),y=c[1]/(v.depth||1);Ku(f,function(n){n.x=(n.x+d)*m,n.y=n.depth*y})}return s}function t(n){for(var t,e={A:null,children:[n]},r=[e];null!=(t=r.pop());)for(var u,i=t.children,o=0,a=i.length;a>o;++o)r.push((i[o]=u={_:i[o],parent:t,children:(u=i[o].children)&&u.slice()||[],A:null,a:null,z:0,m:0,c:0,s:0,t:null,i:o}).a=u);return e.children[0]}function e(n){var t=n.children,e=n.parent.children,r=n.i?e[n.i-1]:null;if(t.length){Ni(n);var i=(t[0].z+t[t.length-1].z)/2;r?(n.z=r.z+a(n._,r._),n.m=n.z-i):n.z=i}else r&&(n.z=r.z+a(n._,r._));n.parent.A=u(n,r,n.parent.A||e[0])}function r(n){n._.x=n.z+n.parent.m,n.m+=n.parent.m}function u(n,t,e){if(t){for(var r,u=n,i=n,o=t,c=u.parent.children[0],l=u.m,s=i.m,f=o.m,h=c.m;o=Ei(o),u=ki(u),o&&u;)c=ki(c),i=Ei(i),i.a=n,r=o.z+f-u.z-l+a(o._,u._),r>0&&(Ai(Ci(o,n,e),n,r),l+=r,s+=r),f+=o.m,l+=u.m,h+=c.m,s+=i.m;o&&!Ei(i)&&(i.t=o,i.m+=f-s),u&&!ki(c)&&(c.t=u,c.m+=l-h,e=n)}return e}function i(n){n.x*=c[0],n.y=n.depth*c[1]}var o=ta.layout.hierarchy().sort(null).value(null),a=Si,c=[1,1],l=null;return n.separation=function(t){return arguments.length?(a=t,n):a},n.size=function(t){return arguments.length?(l=null==(c=t)?i:null,n):l?null:c},n.nodeSize=function(t){return arguments.length?(l=null==(c=t)?null:i,n):l?c:null},Gu(n,o)},ta.layout.cluster=function(){function n(n,i){var o,a=t.call(this,n,i),c=a[0],l=0;Qu(c,function(n){var t=n.children;t&&t.length?(n.x=qi(t),n.y=zi(t)):(n.x=o?l+=e(n,o):0,n.y=0,o=n)});var s=Li(c),f=Ti(c),h=s.x-e(s,f)/2,g=f.x+e(f,s)/2;return Qu(c,u?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]}),a}var t=ta.layout.hierarchy().sort(null).value(null),e=Si,r=[1,1],u=!1;return n.separation=function(t){return arguments.length?(e=t,n):e},n.size=function(t){return arguments.length?(u=null==(r=t),n):u?null:r},n.nodeSize=function(t){return arguments.length?(u=null!=(r=t),n):u?r:null},Gu(n,t)},ta.layout.treemap=function(){function n(n,t){for(var e,r,u=-1,i=n.length;++u<i;)r=(e=n[u]).value*(0>t?0:t),e.area=isNaN(r)||0>=r?0:r}function t(e){var i=e.children;if(i&&i.length){var o,a,c,l=f(e),s=[],h=i.slice(),p=1/0,v="slice"===g?l.dx:"dice"===g?l.dy:"slice-dice"===g?1&e.depth?l.dy:l.dx:Math.min(l.dx,l.dy);for(n(h,l.dx*l.dy/e.value),s.area=0;(c=h.length)>0;)s.push(o=h[c-1]),s.area+=o.area,"squarify"!==g||(a=r(s,v))<=p?(h.pop(),p=a):(s.area-=s.pop().area,u(s,v,l,!1),v=Math.min(l.dx,l.dy),s.length=s.area=0,p=1/0);s.length&&(u(s,v,l,!0),s.length=s.area=0),i.forEach(t)}}function e(t){var r=t.children;if(r&&r.length){var i,o=f(t),a=r.slice(),c=[];for(n(a,o.dx*o.dy/t.value),c.area=0;i=a.pop();)c.push(i),c.area+=i.area,null!=i.z&&(u(c,i.z?o.dx:o.dy,o,!a.length),c.length=c.area=0);r.forEach(e)}}function r(n,t){for(var e,r=n.area,u=0,i=1/0,o=-1,a=n.length;++o<a;)(e=n[o].area)&&(i>e&&(i=e),e>u&&(u=e));return r*=r,t*=t,r?Math.max(t*u*p/r,r/(t*i*p)):1/0}function u(n,t,e,r){var u,i=-1,o=n.length,a=e.x,l=e.y,s=t?c(n.area/t):0;if(t==e.dx){for((r||s>e.dy)&&(s=e.dy);++i<o;)u=n[i],u.x=a,u.y=l,u.dy=s,a+=u.dx=Math.min(e.x+e.dx-a,s?c(u.area/s):0);u.z=!0,u.dx+=e.x+e.dx-a,e.y+=s,e.dy-=s}else{for((r||s>e.dx)&&(s=e.dx);++i<o;)u=n[i],u.x=a,u.y=l,u.dx=s,l+=u.dy=Math.min(e.y+e.dy-l,s?c(u.area/s):0);u.z=!1,u.dy+=e.y+e.dy-l,e.x+=s,e.dx-=s}}function i(r){var u=o||a(r),i=u[0];return i.x=0,i.y=0,i.dx=l[0],i.dy=l[1],o&&a.revalue(i),n([i],i.dx*i.dy/i.value),(o?e:t)(i),h&&(o=u),u}var o,a=ta.layout.hierarchy(),c=Math.round,l=[1,1],s=null,f=Ri,h=!1,g="squarify",p=.5*(1+Math.sqrt(5)); +return i.size=function(n){return arguments.length?(l=n,i):l},i.padding=function(n){function t(t){var e=n.call(i,t,t.depth);return null==e?Ri(t):Di(t,"number"==typeof e?[e,e,e,e]:e)}function e(t){return Di(t,n)}if(!arguments.length)return s;var r;return f=null==(s=n)?Ri:"function"==(r=typeof n)?t:"number"===r?(n=[n,n,n,n],e):e,i},i.round=function(n){return arguments.length?(c=n?Math.round:Number,i):c!=Number},i.sticky=function(n){return arguments.length?(h=n,o=null,i):h},i.ratio=function(n){return arguments.length?(p=n,i):p},i.mode=function(n){return arguments.length?(g=n+"",i):g},Gu(i,a)},ta.random={normal:function(n,t){var e=arguments.length;return 2>e&&(t=1),1>e&&(n=0),function(){var e,r,u;do e=2*Math.random()-1,r=2*Math.random()-1,u=e*e+r*r;while(!u||u>1);return n+t*e*Math.sqrt(-2*Math.log(u)/u)}},logNormal:function(){var n=ta.random.normal.apply(ta,arguments);return function(){return Math.exp(n())}},bates:function(n){var t=ta.random.irwinHall(n);return function(){return t()/n}},irwinHall:function(n){return function(){for(var t=0,e=0;n>e;e++)t+=Math.random();return t}}},ta.scale={};var ml={floor:y,ceil:y};ta.scale.linear=function(){return Ii([0,1],[0,1],mu,!1)};var yl={s:1,g:1,p:1,r:1,e:1};ta.scale.log=function(){return Ji(ta.scale.linear().domain([0,1]),10,!0,[1,10])};var Ml=ta.format(".0e"),xl={floor:function(n){return-Math.ceil(-n)},ceil:function(n){return-Math.floor(-n)}};ta.scale.pow=function(){return Gi(ta.scale.linear(),1,[0,1])},ta.scale.sqrt=function(){return ta.scale.pow().exponent(.5)},ta.scale.ordinal=function(){return Qi([],{t:"range",a:[[]]})},ta.scale.category10=function(){return ta.scale.ordinal().range(bl)},ta.scale.category20=function(){return ta.scale.ordinal().range(_l)},ta.scale.category20b=function(){return ta.scale.ordinal().range(wl)},ta.scale.category20c=function(){return ta.scale.ordinal().range(Sl)};var bl=[2062260,16744206,2924588,14034728,9725885,9197131,14907330,8355711,12369186,1556175].map(Mt),_l=[2062260,11454440,16744206,16759672,2924588,10018698,14034728,16750742,9725885,12955861,9197131,12885140,14907330,16234194,8355711,13092807,12369186,14408589,1556175,10410725].map(Mt),wl=[3750777,5395619,7040719,10264286,6519097,9216594,11915115,13556636,9202993,12426809,15186514,15190932,8666169,11356490,14049643,15177372,8077683,10834324,13528509,14589654].map(Mt),Sl=[3244733,7057110,10406625,13032431,15095053,16616764,16625259,16634018,3253076,7652470,10607003,13101504,7695281,10394312,12369372,14342891,6513507,9868950,12434877,14277081].map(Mt);ta.scale.quantile=function(){return no([],[])},ta.scale.quantize=function(){return to(0,1,[0,1])},ta.scale.threshold=function(){return eo([.5],[0,1])},ta.scale.identity=function(){return ro([0,1])},ta.svg={},ta.svg.arc=function(){function n(){var n=Math.max(0,+e.apply(this,arguments)),l=Math.max(0,+r.apply(this,arguments)),s=o.apply(this,arguments)-Ra,f=a.apply(this,arguments)-Ra,h=Math.abs(f-s),g=s>f?0:1;if(n>l&&(p=l,l=n,n=p),h>=Ta)return t(l,g)+(n?t(n,1-g):"")+"Z";var p,v,d,m,y,M,x,b,_,w,S,k,E=0,A=0,N=[];if((m=(+c.apply(this,arguments)||0)/2)&&(d=i===kl?Math.sqrt(n*n+l*l):+i.apply(this,arguments),g||(A*=-1),l&&(A=tt(d/l*Math.sin(m))),n&&(E=tt(d/n*Math.sin(m)))),l){y=l*Math.cos(s+A),M=l*Math.sin(s+A),x=l*Math.cos(f-A),b=l*Math.sin(f-A);var C=Math.abs(f-s-2*A)<=qa?0:1;if(A&&so(y,M,x,b)===g^C){var z=(s+f)/2;y=l*Math.cos(z),M=l*Math.sin(z),x=b=null}}else y=M=0;if(n){_=n*Math.cos(f-E),w=n*Math.sin(f-E),S=n*Math.cos(s+E),k=n*Math.sin(s+E);var q=Math.abs(s-f+2*E)<=qa?0:1;if(E&&so(_,w,S,k)===1-g^q){var L=(s+f)/2;_=n*Math.cos(L),w=n*Math.sin(L),S=k=null}}else _=w=0;if((p=Math.min(Math.abs(l-n)/2,+u.apply(this,arguments)))>.001){v=l>n^g?0:1;var T=null==S?[_,w]:null==x?[y,M]:Lr([y,M],[S,k],[x,b],[_,w]),R=y-T[0],D=M-T[1],P=x-T[0],U=b-T[1],j=1/Math.sin(Math.acos((R*P+D*U)/(Math.sqrt(R*R+D*D)*Math.sqrt(P*P+U*U)))/2),F=Math.sqrt(T[0]*T[0]+T[1]*T[1]);if(null!=x){var H=Math.min(p,(l-F)/(j+1)),O=fo(null==S?[_,w]:[S,k],[y,M],l,H,g),I=fo([x,b],[_,w],l,H,g);p===H?N.push("M",O[0],"A",H,",",H," 0 0,",v," ",O[1],"A",l,",",l," 0 ",1-g^so(O[1][0],O[1][1],I[1][0],I[1][1]),",",g," ",I[1],"A",H,",",H," 0 0,",v," ",I[0]):N.push("M",O[0],"A",H,",",H," 0 1,",v," ",I[0])}else N.push("M",y,",",M);if(null!=S){var Y=Math.min(p,(n-F)/(j-1)),Z=fo([y,M],[S,k],n,-Y,g),V=fo([_,w],null==x?[y,M]:[x,b],n,-Y,g);p===Y?N.push("L",V[0],"A",Y,",",Y," 0 0,",v," ",V[1],"A",n,",",n," 0 ",g^so(V[1][0],V[1][1],Z[1][0],Z[1][1]),",",1-g," ",Z[1],"A",Y,",",Y," 0 0,",v," ",Z[0]):N.push("L",V[0],"A",Y,",",Y," 0 0,",v," ",Z[0])}else N.push("L",_,",",w)}else N.push("M",y,",",M),null!=x&&N.push("A",l,",",l," 0 ",C,",",g," ",x,",",b),N.push("L",_,",",w),null!=S&&N.push("A",n,",",n," 0 ",q,",",1-g," ",S,",",k);return N.push("Z"),N.join("")}function t(n,t){return"M0,"+n+"A"+n+","+n+" 0 1,"+t+" 0,"+-n+"A"+n+","+n+" 0 1,"+t+" 0,"+n}var e=io,r=oo,u=uo,i=kl,o=ao,a=co,c=lo;return n.innerRadius=function(t){return arguments.length?(e=Et(t),n):e},n.outerRadius=function(t){return arguments.length?(r=Et(t),n):r},n.cornerRadius=function(t){return arguments.length?(u=Et(t),n):u},n.padRadius=function(t){return arguments.length?(i=t==kl?kl:Et(t),n):i},n.startAngle=function(t){return arguments.length?(o=Et(t),n):o},n.endAngle=function(t){return arguments.length?(a=Et(t),n):a},n.padAngle=function(t){return arguments.length?(c=Et(t),n):c},n.centroid=function(){var n=(+e.apply(this,arguments)+ +r.apply(this,arguments))/2,t=(+o.apply(this,arguments)+ +a.apply(this,arguments))/2-Ra;return[Math.cos(t)*n,Math.sin(t)*n]},n};var kl="auto";ta.svg.line=function(){return ho(y)};var El=ta.map({linear:go,"linear-closed":po,step:vo,"step-before":mo,"step-after":yo,basis:So,"basis-open":ko,"basis-closed":Eo,bundle:Ao,cardinal:bo,"cardinal-open":Mo,"cardinal-closed":xo,monotone:To});El.forEach(function(n,t){t.key=n,t.closed=/-closed$/.test(n)});var Al=[0,2/3,1/3,0],Nl=[0,1/3,2/3,0],Cl=[0,1/6,2/3,1/6];ta.svg.line.radial=function(){var n=ho(Ro);return n.radius=n.x,delete n.x,n.angle=n.y,delete n.y,n},mo.reverse=yo,yo.reverse=mo,ta.svg.area=function(){return Do(y)},ta.svg.area.radial=function(){var n=Do(Ro);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},ta.svg.chord=function(){function n(n,a){var c=t(this,i,n,a),l=t(this,o,n,a);return"M"+c.p0+r(c.r,c.p1,c.a1-c.a0)+(e(c,l)?u(c.r,c.p1,c.r,c.p0):u(c.r,c.p1,l.r,l.p0)+r(l.r,l.p1,l.a1-l.a0)+u(l.r,l.p1,c.r,c.p0))+"Z"}function t(n,t,e,r){var u=t.call(n,e,r),i=a.call(n,u,r),o=c.call(n,u,r)-Ra,s=l.call(n,u,r)-Ra;return{r:i,a0:o,a1:s,p0:[i*Math.cos(o),i*Math.sin(o)],p1:[i*Math.cos(s),i*Math.sin(s)]}}function e(n,t){return n.a0==t.a0&&n.a1==t.a1}function r(n,t,e){return"A"+n+","+n+" 0 "+ +(e>qa)+",1 "+t}function u(n,t,e,r){return"Q 0,0 "+r}var i=mr,o=yr,a=Po,c=ao,l=co;return n.radius=function(t){return arguments.length?(a=Et(t),n):a},n.source=function(t){return arguments.length?(i=Et(t),n):i},n.target=function(t){return arguments.length?(o=Et(t),n):o},n.startAngle=function(t){return arguments.length?(c=Et(t),n):c},n.endAngle=function(t){return arguments.length?(l=Et(t),n):l},n},ta.svg.diagonal=function(){function n(n,u){var i=t.call(this,n,u),o=e.call(this,n,u),a=(i.y+o.y)/2,c=[i,{x:i.x,y:a},{x:o.x,y:a},o];return c=c.map(r),"M"+c[0]+"C"+c[1]+" "+c[2]+" "+c[3]}var t=mr,e=yr,r=Uo;return n.source=function(e){return arguments.length?(t=Et(e),n):t},n.target=function(t){return arguments.length?(e=Et(t),n):e},n.projection=function(t){return arguments.length?(r=t,n):r},n},ta.svg.diagonal.radial=function(){var n=ta.svg.diagonal(),t=Uo,e=n.projection;return n.projection=function(n){return arguments.length?e(jo(t=n)):t},n},ta.svg.symbol=function(){function n(n,r){return(zl.get(t.call(this,n,r))||Oo)(e.call(this,n,r))}var t=Ho,e=Fo;return n.type=function(e){return arguments.length?(t=Et(e),n):t},n.size=function(t){return arguments.length?(e=Et(t),n):e},n};var zl=ta.map({circle:Oo,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*Ll)),e=t*Ll;return"M0,"+-t+"L"+e+",0 0,"+t+" "+-e+",0Z"},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/ql),e=t*ql/2;return"M0,"+e+"L"+t+","+-e+" "+-t+","+-e+"Z"},"triangle-up":function(n){var t=Math.sqrt(n/ql),e=t*ql/2;return"M0,"+-e+"L"+t+","+e+" "+-t+","+e+"Z"}});ta.svg.symbolTypes=zl.keys();var ql=Math.sqrt(3),Ll=Math.tan(30*Da);_a.transition=function(n){for(var t,e,r=Tl||++Ul,u=Xo(n),i=[],o=Rl||{time:Date.now(),ease:Su,delay:0,duration:250},a=-1,c=this.length;++a<c;){i.push(t=[]);for(var l=this[a],s=-1,f=l.length;++s<f;)(e=l[s])&&$o(e,s,u,r,o),t.push(e)}return Yo(i,u,r)},_a.interrupt=function(n){return this.each(null==n?Dl:Io(Xo(n)))};var Tl,Rl,Dl=Io(Xo()),Pl=[],Ul=0;Pl.call=_a.call,Pl.empty=_a.empty,Pl.node=_a.node,Pl.size=_a.size,ta.transition=function(n,t){return n&&n.transition?Tl?n.transition(t):n:ta.selection().transition(n)},ta.transition.prototype=Pl,Pl.select=function(n){var t,e,r,u=this.id,i=this.namespace,o=[];n=N(n);for(var a=-1,c=this.length;++a<c;){o.push(t=[]);for(var l=this[a],s=-1,f=l.length;++s<f;)(r=l[s])&&(e=n.call(r,r.__data__,s,a))?("__data__"in r&&(e.__data__=r.__data__),$o(e,s,i,u,r[i][u]),t.push(e)):t.push(null)}return Yo(o,i,u)},Pl.selectAll=function(n){var t,e,r,u,i,o=this.id,a=this.namespace,c=[];n=C(n);for(var l=-1,s=this.length;++l<s;)for(var f=this[l],h=-1,g=f.length;++h<g;)if(r=f[h]){i=r[a][o],e=n.call(r,r.__data__,h,l),c.push(t=[]);for(var p=-1,v=e.length;++p<v;)(u=e[p])&&$o(u,p,a,o,i),t.push(u)}return Yo(c,a,o)},Pl.filter=function(n){var t,e,r,u=[];"function"!=typeof n&&(n=O(n));for(var i=0,o=this.length;o>i;i++){u.push(t=[]);for(var e=this[i],a=0,c=e.length;c>a;a++)(r=e[a])&&n.call(r,r.__data__,a,i)&&t.push(r)}return Yo(u,this.namespace,this.id)},Pl.tween=function(n,t){var e=this.id,r=this.namespace;return arguments.length<2?this.node()[r][e].tween.get(n):Y(this,null==t?function(t){t[r][e].tween.remove(n)}:function(u){u[r][e].tween.set(n,t)})},Pl.attr=function(n,t){function e(){this.removeAttribute(a)}function r(){this.removeAttributeNS(a.space,a.local)}function u(n){return null==n?e:(n+="",function(){var t,e=this.getAttribute(a);return e!==n&&(t=o(e,n),function(n){this.setAttribute(a,t(n))})})}function i(n){return null==n?r:(n+="",function(){var t,e=this.getAttributeNS(a.space,a.local);return e!==n&&(t=o(e,n),function(n){this.setAttributeNS(a.space,a.local,t(n))})})}if(arguments.length<2){for(t in n)this.attr(t,n[t]);return this}var o="transform"==n?Hu:mu,a=ta.ns.qualify(n);return Zo(this,"attr."+n,t,a.local?i:u)},Pl.attrTween=function(n,t){function e(n,e){var r=t.call(this,n,e,this.getAttribute(u));return r&&function(n){this.setAttribute(u,r(n))}}function r(n,e){var r=t.call(this,n,e,this.getAttributeNS(u.space,u.local));return r&&function(n){this.setAttributeNS(u.space,u.local,r(n))}}var u=ta.ns.qualify(n);return this.tween("attr."+n,u.local?r:e)},Pl.style=function(n,e,r){function u(){this.style.removeProperty(n)}function i(e){return null==e?u:(e+="",function(){var u,i=t(this).getComputedStyle(this,null).getPropertyValue(n);return i!==e&&(u=mu(i,e),function(t){this.style.setProperty(n,u(t),r)})})}var o=arguments.length;if(3>o){if("string"!=typeof n){2>o&&(e="");for(r in n)this.style(r,n[r],e);return this}r=""}return Zo(this,"style."+n,e,i)},Pl.styleTween=function(n,e,r){function u(u,i){var o=e.call(this,u,i,t(this).getComputedStyle(this,null).getPropertyValue(n));return o&&function(t){this.style.setProperty(n,o(t),r)}}return arguments.length<3&&(r=""),this.tween("style."+n,u)},Pl.text=function(n){return Zo(this,"text",n,Vo)},Pl.remove=function(){var n=this.namespace;return this.each("end.transition",function(){var t;this[n].count<2&&(t=this.parentNode)&&t.removeChild(this)})},Pl.ease=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].ease:("function"!=typeof n&&(n=ta.ease.apply(ta,arguments)),Y(this,function(r){r[e][t].ease=n}))},Pl.delay=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].delay:Y(this,"function"==typeof n?function(r,u,i){r[e][t].delay=+n.call(r,r.__data__,u,i)}:(n=+n,function(r){r[e][t].delay=n}))},Pl.duration=function(n){var t=this.id,e=this.namespace;return arguments.length<1?this.node()[e][t].duration:Y(this,"function"==typeof n?function(r,u,i){r[e][t].duration=Math.max(1,n.call(r,r.__data__,u,i))}:(n=Math.max(1,n),function(r){r[e][t].duration=n}))},Pl.each=function(n,t){var e=this.id,r=this.namespace;if(arguments.length<2){var u=Rl,i=Tl;try{Tl=e,Y(this,function(t,u,i){Rl=t[r][e],n.call(t,t.__data__,u,i)})}finally{Rl=u,Tl=i}}else Y(this,function(u){var i=u[r][e];(i.event||(i.event=ta.dispatch("start","end","interrupt"))).on(n,t)});return this},Pl.transition=function(){for(var n,t,e,r,u=this.id,i=++Ul,o=this.namespace,a=[],c=0,l=this.length;l>c;c++){a.push(n=[]);for(var t=this[c],s=0,f=t.length;f>s;s++)(e=t[s])&&(r=e[o][u],$o(e,s,o,i,{time:r.time,ease:r.ease,delay:r.delay+r.duration,duration:r.duration})),n.push(e)}return Yo(a,o,i)},ta.svg.axis=function(){function n(n){n.each(function(){var n,l=ta.select(this),s=this.__chart__||e,f=this.__chart__=e.copy(),h=null==c?f.ticks?f.ticks.apply(f,a):f.domain():c,g=null==t?f.tickFormat?f.tickFormat.apply(f,a):y:t,p=l.selectAll(".tick").data(h,f),v=p.enter().insert("g",".domain").attr("class","tick").style("opacity",Ca),d=ta.transition(p.exit()).style("opacity",Ca).remove(),m=ta.transition(p.order()).style("opacity",1),M=Math.max(u,0)+o,x=Ui(f),b=l.selectAll(".domain").data([0]),_=(b.enter().append("path").attr("class","domain"),ta.transition(b));v.append("line"),v.append("text");var w,S,k,E,A=v.select("line"),N=m.select("line"),C=p.select("text").text(g),z=v.select("text"),q=m.select("text"),L="top"===r||"left"===r?-1:1;if("bottom"===r||"top"===r?(n=Bo,w="x",k="y",S="x2",E="y2",C.attr("dy",0>L?"0em":".71em").style("text-anchor","middle"),_.attr("d","M"+x[0]+","+L*i+"V0H"+x[1]+"V"+L*i)):(n=Wo,w="y",k="x",S="y2",E="x2",C.attr("dy",".32em").style("text-anchor",0>L?"end":"start"),_.attr("d","M"+L*i+","+x[0]+"H0V"+x[1]+"H"+L*i)),A.attr(E,L*u),z.attr(k,L*M),N.attr(S,0).attr(E,L*u),q.attr(w,0).attr(k,L*M),f.rangeBand){var T=f,R=T.rangeBand()/2;s=f=function(n){return T(n)+R}}else s.rangeBand?s=f:d.call(n,f,s);v.call(n,s,f),m.call(n,f,f)})}var t,e=ta.scale.linear(),r=jl,u=6,i=6,o=3,a=[10],c=null;return n.scale=function(t){return arguments.length?(e=t,n):e},n.orient=function(t){return arguments.length?(r=t in Fl?t+"":jl,n):r},n.ticks=function(){return arguments.length?(a=arguments,n):a},n.tickValues=function(t){return arguments.length?(c=t,n):c},n.tickFormat=function(e){return arguments.length?(t=e,n):t},n.tickSize=function(t){var e=arguments.length;return e?(u=+t,i=+arguments[e-1],n):u},n.innerTickSize=function(t){return arguments.length?(u=+t,n):u},n.outerTickSize=function(t){return arguments.length?(i=+t,n):i},n.tickPadding=function(t){return arguments.length?(o=+t,n):o},n.tickSubdivide=function(){return arguments.length&&n},n};var jl="bottom",Fl={top:1,right:1,bottom:1,left:1};ta.svg.brush=function(){function n(t){t.each(function(){var t=ta.select(this).style("pointer-events","all").style("-webkit-tap-highlight-color","rgba(0,0,0,0)").on("mousedown.brush",i).on("touchstart.brush",i),o=t.selectAll(".background").data([0]);o.enter().append("rect").attr("class","background").style("visibility","hidden").style("cursor","crosshair"),t.selectAll(".extent").data([0]).enter().append("rect").attr("class","extent").style("cursor","move");var a=t.selectAll(".resize").data(v,y);a.exit().remove(),a.enter().append("g").attr("class",function(n){return"resize "+n}).style("cursor",function(n){return Hl[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"),a.style("display",n.empty()?"none":null);var c,f=ta.transition(t),h=ta.transition(o);l&&(c=Ui(l),h.attr("x",c[0]).attr("width",c[1]-c[0]),r(f)),s&&(c=Ui(s),h.attr("y",c[0]).attr("height",c[1]-c[0]),u(f)),e(f)})}function e(n){n.selectAll(".resize").attr("transform",function(n){return"translate("+f[+/e$/.test(n)]+","+h[+/^s/.test(n)]+")"})}function r(n){n.select(".extent").attr("x",f[0]),n.selectAll(".extent,.n>rect,.s>rect").attr("width",f[1]-f[0])}function u(n){n.select(".extent").attr("y",h[0]),n.selectAll(".extent,.e>rect,.w>rect").attr("height",h[1]-h[0])}function i(){function i(){32==ta.event.keyCode&&(C||(M=null,q[0]-=f[1],q[1]-=h[1],C=2),S())}function v(){32==ta.event.keyCode&&2==C&&(q[0]+=f[1],q[1]+=h[1],C=0,S())}function d(){var n=ta.mouse(b),t=!1;x&&(n[0]+=x[0],n[1]+=x[1]),C||(ta.event.altKey?(M||(M=[(f[0]+f[1])/2,(h[0]+h[1])/2]),q[0]=f[+(n[0]<M[0])],q[1]=h[+(n[1]<M[1])]):M=null),A&&m(n,l,0)&&(r(k),t=!0),N&&m(n,s,1)&&(u(k),t=!0),t&&(e(k),w({type:"brush",mode:C?"move":"resize"}))}function m(n,t,e){var r,u,i=Ui(t),c=i[0],l=i[1],s=q[e],v=e?h:f,d=v[1]-v[0];return C&&(c-=s,l-=d+s),r=(e?p:g)?Math.max(c,Math.min(l,n[e])):n[e],C?u=(r+=s)+d:(M&&(s=Math.max(c,Math.min(l,2*M[e]-r))),r>s?(u=r,r=s):u=s),v[0]!=r||v[1]!=u?(e?a=null:o=null,v[0]=r,v[1]=u,!0):void 0}function y(){d(),k.style("pointer-events","all").selectAll(".resize").style("display",n.empty()?"none":null),ta.select("body").style("cursor",null),L.on("mousemove.brush",null).on("mouseup.brush",null).on("touchmove.brush",null).on("touchend.brush",null).on("keydown.brush",null).on("keyup.brush",null),z(),w({type:"brushend"})}var M,x,b=this,_=ta.select(ta.event.target),w=c.of(b,arguments),k=ta.select(b),E=_.datum(),A=!/^(n|s)$/.test(E)&&l,N=!/^(e|w)$/.test(E)&&s,C=_.classed("extent"),z=W(b),q=ta.mouse(b),L=ta.select(t(b)).on("keydown.brush",i).on("keyup.brush",v);if(ta.event.changedTouches?L.on("touchmove.brush",d).on("touchend.brush",y):L.on("mousemove.brush",d).on("mouseup.brush",y),k.interrupt().selectAll("*").interrupt(),C)q[0]=f[0]-q[0],q[1]=h[0]-q[1];else if(E){var T=+/w$/.test(E),R=+/^n/.test(E);x=[f[1-T]-q[0],h[1-R]-q[1]],q[0]=f[T],q[1]=h[R]}else ta.event.altKey&&(M=q.slice());k.style("pointer-events","none").selectAll(".resize").style("display",null),ta.select("body").style("cursor",_.style("cursor")),w({type:"brushstart"}),d()}var o,a,c=E(n,"brushstart","brush","brushend"),l=null,s=null,f=[0,0],h=[0,0],g=!0,p=!0,v=Ol[0];return n.event=function(n){n.each(function(){var n=c.of(this,arguments),t={x:f,y:h,i:o,j:a},e=this.__chart__||t;this.__chart__=t,Tl?ta.select(this).transition().each("start.brush",function(){o=e.i,a=e.j,f=e.x,h=e.y,n({type:"brushstart"})}).tween("brush:brush",function(){var e=yu(f,t.x),r=yu(h,t.y);return o=a=null,function(u){f=t.x=e(u),h=t.y=r(u),n({type:"brush",mode:"resize"})}}).each("end.brush",function(){o=t.i,a=t.j,n({type:"brush",mode:"resize"}),n({type:"brushend"})}):(n({type:"brushstart"}),n({type:"brush",mode:"resize"}),n({type:"brushend"}))})},n.x=function(t){return arguments.length?(l=t,v=Ol[!l<<1|!s],n):l},n.y=function(t){return arguments.length?(s=t,v=Ol[!l<<1|!s],n):s},n.clamp=function(t){return arguments.length?(l&&s?(g=!!t[0],p=!!t[1]):l?g=!!t:s&&(p=!!t),n):l&&s?[g,p]:l?g:s?p:null},n.extent=function(t){var e,r,u,i,c;return arguments.length?(l&&(e=t[0],r=t[1],s&&(e=e[0],r=r[0]),o=[e,r],l.invert&&(e=l(e),r=l(r)),e>r&&(c=e,e=r,r=c),(e!=f[0]||r!=f[1])&&(f=[e,r])),s&&(u=t[0],i=t[1],l&&(u=u[1],i=i[1]),a=[u,i],s.invert&&(u=s(u),i=s(i)),u>i&&(c=u,u=i,i=c),(u!=h[0]||i!=h[1])&&(h=[u,i])),n):(l&&(o?(e=o[0],r=o[1]):(e=f[0],r=f[1],l.invert&&(e=l.invert(e),r=l.invert(r)),e>r&&(c=e,e=r,r=c))),s&&(a?(u=a[0],i=a[1]):(u=h[0],i=h[1],s.invert&&(u=s.invert(u),i=s.invert(i)),u>i&&(c=u,u=i,i=c))),l&&s?[[e,u],[r,i]]:l?[e,r]:s&&[u,i])},n.clear=function(){return n.empty()||(f=[0,0],h=[0,0],o=a=null),n},n.empty=function(){return!!l&&f[0]==f[1]||!!s&&h[0]==h[1]},ta.rebind(n,c,"on")};var Hl={n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},Ol=[["n","e","s","w","nw","ne","se","sw"],["e","w"],["n","s"],[]],Il=ac.format=gc.timeFormat,Yl=Il.utc,Zl=Yl("%Y-%m-%dT%H:%M:%S.%LZ");Il.iso=Date.prototype.toISOString&&+new Date("2000-01-01T00:00:00.000Z")?Jo:Zl,Jo.parse=function(n){var t=new Date(n);return isNaN(t)?null:t},Jo.toString=Zl.toString,ac.second=Ft(function(n){return new cc(1e3*Math.floor(n/1e3))},function(n,t){n.setTime(n.getTime()+1e3*Math.floor(t))},function(n){return n.getSeconds()}),ac.seconds=ac.second.range,ac.seconds.utc=ac.second.utc.range,ac.minute=Ft(function(n){return new cc(6e4*Math.floor(n/6e4))},function(n,t){n.setTime(n.getTime()+6e4*Math.floor(t))},function(n){return n.getMinutes()}),ac.minutes=ac.minute.range,ac.minutes.utc=ac.minute.utc.range,ac.hour=Ft(function(n){var t=n.getTimezoneOffset()/60;return new cc(36e5*(Math.floor(n/36e5-t)+t))},function(n,t){n.setTime(n.getTime()+36e5*Math.floor(t))},function(n){return n.getHours()}),ac.hours=ac.hour.range,ac.hours.utc=ac.hour.utc.range,ac.month=Ft(function(n){return n=ac.day(n),n.setDate(1),n},function(n,t){n.setMonth(n.getMonth()+t)},function(n){return n.getMonth()}),ac.months=ac.month.range,ac.months.utc=ac.month.utc.range;var Vl=[1e3,5e3,15e3,3e4,6e4,3e5,9e5,18e5,36e5,108e5,216e5,432e5,864e5,1728e5,6048e5,2592e6,7776e6,31536e6],Xl=[[ac.second,1],[ac.second,5],[ac.second,15],[ac.second,30],[ac.minute,1],[ac.minute,5],[ac.minute,15],[ac.minute,30],[ac.hour,1],[ac.hour,3],[ac.hour,6],[ac.hour,12],[ac.day,1],[ac.day,2],[ac.week,1],[ac.month,1],[ac.month,3],[ac.year,1]],$l=Il.multi([[".%L",function(n){return n.getMilliseconds()}],[":%S",function(n){return n.getSeconds()}],["%I:%M",function(n){return n.getMinutes()}],["%I %p",function(n){return n.getHours()}],["%a %d",function(n){return n.getDay()&&1!=n.getDate()}],["%b %d",function(n){return 1!=n.getDate()}],["%B",function(n){return n.getMonth()}],["%Y",Ne]]),Bl={range:function(n,t,e){return ta.range(Math.ceil(n/e)*e,+t,e).map(Ko)},floor:y,ceil:y};Xl.year=ac.year,ac.scale=function(){return Go(ta.scale.linear(),Xl,$l)};var Wl=Xl.map(function(n){return[n[0].utc,n[1]]}),Jl=Yl.multi([[".%L",function(n){return n.getUTCMilliseconds()}],[":%S",function(n){return n.getUTCSeconds()}],["%I:%M",function(n){return n.getUTCMinutes()}],["%I %p",function(n){return n.getUTCHours()}],["%a %d",function(n){return n.getUTCDay()&&1!=n.getUTCDate()}],["%b %d",function(n){return 1!=n.getUTCDate()}],["%B",function(n){return n.getUTCMonth()}],["%Y",Ne]]);Wl.year=ac.year.utc,ac.scale.utc=function(){return Go(ta.scale.linear(),Wl,Jl)},ta.text=At(function(n){return n.responseText}),ta.json=function(n,t){return Nt(n,"application/json",Qo,t)},ta.html=function(n,t){return Nt(n,"text/html",na,t)},ta.xml=At(function(n){return n.responseXML}),"function"==typeof define&&define.amd?define(ta):"object"==typeof module&&module.exports&&(module.exports=ta),this.d3=ta}(); \ No newline at end of file diff --git a/play/js/helpers.js b/play/js/helpers.js index 997fa747..2ff3231e 100644 --- a/play/js/helpers.js +++ b/play/js/helpers.js @@ -1,7 +1,602 @@ // YES, TAU. Math.TAU = Math.PI*2; -// For the election sandbox code -function _icon(name){ - return "<img src='img/icon/"+name+".png'/>"; + +function _drawText(text, x, y, textsize, ctx, textAlign) { + var lw = 2 + var color = 'black' + + ctx.save() + ctx.textAlign = textAlign || "center" + ctx.font = textsize + "px Sans-serif" + ctx.lineWidth = lw; + // ctx.strokeStyle = 'black'; + // ctx.strokeText(text, x, y); + ctx.fillStyle = color; + ctx.fillText(text, x, y); + ctx.restore() +} + + +/// expand with color, background etc. +// https://stackoverflow.com/a/18901408 +function drawTextBG(txt, x, y, textsize, ctx, textAlign) { + + /// lets save current state as we make a lot of changes + ctx.save(); + + /// set font + var font = textsize + "px Sans-serif" + ctx.font = textsize + "px Sans-serif" + ctx.textAlign = textAlign || "center" + + /// draw text from top - makes life easier at the moment + ctx.textBaseline = 'top'; + + /// color for background + ctx.fillStyle = '#f50'; + + /// get width of text + var width = ctx.measureText(txt).width; + + /// draw background rect assuming height of font + ctx.fillRect(x, y, width, textsize); + + /// text color + ctx.fillStyle = '#000'; + + /// draw text on top + ctx.fillText(txt, x, y); + + /// restore original state + ctx.restore(); +} + + +function _drawStroked(text, x, y, textsize, ctx, textAlign) { + _drawStrokedColor(text, x, y, textsize, 4, 'white', ctx, false,textAlign) +} +function _drawStrokedColor(text, x, y, textsize,lw, color, ctx, blend,textAlign) { + ctx.save() + ctx.textAlign = textAlign || "center" + ctx.font = textsize + "px Sans-serif" + ctx.lineWidth = lw; + ctx.shadowColor = "rgba(0,0,0,0.3)"; + ctx.shadowBlur = textsize * .1; + if (blend) { // blend color into border + ctx.strokeStyle = color; + ctx.strokeText(text, x, y); + ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)'; + } else { // black border + ctx.strokeStyle = 'black'; + } + ctx.strokeText(text, x, y); + ctx.fillStyle = color; + ctx.fillText(text, x, y); + ctx.restore() +} + +function _drawArrow(ctx, fromx, fromy, tox, toy){ + // https://stackoverflow.com/a/26080467 + + //variables to be used when creating the arrow + + var color = "#444"; + var headlen = 5; + + var angle = Math.atan2(toy-fromy,tox-fromx); + + //starting path of the arrow from the start square to the end square and drawing the stroke + ctx.beginPath(); + ctx.moveTo(fromx, fromy); + ctx.lineTo(tox, toy); + ctx.strokeStyle = color; + ctx.lineWidth = 3; + ctx.stroke(); + + //starting a new path from the head of the arrow to one of the sides of the point + ctx.beginPath(); + ctx.moveTo(tox, toy); + ctx.lineTo(tox-headlen*Math.cos(angle-Math.PI/7),toy-headlen*Math.sin(angle-Math.PI/7)); + + //path from the side point of the arrow, to the other side point + ctx.lineTo(tox-headlen*Math.cos(angle+Math.PI/7),toy-headlen*Math.sin(angle+Math.PI/7)); + + //path from the side point back to the tip of the arrow, and then again to the opposite side point + ctx.lineTo(tox, toy); + ctx.lineTo(tox-headlen*Math.cos(angle-Math.PI/7),toy-headlen*Math.sin(angle-Math.PI/7)); + + //draws the paths created above + ctx.strokeStyle = color; + ctx.lineWidth = 3; + ctx.stroke(); + ctx.fillStyle = color; + ctx.fill(); +} + +function _unitVector(to,from) { + var x1 = from.x + var y1 = from.y + var x2 = to.x + var y2 = to.y + var dx = x2 - x1 + var dy = y2 - y1 + var length = Math.sqrt(dx**2 + dy**2) + var ux = dx / length + var uy = dy / length + return {x:ux, y:uy} +} + +function _copyAttributes(to,from) { + // create copy of 'from' + from = from || {} + to = to || {} // maybe don't need this line? + for (var name in to) { + delete to[name] + } + for (var name in from) { + to[name] = from[name]; + } +} + +function _addAttributes(to,from) { + // create copy of 'from' + from = from || {} + to = to || {} + for (var name in from) { + to[name] = from[name]; + } +} + +function _fillInDefaults(to,from) { + // create copy of 'from' + from = from || {} + to = to || {} // maybe don't need this line? + var copy = _jcopy(from); + for (var name in copy) { + if(to[name] == undefined) to[name] = copy[name]; + } +} + +function _fillInDefaultsByAddress(to,from) { + from = from || {} + to = to || {} // maybe don't need this line? + for (var name in from) { + if(to[name] == undefined) to[name] = from[name]; + } +} + + +function _fillInSomeDefaults(to,from,names) { + from = from || {} + for (var i in names) { + if(from[names[i]] == undefined) continue + if(to[names[i]] == undefined) to[names[i]] = _jcopy(from[names[i]]); + } +} + +function _copySomeAttributes_old(to,from,names) { + // create copy of 'from' + var copy = _jcopy(from); // this ran into problems with self.model being a circular reference + to = to || {} + for (var i in names) { + to[names[i]] = copy[names[i]]; + } +} +function _copySomeAttributes(to,from,names) { + // create copy of 'from' + to = to || {} + for (var i in names) { + if(from[names[i]] == undefined) continue + to[names[i]] = _jcopy(from[names[i]]); + } +} + +function _jcopy(a) { + return JSON.parse(JSON.stringify(a)) +} + +function _objF(obj,f) { // run function if it exists for each item of an object + for(var item in obj ) { + if (obj[item][f]) obj[item][f]() + } + // for example: run ui.menu.systems.configure(), ui.menu.nCandidates.configure(), et al. +} + +function _rand5() { + return Math.round(100000 * Math.random()) +} + +function _randomString(length, chars) { // https://stackoverflow.com/a/10727155 + var result = ''; + for (var i = length; i > 0; --i) result += chars[Math.floor(Math.random() * chars.length)]; + return result; +} + +function _randAlphaNum(n) { + return _randomString(n, '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'); +} + +function _hashCode(s) { // https://stackoverflow.com/a/7616484 + var hash = 0, i, chr; + for (i = 0; i < s.length; i++) { + chr = s.charCodeAt(i); + hash = ((hash << 5) - hash) + chr; + hash |= 0; // Convert to 32bit integer + } + + // I will only use non-negative integers because it might be easier. + // So basically, I'm setting the first bit to 0. + var half = 2147483648 + return (hash + half) % half; +} + + +function _convertImageToDataURLviaCanvas(img, outputFormat){ + var canvas = document.createElement('CANVAS'); + var ctx = canvas.getContext('2d'); + canvas.height = img.height; + canvas.width = img.width; + ctx.drawImage(img, 0, 0); + var dataURL = canvas.toDataURL(); + canvas = null; + return dataURL +} + +function _convertNameToDataURLviaCanvas(letter,color, outputFormat){ + var canvas = document.createElement('CANVAS'); + var ctx = canvas.getContext('2d'); + var textsize = 120; // retina + + ctx.font = textsize + "px Sans-serif" + var text = ctx.measureText(letter) + + canvas.height = textsize * 1.25 // extra space because we want uppercase to be centered + canvas.width = text.width * 1.25 + canvas.style.height = canvas.height + canvas.style.width = canvas.width + + var x = canvas.width / 2 + var y = textsize + ctx.textAlign = "center" + linewidth = textsize / 30 + if (letter.length > 4) { + var reduce = Math.sqrt(letter.length) / 2 + // _drawStrokedColor(text, x, y, textsize,lw, color, ctx, blend) { + // _backgroundRectangleOffset(ctx,x,y/reduce,text.width/reduce,textsize/reduce) + _drawStrokedColor(letter,x, y - textsize/2 + textsize/reduce/2 + textsize/14 ,textsize / reduce,linewidth/reduce,color,ctx, true); + // drawTextBG(letter, x, y, textsize, ctx, "center") + } else { + var reduce = 1 + // _backgroundRectangleOffset(ctx,x,y,text.width,textsize) + _drawStrokedColor(letter,x,y,textsize,linewidth,color,ctx, true); + // drawTextBG(letter, x, y, textsize, ctx, "center") + } + var dataURL = canvas.toDataURL(outputFormat); + canvas = null; + return { + png_b64:dataURL, + widthFrac: text.width/textsize/1.25/reduce, // width as a fraction of image height + heightFrac: 1/1.25/reduce, // height as a fraction of image height + } +} + +function _backgroundRectangleOffset(ctx,x,y,width,height) { + ctx.save() + ctx.globalAlpha = .8 + ctx.fillStyle = "#fff" + ctx.fillRect(x - width/2, y-height/2*1.7, width, height); + ctx.restore() +} + +function _winBackgroundRectangle(ctx,x,y,width,height) { + ctx.save() + ctx.shadowColor = "rgba(0,0,0,0.3)"; + ctx.shadowBlur = 40 * .1; + ctx.globalAlpha = .8 + width += 45 + height += 45 + ctx.fillStyle = "#fff" + ctx.fillRect(x - width/2, y-height/2, width, height); + + + width -= 30 + height -= 30 + + // ctx.globalAlpha = 1 + ctx.lineWidth = 8 + ctx.strokeStyle = "#555" + // ctx.setLineDash([5, 5]); + ctx.strokeRect(x - width/2, y-height/2, width, height); + ctx.restore() +} + +function _winBackgroundRectangle2(ctx,x,y,width,height) { + ctx.save() + ctx.globalAlpha = .8 + width += 15 + height += 15 + ctx.fillStyle = "#fff" + ctx.fillRect(x - width/2, y-height/2, width, height); + + // ctx.globalAlpha = 1 + ctx.lineWidth = 8 + ctx.strokeStyle = "#555" + // ctx.setLineDash([5, 5]); + ctx.strokeRect(x - width/2, y-height/2, width, height); + if (1) { + ctx.strokeStyle = "#fff" + width += 8 + height += 8 + // ctx.setLineDash([15, 5]); + ctx.strokeRect(x - width/2, y-height/2, width, height); + } + ctx.restore() +} + +function _backgroundRectangle(ctx,x,y,width,height) { + ctx.save() + ctx.shadowColor = "rgba(0,0,0,0.3)"; + ctx.shadowBlur = 40 * .1; + width += 15 + height += 15 + ctx.globalAlpha = .8 + ctx.fillStyle = "#fff" + ctx.fillRect(x - width/2, y-height/2, width, height); + ctx.restore() +} + + +function _convertSVGToDataURLviaCanvas(img, outputFormat){ + var canvas = document.createElement('CANVAS'); + var ctx = canvas.getContext('2d'); + canvas.height = img.height; + canvas.width = img.width; + ctx.drawImage(img, 0, 0); + var dataURL = canvas.toDataURL(outputFormat); + canvas = null; + return dataURL +} + +// helper + +function _insertFunctionAfter(object,oldName,next) { + var old = object[oldName] + var newf = function () { + old() + next() + } + object[oldName] = newf +} + +function _removeSubnodes(n) { + while (n.firstChild) { + n.removeChild(n.lastChild); + } +} + +function _removeClass(element,name) { + element.className = element.className.replace(new RegExp(name,"g"), "") +} +function _addClass(element,name) { + _removeClass(element,name) // only one + element.className = element.className + " " + name +} + +function _displayNoneIf(dom,condition) { + if (condition) { + _addClass(dom,"displayNoneClass") + } else { + _removeClass(dom,"displayNoneClass") + } +} + +function _isEquivalent(a, b) { + // Create arrays of property names + var aProps = Object.getOwnPropertyNames(a); + var bProps = Object.getOwnPropertyNames(b); + + // If number of properties is different, + // objects are not equivalent + if (aProps.length != bProps.length) { + return false; + } + + for (var i = 0; i < aProps.length; i++) { + var propName = aProps[i]; + + // If values of same property are not equal, + // objects are not equivalent + if (a[propName] !== b[propName]) { + return false; + } + } + + // If we made it this far, objects + // are considered equivalent + return true; +} + + +var _ajax = {}; // https://stackoverflow.com/a/18078705 + +_ajax.x = function () { + if (typeof XMLHttpRequest !== 'undefined') { + return new XMLHttpRequest(); + } + var versions = [ + "MSXML2.XmlHttp.6.0", + "MSXML2.XmlHttp.5.0", + "MSXML2.XmlHttp.4.0", + "MSXML2.XmlHttp.3.0", + "MSXML2.XmlHttp.2.0", + "Microsoft.XmlHttp" + ]; + + var xhr; + for (var i = 0; i < versions.length; i++) { + try { + xhr = new ActiveXObject(versions[i]); + break; + } catch (e) { + } + } + return xhr; +}; + +_ajax.send = function (url, callback, method, data, async) { + if (async === undefined) { + async = true; + } + var x = _ajax.x(); + x.open(method, url, async); + x.onreadystatechange = function () { + if (x.readyState == 4) { + callback(x.responseText) + } + }; + if (method == 'POST') { + x.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + } + x.send(data) +}; + +_ajax.get = function (url, data, callback, async) { + var query = []; + for (var key in data) { + query.push(encodeURIComponent(key) + '=' + encodeURIComponent(data[key])); + } + _ajax.send(url + (query.length ? '?' + query.join('&') : ''), callback, 'GET', null, async) +}; + +function _getRequest(url, callback) { + const Http = new XMLHttpRequest(); + Http.open("GET", url); + Http.send(); + + Http.onreadystatechange = function(e) { + if (Http.readyState == 4) { + callback(Http.responseText) + } + } +} + +// https://stackoverflow.com/a/44134328 +function sepHslToHex(h, s, l) { + h /= 360; + s /= 100; + l /= 100; + let r, g, b; + if (s === 0) { + r = g = b = l; // achromatic + } else { + const hue2rgb = (p, q, t) => { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1 / 6) return p + (q - p) * 6 * t; + if (t < 1 / 2) return q; + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; + return p; + }; + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + r = hue2rgb(p, q, h + 1 / 3); + g = hue2rgb(p, q, h); + b = hue2rgb(p, q, h - 1 / 3); + } + const toHex = x => { + const hex = Math.round(x * 255).toString(16); + return hex.length === 1 ? '0' + hex : hex; + }; + return `#${toHex(r)}${toHex(g)}${toHex(b)}`; +} + +// https://stackoverflow.com/a/36721970 +function hslToHex(hsl) { + var a, b, g, h, l, p, q, r, ref, s; + if (hsl.match(/hsla?\((.+?)\)/)) { + ref = hsl.match(/hsla?\((.+?)\)/)[1].split(',').map(function(value) { + value.trim(); + return parseFloat(value); + }), h = ref[0], s = ref[1], l = ref[2], a = ref[3]; + } else { + return hsl + } + + return sepHslToHex(h, s, l) +} + +function addMinusButton(div,opt) { + var control = {} + addMinusButtonC(div,control,opt) +} + +function addMinusButtonC(div,control,opt) { + + opt = opt || {} + opt.caption = opt.caption || false + opt.ballot = opt.ballot || false + // add to dom + // var minus = document.createElement("div") + var minus = document.createElement("div") + div.prepend(minus) + // div.parentNode.insertBefore(minus, div.nextSibling) + // div.insertAdjacentElement('beforebegin', minus) + // make float to upper right + div.style.position = "relative" + minus.style = ` + position: absolute; + top: 0px; + right: 0px; + width: 20px; + height: 20px; + text-align: center; + vertical-align: middle; + line-height: 20px; + font-size: 20px; + background-color: #fff9; + color: #000d; + margin-top: 2px; + ` + // minus.style.float = "right" + // minus.style.clear = "both" + if (opt.ballot) { + minus.style["margin-right"] = "10px" + } + // minus.style.border = '2px solid black'; + // make minus sign + // var sign = document.createTextNode('-') + // minus.append(sign) + minus.innerText = '-' + // keep track of state + if (control.show == undefined) control.show = true + var stateDisplayDefault = div.style.display + updateMinus() + // add button function + minus.onclick = function () { + control.show = ! control.show + updateMinus() + } + function updateMinus() { + if (control.show) { + minus.innerText = '-' + // div.style.display = stateDisplayDefault + // div.hidden = false + div.style.height = 'unset' + div.style["overflow-y"] = "inherit" + } else { + minus.innerText = '+' + // div.style.display = 'none' + // div.hidden = true + if (opt.caption) { + div.style.height = '5px' + } else if (opt.ballot) { + div.style.height = '39px' + } else { + div.style.height = '25px' + } + div.style["overflow-y"] = "hidden" + } + } + } \ No newline at end of file diff --git a/play/js/hsluv-0.0.3.min.js b/play/js/hsluv-0.0.3.min.js new file mode 100644 index 00000000..69092dc3 --- /dev/null +++ b/play/js/hsluv-0.0.3.min.js @@ -0,0 +1,8 @@ +(function() {function f(a){var c=[],b=Math.pow(a+16,3)/1560896;b=b>g?b:a/k;for(var d=0;3>d;){var e=d++,h=l[e][0],w=l[e][1];e=l[e][2];for(var x=0;2>x;){var y=x++,z=(632260*e-126452*w)*b+126452*y;c.push({b:(284517*h-94839*e)*b/z,a:((838422*e+769860*w+731718*h)*a*b-769860*y*a)/z})}}return c}function m(a){a=f(a);for(var c=Infinity,b=0;b<a.length;){var d=a[b];++b;c=Math.min(c,Math.abs(d.a)/Math.sqrt(Math.pow(d.b,2)+1))}return c} +function n(a,c){c=c/360*Math.PI*2;a=f(a);for(var b=Infinity,d=0;d<a.length;){var e=a[d];++d;e=e.a/(Math.sin(c)-e.b*Math.cos(c));0<=e&&(b=Math.min(b,e))}return b}function p(a,c){for(var b=0,d=0,e=a.length;d<e;){var h=d++;b+=a[h]*c[h]}return b}function q(a){return.0031308>=a?12.92*a:1.055*Math.pow(a,.4166666666666667)-.055}function r(a){return.04045<a?Math.pow((a+.055)/1.055,2.4):a/12.92}function t(a){return[q(p(l[0],a)),q(p(l[1],a)),q(p(l[2],a))]} +function u(a){a=[r(a[0]),r(a[1]),r(a[2])];return[p(v[0],a),p(v[1],a),p(v[2],a)]}function A(a){var c=a[0],b=a[1];a=c+15*b+3*a[2];0!=a?(c=4*c/a,a=9*b/a):a=c=NaN;b=b<=g?b/B*k:116*Math.pow(b/B,.3333333333333333)-16;return 0==b?[0,0,0]:[b,13*b*(c-C),13*b*(a-D)]}function E(a){var c=a[0];if(0==c)return[0,0,0];var b=a[1]/(13*c)+C;a=a[2]/(13*c)+D;c=8>=c?B*c/k:B*Math.pow((c+16)/116,3);b=0-9*c*b/((b-4)*a-b*a);return[b,c,(9*c-15*a*c-a*b)/(3*a)]} +function F(a){var c=a[0],b=a[1],d=a[2];a=Math.sqrt(b*b+d*d);1E-8>a?b=0:(b=180*Math.atan2(d,b)/Math.PI,0>b&&(b=360+b));return[c,a,b]}function G(a){var c=a[1],b=a[2]/360*2*Math.PI;return[a[0],Math.cos(b)*c,Math.sin(b)*c]}function H(a){var c=a[0],b=a[1];a=a[2];if(99.9999999<a)return[100,0,c];if(1E-8>a)return[0,0,c];b=n(a,c)/100*b;return[a,b,c]}function I(a){var c=a[0],b=a[1];a=a[2];if(99.9999999<c)return[a,0,100];if(1E-8>c)return[a,0,0];var d=n(c,a);return[a,b/d*100,c]} +function J(a){var c=a[0],b=a[1];a=a[2];if(99.9999999<a)return[100,0,c];if(1E-8>a)return[0,0,c];b=m(a)/100*b;return[a,b,c]}function K(a){var c=a[0],b=a[1];a=a[2];if(99.9999999<c)return[a,0,100];if(1E-8>c)return[a,0,0];var d=m(c);return[a,b/d*100,c]}function L(a){for(var c="#",b=0;3>b;){var d=b++;d=Math.round(255*a[d]);var e=d%16;c+=M.charAt((d-e)/16|0)+M.charAt(e)}return c} +function N(a){a=a.toLowerCase();for(var c=[],b=0;3>b;){var d=b++;c.push((16*M.indexOf(a.charAt(2*d+1))+M.indexOf(a.charAt(2*d+2)))/255)}return c}function O(a){return t(E(G(a)))}function P(a){return F(A(u(a)))}function Q(a){return O(H(a))}function R(a){return I(P(a))}function S(a){return O(J(a))}function T(a){return K(P(a))} +var l=[[3.240969941904521,-1.537383177570093,-.498610760293],[-.96924363628087,1.87596750150772,.041555057407175],[.055630079696993,-.20397695888897,1.056971514242878]],v=[[.41239079926595,.35758433938387,.18048078840183],[.21263900587151,.71516867876775,.072192315360733],[.019330818715591,.11919477979462,.95053215224966]],B=1,C=.19783000664283,D=.46831999493879,k=903.2962962,g=.0088564516,M="0123456789abcdef"; +window.hsluv={hsluvToRgb:Q,rgbToHsluv:R,hpluvToRgb:S,rgbToHpluv:T,hsluvToHex:function(a){return L(Q(a))},hexToHsluv:function(a){return R(N(a))},hpluvToHex:function(a){return L(S(a))},hexToHpluv:function(a){return T(N(a))},lchToHpluv:K,hpluvToLch:J,lchToHsluv:I,hsluvToLch:H,lchToLuv:G,luvToLch:F,xyzToLuv:A,luvToXyz:E,xyzToRgb:t,rgbToXyz:u,lchToRgb:O,rgbToLch:P};})(); diff --git a/play/js/javascript-lp-solver/prod/solver.js b/play/js/javascript-lp-solver/prod/solver.js new file mode 100644 index 00000000..d312e1cd --- /dev/null +++ b/play/js/javascript-lp-solver/prod/solver.js @@ -0,0 +1,2 @@ +"object"==typeof exports&&(module.exports=require("./main")),function a(n,o,h){function l(e,t){if(!o[e]){if(!n[e]){var i="function"==typeof require&&require;if(!t&&i)return i(e,!0);if(u)return u(e,!0);var r=new Error("Cannot find module '"+e+"'");throw r.code="MODULE_NOT_FOUND",r}var s=o[e]={exports:{}};n[e][0].call(s.exports,function(t){return l(n[e][1][t]||t)},s,s.exports,a,n,o,h)}return o[e].exports}for(var u="function"==typeof require&&require,t=0;t<h.length;t++)l(h[t]);return l}({1:[function(t,e,i){},{}],2:[function(t,e,i){e.exports=function(t){return t.length?function(t){var e={is_blank:/^\W{0,}$/,is_objective:/(max|min)(imize){0,}\:/i,is_int:/^(?!\/\*)\W{0,}int/i,is_bin:/^(?!\/\*)\W{0,}bin/i,is_constraint:/(\>|\<){0,}\=/i,is_unrestricted:/^\S{0,}unrestricted/i,parse_lhs:/(\-|\+){0,1}\s{0,1}\d{0,}\.{0,}\d{0,}\s{0,}[A-Za-z]\S{0,}/gi,parse_rhs:/(\-|\+){0,1}\d{1,}\.{0,}\d{0,}\W{0,}\;{0,1}$/i,parse_dir:/(\>|\<){0,}\=/gi,parse_int:/[^\s|^\,]+/gi,parse_bin:/[^\s|^\,]+/gi,get_num:/(\-|\+){0,1}(\W|^)\d+\.{0,1}\d{0,}/g,get_word:/[A-Za-z].*/},i={opType:"",optimize:"_obj",constraints:{},variables:{}},r={">=":"min","<=":"max","=":"equal"},s="",a=null,n="",o="",h="",l=0;"string"==typeof t&&(t=t.split("\n"));for(var u=0;u<t.length;u++)if(h="__"+u,s=t[u],0,a=null,e.is_objective.test(s))i.opType=s.match(/(max|min)/gi)[0],(a=s.match(e.parse_lhs).map(function(t){return t.replace(/\s+/,"")}).slice(1)).forEach(function(t){n=null===(n=t.match(e.get_num))?"-"===t.substr(0,1)?-1:1:n[0],n=parseFloat(n),o=t.match(e.get_word)[0].replace(/\;$/,""),i.variables[o]=i.variables[o]||{},i.variables[o]._obj=n});else if(e.is_int.test(s))a=s.match(e.parse_int).slice(1),i.ints=i.ints||{},a.forEach(function(t){t=t.replace(";",""),i.ints[t]=1});else if(e.is_bin.test(s))a=s.match(e.parse_bin).slice(1),i.binaries=i.binaries||{},a.forEach(function(t){t=t.replace(";",""),i.binaries[t]=1});else if(e.is_constraint.test(s)){var d=s.indexOf(":");(a=(-1===d?s:s.slice(d+1)).match(e.parse_lhs).map(function(t){return t.replace(/\s+/,"")})).forEach(function(t){n=null===(n=t.match(e.get_num))?"-"===t.substr(0,1)?-1:1:n[0],n=parseFloat(n),o=t.match(e.get_word)[0],i.variables[o]=i.variables[o]||{},i.variables[o][h]=n}),l=parseFloat(s.match(e.parse_rhs)[0]),s=r[s.match(e.parse_dir)[0]],i.constraints[h]=i.constraints[h]||{},i.constraints[h][s]=l}else e.is_unrestricted.test(s)&&(a=s.match(e.parse_int).slice(1),i.unrestricted=i.unrestricted||{},a.forEach(function(t){t=t.replace(";",""),i.unrestricted[t]=1}));return i}(t):function(t){if(!t)throw new Error("Solver requires a model to operate on");var e="",i={max:"<=",min:">=",equal:"="},r=new RegExp("[^A-Za-z0-9_[{}/.&#$%~'@^]","gi");if(t.opType)for(var s in e+=t.opType+":",t.variables)t.variables[s][s]=t.variables[s][s]?t.variables[s][s]:1,t.variables[s][t.optimize]&&(e+=" "+t.variables[s][t.optimize]+" "+s.replace(r,"_"));else e+="max:";for(var a in e+=";\n\n",t.constraints)for(var n in t.constraints[a])if(void 0!==i[n]){for(var o in t.variables)void 0!==t.variables[o][a]&&(e+=" "+t.variables[o][a]+" "+o.replace(r,"_"));e+=" "+i[n]+" "+t.constraints[a][n],e+=";\n"}if(t.ints)for(var h in e+="\n\n",t.ints)e+="int "+h.replace(r,"_")+";\n";if(t.unrestricted)for(var l in e+="\n\n",t.unrestricted)e+="unrestricted "+l.replace(r,"_")+";\n";return e}(t)}},{}],3:[function(n,t,e){function o(t){return t=(t=(t=t.replace("\\r\\n","\r\n")).split("\r\n")).filter(function(t){return!0!==new RegExp(" 0$","gi").test(t)&&!1!==new RegExp("\\d$","gi").test(t)}).map(function(t){return t.split(/\:{0,1} +(?=\d)/)}).reduce(function(t,e,i){return t[e[0]]=e[1],t},{})}e.reformat=n("./Reformat.js"),e.solve=function(a){return new Promise(function(r,s){"undefined"!=typeof window&&s("Function Not Available in Browser");var t=n("./Reformat.js")(a);a.external||s("Data for this function must be contained in the 'external' attribute. Not seeing anything there."),a.external.binPath||s("No Executable | Binary path provided in arguments as 'binPath'"),a.external.args||s("No arguments array for cli | bash provided on 'args' attribute"),a.external.tempName||s("No 'tempName' given. This is necessary to produce a staging file for the solver to operate on"),n("fs").writeFile(a.external.tempName,t,function(t,e){if(t)s(t);else{var i=n("child_process").execFile;a.external.args.push(a.external.tempName),i(a.external.binPath,a.external.args,function(t,e){if(t)if(1===t.code)r(o(e));else{var i={code:t.code,meaning:{"-2":"Out of Memory",1:"SUBOPTIMAL",2:"INFEASIBLE",3:"UNBOUNDED",4:"DEGENERATE",5:"NUMFAILURE",6:"USER-ABORT",7:"TIMEOUT",9:"PRESOLVED",25:"ACCURACY ERROR",255:"FILE-ERROR"}[t.code],data:e};s(i)}else r(o(e))})}})})}},{"./Reformat.js":2,child_process:1,fs:1}],4:[function(t,e,i){e.exports={lpsolve:t("./lpsolve/main.js")}},{"./lpsolve/main.js":3}],5:[function(t,e,i){var r=t("./Tableau/Tableau.js"),s=(t("./Tableau/branchAndCut.js"),t("./expressions.js")),a=s.Constraint,F=s.Equality,o=s.Variable,h=s.IntegerVariable;s.Term;function n(t,e){this.tableau=new r(t),this.name=e,this.variables=[],this.integerVariables=[],this.unrestrictedVariables={},this.constraints=[],this.nConstraints=0,this.nVariables=0,this.isMinimization=!0,this.tableauInitialized=!1,this.relaxationIndex=1,this.useMIRCuts=!1,this.checkForCycles=!0,this.messages=[]}(e.exports=n).prototype.minimize=function(){return this.isMinimization=!0,this},n.prototype.maximize=function(){return this.isMinimization=!1,this},n.prototype._getNewElementIndex=function(){if(0<this.availableIndexes.length)return this.availableIndexes.pop();var t=this.lastElementIndex;return this.lastElementIndex+=1,t},n.prototype._addConstraint=function(t){var e=t.slack;this.tableau.variablesPerIndex[e.index]=e,this.constraints.push(t),this.nConstraints+=1,!0===this.tableauInitialized&&this.tableau.addConstraint(t)},n.prototype.smallerThan=function(t){var e=new a(t,!0,this.tableau.getNewElementIndex(),this);return this._addConstraint(e),e},n.prototype.greaterThan=function(t){var e=new a(t,!1,this.tableau.getNewElementIndex(),this);return this._addConstraint(e),e},n.prototype.equal=function(t){var e=new a(t,!0,this.tableau.getNewElementIndex(),this);this._addConstraint(e);var i=new a(t,!1,this.tableau.getNewElementIndex(),this);return this._addConstraint(i),new F(e,i)},n.prototype.addVariable=function(t,e,i,r,s){if("string"==typeof s)switch(s){case"required":s=0;break;case"strong":s=1;break;case"medium":s=2;break;case"weak":s=3;break;default:s=0}var a,n=this.tableau.getNewElementIndex();return null==e&&(e="v"+n),null==t&&(t=0),null==s&&(s=0),i?(a=new h(e,t,n,s),this.integerVariables.push(a)):a=new o(e,t,n,s),this.variables.push(a),this.tableau.variablesPerIndex[n]=a,r&&(this.unrestrictedVariables[n]=!0),this.nVariables+=1,!0===this.tableauInitialized&&this.tableau.addVariable(a),a},n.prototype._removeConstraint=function(t){var e=this.constraints.indexOf(t);-1!==e?(this.constraints.splice(e,1),this.nConstraints-=1,!0===this.tableauInitialized&&this.tableau.removeConstraint(t),t.relaxation&&this.removeVariable(t.relaxation)):console.warn("[Model.removeConstraint] Constraint not present in model")},n.prototype.removeConstraint=function(t){return t.isEquality?(this._removeConstraint(t.upperBound),this._removeConstraint(t.lowerBound)):this._removeConstraint(t),this},n.prototype.removeVariable=function(t){var e=this.variables.indexOf(t);if(-1!==e)return this.variables.splice(e,1),!0===this.tableauInitialized&&this.tableau.removeVariable(t),this;console.warn("[Model.removeVariable] Variable not present in model")},n.prototype.updateRightHandSide=function(t,e){return!0===this.tableauInitialized&&this.tableau.updateRightHandSide(t,e),this},n.prototype.updateConstraintCoefficient=function(t,e,i){return!0===this.tableauInitialized&&this.tableau.updateConstraintCoefficient(t,e,i),this},n.prototype.setCost=function(t,e){var i=t-e.cost;return!1===this.isMinimization&&(i=-i),e.cost=t,this.tableau.updateCost(e,i),this},n.prototype.loadJson=function(t){this.isMinimization="max"!==t.opType;for(var e=t.variables,i=t.constraints,r={},s={},a=Object.keys(i),n=a.length,o=0;o<n;o+=1){var h,l,u=a[o],d=i[u],c=d.equal,v=d.weight,p=d.priority,f=void 0!==v||void 0!==p;if(void 0===c){var x=d.min;void 0!==x&&(h=this.greaterThan(x),r[u]=h,f&&h.relax(v,p));var b=d.max;void 0!==b&&(l=this.smallerThan(b),s[u]=l,f&&l.relax(v,p))}else{h=this.greaterThan(c),r[u]=h,l=this.smallerThan(c),s[u]=l;var m=new F(h,l);f&&m.relax(v,p)}}var y=Object.keys(e),I=y.length;this.tolerance=t.tolerance||0,t.timeout&&(this.timeout=t.timeout),t.options&&(t.options.timeout&&(this.timeout=t.options.timeout),0===this.tolerance&&(this.tolerance=t.options.tolerance||0),t.options.useMIRCuts&&(this.useMIRCuts=t.options.useMIRCuts),void 0===t.options.exitOnCycles?this.checkForCycles=!0:this.checkForCycles=t.options.exitOnCycles);for(var g=t.ints||{},w=t.binaries||{},C=t.unrestricted||{},B=t.optimize,V=0;V<I;V+=1){var j=y[V],O=e[j],R=O[B]||0,M=!!w[j],E=!!g[j]||M,_=!!C[j],T=this.addVariable(R,j,E,_);M&&this.smallerThan(1).addTerm(1,T);var S=Object.keys(O);for(o=0;o<S.length;o+=1){var z=S[o];if(z!==B){var P=O[z],N=r[z];void 0!==N&&N.addTerm(P,T);var k=s[z];void 0!==k&&k.addTerm(P,T)}}}return this},n.prototype.getNumberOfIntegerVariables=function(){return this.integerVariables.length},n.prototype.solve=function(){return!1===this.tableauInitialized&&(this.tableau.setModel(this),this.tableauInitialized=!0),this.tableau.solve()},n.prototype.isFeasible=function(){return this.tableau.feasible},n.prototype.save=function(){return this.tableau.save()},n.prototype.restore=function(){return this.tableau.restore()},n.prototype.activateMIRCuts=function(t){this.useMIRCuts=t},n.prototype.debug=function(t){this.checkForCycles=t},n.prototype.log=function(t){return this.tableau.log(t)}},{"./Tableau/Tableau.js":9,"./Tableau/branchAndCut.js":11,"./expressions.js":20}],6:[function(t,e,i){e.exports=function(t,e){var i,r,s,a,n,o=e.optimize,h=JSON.parse(JSON.stringify(e.optimize)),l=Object.keys(e.optimize),u=0,d={},c="",v={},p=[];for(delete e.optimize,r=0;r<l.length;r++)h[l[r]]=0;for(r=0;r<l.length;r++){for(n in e.optimize=l[r],e.opType=o[l[r]],i=t.Solve(e,void 0,void 0,!0),l)if(!e.variables[l[n]])for(a in i[l[n]]=i[l[n]]?i[l[n]]:0,e.variables)e.variables[a][l[n]]&&i[a]&&(i[l[n]]+=i[a]*e.variables[a][l[n]]);for(c="base",s=0;s<l.length;s++)i[l[s]]?c+="-"+(1e3*i[l[s]]|0)/1e3:c+="-0";if(!d[c]){for(d[c]=1,u++,s=0;s<l.length;s++)i[l[s]]&&(h[l[s]]+=i[l[s]]);delete i.feasible,delete i.result,p.push(i)}}for(r=0;r<l.length;r++)e.constraints[l[r]]={equal:h[l[r]]/u};for(r in e.optimize="cheater-"+Math.random(),e.opType="max",e.variables)e.variables[r].cheater=1;for(r in p)for(a in p[r])v[a]=v[a]||{min:1e99,max:-1e99};for(r in v)for(a in p)p[a][r]?(p[a][r]>v[r].max&&(v[r].max=p[a][r]),p[a][r]<v[r].min&&(v[r].min=p[a][r])):(p[a][r]=0,v[r].min=0);return{midpoint:i=t.Solve(e,void 0,void 0,!0),vertices:p,ranges:v}}},{}],7:[function(t,e,i){var a=t("./Solution.js");function r(t,e,i,r,s){a.call(this,t,e,i,r),this.iter=s}(e.exports=r).prototype=Object.create(a.prototype),r.constructor=r},{"./Solution.js":8}],8:[function(t,e,i){function r(t,e,i,r){this.feasible=i,this.evaluation=e,this.bounded=r,this._tableau=t}(e.exports=r).prototype.generateSolutionSet=function(){for(var t={},e=this._tableau,i=e.varIndexByRow,r=e.variablesPerIndex,s=e.matrix,a=e.rhsColumn,n=e.height-1,o=Math.round(1/e.precision),h=1;h<=n;h+=1){var l=r[i[h]];if(void 0!==l&&!0!==l.isSlack){var u=s[h][a];t[l.id]=Math.round((Number.EPSILON+u)*o)/o}}return t}},{}],9:[function(t,e,i){var r=t("./Solution.js"),s=t("./MilpSolution.js");function a(t){this.model=null,this.matrix=null,this.width=0,this.height=0,this.costRowIndex=0,this.rhsColumn=0,this.variablesPerIndex=[],this.unrestrictedVars=null,this.feasible=!0,this.evaluation=0,this.simplexIters=0,this.varIndexByRow=null,this.varIndexByCol=null,this.rowByVarIndex=null,this.colByVarIndex=null,this.precision=t||1e-8,this.optionalObjectives=[],this.objectivesByPriority={},this.savedState=null,this.availableIndexes=[],this.lastElementIndex=0,this.variables=null,this.nVars=0,this.bounded=!0,this.unboundedVarIndex=null,this.branchAndCutIterations=0}function n(t,e){this.priority=t,this.reducedCosts=new Array(e);for(var i=0;i<e;i+=1)this.reducedCosts[i]=0}(e.exports=a).prototype.solve=function(){return 0<this.model.getNumberOfIntegerVariables()?this.branchAndCut():this.simplex(),this.updateVariableValues(),this.getSolution()},n.prototype.copy=function(){var t=new n(this.priority,this.reducedCosts.length);return t.reducedCosts=this.reducedCosts.slice(),t},a.prototype.setOptionalObjective=function(t,e,i){var r=this.objectivesByPriority[t];void 0===r&&(r=new n(t,Math.max(this.width,e+1)),this.objectivesByPriority[t]=r,this.optionalObjectives.push(r),this.optionalObjectives.sort(function(t,e){return t.priority-e.priority}));r.reducedCosts[e]=i},a.prototype.initialize=function(t,e,i,r){this.variables=i,this.unrestrictedVars=r,this.width=t,this.height=e;for(var s=new Array(t),a=0;a<t;a++)s[a]=0;this.matrix=new Array(e);for(var n=0;n<e;n++)this.matrix[n]=s.slice();this.varIndexByRow=new Array(this.height),this.varIndexByCol=new Array(this.width),this.varIndexByRow[0]=-1,this.varIndexByCol[0]=-1,this.nVars=t+e-2,this.rowByVarIndex=new Array(this.nVars),this.colByVarIndex=new Array(this.nVars),this.lastElementIndex=this.nVars},a.prototype._resetMatrix=function(){var t,e,i=this.model.variables,r=this.model.constraints,s=i.length,a=r.length,n=this.matrix[0],o=!0===this.model.isMinimization?-1:1;for(t=0;t<s;t+=1){var h=i[t],l=h.priority,u=o*h.cost;0===l?n[t+1]=u:this.setOptionalObjective(l,t+1,u),e=i[t].index,this.rowByVarIndex[e]=-1,this.colByVarIndex[e]=t+1,this.varIndexByCol[t+1]=e}for(var d=1,c=0;c<a;c+=1){var v,p,f=r[c],x=f.index;this.rowByVarIndex[x]=d,this.colByVarIndex[x]=-1,this.varIndexByRow[d]=x;var b=f.terms,m=b.length,y=this.matrix[d++];if(f.isUpperBound){for(v=0;v<m;v+=1)p=b[v],y[this.colByVarIndex[p.variable.index]]=p.coefficient;y[0]=f.rhs}else{for(v=0;v<m;v+=1)p=b[v],y[this.colByVarIndex[p.variable.index]]=-p.coefficient;y[0]=-f.rhs}}},a.prototype.setModel=function(t){var e=(this.model=t).nVariables+1,i=t.nConstraints+1;return this.initialize(e,i,t.variables,t.unrestrictedVariables),this._resetMatrix(),this},a.prototype.getNewElementIndex=function(){if(0<this.availableIndexes.length)return this.availableIndexes.pop();var t=this.lastElementIndex;return this.lastElementIndex+=1,t},a.prototype.density=function(){for(var t=0,e=this.matrix,i=0;i<this.height;i++)for(var r=e[i],s=0;s<this.width;s++)0!==r[s]&&(t+=1);return t/(this.height*this.width)},a.prototype.setEvaluation=function(){var t=Math.round(1/this.precision),e=this.matrix[this.costRowIndex][this.rhsColumn],i=Math.round((Number.EPSILON+e)*t)/t;this.evaluation=i,0===this.simplexIters&&(this.bestPossibleEval=i)},a.prototype.getSolution=function(){var t=!0===this.model.isMinimization?this.evaluation:-this.evaluation;return 0<this.model.getNumberOfIntegerVariables()?new s(this,t,this.feasible,this.bounded,this.branchAndCutIterations):new r(this,t,this.feasible,this.bounded)}},{"./MilpSolution.js":7,"./Solution.js":8}],10:[function(t,e,i){var n=t("./Tableau.js");n.prototype.copy=function(){var t=new n(this.precision);t.width=this.width,t.height=this.height,t.nVars=this.nVars,t.model=this.model,t.variables=this.variables,t.variablesPerIndex=this.variablesPerIndex,t.unrestrictedVars=this.unrestrictedVars,t.lastElementIndex=this.lastElementIndex,t.varIndexByRow=this.varIndexByRow.slice(),t.varIndexByCol=this.varIndexByCol.slice(),t.rowByVarIndex=this.rowByVarIndex.slice(),t.colByVarIndex=this.colByVarIndex.slice(),t.availableIndexes=this.availableIndexes.slice();for(var e=[],i=0;i<this.optionalObjectives.length;i++)e[i]=this.optionalObjectives[i].copy();t.optionalObjectives=e;for(var r=this.matrix,s=new Array(this.height),a=0;a<this.height;a++)s[a]=r[a].slice();return t.matrix=s,t},n.prototype.save=function(){this.savedState=this.copy()},n.prototype.restore=function(){if(null!==this.savedState){var t,e,i=this.savedState,r=i.matrix;for(this.nVars=i.nVars,this.model=i.model,this.variables=i.variables,this.variablesPerIndex=i.variablesPerIndex,this.unrestrictedVars=i.unrestrictedVars,this.lastElementIndex=i.lastElementIndex,this.width=i.width,this.height=i.height,t=0;t<this.height;t+=1){var s=r[t],a=this.matrix[t];for(e=0;e<this.width;e+=1)a[e]=s[e]}var n=i.varIndexByRow;for(e=0;e<this.height;e+=1)this.varIndexByRow[e]=n[e];for(;this.varIndexByRow.length>this.height;)this.varIndexByRow.pop();var o=i.varIndexByCol;for(t=0;t<this.width;t+=1)this.varIndexByCol[t]=o[t];for(;this.varIndexByCol.length>this.width;)this.varIndexByCol.pop();for(var h=i.rowByVarIndex,l=i.colByVarIndex,u=0;u<this.nVars;u+=1)this.rowByVarIndex[u]=h[u],this.colByVarIndex[u]=l[u];if(0<i.optionalObjectives.length&&0<this.optionalObjectives.length){this.optionalObjectives=[],this.optionalObjectivePerPriority={};for(var d=0;d<i.optionalObjectives.length;d++){var c=i.optionalObjectives[d].copy();this.optionalObjectives[d]=c,this.optionalObjectivePerPriority[c.priority]=c}}}}},{"./Tableau.js":9}],11:[function(t,e,i){var r=t("./Tableau.js");function O(t,e,i){this.type=t,this.varIndex=e,this.value=i}function R(t,e){this.relaxedEvaluation=t,this.cuts=e}function M(t,e){return e.relaxedEvaluation-t.relaxedEvaluation}r.prototype.applyCuts=function(t){if(this.restore(),this.addCutConstraints(t),this.simplex(),this.model.useMIRCuts)for(var e=!0;e;){var i=this.computeFractionalVolume(!0);this.applyMIRCuts(),this.simplex(),.9*i<=this.computeFractionalVolume(!0)&&(e=!1)}},r.prototype.branchAndCut=function(){var t=[],e=0,i=this.model.tolerance,r=!0,s=1e99;this.model.timeout&&(s=Date.now()+this.model.timeout);for(var a=1/0,n=null,o=[],h=0;h<this.optionalObjectives.length;h+=1)o.push(1/0);var l,u=new R(-1/0,[]);for(t.push(u);0<t.length&&!0===r&&Date.now()<s;)if(l=this.model.isMinimization?this.bestPossibleEval*(1+i):this.bestPossibleEval*(1-i),0<i&&a<l&&(r=!1),!((u=t.pop()).relaxedEvaluation>a)){var d=u.cuts;if(this.applyCuts(d),e++,!1!==this.feasible){var c=this.evaluation;if(!(a<c)){if(c===a){for(var v=!0,p=0;p<this.optionalObjectives.length&&!(this.optionalObjectives[p].reducedCosts[0]>o[p]);p+=1)if(this.optionalObjectives[p].reducedCosts[0]<o[p]){v=!1;break}if(v)continue}if(!0===this.isIntegral()){if(this.__isIntegral=!0,1===e)return void(this.branchAndCutIterations=e);n=u,a=c;for(var f=0;f<this.optionalObjectives.length;f+=1)o[f]=this.optionalObjectives[f].reducedCosts[0]}else{1===e&&this.save();for(var x=this.getMostFractionalVar(),b=x.index,m=[],y=[],I=d.length,g=0;g<I;g+=1){var w=d[g];w.varIndex===b?"min"===w.type?y.push(w):m.push(w):(m.push(w),y.push(w))}var C=Math.ceil(x.value),B=Math.floor(x.value),V=new O("min",b,C);m.push(V);var j=new O("max",b,B);y.push(j),t.push(new R(c,m)),t.push(new R(c,y)),t.sort(M)}}}}null!==n&&this.applyCuts(n.cuts),this.branchAndCutIterations=e}},{"./Tableau.js":9}],12:[function(t,e,i){var r=t("./Tableau.js");function d(t,e){this.index=t,this.value=e}r.prototype.getMostFractionalVar=function(){for(var t=0,e=null,i=null,r=this.model.integerVariables,s=r.length,a=0;a<s;a++){var n=r[a].index,o=this.rowByVarIndex[n];if(-1!==o){var h=this.matrix[o][this.rhsColumn],l=Math.abs(h-Math.round(h));t<l&&(t=l,e=n,i=h)}}return new d(e,i)},r.prototype.getFractionalVarWithLowestCost=function(){for(var t=1/0,e=null,i=null,r=this.model.integerVariables,s=r.length,a=0;a<s;a++){var n=r[a],o=n.index,h=this.rowByVarIndex[o];if(-1!==h){var l=this.matrix[h][this.rhsColumn];if(Math.abs(l-Math.round(l))>this.precision){var u=n.cost;u<t&&(t=u,e=o,i=l)}}}return new d(e,i)}},{"./Tableau.js":9}],13:[function(t,e,i){var r=t("./Tableau.js"),b=t("../expressions.js").SlackVariable;r.prototype.addCutConstraints=function(t){for(var e,i=t.length,r=this.height,s=r+i,a=r;a<s;a+=1)void 0===this.matrix[a]&&(this.matrix[a]=this.matrix[a-1].slice());this.height=s,this.nVars=this.width+this.height-2;for(var n=this.width-1,o=0;o<i;o+=1){var h=t[o],l=r+o,u="min"===h.type?-1:1,d=h.varIndex,c=this.rowByVarIndex[d],v=this.matrix[l];if(-1===c){for(v[this.rhsColumn]=u*h.value,e=1;e<=n;e+=1)v[e]=0;v[this.colByVarIndex[d]]=u}else{var p=this.matrix[c],f=p[this.rhsColumn];for(v[this.rhsColumn]=u*(h.value-f),e=1;e<=n;e+=1)v[e]=-u*p[e]}var x=this.getNewElementIndex();this.varIndexByRow[l]=x,this.rowByVarIndex[x]=l,this.colByVarIndex[x]=-1,this.variablesPerIndex[x]=new b("s"+x,x),this.nVars+=1}},r.prototype._addLowerBoundMIRCut=function(t){if(t===this.costRowIndex)return!1;this.model;var e=this.matrix;if(!this.variablesPerIndex[this.varIndexByRow[t]].isInteger)return!1;var i=e[t][this.rhsColumn],r=i-Math.floor(i);if(r<this.precision||1-this.precision<r)return!1;var s=this.height;e[s]=e[s-1].slice(),this.height+=1,this.nVars+=1;var a=this.getNewElementIndex();this.varIndexByRow[s]=a,this.rowByVarIndex[a]=s,this.colByVarIndex[a]=-1,this.variablesPerIndex[a]=new b("s"+a,a),e[s][this.rhsColumn]=Math.floor(i);for(var n=1;n<this.varIndexByCol.length;n+=1){if(this.variablesPerIndex[this.varIndexByCol[n]].isInteger){var o=e[t][n],h=Math.floor(o)+Math.max(0,o-Math.floor(o)-r)/(1-r);e[s][n]=h}else e[s][n]=Math.min(0,e[t][n]/(1-r))}for(var l=0;l<this.width;l+=1)e[s][l]-=e[t][l];return!0},r.prototype._addUpperBoundMIRCut=function(t){if(t===this.costRowIndex)return!1;this.model;var e=this.matrix;if(!this.variablesPerIndex[this.varIndexByRow[t]].isInteger)return!1;var i=e[t][this.rhsColumn],r=i-Math.floor(i);if(r<this.precision||1-this.precision<r)return!1;var s=this.height;e[s]=e[s-1].slice(),this.height+=1,this.nVars+=1;var a=this.getNewElementIndex();this.varIndexByRow[s]=a,this.rowByVarIndex[a]=s,this.colByVarIndex[a]=-1,this.variablesPerIndex[a]=new b("s"+a,a),e[s][this.rhsColumn]=-r;for(var n=1;n<this.varIndexByCol.length;n+=1){var o=this.variablesPerIndex[this.varIndexByCol[n]],h=e[t][n],l=h-Math.floor(h);o.isInteger?e[s][n]=l<=r?-l:-(1-l)*r/l:e[s][n]=0<=h?-h:h*r/(1-r)}return!0},r.prototype.applyMIRCuts=function(){}},{"../expressions.js":20,"./Tableau.js":9}],14:[function(t,e,i){var r=t("./Tableau.js");r.prototype._putInBase=function(t){var e=this.rowByVarIndex[t];if(-1===e){for(var i=this.colByVarIndex[t],r=1;r<this.height;r+=1){var s=this.matrix[r][i];if(s<-this.precision||this.precision<s){e=r;break}}this.pivot(e,i)}return e},r.prototype._takeOutOfBase=function(t){var e=this.colByVarIndex[t];if(-1===e){for(var i=this.rowByVarIndex[t],r=this.matrix[i],s=1;s<this.height;s+=1){var a=r[s];if(a<-this.precision||this.precision<a){e=s;break}}this.pivot(i,e)}return e},r.prototype.updateVariableValues=function(){for(var t=this.variables.length,e=Math.round(1/this.precision),i=0;i<t;i+=1){var r=this.variables[i],s=r.index,a=this.rowByVarIndex[s];if(-1===a)r.value=0;else{var n=this.matrix[a][this.rhsColumn];r.value=Math.round((n+Number.EPSILON)*e)/e}}},r.prototype.updateRightHandSide=function(t,e){var i=this.height-1,r=this.rowByVarIndex[t.index];if(-1===r){for(var s=this.colByVarIndex[t.index],a=0;a<=i;a+=1){var n=this.matrix[a];n[this.rhsColumn]-=e*n[s]}var o=this.optionalObjectives.length;if(0<o)for(var h=0;h<o;h+=1){var l=this.optionalObjectives[h].reducedCosts;l[this.rhsColumn]-=e*l[s]}}else this.matrix[r][this.rhsColumn]-=e},r.prototype.updateConstraintCoefficient=function(t,e,i){if(t.index===e.index)throw new Error("[Tableau.updateConstraintCoefficient] constraint index should not be equal to variable index !");var r=this._putInBase(t.index),s=this.colByVarIndex[e.index];if(-1===s)for(var a=this.rowByVarIndex[e.index],n=0;n<this.width;n+=1)this.matrix[r][n]+=i*this.matrix[a][n];else this.matrix[r][s]-=i},r.prototype.updateCost=function(t,e){var i=t.index,r=this.width-1,s=this.colByVarIndex[i];if(-1===s){var a,n=this.matrix[this.rowByVarIndex[i]];if(0===t.priority){var o=this.matrix[0];for(a=0;a<=r;a+=1)o[a]+=e*n[a]}else{var h=this.objectivesByPriority[t.priority].reducedCosts;for(a=0;a<=r;a+=1)h[a]+=e*n[a]}}else this.matrix[0][s]-=e},r.prototype.addConstraint=function(t){var e=t.isUpperBound?1:-1,i=this.height,r=this.matrix[i];void 0===r&&(r=this.matrix[0].slice(),this.matrix[i]=r);for(var s=this.width-1,a=0;a<=s;a+=1)r[a]=0;r[this.rhsColumn]=e*t.rhs;for(var n=t.terms,o=n.length,h=0;h<o;h+=1){var l=n[h],u=l.coefficient,d=l.variable.index,c=this.rowByVarIndex[d];if(-1===c)r[this.colByVarIndex[d]]+=e*u;else{var v=this.matrix[c];v[this.rhsColumn];for(a=0;a<=s;a+=1)r[a]-=e*u*v[a]}}var p=t.index;this.varIndexByRow[i]=p,this.rowByVarIndex[p]=i,this.colByVarIndex[p]=-1,this.height+=1},r.prototype.removeConstraint=function(t){var e=t.index,i=this.height-1,r=this._putInBase(e),s=this.matrix[i];this.matrix[i]=this.matrix[r],this.matrix[r]=s,this.varIndexByRow[r]=this.varIndexByRow[i],this.varIndexByRow[i]=-1,this.rowByVarIndex[e]=-1,this.availableIndexes[this.availableIndexes.length]=e,t.slack.index=-1,this.height-=1},r.prototype.addVariable=function(t){var e=this.height-1,i=this.width,r=!0===this.model.isMinimization?-t.cost:t.cost,s=t.priority,a=this.optionalObjectives.length;if(0<a)for(var n=0;n<a;n+=1)this.optionalObjectives[n].reducedCosts[i]=0;0===s?this.matrix[0][i]=r:(this.setOptionalObjective(s,i,r),this.matrix[0][i]=0);for(var o=1;o<=e;o+=1)this.matrix[o][i]=0;var h=t.index;this.varIndexByCol[i]=h,this.rowByVarIndex[h]=-1,this.colByVarIndex[h]=i,this.width+=1},r.prototype.removeVariable=function(t){var e=t.index,i=this._takeOutOfBase(e),r=this.width-1;if(i!==r){for(var s=this.height-1,a=0;a<=s;a+=1){var n=this.matrix[a];n[i]=n[r]}var o=this.optionalObjectives.length;if(0<o)for(var h=0;h<o;h+=1){var l=this.optionalObjectives[h].reducedCosts;l[i]=l[r]}var u=this.varIndexByCol[r];this.varIndexByCol[i]=u,this.colByVarIndex[u]=i}this.varIndexByCol[r]=-1,this.colByVarIndex[e]=-1,this.availableIndexes[this.availableIndexes.length]=e,t.index=-1,this.width-=1}},{"./Tableau.js":9}],15:[function(t,e,i){t("./simplex.js"),t("./cuttingStrategies.js"),t("./dynamicModification.js"),t("./log.js"),t("./backup.js"),t("./branchingStrategies.js"),t("./integerProperties.js"),e.exports=t("./Tableau.js")},{"./Tableau.js":9,"./backup.js":10,"./branchingStrategies.js":12,"./cuttingStrategies.js":13,"./dynamicModification.js":14,"./integerProperties.js":16,"./log.js":17,"./simplex.js":18}],16:[function(t,e,i){var r=t("./Tableau.js");r.prototype.countIntegerValues=function(){for(var t=0,e=1;e<this.height;e+=1)if(this.variablesPerIndex[this.varIndexByRow[e]].isInteger){var i=this.matrix[e][this.rhsColumn];(i-=Math.floor(i))<this.precision&&-i<this.precision&&(t+=1)}return t},r.prototype.isIntegral=function(){for(var t=this.model.integerVariables,e=t.length,i=0;i<e;i++){var r=this.rowByVarIndex[t[i].index];if(-1!==r){var s=this.matrix[r][this.rhsColumn];if(Math.abs(s-Math.round(s))>this.precision)return!1}}return!0},r.prototype.computeFractionalVolume=function(t){for(var e=-1,i=1;i<this.height;i+=1)if(this.variablesPerIndex[this.varIndexByRow[i]].isInteger){var r=this.matrix[i][this.rhsColumn];if(r=Math.abs(r),Math.min(r-Math.floor(r),Math.floor(r+1))<this.precision){if(!t)return 0}else-1===e?e=r:e*=r}return-1===e?0:e}},{"./Tableau.js":9}],17:[function(t,e,i){t("./Tableau.js").prototype.log=function(t,e){console.log("****",t,"****"),console.log("Nb Variables",this.width-1),console.log("Nb Constraints",this.height-1),console.log("Basic Indexes",this.varIndexByRow),console.log("Non Basic Indexes",this.varIndexByCol),console.log("Rows",this.rowByVarIndex),console.log("Cols",this.colByVarIndex);var i,r,s,a,n,o,h,l,u,d,c,v="",p=[" "];for(r=1;r<this.width;r+=1)n=this.varIndexByCol[r],h=(o=void 0===(a=this.variablesPerIndex[n])?"c"+n:a.id).length,Math.abs(h-5),l=" ",u="\t",5<h?l+=" ":u+="\t",p[r]=l,v+=u+o;console.log(v);var f=this.matrix[this.costRowIndex],x="\t";for(i=1;i<this.width;i+=1)x+="\t",x+=p[i],x+=f[i].toFixed(5);for(x+="\t"+p[0]+f[0].toFixed(5),console.log(x+"\tZ"),s=1;s<this.height;s+=1){for(d=this.matrix[s],c="\t",r=1;r<this.width;r+=1)c+="\t"+p[r]+d[r].toFixed(5);c+="\t"+p[0]+d[0].toFixed(5),n=this.varIndexByRow[s],o=void 0===(a=this.variablesPerIndex[n])?"c"+n:a.id,console.log(c+"\t"+o)}console.log("");var b=this.optionalObjectives.length;if(0<b){console.log(" Optional objectives:");for(var m=0;m<b;m+=1){var y=this.optionalObjectives[m].reducedCosts,I="";for(i=1;i<this.width;i+=1)I+=y[i]<0?"":" ",I+=p[i],I+=y[i].toFixed(5);I+=(y[0]<0?"":" ")+p[0]+y[0].toFixed(5),console.log(I+" z"+m)}}return console.log("Feasible?",this.feasible),console.log("evaluation",this.evaluation),this}},{"./Tableau.js":9}],18:[function(t,e,i){var r=t("./Tableau.js");r.prototype.simplex=function(){return this.bounded=!0,this.phase1(),!0===this.feasible&&this.phase2(),this},r.prototype.phase1=function(){for(var t=this.model.checkForCycles,e=[],i=this.matrix,r=this.rhsColumn,s=this.width-1,a=this.height-1,n=0;;){for(var o=0,h=-this.precision,l=1;l<=a;l++){!0===this.unrestrictedVars[this.varIndexByRow[l]];var u=i[l][r];u<h&&(h=u,o=l)}if(0===o)return this.feasible=!0,n;for(var d=0,c=-1/0,v=i[0],p=i[o],f=1;f<=s;f++){var x=p[f];if(!0===this.unrestrictedVars[this.varIndexByCol[f]]||x<-this.precision){var b=-v[f]/x;c<b&&(c=b,d=f)}}if(0===d)return this.feasible=!1,n;if(t){e.push([this.varIndexByRow[o],this.varIndexByCol[d]]);var m=this.checkForCycles(e);if(0<m.length)return this.model.messages.push("Cycle in phase 1"),this.model.messages.push("Start :"+m[0]),this.model.messages.push("Length :"+m[1]),this.feasible=!1,n}this.pivot(o,d),n+=1}},r.prototype.phase2=function(){for(var t,e,i=this.model.checkForCycles,r=[],s=this.matrix,a=this.rhsColumn,n=this.width-1,o=this.height-1,h=this.precision,l=this.optionalObjectives.length,u=null,d=0;;){var c=s[this.costRowIndex];0<l&&(u=[]);for(var v=0,p=h,f=!1,x=1;x<=n;x++)t=c[x],e=!0===this.unrestrictedVars[this.varIndexByCol[x]],0<l&&-h<t&&t<h?u.push(x):e&&t<0?p<-t&&(p=-t,v=x,f=!0):p<t&&(p=t,v=x,f=!1);if(0<l)for(var b=0;0===v&&0<u.length&&b<l;){var m=[],y=this.optionalObjectives[b].reducedCosts;p=h;for(var I=0;I<u.length;I++)t=y[x=u[I]],e=!0===this.unrestrictedVars[this.varIndexByCol[x]],-h<t&&t<h?m.push(x):e&&t<0?p<-t&&(p=-t,v=x,f=!0):p<t&&(p=t,v=x,f=!1);u=m,b+=1}if(0===v)return this.setEvaluation(),this.simplexIters+=1,d;for(var g=0,w=1/0,C=(this.varIndexByRow,1);C<=o;C++){var B=s[C],V=B[a],j=B[v];if(!(-h<j&&j<h)){if(0<j&&V<h&&-h<V){w=0,g=C;break}var O=f?-V/j:V/j;h<O&&O<w&&(w=O,g=C)}}if(w===1/0)return this.evaluation=-1/0,this.bounded=!1,this.unboundedVarIndex=this.varIndexByCol[v],d;if(i){r.push([this.varIndexByRow[g],this.varIndexByCol[v]]);var R=this.checkForCycles(r);if(0<R.length)return this.model.messages.push("Cycle in phase 2"),this.model.messages.push("Start :"+R[0]),this.model.messages.push("Length :"+R[1]),this.feasible=!1,d}this.pivot(g,v,!0),d+=1}};var y=[];r.prototype.pivot=function(t,e){var i=this.matrix,r=i[t][e],s=this.height-1,a=this.width-1,n=this.varIndexByRow[t],o=this.varIndexByCol[e];this.varIndexByRow[t]=o,this.varIndexByCol[e]=n,this.rowByVarIndex[o]=t,this.rowByVarIndex[n]=-1,this.colByVarIndex[o]=-1,this.colByVarIndex[n]=e;for(var h,l,u,d=i[t],c=0,v=0;v<=a;v++)-1e-16<=d[v]&&d[v]<=1e-16?d[v]=0:(d[v]/=r,y[c]=v,c+=1);d[e]=1/r;this.precision;for(var p=0;p<=s;p++)if(p!==t&&!(-1e-16<=i[p][e]&&i[p][e]<=1e-16)){var f=i[p];if(-1e-16<=(h=f[e])&&h<=1e-16)0!==h&&(f[e]=0);else{for(l=0;l<c;l++)-1e-16<=(u=d[v=y[l]])&&u<=1e-16?0!==u&&(d[v]=0):f[v]=f[v]-h*u;f[e]=-h/r}}var x=this.optionalObjectives.length;if(0<x)for(var b=0;b<x;b+=1){var m=this.optionalObjectives[b].reducedCosts;if(0!==(h=m[e])){for(l=0;l<c;l++)0!==(u=d[v=y[l]])&&(m[v]=m[v]-h*u);m[e]=-h/r}}},r.prototype.checkForCycles=function(t){for(var e=0;e<t.length-1;e++)for(var i=e+1;i<t.length;i++){var r=t[e],s=t[i];if(r[0]===s[0]&&r[1]===s[1]){if(i-e>t.length-i)break;for(var a=!0,n=1;n<i-e;n++){var o=t[e+n],h=t[i+n];if(o[0]!==h[0]||o[1]!==h[1]){a=!1;break}}if(a)return[e,i-e]}}return[]}},{"./Tableau.js":9}],19:[function(t,e,i){i.CleanObjectiveAttributes=function(t){var e,i,r;if("string"==typeof t.optimize){if(t.constraints[t.optimize]){for(i in e=Math.random(),t.variables)t.variables[i][t.optimize]&&(t.variables[i][e]=t.variables[i][t.optimize]);return t.constraints[e]=t.constraints[t.optimize],delete t.constraints[t.optimize],t}return t}for(r in t.optimize)if(t.constraints[r])if("equal"===t.constraints[r])delete t.optimize[r];else{for(i in e=Math.random(),t.variables)t.variables[i][r]&&(t.variables[i][e]=t.variables[i][r]);t.constraints[e]=t.constraints[r],delete t.constraints[r]}return t}},{}],20:[function(t,e,i){function s(t,e,i,r){this.id=t,this.cost=e,this.index=i,this.value=0,this.priority=r}function r(t,e,i,r){s.call(this,t,e,i,r)}function a(t,e){s.call(this,t,0,e,0)}function n(t,e){this.variable=t,this.coefficient=e}function o(t,e,i){return 0===i||"required"===i?null:(e=e||1,i=i||1,!1===t.isMinimization&&(e=-e),t.addVariable(e,"r"+t.relaxationIndex++,!1,!1,i))}function h(t,e,i,r){this.slack=new a("s"+i,i),this.index=i,this.model=r,this.rhs=t,this.isUpperBound=e,this.terms=[],this.termsByVarIndex={},this.relaxation=null}function l(t,e){this.upperBound=t,this.lowerBound=e,this.model=t.model,this.rhs=t.rhs,this.relaxation=null}a.prototype.isSlack=r.prototype.isInteger=!0,h.prototype.addTerm=function(t,e){var i=e.index,r=this.termsByVarIndex[i];if(void 0===r)r=new n(e,t),this.termsByVarIndex[i]=r,this.terms.push(r),!0===this.isUpperBound&&(t=-t),this.model.updateConstraintCoefficient(this,e,t);else{var s=r.coefficient+t;this.setVariableCoefficient(s,e)}return this},h.prototype.removeTerm=function(t){return this},h.prototype.setRightHandSide=function(t){if(t!==this.rhs){var e=t-this.rhs;!0===this.isUpperBound&&(e=-e),this.rhs=t,this.model.updateRightHandSide(this,e)}return this},h.prototype.setVariableCoefficient=function(t,e){var i=e.index;if(-1!==i){var r=this.termsByVarIndex[i];if(void 0===r)this.addTerm(t,e);else if(t!==r.coefficient){var s=t-r.coefficient;!0===this.isUpperBound&&(s=-s),r.coefficient=t,this.model.updateConstraintCoefficient(this,e,s)}return this}console.warn("[Constraint.setVariableCoefficient] Trying to change coefficient of inexistant variable.")},h.prototype.relax=function(t,e){this.relaxation=o(this.model,t,e),this._relax(this.relaxation)},h.prototype._relax=function(t){null!==t&&(this.isUpperBound?this.setVariableCoefficient(-1,t):this.setVariableCoefficient(1,t))},l.prototype.isEquality=!0,l.prototype.addTerm=function(t,e){return this.upperBound.addTerm(t,e),this.lowerBound.addTerm(t,e),this},l.prototype.removeTerm=function(t){return this.upperBound.removeTerm(t),this.lowerBound.removeTerm(t),this},l.prototype.setRightHandSide=function(t){this.upperBound.setRightHandSide(t),this.lowerBound.setRightHandSide(t),this.rhs=t},l.prototype.relax=function(t,e){this.relaxation=o(this.model,t,e),this.upperBound.relaxation=this.relaxation,this.upperBound._relax(this.relaxation),this.lowerBound.relaxation=this.relaxation,this.lowerBound._relax(this.relaxation)},e.exports={Constraint:h,Variable:s,IntegerVariable:r,SlackVariable:a,Equality:l,Term:n}},{}],21:[function(h,t,e){function i(){"use strict";this.Model=l,this.branchAndCut=s,this.Constraint=n,this.Variable=o,this.Numeral=d,this.Term=c,this.Tableau=r,this.lastSolvedModel=null,this.External=v,this.Solve=function(t,e,i,r){if(r)for(var s in u)t=u[s](t);if(!t)throw new Error("Solver requires a model to operate on");if("object"==typeof t.optimize&&Object.keys(1<t.optimize))return h("./Polyopt")(this,t);if(t.external){var a=Object.keys(v);if(a=JSON.stringify(a),!t.external.solver)throw new Error("The model you provided has an 'external' object that doesn't have a solver attribute. Use one of the following:"+a);if(!v[t.external.solver])throw new Error("No support (yet) for "+t.external.solver+". Please use one of these instead:"+a);return v[t.external.solver].solve(t)}t instanceof l==!1&&(t=new l(e).loadJson(t));var n=t.solve();if(this.lastSolvedModel=t,n.solutionSet=n.generateSolutionSet(),i)return n;var o={};return o.feasible=n.feasible,o.result=n.evaluation,o.bounded=n.bounded,n._tableau.__isIntegral&&(o.isIntegral=!0),Object.keys(n.solutionSet).forEach(function(t){0!==n.solutionSet[t]&&(o[t]=n.solutionSet[t])}),o},this.ReformatLP=h("./External/lpsolve/Reformat.js"),this.MultiObjective=function(t){return h("./Polyopt")(this,t)}}var r=h("./Tableau/index.js"),l=h("./Model"),s=h("./Tableau/branchAndCut"),a=h("./expressions.js"),u=h("./Validation"),n=a.Constraint,o=a.Variable,d=a.Numeral,c=a.Term,v=h("./External/main.js");"function"==typeof define?define([],function(){return new i}):"object"==typeof window?window.solver=new i:"object"==typeof self&&(self.solver=new i),t.exports=new i},{"./External/lpsolve/Reformat.js":2,"./External/main.js":4,"./Model":5,"./Polyopt":6,"./Tableau/branchAndCut":11,"./Tableau/index.js":15,"./Validation":19,"./expressions.js":20}]},{},[21]); +//# sourceMappingURL=solver.js.map \ No newline at end of file diff --git a/play/js/javascript-lp-solver/prod/solver.js.map b/play/js/javascript-lp-solver/prod/solver.js.map new file mode 100644 index 00000000..2cb0070c --- /dev/null +++ b/play/js/javascript-lp-solver/prod/solver.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["../src/solver.js"],"names":["exports","module","require","r","e","n","t","o","i","f","c","u","a","Error","code","p","call","length","1","2","model","input","rxo","is_blank","is_objective","is_int","is_bin","is_constraint","is_unrestricted","parse_lhs","parse_rhs","parse_dir","parse_int","parse_bin","get_num","get_word","opType","optimize","constraints","variables",">=","<=","=","tmp","ary","hldr","hldr2","constraint","rhs","split","test","match","map","d","replace","slice","forEach","substr","parseFloat","_obj","ints","binaries","separatorIndex","indexOf","unrestricted","to_JSON","output","lookup","max","min","equal","rxClean","RegExp","x","xx","y","z","xxx","xxxx","from_JSON","3","clean_data","data","filter","reduce","k","reformat","solve","Promise","res","rej","window","external","binPath","args","tempName","writeFile","fe","fd","exec","execFile","push","ret_obj","meaning","-2","4","5","6","7","9","25","255","./Reformat.js","child_process","fs","lpsolve","./lpsolve/main.js","Tableau","expressions","Constraint","Equality","Variable","IntegerVariable","Term","Model","precision","name","this","tableau","integerVariables","unrestrictedVariables","nConstraints","nVariables","isMinimization","tableauInitialized","relaxationIndex","useMIRCuts","checkForCycles","messages","prototype","minimize","maximize","_getNewElementIndex","availableIndexes","pop","index","lastElementIndex","_addConstraint","slackVariable","slack","variablesPerIndex","addConstraint","smallerThan","getNewElementIndex","greaterThan","constraintUpper","constraintLower","addVariable","cost","id","isInteger","isUnrestricted","priority","variable","varIndex","_removeConstraint","idx","splice","removeConstraint","relaxation","removeVariable","console","warn","isEquality","upperBound","lowerBound","updateRightHandSide","difference","updateConstraintCoefficient","setCost","updateCost","loadJson","jsonModel","constraintsMin","constraintsMax","constraintIds","Object","keys","nConstraintIds","constraintId","weight","relaxed","undefined","relax","equality","variableIds","tolerance","timeout","options","exitOnCycles","integerVarIds","binaryVarIds","unrestrictedVarIds","objectiveName","v","variableId","variableConstraints","isBinary","addTerm","constraintNames","constraintName","coefficient","constraintMin","constraintMax","getNumberOfIntegerVariables","setModel","isFeasible","feasible","save","restore","activateMIRCuts","debug","debugCheckForCycles","log","message","./Tableau/Tableau.js","./Tableau/branchAndCut.js","./expressions.js","solver","j","objectives","new_constraints","JSON","parse","stringify","counter","vectors","vector_key","obj","pareto","Solve","result","Math","random","cheater","midpoint","vertices","ranges","Solution","MilpSolution","evaluation","bounded","branchAndCutIterations","iter","create","constructor","./Solution.js","8","_tableau","generateSolutionSet","solutionSet","varIndexByRow","matrix","rhsColumn","lastRow","height","roundingCoeff","round","isSlack","varValue","Number","EPSILON","width","costRowIndex","unrestrictedVars","simplexIters","varIndexByCol","rowByVarIndex","colByVarIndex","optionalObjectives","objectivesByPriority","savedState","nVars","unboundedVarIndex","OptionalObjective","nColumns","reducedCosts","Array","branchAndCut","simplex","updateVariableValues","getSolution","copy","setOptionalObjective","column","objectiveForPriority","sort","b","initialize","tmpRow","_resetMatrix","costRow","coeff","rowIndex","term","constraintIndex","terms","nTerms","row","isUpperBound","density","setEvaluation","roundedEvaluation","bestPossibleEval","./MilpSolution.js","10","optionalObjectivesCopy","matrixCopy","savedMatrix","savedRow","savedBasicIndexes","savedNonBasicIndexes","savedRows","savedCols","optionalObjectivePerPriority","optionalObjectiveCopy","./Tableau.js","11","Cut","type","value","Branch","relaxedEvaluation","cuts","sortByEvaluation","applyCuts","branchingCuts","addCutConstraints","fractionalVolumeImproved","fractionalVolumeBefore","computeFractionalVolume","applyMIRCuts","branches","iterations","toleranceFlag","terminalTime","Date","now","bestEvaluation","Infinity","bestBranch","bestOptionalObjectivesEvaluations","oInit","acceptableThreshold","branch","isCurrentEvaluationWorse","isIntegral","__isIntegral","oCopy","getMostFractionalVar","cutsHigh","cutsLow","nCuts","cut","ceil","floor","cutHigh","cutLow","12","VariableData","biggestFraction","selectedVarIndex","selectedVarValue","nIntegerVars","varRow","fraction","abs","getFractionalVarWithLowestCost","highestCost","13","SlackVariable","cutConstraints","nCutConstraints","heightWithCuts","h","lastColumn","sign","varRowIndex","constraintRow","slackVarIndex","_addLowerBoundMIRCut","frac_d","colIndex","coef","termCoeff","_addUpperBoundMIRCut","aj","fj","../expressions.js","14","_putInBase","r1","pivot","_takeOutOfBase","pivotRow","c1","slackColumn","nOptionalObjectives","colVar","rowVar","varColumn","variableRow","slackIndex","switchVarIndex","15","./backup.js","./branchingStrategies.js","./cuttingStrategies.js","./dynamicModification.js","./integerProperties.js","./log.js","./simplex.js","16","countIntegerValues","count","decimalPart","ignoreIntegerValues","volume","17","force","varName","varNameLength","valueSpace","nameSpace","rowString","varNameRowString","spacePerColumn","firstRow","firstRowString","toFixed","reducedCostsString","18","phase1","phase2","varIndexesCycle","leavingRowIndex","rhsValue","enteringColumn","maxQuotient","leavingRow","quotient","cycleData","reducedCost","optionalCostsColumns","enteringValue","isReducedCostNegative","optionalCostsColumns2","minQuotient","colValue","nonZeroColumns","pivotRowIndex","pivotColumnIndex","leavingBasicIndex","enteringBasicIndex","v0","nNonZeroColumns","varIndexes","e1","e2","elt1","elt2","cycleFound","tmp1","tmp2","19","CleanObjectiveAttributes","fakeAttr","20","createRelaxationVariable","termsByVarIndex","newCoefficient","setVariableCoefficient","removeTerm","setRightHandSide","newRhs","_relax","relaxationVariable","21","Solver","Numeral","lastSolvedModel","External","full","validate","validation","solvers","solution","store","ReformatLP","MultiObjective","define","self","./External/lpsolve/Reformat.js","./External/main.js","./Model","./Polyopt","./Tableau/branchAndCut","./Tableau/index.js","./Validation"],"mappings":"AAAmC,iBAAZA,UAAuBC,OAAOD,QAAWE,QAAQ,WAC5D,SAASC,EAAEC,EAAEC,EAAEC,GAAG,SAASC,EAAEC,EAAEC,GAAG,IAAIJ,EAAEG,GAAG,CAAC,IAAIJ,EAAEI,GAAG,CAAC,IAAIE,EAAE,mBAAmBR,SAASA,QAAQ,IAAIO,GAAGC,EAAE,OAAOA,EAAEF,GAAE,GAAI,GAAGG,EAAE,OAAOA,EAAEH,GAAE,GAAI,IAAII,EAAE,IAAIC,MAAM,uBAAuBL,EAAE,KAAK,MAAMI,EAAEE,KAAK,mBAAmBF,EAAE,IAAIG,EAAEV,EAAEG,GAAG,CAACR,QAAQ,IAAII,EAAEI,GAAG,GAAGQ,KAAKD,EAAEf,QAAQ,SAASG,GAAoB,OAAOI,EAAlBH,EAAEI,GAAG,GAAGL,IAAeA,IAAIY,EAAEA,EAAEf,QAAQG,EAAEC,EAAEC,EAAEC,GAAG,OAAOD,EAAEG,GAAGR,QAAQ,IAAI,IAAIW,EAAE,mBAAmBT,SAASA,QAAQM,EAAE,EAAEA,EAAEF,EAAEW,OAAOT,IAAID,EAAED,EAAEE,IAAI,OAAOD,EAA7b,CAA4c,CAACW,EAAE,CAAC,SAAShB,EAAQD,EAAOD,KAEte,IAAImB,EAAE,CAAC,SAASjB,EAAQD,EAAOD,GAuSjCC,EAAOD,QAAU,SAAUoB,GAIvB,OAAGA,EAAMH,OA1Rb,SAAiBI,GACb,IAAIC,EAAM,CAENC,SAAY,WACZC,aAAgB,0BAChBC,OAAU,sBACVC,OAAU,sBACVC,cAAiB,iBACjBC,gBAAmB,uBACnBC,UAAc,8DACdC,UAAa,gDACbC,UAAa,kBACbC,UAAa,eACbC,UAAa,eACbC,QAAW,sCACXC,SAAY,cAGhBf,EAAQ,CACJgB,OAAU,GACVC,SAAY,OACZC,YAAe,GACfC,UAAa,IAEjBD,EAAc,CACVE,KAAM,MACNC,KAAM,MACNC,IAAK,SAETC,EAAM,GAAaC,EAAM,KAAMC,EAAO,GAAIC,EAAQ,GAClDC,EAAa,GAAIC,EAAM,EAMH,iBAAV3B,IACNA,EAAQA,EAAM4B,MAAM,OAKxB,IAAI,IAAIzC,EAAI,EAAGA,EAAIa,EAAMJ,OAAQT,IAc7B,GAZAuC,EAAa,KAAOvC,EAGpBmC,EAAMtB,EAAMb,GAGN,EAGNoC,EAAM,KAGHtB,EAAIE,aAAa0B,KAAKP,GAErBvB,EAAMgB,OAASO,EAAIQ,MAAM,eAAe,IAGxCP,EAAMD,EAAIQ,MAAM7B,EAAIO,WAAWuB,IAAI,SAASC,GACxC,OAAOA,EAAEC,QAAQ,MAAM,MACxBC,MAAM,IAMLC,QAAQ,SAASH,GASTR,EAFI,QAJZA,EAAOQ,EAAEF,MAAM7B,EAAIY,UAKM,MAAlBmB,EAAEI,OAAO,EAAE,IACF,EAED,EAGJZ,EAAK,GAGhBA,EAAOa,WAAWb,GAGlBC,EAAQO,EAAEF,MAAM7B,EAAIa,UAAU,GAAGmB,QAAQ,MAAM,IAG/ClC,EAAMmB,UAAUO,GAAS1B,EAAMmB,UAAUO,IAAU,GACnD1B,EAAMmB,UAAUO,GAAOa,KAAOd,SAIhC,GAAGvB,EAAIG,OAAOyB,KAAKP,GAErBC,EAAMD,EAAIQ,MAAM7B,EAAIU,WAAWuB,MAAM,GAGrCnC,EAAMwC,KAAOxC,EAAMwC,MAAQ,GAE3BhB,EAAIY,QAAQ,SAASH,GACjBA,EAAIA,EAAEC,QAAQ,IAAI,IAClBlC,EAAMwC,KAAKP,GAAK,SAGjB,GAAG/B,EAAII,OAAOwB,KAAKP,GAEtBC,EAAMD,EAAIQ,MAAM7B,EAAIW,WAAWsB,MAAM,GAGrCnC,EAAMyC,SAAWzC,EAAMyC,UAAY,GAEnCjB,EAAIY,QAAQ,SAASH,GACjBA,EAAIA,EAAEC,QAAQ,IAAI,IAClBlC,EAAMyC,SAASR,GAAK,SAGrB,GAAG/B,EAAIK,cAAcuB,KAAKP,GAAK,CAClC,IAAImB,EAAiBnB,EAAIoB,QAAQ,MAIjCnB,IAHgD,IAApBkB,EAAyBnB,EAAMA,EAAIY,MAAMO,EAAiB,IAG3DX,MAAM7B,EAAIO,WAAWuB,IAAI,SAASC,GACzD,OAAOA,EAAEC,QAAQ,MAAM,OAKvBE,QAAQ,SAASH,GAMTR,EAFI,QAFZA,EAAOQ,EAAEF,MAAM7B,EAAIY,UAGM,MAAlBmB,EAAEI,OAAO,EAAE,IACF,EAED,EAGJZ,EAAK,GAGhBA,EAAOa,WAAWb,GAIlBC,EAAQO,EAAEF,MAAM7B,EAAIa,UAAU,GAG9Bf,EAAMmB,UAAUO,GAAS1B,EAAMmB,UAAUO,IAAU,GACnD1B,EAAMmB,UAAUO,GAAOC,GAAcF,IAMzCG,EAAMU,WAAWf,EAAIQ,MAAM7B,EAAIQ,WAAW,IAI1Ca,EAAML,EAAYK,EAAIQ,MAAM7B,EAAIS,WAAW,IAC3CX,EAAMkB,YAAYS,GAAc3B,EAAMkB,YAAYS,IAAe,GACjE3B,EAAMkB,YAAYS,GAAYJ,GAAOK,OAE/B1B,EAAIM,gBAAgBsB,KAAKP,KAE/BC,EAAMD,EAAIQ,MAAM7B,EAAIU,WAAWuB,MAAM,GAGrCnC,EAAM4C,aAAe5C,EAAM4C,cAAgB,GAE3CpB,EAAIY,QAAQ,SAASH,GACjBA,EAAIA,EAAEC,QAAQ,IAAI,IAClBlC,EAAM4C,aAAaX,GAAK,KAIpC,OAAOjC,EAsGI6C,CAAQ7C,GA1FvB,SAAmBA,GAEf,IAAKA,EACD,MAAM,IAAIP,MAAM,yCAGpB,IAAIqD,EAAS,GAGTC,EAAS,CACLC,IAAO,KACPC,IAAO,KACPC,MAAS,KAEbC,EAAU,IAAIC,OAAO,6BAA2C,MAIpE,GAAGpD,EAAMgB,OAKL,IAAI,IAAIqC,KAHRP,GAAU9C,EAAMgB,OAAS,IAGZhB,EAAMmB,UAGfnB,EAAMmB,UAAUkC,GAAGA,GAAKrD,EAAMmB,UAAUkC,GAAGA,GAAKrD,EAAMmB,UAAUkC,GAAGA,GAAK,EAGrErD,EAAMmB,UAAUkC,GAAGrD,EAAMiB,YACxB6B,GAAU,IAAM9C,EAAMmB,UAAUkC,GAAGrD,EAAMiB,UAAY,IAAMoC,EAAEnB,QAAQiB,EAAQ,WAIrFL,GAAU,OASd,IAAI,IAAIQ,KAHRR,GAAU,QAGI9C,EAAMkB,YAChB,IAAI,IAAIqC,KAAKvD,EAAMkB,YAAYoC,GAC3B,QAAwB,IAAdP,EAAOQ,GAAmB,CAEhC,IAAI,IAAIC,KAAKxD,EAAMmB,eAGsB,IAA3BnB,EAAMmB,UAAUqC,GAAGF,KACzBR,GAAU,IAAM9C,EAAMmB,UAAUqC,GAAGF,GAAM,IAAME,EAAEtB,QAAQiB,EAAQ,MAKzEL,GAAU,IAAMC,EAAOQ,GAAK,IAAMvD,EAAMkB,YAAYoC,GAAIC,GACxDT,GAAU,MAOtB,GAAG9C,EAAMwC,KAEL,IAAI,IAAIiB,KADRX,GAAU,OACK9C,EAAMwC,KACjBM,GAAU,OAASW,EAAIvB,QAAQiB,EAAQ,KAAO,MAKtD,GAAGnD,EAAM4C,aAEL,IAAI,IAAIc,KADRZ,GAAU,OACM9C,EAAM4C,aAClBE,GAAU,gBAAkBY,EAAKxB,QAAQiB,EAAQ,KAAO,MAKhE,OAAOL,EAYIa,CAAU3D,KAIvB,IAAI4D,EAAE,CAAC,SAAS9E,EAAQD,EAAOD,GAmBjC,SAASiF,EAAWC,GAuChB,OA7BAA,GADAA,GAHAA,EAAOA,EAAK5B,QAAQ,SAAS,SAGjBL,MAAM,SACNkC,OAAO,SAASV,GAOxB,OAAkB,IADb,IAAID,OAAO,MAAM,MAChBtB,KAAKuB,KAOO,IADb,IAAID,OAAO,OAAO,MACjBtB,KAAKuB,KAOdrB,IAAI,SAASqB,GACV,OAAOA,EAAExB,MAAM,qBAElBmC,OAAO,SAAS7E,EAAE8E,EAAE7E,GAEjB,OADAD,EAAE8E,EAAE,IAAMA,EAAE,GACL9E,GACT,IAvCNP,EAAQsF,SAAWpF,EAAQ,iBAgD3BF,EAAQuF,MAAQ,SAASnE,GAErB,OAAO,IAAIoE,QAAQ,SAASC,EAAKC,GAIR,oBAAXC,QACND,EAAI,qCAKR,IAAIR,EAAOhF,EAAQ,gBAARA,CAAyBkB,GAGhCA,EAAMwE,UACNF,EAAI,oGAOJtE,EAAMwE,SAASC,SACfH,EAAI,kEAMJtE,EAAMwE,SAASE,MACfJ,EAAI,kEAOJtE,EAAMwE,SAASG,UACfL,EAAI,iGAkBCxF,EAAQ,MAEd8F,UAAU5E,EAAMwE,SAASG,SAAUb,EAAM,SAASe,EAAIC,GACrD,GAAGD,EACCP,EAAIO,OACD,CAMH,IAAIE,EAAOjG,EAAQ,iBAAiBkG,SAKpChF,EAAMwE,SAASE,KAAKO,KAAKjF,EAAMwE,SAASG,UAExCI,EAAK/E,EAAMwE,SAASC,QAASzE,EAAMwE,SAASE,KAAM,SAAS1F,EAAE8E,GACzD,GAAG9E,EAEC,GAAc,IAAXA,EAAEU,KACD2E,EAAIR,EAAWC,QACZ,CAEH,IAcIoB,EAAU,CACVxF,KAAQV,EAAEU,KACVyF,QAhBQ,CACRC,KAAM,gBACNtF,EAAK,aACLC,EAAK,aACL6D,EAAK,YACLyB,EAAK,aACLC,EAAK,aACLC,EAAK,aACLC,EAAK,UACLC,EAAK,YACLC,GAAM,iBACNC,IAAO,cAKU3G,EAAEU,MACnBoE,KAAQA,GAGZQ,EAAIY,QAKRb,EAAIR,EAAWC,aAuBrC,CAAC8B,gBAAgB,EAAEC,cAAgB,EAAEC,GAAK,IAAIT,EAAE,CAAC,SAASvG,EAAQD,EAAOD,GAU3EC,EAAOD,QAAU,CACbmH,QAAWjH,EAAQ,uBAErB,CAACkH,oBAAoB,IAAIV,EAAE,CAAC,SAASxG,EAAQD,EAAOD,GAQtD,IAAIqH,EAAUnH,EAAQ,wBAElBoH,GADepH,EAAQ,6BACTA,EAAQ,qBACtBqH,EAAaD,EAAYC,WACzBC,EAAWF,EAAYE,SACvBC,EAAWH,EAAYG,SACvBC,EAAkBJ,EAAYI,gBACvBJ,EAAYK,KAMvB,SAASC,EAAMC,EAAWC,GACtBC,KAAKC,QAAU,IAAIX,EAAQQ,GAE3BE,KAAKD,KAAOA,EAEZC,KAAKxF,UAAY,GAEjBwF,KAAKE,iBAAmB,GAExBF,KAAKG,sBAAwB,GAE7BH,KAAKzF,YAAc,GAEnByF,KAAKI,aAAe,EAEpBJ,KAAKK,WAAa,EAElBL,KAAKM,gBAAiB,EAEtBN,KAAKO,oBAAqB,EAE1BP,KAAKQ,gBAAkB,EAEvBR,KAAKS,YAAa,EAElBT,KAAKU,gBAAiB,EAOtBV,KAAKW,SAAW,IAEpBzI,EAAOD,QAAU4H,GAEXe,UAAUC,SAAW,WAEvB,OADAb,KAAKM,gBAAiB,EACfN,MAGXH,EAAMe,UAAUE,SAAW,WAEvB,OADAd,KAAKM,gBAAiB,EACfN,MAUXH,EAAMe,UAAUG,oBAAsB,WAClC,GAAmC,EAA/Bf,KAAKgB,iBAAiB9H,OACtB,OAAO8G,KAAKgB,iBAAiBC,MAGjC,IAAIC,EAAQlB,KAAKmB,iBAEjB,OADAnB,KAAKmB,kBAAoB,EAClBD,GAGXrB,EAAMe,UAAUQ,eAAiB,SAAUpG,GACvC,IAAIqG,EAAgBrG,EAAWsG,MAC/BtB,KAAKC,QAAQsB,kBAAkBF,EAAcH,OAASG,EACtDrB,KAAKzF,YAAY+D,KAAKtD,GACtBgF,KAAKI,cAAgB,GACW,IAA5BJ,KAAKO,oBACLP,KAAKC,QAAQuB,cAAcxG,IAInC6E,EAAMe,UAAUa,YAAc,SAAUxG,GACpC,IAAID,EAAa,IAAIwE,EAAWvE,GAAK,EAAM+E,KAAKC,QAAQyB,qBAAsB1B,MAE9E,OADAA,KAAKoB,eAAepG,GACbA,GAGX6E,EAAMe,UAAUe,YAAc,SAAU1G,GACpC,IAAID,EAAa,IAAIwE,EAAWvE,GAAK,EAAO+E,KAAKC,QAAQyB,qBAAsB1B,MAE/E,OADAA,KAAKoB,eAAepG,GACbA,GAGX6E,EAAMe,UAAUrE,MAAQ,SAAUtB,GAC9B,IAAI2G,EAAkB,IAAIpC,EAAWvE,GAAK,EAAM+E,KAAKC,QAAQyB,qBAAsB1B,MACnFA,KAAKoB,eAAeQ,GAEpB,IAAIC,EAAkB,IAAIrC,EAAWvE,GAAK,EAAO+E,KAAKC,QAAQyB,qBAAsB1B,MAGpF,OAFAA,KAAKoB,eAAeS,GAEb,IAAIpC,EAASmC,EAAiBC,IAGzChC,EAAMe,UAAUkB,YAAc,SAAUC,EAAMC,EAAIC,EAAWC,EAAgBC,GACzE,GAAwB,iBAAbA,EACP,OAAQA,GACR,IAAK,WACDA,EAAW,EACX,MACJ,IAAK,SACDA,EAAW,EACX,MACJ,IAAK,SACDA,EAAW,EACX,MACJ,IAAK,OACDA,EAAW,EACX,MACJ,QACIA,EAAW,EAKnB,IAaIC,EAbAC,EAAWrC,KAAKC,QAAQyB,qBAkC5B,OAjCIM,MAAAA,IACAA,EAAK,IAAMK,GAGXN,MAAAA,IACAA,EAAO,GAGPI,MAAAA,IACAA,EAAW,GAIXF,GACAG,EAAW,IAAIzC,EAAgBqC,EAAID,EAAMM,EAAUF,GACnDnC,KAAKE,iBAAiB5B,KAAK8D,IAE3BA,EAAW,IAAI1C,EAASsC,EAAID,EAAMM,EAAUF,GAGhDnC,KAAKxF,UAAU8D,KAAK8D,GACpBpC,KAAKC,QAAQsB,kBAAkBc,GAAYD,EAEvCF,IACAlC,KAAKG,sBAAsBkC,IAAY,GAG3CrC,KAAKK,YAAc,GAEa,IAA5BL,KAAKO,oBACLP,KAAKC,QAAQ6B,YAAYM,GAGtBA,GAGXvC,EAAMe,UAAU0B,kBAAoB,SAAUtH,GAC1C,IAAIuH,EAAMvC,KAAKzF,YAAYyB,QAAQhB,IACtB,IAATuH,GAKJvC,KAAKzF,YAAYiI,OAAOD,EAAK,GAC7BvC,KAAKI,cAAgB,GAEW,IAA5BJ,KAAKO,oBACLP,KAAKC,QAAQwC,iBAAiBzH,GAG9BA,EAAW0H,YACX1C,KAAK2C,eAAe3H,EAAW0H,aAZ/BE,QAAQC,KAAK,6DAmBrBhD,EAAMe,UAAU6B,iBAAmB,SAAUzH,GAQzC,OAPIA,EAAW8H,YACX9C,KAAKsC,kBAAkBtH,EAAW+H,YAClC/C,KAAKsC,kBAAkBtH,EAAWgI,aAElChD,KAAKsC,kBAAkBtH,GAGpBgF,MAGXH,EAAMe,UAAU+B,eAAiB,SAAUP,GACvC,IAAIG,EAAMvC,KAAKxF,UAAUwB,QAAQoG,GACjC,IAAa,IAATG,EAUJ,OANAvC,KAAKxF,UAAUgI,OAAOD,EAAK,IAEK,IAA5BvC,KAAKO,oBACLP,KAAKC,QAAQ0C,eAAeP,GAGzBpC,KATH4C,QAAQC,KAAK,yDAYrBhD,EAAMe,UAAUqC,oBAAsB,SAAUjI,EAAYkI,GAIxD,OAHgC,IAA5BlD,KAAKO,oBACLP,KAAKC,QAAQgD,oBAAoBjI,EAAYkI,GAE1ClD,MAGXH,EAAMe,UAAUuC,4BAA8B,SAAUnI,EAAYoH,EAAUc,GAI1E,OAHgC,IAA5BlD,KAAKO,oBACLP,KAAKC,QAAQkD,4BAA4BnI,EAAYoH,EAAUc,GAE5DlD,MAIXH,EAAMe,UAAUwC,QAAU,SAAUrB,EAAMK,GACtC,IAAIc,EAAanB,EAAOK,EAASL,KAOjC,OAN4B,IAAxB/B,KAAKM,iBACL4C,GAAcA,GAGlBd,EAASL,KAAOA,EAChB/B,KAAKC,QAAQoD,WAAWjB,EAAUc,GAC3BlD,MAKXH,EAAMe,UAAU0C,SAAW,SAAUC,GACjCvD,KAAKM,eAAuC,QAArBiD,EAAUlJ,OAYjC,IAVA,IAAIG,EAAY+I,EAAU/I,UACtBD,EAAcgJ,EAAUhJ,YAExBiJ,EAAiB,GACjBC,EAAiB,GAGjBC,EAAgBC,OAAOC,KAAKrJ,GAC5BsJ,EAAiBH,EAAcxK,OAE1BP,EAAI,EAAGA,EAAIkL,EAAgBlL,GAAK,EAAG,CACxC,IAQIqK,EAAYD,EARZe,EAAeJ,EAAc/K,GAC7BqC,EAAaT,EAAYuJ,GACzBvH,EAAQvB,EAAWuB,MAEnBwH,EAAS/I,EAAW+I,OACpB5B,EAAWnH,EAAWmH,SACtB6B,OAAqBC,IAAXF,QAAqCE,IAAb9B,EAGtC,QAAc8B,IAAV1H,EAAqB,CACrB,IAAID,EAAMtB,EAAWsB,SACT2H,IAAR3H,IACA0G,EAAahD,KAAK2B,YAAYrF,GAC9BkH,EAAeM,GAAgBd,EAC3BgB,GAAWhB,EAAWkB,MAAMH,EAAQ5B,IAG5C,IAAI9F,EAAMrB,EAAWqB,SACT4H,IAAR5H,IACA0G,EAAa/C,KAAKyB,YAAYpF,GAC9BoH,EAAeK,GAAgBf,EAC3BiB,GAAWjB,EAAWmB,MAAMH,EAAQ5B,QAEzC,CACHa,EAAahD,KAAK2B,YAAYpF,GAC9BiH,EAAeM,GAAgBd,EAE/BD,EAAa/C,KAAKyB,YAAYlF,GAC9BkH,EAAeK,GAAgBf,EAE/B,IAAIoB,EAAW,IAAI1E,EAASuD,EAAYD,GACpCiB,GAAWG,EAASD,MAAMH,EAAQ5B,IAI9C,IAAIiC,EAAcT,OAAOC,KAAKpJ,GAC1B6F,EAAa+D,EAAYlL,OAU7B8G,KAAKqE,UAAYd,EAAUc,WAAa,EAErCd,EAAUe,UACTtE,KAAKsE,QAAUf,EAAUe,SAa1Bf,EAAUgB,UAKNhB,EAAUgB,QAAQD,UACjBtE,KAAKsE,QAAUf,EAAUgB,QAAQD,SAMf,IAAnBtE,KAAKqE,YACJrE,KAAKqE,UAAYd,EAAUgB,QAAQF,WAAa,GAMjDd,EAAUgB,QAAQ9D,aACjBT,KAAKS,WAAa8C,EAAUgB,QAAQ9D,iBASK,IAAnC8C,EAAUgB,QAAQC,aACxBxE,KAAKU,gBAAiB,EAEtBV,KAAKU,eAAiB6C,EAAUgB,QAAQC,cAmBhD,IANA,IAAIC,EAAgBlB,EAAU1H,MAAQ,GAClC6I,EAAenB,EAAUzH,UAAY,GACrC6I,EAAqBpB,EAAUtH,cAAgB,GAG/C2I,EAAgBrB,EAAUjJ,SACrBuK,EAAI,EAAGA,EAAIxE,EAAYwE,GAAK,EAAG,CAEpC,IAAIC,EAAaV,EAAYS,GACzBE,EAAsBvK,EAAUsK,GAChC/C,EAAOgD,EAAoBH,IAAkB,EAC7CI,IAAaN,EAAaI,GAC1B7C,IAAcwC,EAAcK,IAAeE,EAC3C9C,IAAmByC,EAAmBG,GACtC1C,EAAWpC,KAAK8B,YAAYC,EAAM+C,EAAY7C,EAAWC,GAEzD8C,GAEAhF,KAAKyB,YAAY,GAAGwD,QAAQ,EAAG7C,GAGnC,IAAI8C,EAAkBvB,OAAOC,KAAKmB,GAClC,IAAKpM,EAAI,EAAGA,EAAIuM,EAAgBhM,OAAQP,GAAK,EAAG,CAC5C,IAAIwM,EAAiBD,EAAgBvM,GACrC,GAAIwM,IAAmBP,EAAvB,CAIA,IAAIQ,EAAcL,EAAoBI,GAElCE,EAAgB7B,EAAe2B,QACblB,IAAlBoB,GACAA,EAAcJ,QAAQG,EAAahD,GAGvC,IAAIkD,EAAgB7B,EAAe0B,QACblB,IAAlBqB,GACAA,EAAcL,QAAQG,EAAahD,KAK/C,OAAOpC,MAKXH,EAAMe,UAAU2E,4BAA8B,WAC1C,OAAOvF,KAAKE,iBAAiBhH,QAGjC2G,EAAMe,UAAUpD,MAAQ,WAOpB,OALgC,IAA5BwC,KAAKO,qBACLP,KAAKC,QAAQuF,SAASxF,MACtBA,KAAKO,oBAAqB,GAGvBP,KAAKC,QAAQzC,SAGxBqC,EAAMe,UAAU6E,WAAa,WACzB,OAAOzF,KAAKC,QAAQyF,UAGxB7F,EAAMe,UAAU+E,KAAO,WACnB,OAAO3F,KAAKC,QAAQ0F,QAGxB9F,EAAMe,UAAUgF,QAAU,WACtB,OAAO5F,KAAKC,QAAQ2F,WAGxB/F,EAAMe,UAAUiF,gBAAkB,SAAUpF,GACxCT,KAAKS,WAAaA,GAGtBZ,EAAMe,UAAUkF,MAAQ,SAAUC,GAC9B/F,KAAKU,eAAiBqF,GAG1BlG,EAAMe,UAAUoF,IAAM,SAAUC,GAC5B,OAAOjG,KAAKC,QAAQ+F,IAAIC,KAG1B,CAACC,uBAAuB,EAAEC,4BAA4B,GAAGC,mBAAmB,KAAKxH,EAAE,CAAC,SAASzG,EAAQD,EAAOD,GAsC9GC,EAAOD,QAAU,SAASoO,EAAQhN,GAc9B,IAGIuB,EAMAnC,EAAE6N,EAAE5J,EAAEE,EATN2J,EAAalN,EAAMiB,SACnBkM,EAAkBC,KAAKC,MAAMD,KAAKE,UAAUtN,EAAMiB,WAClDsJ,EAAOD,OAAOC,KAAKvK,EAAMiB,UAEzBsM,EAAU,EACVC,EAAU,GACVC,EAAa,GACbC,EAAM,GACNC,EAAS,GAOb,WAHO3N,EAAMiB,SAGT7B,EAAI,EAAGA,EAAImL,EAAK1K,OAAQT,IAExB+N,EAAgB5C,EAAKnL,IAAM,EAI/B,IAAIA,EAAI,EAAGA,EAAImL,EAAK1K,OAAQT,IAAI,CAiB5B,IAAImE,KAdJvD,EAAMiB,SAAWsJ,EAAKnL,GACtBY,EAAMgB,OAASkM,EAAW3C,EAAKnL,IAG/BmC,EAAMyL,EAAOY,MAAM5N,OAAO4K,OAAWA,GAAW,GAUvCL,EAEL,IAAIvK,EAAMmB,UAAUoJ,EAAKhH,IAIrB,IAAIF,KAFJ9B,EAAIgJ,EAAKhH,IAAMhC,EAAIgJ,EAAKhH,IAAMhC,EAAIgJ,EAAKhH,IAAM,EAEpCvD,EAAMmB,UAERnB,EAAMmB,UAAUkC,GAAGkH,EAAKhH,KAAOhC,EAAI8B,KAElC9B,EAAIgJ,EAAKhH,KAAOhC,EAAI8B,GAAKrD,EAAMmB,UAAUkC,GAAGkH,EAAKhH,KAYjE,IALAkK,EAAa,OAKTR,EAAI,EAAGA,EAAI1C,EAAK1K,OAAQoN,IACrB1L,EAAIgJ,EAAK0C,IACRQ,GAAc,KAAuB,IAAflM,EAAIgJ,EAAK0C,IAAc,GAAK,IAElDQ,GAAc,KAKtB,IAAID,EAAQC,GAAY,CAOpB,IALAD,EAAQC,GAAc,EACtBF,IAIIN,EAAI,EAAGA,EAAI1C,EAAK1K,OAAQoN,IACrB1L,EAAIgJ,EAAK0C,MACRE,EAAgB5C,EAAK0C,KAAO1L,EAAIgJ,EAAK0C,YAQtC1L,EAAI8K,gBACJ9K,EAAIsM,OACXF,EAAO1I,KAAK1D,IASpB,IAAInC,EAAI,EAAGA,EAAImL,EAAK1K,OAAQT,IACxBY,EAAMkB,YAAYqJ,EAAKnL,IAAM,CAAC8D,MAASiK,EAAgB5C,EAAKnL,IAAMmO,GAStE,IAAInO,KALJY,EAAMiB,SAAW,WAAa6M,KAAKC,SACnC/N,EAAMgB,OAAS,MAINhB,EAAMmB,UACXnB,EAAMmB,UAAU/B,GAAG4O,QAAU,EAIjC,IAAI5O,KAAKuO,EACL,IAAItK,KAAKsK,EAAOvO,GACZsO,EAAIrK,GAAKqK,EAAIrK,IAAM,CAACJ,IAAK,KAAMD,KAAM,MAO7C,IAAI5D,KAAKsO,EACL,IAAIrK,KAAKsK,EACFA,EAAOtK,GAAGjE,IACNuO,EAAOtK,GAAGjE,GAAKsO,EAAItO,GAAG4D,MACrB0K,EAAItO,GAAG4D,IAAM2K,EAAOtK,GAAGjE,IAExBuO,EAAOtK,GAAGjE,GAAKsO,EAAItO,GAAG6D,MACrByK,EAAItO,GAAG6D,IAAM0K,EAAOtK,GAAGjE,MAG3BuO,EAAOtK,GAAGjE,GAAK,EACfsO,EAAItO,GAAG6D,IAAM,GAOzB,MAAO,CACHgL,SAHJ1M,EAAOyL,EAAOY,MAAM5N,OAAO4K,OAAWA,GAAW,GAI7CsD,SAAUP,EACVQ,OAAQT,KAKd,IAAIlI,EAAE,CAAC,SAAS1G,EAAQD,EAAOD,GAGjC,IAAIwP,EAAWtP,EAAQ,iBAEvB,SAASuP,EAAazH,EAAS0H,EAAYjC,EAAUkC,EAASC,GAC1DJ,EAASxO,KAAK+G,KAAMC,EAAS0H,EAAYjC,EAAUkC,GACnD5H,KAAK8H,KAAOD,GAEhB3P,EAAOD,QAAUyP,GACJ9G,UAAY+C,OAAOoE,OAAON,EAAS7G,WAChD8G,EAAaM,YAAcN,GAEzB,CAACO,gBAAgB,IAAIC,EAAE,CAAC,SAAS/P,EAAQD,EAAOD,GAGlD,SAASwP,EAASxH,EAAS0H,EAAYjC,EAAUkC,GAC7C5H,KAAK0F,SAAWA,EAChB1F,KAAK2H,WAAaA,EAClB3H,KAAK4H,QAAUA,EACf5H,KAAKmI,SAAWlI,GAEpB/H,EAAOD,QAAUwP,GAER7G,UAAUwH,oBAAsB,WAWrC,IAVA,IAAIC,EAAc,GAEdpI,EAAUD,KAAKmI,SACfG,EAAgBrI,EAAQqI,cACxB/G,EAAoBtB,EAAQsB,kBAC5BgH,EAAStI,EAAQsI,OACjBC,EAAYvI,EAAQuI,UACpBC,EAAUxI,EAAQyI,OAAS,EAC3BC,EAAgBxB,KAAKyB,MAAM,EAAI3I,EAAQH,WAElC1H,EAAI,EAAGA,GAAKqQ,EAASrQ,GAAK,EAAG,CAClC,IACIgK,EAAWb,EADA+G,EAAclQ,IAE7B,QAAiB6L,IAAb7B,IAA+C,IAArBA,EAASyG,QAAvC,CAIA,IAAIC,EAAWP,EAAOnQ,GAAGoQ,GACzBH,EAAYjG,EAASJ,IACjBmF,KAAKyB,OAAOG,OAAOC,QAAUF,GAAYH,GAAiBA,GAGlE,OAAON,IAGT,IAAIvJ,EAAE,CAAC,SAAS3G,EAAQD,EAAOD,GAOjC,IAAIwP,EAAWtP,EAAQ,iBACnBuP,EAAevP,EAAQ,qBAa3B,SAASmH,EAAQQ,GACbE,KAAK3G,MAAQ,KAEb2G,KAAKuI,OAAS,KACdvI,KAAKiJ,MAAQ,EACbjJ,KAAK0I,OAAS,EAEd1I,KAAKkJ,aAAe,EACpBlJ,KAAKwI,UAAY,EAEjBxI,KAAKuB,kBAAoB,GACzBvB,KAAKmJ,iBAAmB,KAGxBnJ,KAAK0F,UAAW,EAChB1F,KAAK2H,WAAa,EAClB3H,KAAKoJ,aAAe,EAEpBpJ,KAAKsI,cAAgB,KACrBtI,KAAKqJ,cAAgB,KAErBrJ,KAAKsJ,cAAgB,KACrBtJ,KAAKuJ,cAAgB,KAErBvJ,KAAKF,UAAYA,GAAa,KAE9BE,KAAKwJ,mBAAqB,GAC1BxJ,KAAKyJ,qBAAuB,GAE5BzJ,KAAK0J,WAAa,KAElB1J,KAAKgB,iBAAmB,GACxBhB,KAAKmB,iBAAmB,EAExBnB,KAAKxF,UAAY,KACjBwF,KAAK2J,MAAQ,EAEb3J,KAAK4H,SAAU,EACf5H,KAAK4J,kBAAoB,KAEzB5J,KAAK6H,uBAAyB,EAclC,SAASgC,EAAkB1H,EAAU2H,GACjC9J,KAAKmC,SAAWA,EAChBnC,KAAK+J,aAAe,IAAIC,MAAMF,GAC9B,IAAK,IAAInR,EAAI,EAAGA,EAAImR,EAAUnR,GAAK,EAC/BqH,KAAK+J,aAAapR,GAAK,GAhB/BT,EAAOD,QAAUqH,GAETsB,UAAUpD,MAAQ,WAOtB,OAN+C,EAA3CwC,KAAK3G,MAAMkM,8BACXvF,KAAKiK,eAELjK,KAAKkK,UAETlK,KAAKmK,uBACEnK,KAAKoK,eAWhBP,EAAkBjJ,UAAUyJ,KAAO,WAC/B,IAAIA,EAAO,IAAIR,EAAkB7J,KAAKmC,SAAUnC,KAAK+J,aAAa7Q,QAElE,OADAmR,EAAKN,aAAe/J,KAAK+J,aAAavO,QAC/B6O,GAGX/K,EAAQsB,UAAU0J,qBAAuB,SAAUnI,EAAUoI,EAAQxI,GACjE,IAAIyI,EAAuBxK,KAAKyJ,qBAAqBtH,QACxB8B,IAAzBuG,IAEAA,EAAuB,IAAIX,EAAkB1H,EAD9BgF,KAAK9K,IAAI2D,KAAKiJ,MAAOsB,EAAS,IAE7CvK,KAAKyJ,qBAAqBtH,GAAYqI,EACtCxK,KAAKwJ,mBAAmBlL,KAAKkM,GAC7BxK,KAAKwJ,mBAAmBiB,KAAK,SAAU5R,EAAG6R,GACtC,OAAO7R,EAAEsJ,SAAWuI,EAAEvI,YAI9BqI,EAAqBT,aAAaQ,GAAUxI,GAKhDzC,EAAQsB,UAAU+J,WAAa,SAAU1B,EAAOP,EAAQlO,EAAW2O,GAC/DnJ,KAAKxF,UAAYA,EACjBwF,KAAKmJ,iBAAmBA,EAExBnJ,KAAKiJ,MAAQA,EACbjJ,KAAK0I,OAASA,EAMd,IADA,IAAIkC,EAAS,IAAIZ,MAAMf,GACdxQ,EAAI,EAAGA,EAAIwQ,EAAOxQ,IACvBmS,EAAOnS,GAAK,EAIhBuH,KAAKuI,OAAS,IAAIyB,MAAMtB,GACxB,IAAK,IAAIpC,EAAI,EAAGA,EAAIoC,EAAQpC,IACxBtG,KAAKuI,OAAOjC,GAAKsE,EAAOpP,QAc5BwE,KAAKsI,cAAgB,IAAI0B,MAAMhK,KAAK0I,QACpC1I,KAAKqJ,cAAgB,IAAIW,MAAMhK,KAAKiJ,OAEpCjJ,KAAKsI,cAAc,IAAM,EACzBtI,KAAKqJ,cAAc,IAAM,EAEzBrJ,KAAK2J,MAAQV,EAAQP,EAAS,EAC9B1I,KAAKsJ,cAAgB,IAAIU,MAAMhK,KAAK2J,OACpC3J,KAAKuJ,cAAgB,IAAIS,MAAMhK,KAAK2J,OAEpC3J,KAAKmB,iBAAmBnB,KAAK2J,OAGjCrK,EAAQsB,UAAUiK,aAAe,WAC7B,IAMIhG,EAAGxC,EANH7H,EAAYwF,KAAK3G,MAAMmB,UACvBD,EAAcyF,KAAK3G,MAAMkB,YAEzBoP,EAAQnP,EAAUtB,OAClBkH,EAAe7F,EAAYrB,OAG3B4R,EAAU9K,KAAKuI,OAAO,GACtBwC,GAAuC,IAA9B/K,KAAK3G,MAAMiH,gBAA4B,EAAI,EACxD,IAAKuE,EAAI,EAAGA,EAAI8E,EAAO9E,GAAK,EAAG,CAC3B,IAAIzC,EAAW5H,EAAUqK,GACrB1C,EAAWC,EAASD,SACpBJ,EAAOgJ,EAAQ3I,EAASL,KACX,IAAbI,EACA2I,EAAQjG,EAAI,GAAK9C,EAEjB/B,KAAKsK,qBAAqBnI,EAAU0C,EAAI,EAAG9C,GAG/CM,EAAW7H,EAAUqK,GAAG3D,MACxBlB,KAAKsJ,cAAcjH,IAAa,EAChCrC,KAAKuJ,cAAclH,GAAYwC,EAAI,EACnC7E,KAAKqJ,cAAcxE,EAAI,GAAKxC,EAIhC,IADA,IAAI2I,EAAW,EACNrS,EAAI,EAAGA,EAAIyH,EAAczH,GAAK,EAAG,CACtC,IAOIJ,EAAG0S,EAPHjQ,EAAaT,EAAY5B,GAEzBuS,EAAkBlQ,EAAWkG,MACjClB,KAAKsJ,cAAc4B,GAAmBF,EACtChL,KAAKuJ,cAAc2B,IAAoB,EACvClL,KAAKsI,cAAc0C,GAAYE,EAG/B,IAAIC,EAAQnQ,EAAWmQ,MACnBC,EAASD,EAAMjS,OACfmS,EAAMrL,KAAKuI,OAAOyC,KACtB,GAAIhQ,EAAWsQ,aAAc,CACzB,IAAK/S,EAAI,EAAGA,EAAI6S,EAAQ7S,GAAK,EACzB0S,EAAOE,EAAM5S,GAEb8S,EADSrL,KAAKuJ,cAAc0B,EAAK7I,SAASlB,QAC5B+J,EAAK7F,YAGvBiG,EAAI,GAAKrQ,EAAWC,QACjB,CACH,IAAK1C,EAAI,EAAGA,EAAI6S,EAAQ7S,GAAK,EACzB0S,EAAOE,EAAM5S,GAEb8S,EADSrL,KAAKuJ,cAAc0B,EAAK7I,SAASlB,SAC3B+J,EAAK7F,YAGxBiG,EAAI,IAAMrQ,EAAWC,OAOjCqE,EAAQsB,UAAU4E,SAAW,SAAUnM,GAGnC,IAAI4P,GAFJjJ,KAAK3G,MAAQA,GAEKgH,WAAa,EAC3BqI,EAASrP,EAAM+G,aAAe,EAKlC,OAFAJ,KAAK2K,WAAW1B,EAAOP,EAAQrP,EAAMmB,UAAWnB,EAAM8G,uBACtDH,KAAK6K,eACE7K,MAGXV,EAAQsB,UAAUc,mBAAqB,WACnC,GAAmC,EAA/B1B,KAAKgB,iBAAiB9H,OACtB,OAAO8G,KAAKgB,iBAAiBC,MAGjC,IAAIC,EAAQlB,KAAKmB,iBAEjB,OADAnB,KAAKmB,kBAAoB,EAClBD,GAGX5B,EAAQsB,UAAU2K,QAAU,WAIxB,IAHA,IAAIA,EAAU,EAEVhD,EAASvI,KAAKuI,OACTnQ,EAAI,EAAGA,EAAI4H,KAAK0I,OAAQtQ,IAE7B,IADA,IAAIiT,EAAM9C,EAAOnQ,GACRO,EAAI,EAAGA,EAAIqH,KAAKiJ,MAAOtQ,IACb,IAAX0S,EAAI1S,KACJ4S,GAAW,GAKvB,OAAOA,GAAWvL,KAAK0I,OAAS1I,KAAKiJ,QAKzC3J,EAAQsB,UAAU4K,cAAgB,WAE9B,IAAI7C,EAAgBxB,KAAKyB,MAAM,EAAI5I,KAAKF,WACpC6H,EAAa3H,KAAKuI,OAAOvI,KAAKkJ,cAAclJ,KAAKwI,WACjDiD,EACAtE,KAAKyB,OAAOG,OAAOC,QAAUrB,GAAcgB,GAAiBA,EAEhE3I,KAAK2H,WAAa8D,EACQ,IAAtBzL,KAAKoJ,eACLpJ,KAAK0L,iBAAmBD,IAMhCnM,EAAQsB,UAAUwJ,YAAc,WAC5B,IAAIzC,GAA4C,IAA9B3H,KAAK3G,MAAMiH,eACzBN,KAAK2H,YAAc3H,KAAK2H,WAE5B,OAA+C,EAA3C3H,KAAK3G,MAAMkM,8BACJ,IAAImC,EAAa1H,KAAM2H,EAAY3H,KAAK0F,SAAU1F,KAAK4H,QAAS5H,KAAK6H,wBAErE,IAAIJ,EAASzH,KAAM2H,EAAY3H,KAAK0F,SAAU1F,KAAK4H,WAIhE,CAAC+D,oBAAoB,EAAE1D,gBAAgB,IAAI2D,GAAG,CAAC,SAASzT,EAAQD,EAAOD,GAEzE,IAAIqH,EAAUnH,EAAQ,gBAEtBmH,EAAQsB,UAAUyJ,KAAO,WACrB,IAAIA,EAAO,IAAI/K,EAAQU,KAAKF,WAE5BuK,EAAKpB,MAAQjJ,KAAKiJ,MAClBoB,EAAK3B,OAAS1I,KAAK0I,OAEnB2B,EAAKV,MAAQ3J,KAAK2J,MAClBU,EAAKhR,MAAQ2G,KAAK3G,MAIlBgR,EAAK7P,UAAYwF,KAAKxF,UACtB6P,EAAK9I,kBAAoBvB,KAAKuB,kBAC9B8I,EAAKlB,iBAAmBnJ,KAAKmJ,iBAC7BkB,EAAKlJ,iBAAmBnB,KAAKmB,iBAG7BkJ,EAAK/B,cAAgBtI,KAAKsI,cAAc9M,QACxC6O,EAAKhB,cAAgBrJ,KAAKqJ,cAAc7N,QAExC6O,EAAKf,cAAgBtJ,KAAKsJ,cAAc9N,QACxC6O,EAAKd,cAAgBvJ,KAAKuJ,cAAc/N,QAExC6O,EAAKrJ,iBAAmBhB,KAAKgB,iBAAiBxF,QAG9C,IADA,IAAIqQ,EAAyB,GACrBrT,EAAI,EAAGA,EAAIwH,KAAKwJ,mBAAmBtQ,OAAQV,IAC/CqT,EAAuBrT,GAAKwH,KAAKwJ,mBAAmBhR,GAAG6R,OAE3DA,EAAKb,mBAAqBqC,EAK1B,IAFA,IAAItD,EAASvI,KAAKuI,OACduD,EAAa,IAAI9B,MAAMhK,KAAK0I,QACvBtQ,EAAI,EAAGA,EAAI4H,KAAK0I,OAAQtQ,IAC7B0T,EAAW1T,GAAKmQ,EAAOnQ,GAAGoD,QAK9B,OAFA6O,EAAK9B,OAASuD,EAEPzB,GAGX/K,EAAQsB,UAAU+E,KAAO,WACrB3F,KAAK0J,WAAa1J,KAAKqK,QAG3B/K,EAAQsB,UAAUgF,QAAU,WACxB,GAAwB,OAApB5F,KAAK0J,WAAT,CAIA,IAeItR,EAAGO,EAfHgN,EAAO3F,KAAK0J,WACZqC,EAAcpG,EAAK4C,OAevB,IAdAvI,KAAK2J,MAAQhE,EAAKgE,MAClB3J,KAAK3G,MAAQsM,EAAKtM,MAGlB2G,KAAKxF,UAAYmL,EAAKnL,UACtBwF,KAAKuB,kBAAoBoE,EAAKpE,kBAC9BvB,KAAKmJ,iBAAmBxD,EAAKwD,iBAC7BnJ,KAAKmB,iBAAmBwE,EAAKxE,iBAE7BnB,KAAKiJ,MAAQtD,EAAKsD,MAClBjJ,KAAK0I,OAAS/C,EAAK+C,OAIdtQ,EAAI,EAAGA,EAAI4H,KAAK0I,OAAQtQ,GAAK,EAAG,CACjC,IAAI4T,EAAWD,EAAY3T,GACvBiT,EAAMrL,KAAKuI,OAAOnQ,GACtB,IAAKO,EAAI,EAAGA,EAAIqH,KAAKiJ,MAAOtQ,GAAK,EAC7B0S,EAAI1S,GAAKqT,EAASrT,GAK1B,IAAIsT,EAAoBtG,EAAK2C,cAC7B,IAAK3P,EAAI,EAAGA,EAAIqH,KAAK0I,OAAQ/P,GAAK,EAC9BqH,KAAKsI,cAAc3P,GAAKsT,EAAkBtT,GAG9C,KAAOqH,KAAKsI,cAAcpP,OAAS8G,KAAK0I,QACpC1I,KAAKsI,cAAcrH,MAGvB,IAAIiL,EAAuBvG,EAAK0D,cAChC,IAAKjR,EAAI,EAAGA,EAAI4H,KAAKiJ,MAAO7Q,GAAK,EAC7B4H,KAAKqJ,cAAcjR,GAAK8T,EAAqB9T,GAGjD,KAAO4H,KAAKqJ,cAAcnQ,OAAS8G,KAAKiJ,OACpCjJ,KAAKqJ,cAAcpI,MAKvB,IAFA,IAAIkL,EAAYxG,EAAK2D,cACjB8C,EAAYzG,EAAK4D,cACZ1E,EAAI,EAAGA,EAAI7E,KAAK2J,MAAO9E,GAAK,EACjC7E,KAAKsJ,cAAczE,GAAKsH,EAAUtH,GAClC7E,KAAKuJ,cAAc1E,GAAKuH,EAAUvH,GAItC,GAAqC,EAAjCc,EAAK6D,mBAAmBtQ,QAA+C,EAAjC8G,KAAKwJ,mBAAmBtQ,OAAY,CAC1E8G,KAAKwJ,mBAAqB,GAC1BxJ,KAAKqM,6BAA+B,GACpC,IAAI,IAAI7T,EAAI,EAAGA,EAAImN,EAAK6D,mBAAmBtQ,OAAQV,IAAI,CACnD,IAAI8T,EAAwB3G,EAAK6D,mBAAmBhR,GAAG6R,OACvDrK,KAAKwJ,mBAAmBhR,GAAK8T,EAC7BtM,KAAKqM,6BAA6BC,EAAsBnK,UAAYmK,OAK9E,CAACC,eAAe,IAAIC,GAAG,CAAC,SAASrU,EAAQD,EAAOD,GAOlD,IAAIqH,EAAUnH,EAAQ,gBAItB,SAASsU,EAAIC,EAAMrK,EAAUsK,GACzB3M,KAAK0M,KAAOA,EACZ1M,KAAKqC,SAAWA,EAChBrC,KAAK2M,MAAQA,EAKjB,SAASC,EAAOC,EAAmBC,GAC/B9M,KAAK6M,kBAAoBA,EACzB7M,KAAK8M,KAAOA,EAMhB,SAASC,EAAiBlU,EAAG6R,GACzB,OAAOA,EAAEmC,kBAAoBhU,EAAEgU,kBAOnCvN,EAAQsB,UAAUoM,UAAY,SAAUC,GAOpC,GALAjN,KAAK4F,UAEL5F,KAAKkN,kBAAkBD,GACvBjN,KAAKkK,UAEDlK,KAAK3G,MAAMoH,WAEX,IADA,IAAI0M,GAA2B,EACzBA,GAAyB,CAC3B,IAAIC,EAAyBpN,KAAKqN,yBAAwB,GAC1DrN,KAAKsN,eACLtN,KAAKkK,UAMuB,GAAMkD,GAJNpN,KAAKqN,yBAAwB,KAKrDF,GAA2B,KAW3C7N,EAAQsB,UAAUqJ,aAAe,WAC7B,IAAIsD,EAAW,GACXC,EAAa,EACbnJ,EAAYrE,KAAK3G,MAAMgL,UACvBoJ,GAAgB,EAChBC,EAAe,KAUhB1N,KAAK3G,MAAMiL,UAIVoJ,EAAeC,KAAKC,MAAQ5N,KAAK3G,MAAMiL,SAQ3C,IAHA,IAAIuJ,EAAiBC,EAAAA,EACjBC,EAAa,KACbC,EAAoC,GAC/BC,EAAQ,EAAGA,EAAQjO,KAAKwJ,mBAAmBtQ,OAAQ+U,GAAS,EACjED,EAAkC1P,KAAKwP,EAAAA,GAM3C,IACII,EADAC,EAAS,IAAIvB,GAAQkB,EAAAA,EAAU,IAKnC,IAFAP,EAASjP,KAAK6P,GAEW,EAAlBZ,EAASrU,SAAgC,IAAlBuU,GAA0BE,KAAKC,MAAQF,GAiBjE,GAdIQ,EADDlO,KAAK3G,MAAMiH,eACYN,KAAK0L,kBAAoB,EAAIrH,GAE7BrE,KAAK0L,kBAAoB,EAAIrH,GAIvC,EAAZA,GACIwJ,EAAiBK,IACjBT,GAAgB,MAKxBU,EAASZ,EAAStM,OACP4L,kBAAoBgB,GAA/B,CAQA,IAAIf,EAAOqB,EAAOrB,KAIlB,GAHA9M,KAAKgN,UAAUF,GAEfU,KACsB,IAAlBxN,KAAK0F,SAAT,CAIA,IAAIiC,EAAa3H,KAAK2H,WACtB,KAAiBkG,EAAblG,GAAJ,CAMA,GAAIA,IAAekG,EAAe,CAE9B,IADA,IAAIO,GAA2B,EACtB5V,EAAI,EAAGA,EAAIwH,KAAKwJ,mBAAmBtQ,UACpC8G,KAAKwJ,mBAAmBhR,GAAGuR,aAAa,GAAKiE,EAAkCxV,IADnCA,GAAK,EAG9C,GAAIwH,KAAKwJ,mBAAmBhR,GAAGuR,aAAa,GAAKiE,EAAkCxV,GAAI,CAC1F4V,GAA2B,EAC3B,MAIR,GAAIA,EACA,SAKR,IAA0B,IAAtBpO,KAAKqO,aAAuB,CAQ5B,GAHArO,KAAKsO,cAAe,EAGD,IAAfd,EAEA,YADAxN,KAAK6H,uBAAyB2F,GAIlCO,EAAaI,EACbN,EAAiBlG,EACjB,IAAK,IAAI4G,EAAQ,EAAGA,EAAQvO,KAAKwJ,mBAAmBtQ,OAAQqV,GAAS,EACjEP,EAAkCO,GAASvO,KAAKwJ,mBAAmB+E,GAAOxE,aAAa,OAExF,CACgB,IAAfyD,GAGAxN,KAAK2F,OAgDT,IARA,IAAIvD,EAAWpC,KAAKwO,uBAEhBnM,EAAWD,EAASlB,MAEpBuN,EAAW,GACXC,EAAU,GAEVC,EAAQ7B,EAAK5T,OACRP,EAAI,EAAGA,EAAIgW,EAAOhW,GAAK,EAAG,CAC/B,IAAIiW,EAAM9B,EAAKnU,GACXiW,EAAIvM,WAAaA,EACA,QAAbuM,EAAIlC,KACJgC,EAAQpQ,KAAKsQ,GAEbH,EAASnQ,KAAKsQ,IAGlBH,EAASnQ,KAAKsQ,GACdF,EAAQpQ,KAAKsQ,IAIrB,IAAItS,EAAM6K,KAAK0H,KAAKzM,EAASuK,OACzBtQ,EAAM8K,KAAK2H,MAAM1M,EAASuK,OAE1BoC,EAAU,IAAItC,EAAI,MAAOpK,EAAU/F,GACvCmS,EAASnQ,KAAKyQ,GAEd,IAAIC,EAAS,IAAIvC,EAAI,MAAOpK,EAAUhG,GACtCqS,EAAQpQ,KAAK0Q,GAEbzB,EAASjP,KAAK,IAAIsO,EAAOjF,EAAY8G,IACrClB,EAASjP,KAAK,IAAIsO,EAAOjF,EAAY+G,IAKrCnB,EAAS9C,KAAKsC,MAKH,OAAfgB,GAEA/N,KAAKgN,UAAUe,EAAWjB,MAE9B9M,KAAK6H,uBAAyB2F,IAGhC,CAACjB,eAAe,IAAI0C,GAAG,CAAC,SAAS9W,EAAQD,EAAOD,GAElD,IAAIqH,EAAUnH,EAAQ,gBAEtB,SAAS+W,EAAahO,EAAOyL,GACzB3M,KAAKkB,MAAQA,EACblB,KAAK2M,MAAQA,EAKjBrN,EAAQsB,UAAU4N,qBAAuB,WAQrC,IAPA,IAAIW,EAAkB,EAClBC,EAAmB,KACnBC,EAAmB,KAGnBnP,EAAmBF,KAAK3G,MAAM6G,iBAC9BoP,EAAepP,EAAiBhH,OAC3B2L,EAAI,EAAGA,EAAIyK,EAAczK,IAAK,CACnC,IAAIxC,EAAWnC,EAAiB2E,GAAG3D,MAC/BqO,EAASvP,KAAKsJ,cAAcjH,GAChC,IAAgB,IAAZkN,EAAJ,CAIA,IAAIzG,EAAW9I,KAAKuI,OAAOgH,GAAQvP,KAAKwI,WACpCgH,EAAWrI,KAAKsI,IAAI3G,EAAW3B,KAAKyB,MAAME,IAC1CqG,EAAkBK,IAClBL,EAAkBK,EAClBJ,EAAmB/M,EACnBgN,EAAmBvG,IAI3B,OAAO,IAAIoG,EAAaE,EAAkBC,IAK9C/P,EAAQsB,UAAU8O,+BAAiC,WAO/C,IANA,IAAIC,EAAc7B,EAAAA,EACdsB,EAAmB,KACnBC,EAAmB,KAEnBnP,EAAmBF,KAAK3G,MAAM6G,iBAC9BoP,EAAepP,EAAiBhH,OAC3B2L,EAAI,EAAGA,EAAIyK,EAAczK,IAAK,CACnC,IAAIzC,EAAWlC,EAAiB2E,GAC5BxC,EAAWD,EAASlB,MACpBqO,EAASvP,KAAKsJ,cAAcjH,GAChC,IAAgB,IAAZkN,EAAJ,CAMA,IAAIzG,EAAW9I,KAAKuI,OAAOgH,GAAQvP,KAAKwI,WACxC,GAAIrB,KAAKsI,IAAI3G,EAAW3B,KAAKyB,MAAME,IAAa9I,KAAKF,UAAW,CAC5D,IAAIiC,EAAOK,EAASL,KACFA,EAAd4N,IACAA,EAAc5N,EACdqN,EAAmB/M,EACnBgN,EAAmBvG,KAK/B,OAAO,IAAIoG,EAAaE,EAAkBC,KAG5C,CAAC9C,eAAe,IAAIqD,GAAG,CAAC,SAASzX,EAAQD,EAAOD,GAElD,IAAIqH,EAAUnH,EAAQ,gBAClB0X,EAAgB1X,EAAQ,qBAAqB0X,cAEjDvQ,EAAQsB,UAAUsM,kBAAoB,SAAU4C,GAO5C,IANA,IAgBInX,EAhBAoX,EAAkBD,EAAe5W,OAEjCwP,EAAS1I,KAAK0I,OACdsH,EAAiBtH,EAASqH,EAGrBE,EAAIvH,EAAQuH,EAAID,EAAgBC,GAAK,OACnBhM,IAAnBjE,KAAKuI,OAAO0H,KACZjQ,KAAKuI,OAAO0H,GAAKjQ,KAAKuI,OAAO0H,EAAI,GAAGzU,SAK5CwE,KAAK0I,OAASsH,EACdhQ,KAAK2J,MAAQ3J,KAAKiJ,MAAQjJ,KAAK0I,OAAS,EAIxC,IADA,IAAIwH,EAAalQ,KAAKiJ,MAAQ,EACrBxQ,EAAI,EAAGA,EAAIsX,EAAiBtX,GAAK,EAAG,CACzC,IAAImW,EAAMkB,EAAerX,GAGrBL,EAAIsQ,EAASjQ,EAEb0X,EAAqB,QAAbvB,EAAIlC,MAAmB,EAAI,EAGnCrK,EAAWuM,EAAIvM,SACf+N,EAAcpQ,KAAKsJ,cAAcjH,GACjCgO,EAAgBrQ,KAAKuI,OAAOnQ,GAChC,IAAqB,IAAjBgY,EAAoB,CAGpB,IADAC,EAAcrQ,KAAKwI,WAAa2H,EAAOvB,EAAIjC,MACtChU,EAAI,EAAGA,GAAKuX,EAAYvX,GAAK,EAC9B0X,EAAc1X,GAAK,EAEvB0X,EAAcrQ,KAAKuJ,cAAclH,IAAa8N,MAC3C,CAEH,IAAIZ,EAASvP,KAAKuI,OAAO6H,GACrBtH,EAAWyG,EAAOvP,KAAKwI,WAE3B,IADA6H,EAAcrQ,KAAKwI,WAAa2H,GAAQvB,EAAIjC,MAAQ7D,GAC/CnQ,EAAI,EAAGA,GAAKuX,EAAYvX,GAAK,EAC9B0X,EAAc1X,IAAMwX,EAAOZ,EAAO5W,GAK1C,IAAI2X,EAAgBtQ,KAAK0B,qBACzB1B,KAAKsI,cAAclQ,GAAKkY,EACxBtQ,KAAKsJ,cAAcgH,GAAiBlY,EACpC4H,KAAKuJ,cAAc+G,IAAkB,EACrCtQ,KAAKuB,kBAAkB+O,GAAiB,IAAIT,EAAc,IAAIS,EAAeA,GAC7EtQ,KAAK2J,OAAS,IAItBrK,EAAQsB,UAAU2P,qBAAuB,SAASvF,GAEjD,GAAGA,IAAahL,KAAKkJ,aAEpB,OAAO,EAGIlJ,KAAK3G,MAAjB,IACIkP,EAASvI,KAAKuI,OAGlB,IADavI,KAAKuB,kBAAkBvB,KAAKsI,cAAc0C,IAC3C/I,UACX,OAAO,EAGR,IAAI3G,EAAIiN,EAAOyC,GAAUhL,KAAKwI,WAC1BgI,EAASlV,EAAI6L,KAAK2H,MAAMxT,GAE5B,GAAIkV,EAASxQ,KAAKF,WAAa,EAAIE,KAAKF,UAAY0Q,EACnD,OAAO,EAIR,IAAIpY,EAAI4H,KAAK0I,OACbH,EAAOnQ,GAAKmQ,EAAOnQ,EAAI,GAAGoD,QAC1BwE,KAAK0I,QAAU,EAGf1I,KAAK2J,OAAS,EACd,IAAI2G,EAAgBtQ,KAAK0B,qBACzB1B,KAAKsI,cAAclQ,GAAKkY,EACxBtQ,KAAKsJ,cAAcgH,GAAiBlY,EACpC4H,KAAKuJ,cAAc+G,IAAkB,EACrCtQ,KAAKuB,kBAAkB+O,GAAiB,IAAIT,EAAc,IAAIS,EAAeA,GAE7E/H,EAAOnQ,GAAG4H,KAAKwI,WAAarB,KAAK2H,MAAMxT,GAEvC,IAAK,IAAImV,EAAW,EAAGA,EAAWzQ,KAAKqJ,cAAcnQ,OAAQuX,GAAY,EAAG,CAG3E,GAFezQ,KAAKuB,kBAAkBvB,KAAKqJ,cAAcoH,IAE3CxO,UAEP,CACN,IAAIyO,EAAOnI,EAAOyC,GAAUyF,GACxBE,EAAYxJ,KAAK2H,MAAM4B,GAAMvJ,KAAK9K,IAAI,EAAGqU,EAAOvJ,KAAK2H,MAAM4B,GAAQF,IAAW,EAAIA,GACtFjI,EAAOnQ,GAAGqY,GAAYE,OAJtBpI,EAAOnQ,GAAGqY,GAAYtJ,KAAK7K,IAAI,EAAGiM,EAAOyC,GAAUyF,IAAa,EAAID,IAQtE,IAAI,IAAI7X,EAAI,EAAGA,EAAIqH,KAAKiJ,MAAOtQ,GAAK,EACnC4P,EAAOnQ,GAAGO,IAAM4P,EAAOyC,GAAUrS,GAGlC,OAAO,GAGR2G,EAAQsB,UAAUgQ,qBAAuB,SAAS5F,GAEjD,GAAIA,IAAahL,KAAKkJ,aAErB,OAAO,EAGIlJ,KAAK3G,MAAjB,IACIkP,EAASvI,KAAKuI,OAGlB,IADavI,KAAKuB,kBAAkBvB,KAAKsI,cAAc0C,IAC3C/I,UACX,OAAO,EAGR,IAAIyI,EAAInC,EAAOyC,GAAUhL,KAAKwI,WAC1B9P,EAAIgS,EAAIvD,KAAK2H,MAAMpE,GAEvB,GAAIhS,EAAIsH,KAAKF,WAAa,EAAIE,KAAKF,UAAYpH,EAC9C,OAAO,EAIR,IAAIN,EAAI4H,KAAK0I,OACbH,EAAOnQ,GAAKmQ,EAAOnQ,EAAI,GAAGoD,QAC1BwE,KAAK0I,QAAU,EAIf1I,KAAK2J,OAAS,EACd,IAAI2G,EAAgBtQ,KAAK0B,qBACzB1B,KAAKsI,cAAclQ,GAAKkY,EACxBtQ,KAAKsJ,cAAcgH,GAAiBlY,EACpC4H,KAAKuJ,cAAc+G,IAAkB,EACrCtQ,KAAKuB,kBAAkB+O,GAAiB,IAAIT,EAAc,IAAIS,EAAeA,GAE7E/H,EAAOnQ,GAAG4H,KAAKwI,YAAc9P,EAG7B,IAAI,IAAI+X,EAAW,EAAGA,EAAWzQ,KAAKqJ,cAAcnQ,OAAQuX,GAAY,EAAG,CAC1E,IAAIrO,EAAWpC,KAAKuB,kBAAkBvB,KAAKqJ,cAAcoH,IAErDI,EAAKtI,EAAOyC,GAAUyF,GACtBK,EAAKD,EAAK1J,KAAK2H,MAAM+B,GAEtBzO,EAASH,UAEVsG,EAAOnQ,GAAGqY,GADRK,GAAMpY,GACeoY,IAEC,EAAIA,GAAMpY,EAAIoY,EAItCvI,EAAOnQ,GAAGqY,GADD,GAANI,GACoBA,EAEDA,EAAKnY,GAAK,EAAIA,GAKvC,OAAO,GAUR4G,EAAQsB,UAAU0M,aAAe,cAe/B,CAACyD,oBAAoB,GAAGxE,eAAe,IAAIyE,GAAG,CAAC,SAAS7Y,EAAQD,EAAOD,GAGzE,IAAIqH,EAAUnH,EAAQ,gBAItBmH,EAAQsB,UAAUqQ,WAAa,SAAU5O,GAErC,IAAIjK,EAAI4H,KAAKsJ,cAAcjH,GAC3B,IAAW,IAAPjK,EAAU,CAOV,IAJA,IAAIO,EAAIqH,KAAKuJ,cAAclH,GAIlB6O,EAAK,EAAGA,EAAKlR,KAAK0I,OAAQwI,GAAM,EAAG,CACxC,IAAI9L,EAAcpF,KAAKuI,OAAO2I,GAAIvY,GAClC,GAAIyM,GAAepF,KAAKF,WAAaE,KAAKF,UAAYsF,EAAa,CAC/DhN,EAAI8Y,EACJ,OAIRlR,KAAKmR,MAAM/Y,EAAGO,GAGlB,OAAOP,GAGXkH,EAAQsB,UAAUwQ,eAAiB,SAAU/O,GAEzC,IAAI1J,EAAIqH,KAAKuJ,cAAclH,GAC3B,IAAW,IAAP1J,EAAU,CAQV,IALA,IAAIP,EAAI4H,KAAKsJ,cAAcjH,GAIvBgP,EAAWrR,KAAKuI,OAAOnQ,GAClBkZ,EAAK,EAAGA,EAAKtR,KAAK0I,OAAQ4I,GAAM,EAAG,CACxC,IAAIlM,EAAciM,EAASC,GAC3B,GAAIlM,GAAepF,KAAKF,WAAaE,KAAKF,UAAYsF,EAAa,CAC/DzM,EAAI2Y,EACJ,OAIRtR,KAAKmR,MAAM/Y,EAAGO,GAGlB,OAAOA,GAGX2G,EAAQsB,UAAUuJ,qBAAuB,WAGrC,IAFA,IAAIR,EAAQ3J,KAAKxF,UAAUtB,OACvByP,EAAgBxB,KAAKyB,MAAM,EAAI5I,KAAKF,WAC/B+E,EAAI,EAAGA,EAAI8E,EAAO9E,GAAK,EAAG,CAC/B,IAAIzC,EAAWpC,KAAKxF,UAAUqK,GAC1BxC,EAAWD,EAASlB,MAEpB9I,EAAI4H,KAAKsJ,cAAcjH,GAC3B,IAAW,IAAPjK,EAEAgK,EAASuK,MAAQ,MACd,CAEH,IAAI7D,EAAW9I,KAAKuI,OAAOnQ,GAAG4H,KAAKwI,WACnCpG,EAASuK,MAAQxF,KAAKyB,OAAOE,EAAWC,OAAOC,SAAWL,GAAiBA,KAKvFrJ,EAAQsB,UAAUqC,oBAAsB,SAAUjI,EAAYkI,GAE1D,IAAIuF,EAAUzI,KAAK0I,OAAS,EACxB2H,EAAgBrQ,KAAKsJ,cAActO,EAAWkG,OAClD,IAAuB,IAAnBmP,EAAsB,CAKtB,IAHA,IAAIkB,EAAcvR,KAAKuJ,cAAcvO,EAAWkG,OAGvC9I,EAAI,EAAGA,GAAKqQ,EAASrQ,GAAK,EAAG,CAClC,IAAIiT,EAAMrL,KAAKuI,OAAOnQ,GACtBiT,EAAIrL,KAAKwI,YAActF,EAAamI,EAAIkG,GAG5C,IAAIC,EAAsBxR,KAAKwJ,mBAAmBtQ,OAClD,GAA0B,EAAtBsY,EACA,IAAK,IAAIhZ,EAAI,EAAGA,EAAIgZ,EAAqBhZ,GAAK,EAAG,CAC7C,IAAIuR,EAAe/J,KAAKwJ,mBAAmBhR,GAAGuR,aAC9CA,EAAa/J,KAAKwI,YAActF,EAAa6G,EAAawH,SAMlEvR,KAAKuI,OAAO8H,GAAerQ,KAAKwI,YAActF,GAItD5D,EAAQsB,UAAUuC,4BAA8B,SAAUnI,EAAYoH,EAAUc,GAE5E,GAAIlI,EAAWkG,QAAUkB,EAASlB,MAC9B,MAAM,IAAIpI,MAAM,kGAGpB,IAAIV,EAAI4H,KAAKiR,WAAWjW,EAAWkG,OAE/BuQ,EAASzR,KAAKuJ,cAAcnH,EAASlB,OACzC,IAAgB,IAAZuQ,EAEA,IADA,IAAIC,EAAS1R,KAAKsJ,cAAclH,EAASlB,OAChCvI,EAAI,EAAGA,EAAIqH,KAAKiJ,MAAOtQ,GAAK,EACjCqH,KAAKuI,OAAOnQ,GAAGO,IAAMuK,EAAalD,KAAKuI,OAAOmJ,GAAQ/Y,QAG1DqH,KAAKuI,OAAOnQ,GAAGqZ,IAAWvO,GAIlC5D,EAAQsB,UAAUyC,WAAa,SAAUjB,EAAUc,GAE/C,IAAIb,EAAWD,EAASlB,MACpBgP,EAAalQ,KAAKiJ,MAAQ,EAC1B0I,EAAY3R,KAAKuJ,cAAclH,GACnC,IAAmB,IAAfsP,EAAkB,CAElB,IAEIhZ,EAFAiZ,EAAc5R,KAAKuI,OAAOvI,KAAKsJ,cAAcjH,IAGjD,GAA0B,IAAtBD,EAASD,SAAgB,CACzB,IAAI2I,EAAU9K,KAAKuI,OAAO,GAG1B,IAAK5P,EAAI,EAAGA,GAAKuX,EAAYvX,GAAK,EAC9BmS,EAAQnS,IAAMuK,EAAa0O,EAAYjZ,OAExC,CACH,IAAIoR,EAAe/J,KAAKyJ,qBAAqBrH,EAASD,UAAU4H,aAChE,IAAKpR,EAAI,EAAGA,GAAKuX,EAAYvX,GAAK,EAC9BoR,EAAapR,IAAMuK,EAAa0O,EAAYjZ,SAMpDqH,KAAKuI,OAAO,GAAGoJ,IAAczO,GAIrC5D,EAAQsB,UAAUY,cAAgB,SAAUxG,GAExC,IAAImV,EAAOnV,EAAWsQ,aAAe,GAAK,EACtC7C,EAAUzI,KAAK0I,OAEf2H,EAAgBrQ,KAAKuI,OAAOE,QACVxE,IAAlBoM,IACAA,EAAgBrQ,KAAKuI,OAAO,GAAG/M,QAC/BwE,KAAKuI,OAAOE,GAAW4H,GAK3B,IADA,IAAIH,EAAalQ,KAAKiJ,MAAQ,EACrBtQ,EAAI,EAAGA,GAAKuX,EAAYvX,GAAK,EAClC0X,EAAc1X,GAAK,EAIvB0X,EAAcrQ,KAAKwI,WAAa2H,EAAOnV,EAAWC,IAIlD,IAFA,IAAIkQ,EAAQnQ,EAAWmQ,MACnBC,EAASD,EAAMjS,OACVX,EAAI,EAAGA,EAAI6S,EAAQ7S,GAAK,EAAG,CAChC,IAAI0S,EAAOE,EAAM5S,GACb6M,EAAc6F,EAAK7F,YACnB/C,EAAW4I,EAAK7I,SAASlB,MAEzBkP,EAAcpQ,KAAKsJ,cAAcjH,GACrC,IAAqB,IAAjB+N,EAEAC,EAAcrQ,KAAKuJ,cAAclH,KAAc8N,EAAO/K,MACnD,CAEH,IAAImK,EAASvP,KAAKuI,OAAO6H,GACVb,EAAOvP,KAAKwI,WAC3B,IAAK7P,EAAI,EAAGA,GAAKuX,EAAYvX,GAAK,EAC9B0X,EAAc1X,IAAMwX,EAAO/K,EAAcmK,EAAO5W,IAK5D,IAAIkZ,EAAa7W,EAAWkG,MAC5BlB,KAAKsI,cAAcG,GAAWoJ,EAC9B7R,KAAKsJ,cAAcuI,GAAcpJ,EACjCzI,KAAKuJ,cAAcsI,IAAe,EAElC7R,KAAK0I,QAAU,GAGnBpJ,EAAQsB,UAAU6B,iBAAmB,SAAUzH,GAC3C,IAAI6W,EAAa7W,EAAWkG,MACxBuH,EAAUzI,KAAK0I,OAAS,EAGxBtQ,EAAI4H,KAAKiR,WAAWY,GAKpBjH,EAAS5K,KAAKuI,OAAOE,GACzBzI,KAAKuI,OAAOE,GAAWzI,KAAKuI,OAAOnQ,GACnC4H,KAAKuI,OAAOnQ,GAAKwS,EAGjB5K,KAAKsI,cAAclQ,GAAK4H,KAAKsI,cAAcG,GAC3CzI,KAAKsI,cAAcG,IAAY,EAC/BzI,KAAKsJ,cAAcuI,IAAe,EAGlC7R,KAAKgB,iBAAiBhB,KAAKgB,iBAAiB9H,QAAU2Y,EAEtD7W,EAAWsG,MAAMJ,OAAS,EAE1BlB,KAAK0I,QAAU,GAGnBpJ,EAAQsB,UAAUkB,YAAc,SAAUM,GAItC,IAAIqG,EAAUzI,KAAK0I,OAAS,EACxBwH,EAAalQ,KAAKiJ,MAClBlH,GAAqC,IAA9B/B,KAAK3G,MAAMiH,gBAA2B8B,EAASL,KAAOK,EAASL,KACtEI,EAAWC,EAASD,SAGpBqP,EAAsBxR,KAAKwJ,mBAAmBtQ,OAClD,GAA0B,EAAtBsY,EACA,IAAK,IAAIhZ,EAAI,EAAGA,EAAIgZ,EAAqBhZ,GAAK,EAC1CwH,KAAKwJ,mBAAmBhR,GAAGuR,aAAamG,GAAc,EAI7C,IAAb/N,EACAnC,KAAKuI,OAAO,GAAG2H,GAAcnO,GAE7B/B,KAAKsK,qBAAqBnI,EAAU+N,EAAYnO,GAChD/B,KAAKuI,OAAO,GAAG2H,GAAc,GAIjC,IAAK,IAAI9X,EAAI,EAAGA,GAAKqQ,EAASrQ,GAAK,EAC/B4H,KAAKuI,OAAOnQ,GAAG8X,GAAc,EAIjC,IAAI7N,EAAWD,EAASlB,MACxBlB,KAAKqJ,cAAc6G,GAAc7N,EAEjCrC,KAAKsJ,cAAcjH,IAAa,EAChCrC,KAAKuJ,cAAclH,GAAY6N,EAE/BlQ,KAAKiJ,OAAS,GAIlB3J,EAAQsB,UAAU+B,eAAiB,SAAUP,GACzC,IAAIC,EAAWD,EAASlB,MAGpBvI,EAAIqH,KAAKoR,eAAe/O,GACxB6N,EAAalQ,KAAKiJ,MAAQ,EAC9B,GAAItQ,IAAMuX,EAAY,CAElB,IADA,IAAIzH,EAAUzI,KAAK0I,OAAS,EACnBtQ,EAAI,EAAGA,GAAKqQ,EAASrQ,GAAK,EAAG,CAClC,IAAIiT,EAAMrL,KAAKuI,OAAOnQ,GACtBiT,EAAI1S,GAAK0S,EAAI6E,GAGjB,IAAIsB,EAAsBxR,KAAKwJ,mBAAmBtQ,OAClD,GAA0B,EAAtBsY,EACA,IAAK,IAAIhZ,EAAI,EAAGA,EAAIgZ,EAAqBhZ,GAAK,EAAG,CAC7C,IAAIuR,EAAe/J,KAAKwJ,mBAAmBhR,GAAGuR,aAC9CA,EAAapR,GAAKoR,EAAamG,GAIvC,IAAI4B,EAAiB9R,KAAKqJ,cAAc6G,GACxClQ,KAAKqJ,cAAc1Q,GAAKmZ,EACxB9R,KAAKuJ,cAAcuI,GAAkBnZ,EAIzCqH,KAAKqJ,cAAc6G,IAAe,EAClClQ,KAAKuJ,cAAclH,IAAa,EAGhCrC,KAAKgB,iBAAiBhB,KAAKgB,iBAAiB9H,QAAUmJ,EAEtDD,EAASlB,OAAS,EAElBlB,KAAKiJ,OAAS,IAGhB,CAACsD,eAAe,IAAIwF,GAAG,CAAC,SAAS5Z,EAAQD,EAAOD,GAGlDE,EAAQ,gBACRA,EAAQ,0BACRA,EAAQ,4BACRA,EAAQ,YACRA,EAAQ,eACRA,EAAQ,4BACRA,EAAQ,0BAERD,EAAOD,QAAUE,EAAQ,iBAEvB,CAACoU,eAAe,EAAEyF,cAAc,GAAGC,2BAA2B,GAAGC,yBAAyB,GAAGC,2BAA2B,GAAGC,yBAAyB,GAAGC,WAAW,GAAGC,eAAe,KAAKC,GAAG,CAAC,SAASpa,EAAQD,EAAOD,GAEvN,IAAIqH,EAAUnH,EAAQ,gBAEtBmH,EAAQsB,UAAU4R,mBAAqB,WAEnC,IADA,IAAIC,EAAQ,EACHra,EAAI,EAAGA,EAAI4H,KAAK0I,OAAQtQ,GAAK,EAClC,GAAI4H,KAAKuB,kBAAkBvB,KAAKsI,cAAclQ,IAAI6J,UAAW,CACzD,IAAIyQ,EAAc1S,KAAKuI,OAAOnQ,GAAG4H,KAAKwI,YACtCkK,GAA4BvL,KAAK2H,MAAM4D,IACrB1S,KAAKF,YAAc4S,EAAc1S,KAAKF,YACpD2S,GAAS,GAKrB,OAAOA,GAKXnT,EAAQsB,UAAUyN,WAAa,WAG3B,IAFA,IAAInO,EAAmBF,KAAK3G,MAAM6G,iBAC9BoP,EAAepP,EAAiBhH,OAC3B2L,EAAI,EAAGA,EAAIyK,EAAczK,IAAK,CACnC,IAAI0K,EAASvP,KAAKsJ,cAAcpJ,EAAiB2E,GAAG3D,OACpD,IAAgB,IAAZqO,EAAJ,CAIA,IAAIzG,EAAW9I,KAAKuI,OAAOgH,GAAQvP,KAAKwI,WACxC,GAAIrB,KAAKsI,IAAI3G,EAAW3B,KAAKyB,MAAME,IAAa9I,KAAKF,UACjD,OAAO,GAGf,OAAO,GAIXR,EAAQsB,UAAUyM,wBAA0B,SAASsF,GAyBjD,IAxBA,IAAIC,GAAU,EAwBLxa,EAAI,EAAGA,EAAI4H,KAAK0I,OAAQtQ,GAAK,EAClC,GAAI4H,KAAKuB,kBAAkBvB,KAAKsI,cAAclQ,IAAI6J,UAAW,CACzD,IAAIhH,EAAM+E,KAAKuI,OAAOnQ,GAAG4H,KAAKwI,WAG9B,GAFAvN,EAAMkM,KAAKsI,IAAIxU,GACGkM,KAAK7K,IAAIrB,EAAMkM,KAAK2H,MAAM7T,GAAMkM,KAAK2H,MAAM7T,EAAM,IACjD+E,KAAKF,WACnB,IAAK6S,EACD,OAAO,OAGK,IAAZC,EACAA,EAAS3X,EAET2X,GAAU3X,EAM1B,OAAgB,IAAZ2X,EACO,EAEJA,IAGT,CAACrG,eAAe,IAAIsG,GAAG,CAAC,SAAS1a,EAAQD,EAAOD,GAGpCE,EAAQ,gBAOdyI,UAAUoF,IAAM,SAAUC,EAAS6M,GAKvClQ,QAAQoD,IAAI,OAAQC,EAAS,QAC7BrD,QAAQoD,IAAI,eAAgBhG,KAAKiJ,MAAQ,GACzCrG,QAAQoD,IAAI,iBAAkBhG,KAAK0I,OAAS,GAE5C9F,QAAQoD,IAAI,gBAAiBhG,KAAKsI,eAClC1F,QAAQoD,IAAI,oBAAqBhG,KAAKqJ,eACtCzG,QAAQoD,IAAI,OAAQhG,KAAKsJ,eACzB1G,QAAQoD,IAAI,OAAQhG,KAAKuJ,eAEzB,IAKIjD,EACA3N,EAEAP,EACAgK,EACAC,EACA0Q,EACAC,EAEAC,EACAC,EAEA7H,EACA8H,EAfAC,EAAmB,GACnBC,EAAiB,CAAC,KAgBtB,IAAK1a,EAAI,EAAGA,EAAIqH,KAAKiJ,MAAOtQ,GAAK,EAC7B0J,EAAWrC,KAAKqJ,cAAc1Q,GAQ9Bqa,GALID,OADa9O,KADjB7B,EAAWpC,KAAKuB,kBAAkBc,IAEpB,IAAMA,EAEND,EAASJ,IAGC9I,OACdiO,KAAKsI,IAAIuD,EAAgB,GACnCC,EAAa,IACbC,EAAY,KAeQ,EAAhBF,EACAC,GAAc,IAEdC,GAAa,KAGjBG,EAAe1a,GAAKsa,EAEpBG,GAAoBF,EAAYH,EAEpCnQ,QAAQoD,IAAIoN,GAKZ,IAAIE,EAAWtT,KAAKuI,OAAOvI,KAAKkJ,cAC5BqK,EAAiB,KAerB,IAAKjN,EAAI,EAAGA,EAAItG,KAAKiJ,MAAO3C,GAAK,EAE7BiN,GADY,KAEZA,GAAkBF,EAAe/M,GACjCiN,GAAkBD,EAAShN,GAAGkN,QAlFb,GA2FrB,IANAD,GADY,KACkBF,EAAe,GACzCC,EAAS,GAAGE,QAtFK,GAuFrB5Q,QAAQoD,IAAIuN,EAAiB,OAIxBnb,EAAI,EAAGA,EAAI4H,KAAK0I,OAAQtQ,GAAK,EAAG,CAajC,IAZAiT,EAAMrL,KAAKuI,OAAOnQ,GAClB+a,EAAY,KAWPxa,EAAI,EAAGA,EAAIqH,KAAKiJ,MAAOtQ,GAAK,EAE7Bwa,GADY,KACaE,EAAe1a,GAAK0S,EAAI1S,GAAG6a,QA1GvC,GA6GjBL,GADY,KACaE,EAAe,GAAKhI,EAAI,GAAGmI,QA7GnC,GAgHjBnR,EAAWrC,KAAKsI,cAAclQ,GAG1B2a,OADa9O,KADjB7B,EAAWpC,KAAKuB,kBAAkBc,IAEpB,IAAMA,EAEND,EAASJ,GAEvBY,QAAQoD,IAAImN,EAAY,KAAOJ,GAEnCnQ,QAAQoD,IAAI,IAGZ,IAAIwL,EAAsBxR,KAAKwJ,mBAAmBtQ,OAClD,GAA0B,EAAtBsY,EAAyB,CACzB5O,QAAQoD,IAAI,4BACZ,IAAK,IAAIxN,EAAI,EAAGA,EAAIgZ,EAAqBhZ,GAAK,EAAG,CAC7C,IAAIuR,EAAe/J,KAAKwJ,mBAAmBhR,GAAGuR,aAC1C0J,EAAqB,GACzB,IAAKnN,EAAI,EAAGA,EAAItG,KAAKiJ,MAAO3C,GAAK,EAE7BmN,GADY1J,EAAazD,GAAK,EAAI,GAAK,IAEvCmN,GAAsBJ,EAAe/M,GACrCmN,GAAsB1J,EAAazD,GAAGkN,QAtI7B,GAyIbC,IADY1J,EAAa,GAAK,EAAI,GAAK,KACLsJ,EAAe,GAC7CtJ,EAAa,GAAGyJ,QA1IP,GA2Ib5Q,QAAQoD,IAAIyN,EAAqB,KAAOjb,IAMhD,OAHAoK,QAAQoD,IAAI,YAAahG,KAAK0F,UAC9B9C,QAAQoD,IAAI,aAAchG,KAAK2H,YAExB3H,OAGT,CAACuM,eAAe,IAAImH,GAAG,CAAC,SAASvb,EAAQD,EAAOD,GAQlD,IAAIqH,EAAUnH,EAAQ,gBAMtBmH,EAAQsB,UAAUsJ,QAAU,WAcxB,OAZAlK,KAAK4H,SAAU,EAGf5H,KAAK2T,UAGiB,IAAlB3T,KAAK0F,UAGL1F,KAAK4T,SAGF5T,MAUXV,EAAQsB,UAAU+S,OAAS,WAYvB,IAXA,IAAI5N,EAAsB/F,KAAK3G,MAAMqH,eACjCmT,EAAkB,GAElBtL,EAASvI,KAAKuI,OACdC,EAAYxI,KAAKwI,UACjB0H,EAAalQ,KAAKiJ,MAAQ,EAC1BR,EAAUzI,KAAK0I,OAAS,EAGxB8E,EAAa,IAEJ,CAUT,IAFA,IAAIsG,EAAkB,EAClBC,GAAY/T,KAAKF,UACZ1H,EAAI,EAAGA,GAAKqQ,EAASrQ,IAAK,EACiC,IAAjD4H,KAAKmJ,iBAAiBnJ,KAAKsI,cAAclQ,IASxD,IAAIuU,EAAQpE,EAAOnQ,GAAGoQ,GAClBmE,EAAQoH,IACRA,EAAWpH,EACXmH,EAAkB1b,GAK1B,GAAwB,IAApB0b,EAGA,OADA9T,KAAK0F,UAAW,EACT8H,EAcX,IAJA,IAAIwG,EAAiB,EACjBC,GAAenG,EAAAA,EACfhD,EAAUvC,EAAO,GACjB2L,EAAa3L,EAAOuL,GACfnb,EAAI,EAAGA,GAAKuX,EAAYvX,IAAK,CAClC,IAAIyM,EAAc8O,EAAWvb,GAU7B,IADgE,IAAjDqH,KAAKmJ,iBAAiBnJ,KAAKqJ,cAAc1Q,KACpCyM,GAAepF,KAAKF,UAAW,CAC/C,IAAIqU,GAAYrJ,EAAQnS,GAAKyM,EACzB6O,EAAcE,IACdF,EAAcE,EACdH,EAAiBrb,IAK7B,GAAuB,IAAnBqb,EAGA,OADAhU,KAAK0F,UAAW,EACT8H,EAGX,GAAGzH,EAAoB,CACnB8N,EAAgBvV,KAAK,CAAC0B,KAAKsI,cAAcwL,GAAkB9T,KAAKqJ,cAAc2K,KAE9E,IAAII,EAAYpU,KAAKU,eAAemT,GACpC,GAAsB,EAAnBO,EAAUlb,OAOT,OALA8G,KAAK3G,MAAMsH,SAASrC,KAAK,oBACzB0B,KAAK3G,MAAMsH,SAASrC,KAAK,UAAW8V,EAAU,IAC9CpU,KAAK3G,MAAMsH,SAASrC,KAAK,WAAY8V,EAAU,IAE/CpU,KAAK0F,UAAW,EACT8H,EAKfxN,KAAKmR,MAAM2C,EAAiBE,GAC5BxG,GAAc,IAStBlO,EAAQsB,UAAUgT,OAAS,WAgBvB,IAfA,IAaIS,EAAapY,EAbb8J,EAAsB/F,KAAK3G,MAAMqH,eACjCmT,EAAkB,GAElBtL,EAASvI,KAAKuI,OACdC,EAAYxI,KAAKwI,UACjB0H,EAAalQ,KAAKiJ,MAAQ,EAC1BR,EAAUzI,KAAK0I,OAAS,EAExB5I,EAAYE,KAAKF,UACjB0R,EAAsBxR,KAAKwJ,mBAAmBtQ,OAC9Cob,EAAuB,KAEvB9G,EAAa,IAGJ,CACT,IAAI1C,EAAUvC,EAAOvI,KAAKkJ,cAGA,EAAtBsI,IACA8C,EAAuB,IAM3B,IAHA,IAAIN,EAAiB,EACjBO,EAAgBzU,EAChB0U,GAAwB,EACnB7b,EAAI,EAAGA,GAAKuX,EAAYvX,IAC7B0b,EAAcvJ,EAAQnS,GACtBsD,GAAgE,IAAjD+D,KAAKmJ,iBAAiBnJ,KAAKqJ,cAAc1Q,IAE9B,EAAtB6Y,IAA4B1R,EAAYuU,GAAeA,EAAcvU,EACrEwU,EAAqBhW,KAAK3F,GAI1BsD,GAAgBoY,EAAc,EACXE,GAAdF,IACDE,GAAiBF,EACjBL,EAAiBrb,EACjB6b,GAAwB,GAKdD,EAAdF,IACAE,EAAgBF,EAChBL,EAAiBrb,EACjB6b,GAAwB,GAIhC,GAA0B,EAAtBhD,EAGA,IADA,IAAIhZ,EAAI,EACkB,IAAnBwb,GAAsD,EAA9BM,EAAqBpb,QAAcV,EAAIgZ,GAAqB,CACvF,IAAIiD,EAAwB,GACxB1K,EAAe/J,KAAKwJ,mBAAmBhR,GAAGuR,aAE9CwK,EAAgBzU,EAEhB,IAAK,IAAIrH,EAAI,EAAGA,EAAI6b,EAAqBpb,OAAQT,IAG7C4b,EAActK,EAFdpR,EAAI2b,EAAqB7b,IAGzBwD,GAAgE,IAAjD+D,KAAKmJ,iBAAiBnJ,KAAKqJ,cAAc1Q,KAEnDmH,EAAYuU,GAAeA,EAAcvU,EAC1C2U,EAAsBnW,KAAK3F,GAI3BsD,GAAgBoY,EAAc,EACXE,GAAdF,IACDE,GAAiBF,EACjBL,EAAiBrb,EACjB6b,GAAwB,GAKdD,EAAdF,IACAE,EAAgBF,EAChBL,EAAiBrb,EACjB6b,GAAwB,GAGhCF,EAAuBG,EACvBjc,GAAK,EAMb,GAAuB,IAAnBwb,EAGA,OAFAhU,KAAKwL,gBACLxL,KAAKoJ,cAAgB,EACdoE,EASX,IALA,IAAI0G,EAAa,EACbQ,EAAc5G,EAAAA,EAIT1V,GAFW4H,KAAKsI,cAEZ,GAAGlQ,GAAKqQ,EAASrQ,IAAK,CAC/B,IAAIiT,EAAM9C,EAAOnQ,GACb2b,EAAW1I,EAAI7C,GACfmM,EAAWtJ,EAAI2I,GAEnB,MAAKlU,EAAY6U,GAAYA,EAAW7U,GAAxC,CAIA,GAAe,EAAX6U,GAA4BZ,EAAZjU,IAAoCA,EAAZiU,EAAuB,CAC/DW,EAAc,EACdR,EAAa9b,EACb,MAGJ,IAAI+b,EAAWK,GAAyBT,EAAWY,EAAWZ,EAAWY,EAC1D7U,EAAXqU,GAAsCA,EAAdO,IACxBA,EAAcP,EACdD,EAAa9b,IAIrB,GAAIsc,IAAgB5G,EAAAA,EAKhB,OAHA9N,KAAK2H,YAAcmG,EAAAA,EACnB9N,KAAK4H,SAAU,EACf5H,KAAK4J,kBAAoB5J,KAAKqJ,cAAc2K,GACrCxG,EAGX,GAAGzH,EAAoB,CACnB8N,EAAgBvV,KAAK,CAAC0B,KAAKsI,cAAc4L,GAAalU,KAAKqJ,cAAc2K,KAEzE,IAAII,EAAYpU,KAAKU,eAAemT,GACpC,GAAsB,EAAnBO,EAAUlb,OAOT,OALA8G,KAAK3G,MAAMsH,SAASrC,KAAK,oBACzB0B,KAAK3G,MAAMsH,SAASrC,KAAK,UAAW8V,EAAU,IAC9CpU,KAAK3G,MAAMsH,SAASrC,KAAK,WAAY8V,EAAU,IAE/CpU,KAAK0F,UAAW,EACT8H,EAIfxN,KAAKmR,MAAM+C,EAAYF,GAAgB,GACvCxG,GAAc,IAOtB,IAAIoH,EAAiB,GAQrBtV,EAAQsB,UAAUuQ,MAAQ,SAAU0D,EAAeC,GAC/C,IAAIvM,EAASvI,KAAKuI,OAEd4L,EAAW5L,EAAOsM,GAAeC,GAEjCrM,EAAUzI,KAAK0I,OAAS,EACxBwH,EAAalQ,KAAKiJ,MAAQ,EAE1B8L,EAAoB/U,KAAKsI,cAAcuM,GACvCG,EAAqBhV,KAAKqJ,cAAcyL,GAE5C9U,KAAKsI,cAAcuM,GAAiBG,EACpChV,KAAKqJ,cAAcyL,GAAoBC,EAEvC/U,KAAKsJ,cAAc0L,GAAsBH,EACzC7U,KAAKsJ,cAAcyL,IAAsB,EAEzC/U,KAAKuJ,cAAcyL,IAAuB,EAC1ChV,KAAKuJ,cAAcwL,GAAqBD,EAMxC,IAFA,IAiBI1P,EAAa3M,EAAGwc,EAjBhB5D,EAAW9I,EAAOsM,GAClBK,EAAkB,EACbvc,EAAI,EAAGA,GAAKuX,EAAYvX,KACP,OAAhB0Y,EAAS1Y,IAAgB0Y,EAAS1Y,IAAM,MAK1C0Y,EAAS1Y,GAAK,GAJd0Y,EAAS1Y,IAAMwb,EACfS,EAAeM,GAAmBvc,EAClCuc,GAAmB,GAK3B7D,EAASyD,GAAoB,EAAIX,EAOjBnU,KAAKF,UAmBrB,IAnBA,IAmBS1H,EAAI,EAAGA,GAAKqQ,EAASrQ,IAC1B,GAAIA,IAAMyc,MAE+B,OAAhCtM,EAAOnQ,GAAG0c,IAA+BvM,EAAOnQ,GAAG0c,IAAqB,OAAO,CAIhF,IAAIzJ,EAAM9C,EAAOnQ,GAOjB,IAAsB,QAJtBgN,EAAciG,EAAIyJ,KAIa1P,GAAe,MAiBvB,IAAhBA,IACCiG,EAAIyJ,GAAoB,OAlBsB,CAClD,IAAKrc,EAAI,EAAGA,EAAIyc,EAAiBzc,KAKhB,QADbwc,EAAK5D,EAHL1Y,EAAIic,EAAenc,MAIGwc,GAAM,MAGd,IAAPA,IACC5D,EAAS1Y,GAAK,GAHlB0S,EAAI1S,GAAK0S,EAAI1S,GAAKyM,EAAc6P,EAQxC5J,EAAIyJ,IAAqB1P,EAAc+O,GAWvD,IAAI3C,EAAsBxR,KAAKwJ,mBAAmBtQ,OAClD,GAA0B,EAAtBsY,EACA,IAAK,IAAIhZ,EAAI,EAAGA,EAAIgZ,EAAqBhZ,GAAK,EAAG,CAC7C,IAAIuR,EAAe/J,KAAKwJ,mBAAmBhR,GAAGuR,aAE9C,GAAoB,KADpB3E,EAAc2E,EAAa+K,IACJ,CACnB,IAAKrc,EAAI,EAAGA,EAAIyc,EAAiBzc,IAGlB,KADXwc,EAAK5D,EADL1Y,EAAIic,EAAenc,OAGfsR,EAAapR,GAAKoR,EAAapR,GAAKyM,EAAc6P,GAI1DlL,EAAa+K,IAAqB1P,EAAc+O,KAQhE7U,EAAQsB,UAAUF,eAAiB,SAAUyU,GACzC,IAAK,IAAIC,EAAK,EAAGA,EAAKD,EAAWjc,OAAS,EAAGkc,IACzC,IAAK,IAAIC,EAAKD,EAAK,EAAGC,EAAKF,EAAWjc,OAAQmc,IAAM,CAChD,IAAIC,EAAOH,EAAWC,GAClBG,EAAOJ,EAAWE,GACtB,GAAIC,EAAK,KAAOC,EAAK,IAAMD,EAAK,KAAOC,EAAK,GAAI,CAC5C,GAAIF,EAAKD,EAAKD,EAAWjc,OAASmc,EAC9B,MAGJ,IADA,IAAIG,GAAa,EACR/c,EAAI,EAAGA,EAAI4c,EAAKD,EAAI3c,IAAK,CAC9B,IAAIgd,EAAON,EAAWC,EAAG3c,GACrBid,EAAOP,EAAWE,EAAG5c,GACzB,GAAGgd,EAAK,KAAOC,EAAK,IAAMD,EAAK,KAAOC,EAAK,GAAI,CAC3CF,GAAa,EACb,OAGR,GAAIA,EACA,MAAO,CAACJ,EAAIC,EAAKD,IAKjC,MAAO,KAGT,CAAC7I,eAAe,IAAIoJ,GAAG,CAAC,SAASxd,EAAQD,EAAOD,GAelDA,EAAQ2d,yBAA2B,SAASvc,GAMxC,IAAIwc,EACAnZ,EAAGG,EAEP,GAA6B,iBAAnBxD,EAAMiB,SAAsB,CAClC,GAAGjB,EAAMkB,YAAYlB,EAAMiB,UAAU,CAKjC,IAAIoC,KAHJmZ,EAAW1O,KAAKC,SAGP/N,EAAMmB,UAERnB,EAAMmB,UAAUkC,GAAGrD,EAAMiB,YACxBjB,EAAMmB,UAAUkC,GAAGmZ,GAAYxc,EAAMmB,UAAUkC,GAAGrD,EAAMiB,WAQhE,OAFAjB,EAAMkB,YAAYsb,GAAYxc,EAAMkB,YAAYlB,EAAMiB,iBAC/CjB,EAAMkB,YAAYlB,EAAMiB,UACxBjB,EAEP,OAAOA,EAIX,IAAIwD,KAAKxD,EAAMiB,SACX,GAAGjB,EAAMkB,YAAYsC,GAIjB,GAA4B,UAAzBxD,EAAMkB,YAAYsC,UAGVxD,EAAMiB,SAASuC,OAEnB,CAKH,IAAIH,KAHJmZ,EAAW1O,KAAKC,SAGP/N,EAAMmB,UAERnB,EAAMmB,UAAUkC,GAAGG,KAClBxD,EAAMmB,UAAUkC,GAAGmZ,GAAYxc,EAAMmB,UAAUkC,GAAGG,IAK1DxD,EAAMkB,YAAYsb,GAAYxc,EAAMkB,YAAYsC,UACzCxD,EAAMkB,YAAYsC,GAIrC,OAAOxD,IAIb,IAAIyc,GAAG,CAAC,SAAS3d,EAAQD,EAAOD,GAUlC,SAASyH,EAASsC,EAAID,EAAMb,EAAOiB,GAC/BnC,KAAKgC,GAAKA,EACVhC,KAAK+B,KAAOA,EACZ/B,KAAKkB,MAAQA,EACblB,KAAK2M,MAAQ,EACb3M,KAAKmC,SAAWA,EAGpB,SAASxC,EAAgBqC,EAAID,EAAMb,EAAOiB,GACtCzC,EAASzG,KAAK+G,KAAMgC,EAAID,EAAMb,EAAOiB,GAIzC,SAAS0N,EAAc7N,EAAId,GACvBxB,EAASzG,KAAK+G,KAAMgC,EAAI,EAAGd,EAAO,GAMtC,SAAStB,EAAKwC,EAAUgD,GACpBpF,KAAKoC,SAAWA,EAChBpC,KAAKoF,YAAcA,EAGvB,SAAS2Q,EAAyB1c,EAAO0K,EAAQ5B,GAC7C,OAAiB,IAAbA,GAA+B,aAAbA,EACX,MAGX4B,EAASA,GAAU,EACnB5B,EAAWA,GAAY,GAEM,IAAzB9I,EAAMiH,iBACNyD,GAAUA,GAGP1K,EAAMyI,YAAYiC,EAAQ,IAAO1K,EAAMmH,mBAAoB,GAAO,EAAO2B,IAKpF,SAAS3C,EAAWvE,EAAKqQ,EAAcpK,EAAO7H,GAC1C2G,KAAKsB,MAAQ,IAAIuO,EAAc,IAAM3O,EAAOA,GAC5ClB,KAAKkB,MAAQA,EACblB,KAAK3G,MAAQA,EACb2G,KAAK/E,IAAMA,EACX+E,KAAKsL,aAAeA,EAEpBtL,KAAKmL,MAAQ,GACbnL,KAAKgW,gBAAkB,GAGvBhW,KAAK0C,WAAa,KA4FtB,SAASjD,EAASmC,EAAiBC,GAC/B7B,KAAK+C,WAAanB,EAClB5B,KAAKgD,WAAanB,EAClB7B,KAAK3G,MAAQuI,EAAgBvI,MAC7B2G,KAAK/E,IAAM2G,EAAgB3G,IAC3B+E,KAAK0C,WAAa,KAtItBmN,EAAcjP,UAAUiI,QALxBlJ,EAAgBiB,UAAUqB,WAAY,EA6CtCzC,EAAWoB,UAAUqE,QAAU,SAAUG,EAAahD,GAClD,IAAIC,EAAWD,EAASlB,MACpB+J,EAAOjL,KAAKgW,gBAAgB3T,GAChC,QAAa4B,IAATgH,EAEAA,EAAO,IAAIrL,EAAKwC,EAAUgD,GAC1BpF,KAAKgW,gBAAgB3T,GAAY4I,EACjCjL,KAAKmL,MAAM7M,KAAK2M,IACU,IAAtBjL,KAAKsL,eACLlG,GAAeA,GAEnBpF,KAAK3G,MAAM8J,4BAA4BnD,KAAMoC,EAAUgD,OACpD,CAGH,IAAI6Q,EAAiBhL,EAAK7F,YAAcA,EACxCpF,KAAKkW,uBAAuBD,EAAgB7T,GAGhD,OAAOpC,MAGXR,EAAWoB,UAAUuV,WAAa,SAAUlL,GAExC,OAAOjL,MAGXR,EAAWoB,UAAUwV,iBAAmB,SAAUC,GAC9C,GAAIA,IAAWrW,KAAK/E,IAAK,CACrB,IAAIiI,EAAamT,EAASrW,KAAK/E,KACL,IAAtB+E,KAAKsL,eACLpI,GAAcA,GAGlBlD,KAAK/E,IAAMob,EACXrW,KAAK3G,MAAM4J,oBAAoBjD,KAAMkD,GAGzC,OAAOlD,MAGXR,EAAWoB,UAAUsV,uBAAyB,SAAUD,EAAgB7T,GACpE,IAAIC,EAAWD,EAASlB,MACxB,IAAkB,IAAdmB,EAAJ,CAKA,IAAI4I,EAAOjL,KAAKgW,gBAAgB3T,GAChC,QAAa4B,IAATgH,EAEAjL,KAAKiF,QAAQgR,EAAgB7T,QAI7B,GAAI6T,IAAmBhL,EAAK7F,YAAa,CACrC,IAAIlC,EAAa+S,EAAiBhL,EAAK7F,aACb,IAAtBpF,KAAKsL,eACLpI,GAAcA,GAGlB+H,EAAK7F,YAAc6Q,EACnBjW,KAAK3G,MAAM8J,4BAA4BnD,KAAMoC,EAAUc,GAI/D,OAAOlD,KAtBH4C,QAAQC,KAAK,6FAyBrBrD,EAAWoB,UAAUsD,MAAQ,SAAUH,EAAQ5B,GAC3CnC,KAAK0C,WAAaqT,EAAyB/V,KAAK3G,MAAO0K,EAAQ5B,GAC/DnC,KAAKsW,OAAOtW,KAAK0C,aAGrBlD,EAAWoB,UAAU0V,OAAS,SAAUC,GACT,OAAvBA,IAKAvW,KAAKsL,aACLtL,KAAKkW,wBAAwB,EAAGK,GAEhCvW,KAAKkW,uBAAuB,EAAGK,KAcvC9W,EAASmB,UAAUkC,YAAa,EAEhCrD,EAASmB,UAAUqE,QAAU,SAAUG,EAAahD,GAGhD,OAFApC,KAAK+C,WAAWkC,QAAQG,EAAahD,GACrCpC,KAAKgD,WAAWiC,QAAQG,EAAahD,GAC9BpC,MAGXP,EAASmB,UAAUuV,WAAa,SAAUlL,GAGtC,OAFAjL,KAAK+C,WAAWoT,WAAWlL,GAC3BjL,KAAKgD,WAAWmT,WAAWlL,GACpBjL,MAGXP,EAASmB,UAAUwV,iBAAmB,SAAUnb,GAC5C+E,KAAK+C,WAAWqT,iBAAiBnb,GACjC+E,KAAKgD,WAAWoT,iBAAiBnb,GACjC+E,KAAK/E,IAAMA,GAGfwE,EAASmB,UAAUsD,MAAQ,SAAUH,EAAQ5B,GACzCnC,KAAK0C,WAAaqT,EAAyB/V,KAAK3G,MAAO0K,EAAQ5B,GAC/DnC,KAAK+C,WAAWL,WAAa1C,KAAK0C,WAClC1C,KAAK+C,WAAWuT,OAAOtW,KAAK0C,YAC5B1C,KAAKgD,WAAWN,WAAa1C,KAAK0C,WAClC1C,KAAKgD,WAAWsT,OAAOtW,KAAK0C,aAIhCxK,EAAOD,QAAU,CACbuH,WAAYA,EACZE,SAAUA,EACVC,gBAAiBA,EACjBkQ,cAAeA,EACfpQ,SAAUA,EACVG,KAAMA,IAGR,IAAI4W,GAAG,CAAC,SAASre,EAAQD,EAAOD,GAgCrB,SAATwe,IAEA,aAEAzW,KAAKH,MAAQA,EACbG,KAAKiK,aAAeA,EACpBjK,KAAKR,WAAaA,EAClBQ,KAAKN,SAAWA,EAChBM,KAAK0W,QAAUA,EACf1W,KAAKJ,KAAOA,EACZI,KAAKV,QAAUA,EACfU,KAAK2W,gBAAkB,KAEvB3W,KAAK4W,SAAWA,EAgBhB5W,KAAKiH,MAAQ,SAAU5N,EAAOyG,EAAW+W,EAAMC,GAM3C,GAAGA,EACC,IAAI,IAAI3b,KAAQ4b,EACZ1d,EAAQ0d,EAAW5b,GAAM9B,GAKjC,IAAKA,EACD,MAAM,IAAIP,MAAM,yCAOpB,GAA6B,iBAAnBO,EAAMiB,UACTqJ,OAAOC,KAAsB,EAAjBvK,EAAMiB,UACjB,OAAOnC,EAAQ,YAARA,CAAqB6H,KAAM3G,GAW1C,GAAGA,EAAMwE,SAAS,CAEd,IAAImZ,EAAUrT,OAAOC,KAAKgT,GAO1B,GANAI,EAAUvQ,KAAKE,UAAUqQ,IAMrB3d,EAAMwE,SAASwI,OACf,MAAM,IAAIvN,MAAM,kHAAoHke,GAOxI,IAAIJ,EAASvd,EAAMwE,SAASwI,QACxB,MAAM,IAAIvN,MAAM,wBAA0BO,EAAMwE,SAASwI,OAAS,qCAAuC2Q,GAG7G,OAAOJ,EAASvd,EAAMwE,SAASwI,QAAQ7I,MAAMnE,GAazCA,aAAiBwG,IAAU,IAC3BxG,EAAQ,IAAIwG,EAAMC,GAAWwD,SAASjK,IAG1C,IAAI4d,EAAW5d,EAAMmE,QAOrB,GANAwC,KAAK2W,gBAAkBtd,EACvB4d,EAAS5O,YAAc4O,EAAS7O,sBAK5ByO,EACA,OAAOI,EAKP,IAAIC,EAAQ,GA2BZ,OAxBAA,EAAMxR,SAAWuR,EAASvR,SAG1BwR,EAAMhQ,OAAS+P,EAAStP,WAExBuP,EAAMtP,QAAUqP,EAASrP,QAEtBqP,EAAS9O,SAASmG,eACjB4I,EAAM7I,YAAa,GAIvB1K,OAAOC,KAAKqT,EAAS5O,aAChB5M,QAAQ,SAAUH,GAKgB,IAA5B2b,EAAS5O,YAAY/M,KACpB4b,EAAM5b,GAAK2b,EAAS5O,YAAY/M,MAKrC4b,GAenBlX,KAAKmX,WAAahf,EAAQ,kCAsC1B6H,KAAKoX,eAAiB,SAAS/d,GAC3B,OAAOlB,EAAQ,YAARA,CAAqB6H,KAAM3G,IA/M1C,IAAIiG,EAAUnH,EAAQ,sBAClB0H,EAAQ1H,EAAQ,WAChB8R,EAAe9R,EAAQ,0BACvBoH,EAAcpH,EAAQ,oBACtB4e,EAAa5e,EAAQ,gBACrBqH,EAAaD,EAAYC,WACzBE,EAAWH,EAAYG,SACvBgX,EAAUnX,EAAYmX,QACtB9W,EAAOL,EAAYK,KACnBgX,EAAWze,EAAQ,sBA8MD,mBAAXkf,OACPA,OAAO,GAAI,WACP,OAAO,IAAIZ,IAGU,iBAAX7Y,OACdA,OAAOyI,OAAS,IAAIoQ,EACG,iBAATa,OACdA,KAAKjR,OAAS,IAAIoQ,GAGtBve,EAAOD,QAAU,IAAIwe,GAEnB,CAACc,iCAAiC,EAAEC,qBAAqB,EAAEC,UAAU,EAAEC,YAAY,EAAEC,yBAAyB,GAAGC,qBAAqB,GAAGC,eAAe,GAAGzR,mBAAmB,MAAM,GAAG,CAAC","file":"solver.js"} \ No newline at end of file diff --git a/play/js/main_ballot.js b/play/js/main_ballot.js index 3019891a..dc05de98 100644 --- a/play/js/main_ballot.js +++ b/play/js/main_ballot.js @@ -1,166 +1,469 @@ -window.ONLY_ONCE = false; -function main(config){ - ballotType = config.system; - config.strategy = config.strategy || "zero strategy. judge on an absolute scale."; - config.preFrontrunnerIds = config.preFrontrunnerIds || ["square","triangle"]; - config.showChoiceOfStrategy = config.showChoiceOfStrategy || false - config.showChoiceOfFrontrunners = config.showChoiceOfFrontrunners || false +function main_ballot(ui){ + // See sandbox.js for documentation + // main_ballot is a little different because there is no save button + // Some variables in config cannot be changed by the user. + // So, if we were going to save, then it would be good to not keep those in config. + + // The config is basically a text version of the model. + // The model should store all its variables + // and the model should always make decisions based on its own variables + // because maybe those variables could change without updating the config + // because some things happen internally in the model. + + // The only way the model can hook back into the ui is by providing a callback in main_ballot. + // Otherwise, it is a one way street. + // The config updates the model. + // The model does not update the config. + + // The reason why some things are in start and others are not is that some need assets and others do not. + // So, the ui.onload function could include more or less things before it, depending on how we want to laoad assets. + // For instance, we could lazy load all the assets when they become required to draw on a button. + + + + // Summary of changes regarding thePlan: + // I added thePlan as a way to make changes to the ui. + // It's like config but for the ui. + // This is in development. + // Maybe the one important change is with ui.menu.frun.createDOM + // where I am able to make edits on the menu earlier in the code + // because I can create the DOM later. - // make a copy of the config - var initialConfig = JSON.parse(JSON.stringify(config)); - // ONCE. - if(ONLY_ONCE) return; - ONLY_ONCE=true; - - var VoterType = window[ballotType+"Voter"]; - var BallotType = window[ballotType+"Ballot"]; - - Loader.onload = function(){ - - // SELF CONTAINED MODEL - window.model = new Model({ size:250, border:2 }); - document.body.appendChild(model.dom); - model.onInit = function(){ - model.addVoters({ - dist: SingleVoter, - type: VoterType, - strategy: config.strategy, - frontrunners: config.frontrunners, - x:81, y:92 - }); - model.addCandidate("square", 41, 50); - model.addCandidate("triangle", 173, 95); - model.addCandidate("hexagon", 216, 216); - model.preFrontrunnerIds = config.preFrontrunnerIds; - model.strategy = config.strategy; - }; - - // CREATE A BALLOT - window.ballot = new BallotType(); - document.body.appendChild(ballot.dom); - model.onUpdate = function(){ - ballot.update(model.voters[0].ballot); - }; + if (ui == undefined) ui = {} + ui.attach = new Attach(ui) + // handle input + ui.attach.handleInputMain() + + var model = new Model(ui.idModel) + ui.model = model + ui.attach.attachDOM(model) + + var config = {} + var initialConfig = {} + var thePlan = {} + ui.config = config + + // load the config + configBallot(config, initialConfig, ui.preset.config) + + // fix the plan + // get the plan ready for use, if it isn't already. + // config is here because we still just use one variable config to set up two things, ui and model + setPlan(thePlan,config) + + + // really all the possibilities for the user experience are in these bind functions + // the rest is just + // bind this plugin to the model + bindBallotModel(ui,model,config) + // sets up the menu functions before using them + bindBallotMenu(ui,model,config) + // here is an insertion point where you could edit the plan + + // starts ui, according to plan + planUI(ui,thePlan) + model.planModel() + + ui.onload = function(assets){ + // this line of code could go up or down, depending on whether we really need to load assets + // for example, createDOMB needs assets, and initDOM definitely needs assets - // Init! - model.init(); - if(config.showChoiceOfStrategy) { - - var strategyOn = [ - {name:"O", realname:"zero strategy. judge on an absolute scale.", margin:4}, - {name:"N", realname:"normalize", margin:4}, - {name:"F", realname:"normalize frontrunners only", margin:4}, - {name:"B", realname:"best frontrunner", margin:4}, - {name:"W", realname:"not the worst frontrunner"} - ]; - // old ones - // {name:"FL", realname:"justfirstandlast", margin:4}, - // {name:"T", realname:"threshold"}, - // {name:"SNTF", realname:"starnormfrontrunners"} - var onChooseVoterStrategyOn = function(data){ - config.strategy = data.realname; - model.strategy = config.strategy; - model.update(); - - }; - window.chooseVoterStrategyOn = new ButtonGroup({ - label: "which strategy?", - width: 42, - data: strategyOn, - onChoose: onChooseVoterStrategyOn - }); - document.body.appendChild(chooseVoterStrategyOn.dom); + createDOMB(ui,model) // doesn't depend on model + createUIArenaB(ui,model,config,initialConfig) // doesn't depend on model + + createMenuB(ui) // the parts that the depend on the model get re-done with onUpdate + + // INIT + model.assets = assets // doesn't need to be run with start because assets don't change + model.initPlugin(); // doesn't need initDOM or createDOMB internally because there are no settings on the ballot that change these + + if (ui.preset.update) ui.preset.update() // depends on the ui.menu being done (after createMenuB) // run some extra stuff specified by the preset + + + // UPDATE + model.update() // depends on the menu having been created: createMenuB() + ui.selectMENU() + }; + + var l = new Loader() + l.onload = ui.onload + l.load(main_ballot.assets); + + + + +} + +function configBallot(config,initialConfig, presetConfig){ + + _copyAttributes(config, presetConfig) + + var translate = { + Plurality:"FPTP", + Ranked:"IRV", + Approval:"Approval", + Score:"Score" + } + config.system = config.system || translate[config.ballotType] || "FPTP" + config.firstStrategy = config.firstStrategy || "zero strategy. judge on an absolute scale."; + config.preFrontrunnerIds = config.preFrontrunnerIds || ["square","triangle"]; + config.doStarStrategy = config.doStarStrategy || false + config.theme = config.theme || "Default" + config.dimensions = config.dimensions || "2D" + config.namelist = "" + config.customNames = "No" + + // grandfather name for firstStrategy used only for main_ballot + if (config.strategy != undefined) config.firstStrategy = config.strategy + + // make a copy of the config + _copyAttributes(initialConfig,config) + +} + +function setPlan(thePlan,config) { + thePlan.newWay = config.newWay || false + thePlan.way1 = true + thePlan.ballotType = config.ballotType // it would also make it look nicer to separate config and plan + thePlan.showChoiceOfStrategy = config.showChoiceOfStrategy || false + thePlan.showChoiceOfFrontrunners = config.showChoiceOfFrontrunners || false +} + + +function planUI (ui,thePlan) { + // binds UI to plan + ui.newWay = thePlan.newWay + ui.way1 = thePlan.way1 + ui.BallotType = window[thePlan.ballotType+"Ballot"] + ui.showChoiceOfStrategy = thePlan.showChoiceOfStrategy + ui.showChoiceOfFrontrunners = thePlan.showChoiceOfFrontrunners +} + + +function createDOMB(ui,model) { + // This is what the ui is all about. It makes the DOM. + + // CREATE div stuff + model.createDOM() + + ui.dom.left = newDivOnBase("b-left") + ui.dom.right = newDivOnBase("b-right") + function newDivOnBase(name) { + var a = document.createElement("div"); + a.setAttribute("id", name); + ui.dom.basediv.appendChild(a); + return a + } + + ui.dom.left.appendChild(model.dom); + + // CREATE A BALLOT + if (ui.newWay) { // build ui + if (ui.way1) { + ui.dom.caption = document.createElement("div"); + ui.dom.caption.id = "caption"; + ui.dom.right.appendChild(ui.dom.caption) } - - if(config.showChoiceOfFrontrunners) { - - var h1 = function(x) {return "<span class='buttonshape'>"+_icon(x)+"</span>";}; - var frun = [ - {name:h1("square"),realname:"square",margin:4}, - {name:h1("triangle"),realname:"triangle",margin:4}, - {name:h1("hexagon"),realname:"hexagon",margin:4}, - //{name:h1("pentagon"),realname:"pentagon",margin:4}, - //{name:h1("bob"),realname:"bob"} - ]; - var onChooseFrun = function(data){ - - // update config... - // no reset... - if (data.isOn) { - if (!config.preFrontrunnerIds.includes(data.realname) ) {config.preFrontrunnerIds.push(data.realname)} - } else { - var index = config.preFrontrunnerIds.indexOf(data.realname); - if (index > -1) { - config.preFrontrunnerIds.splice(index, 1); - } - } - model.preFrontrunnerIds = config.preFrontrunnerIds - model.update(); - - }; - window.chooseFrun = new ButtonGroup({ - label: "who are the frontrunners?", - width: 42, - data: frun, - onChoose: onChooseFrun, - isCheckbox: true - }); - document.body.appendChild(chooseFrun.dom); + } else { + ui.dom.paperBallot = new ui.BallotType(model); + ui.dom.right.appendChild(ui.dom.paperBallot.dom) + } + + +} + +function bindBallotMenu(ui,model,config) { + + // the ui is a model that plugs into Model + // it needs an update + // and an init + // and those functions go into bindModel + // so these menu items may also need .update() and .draw() + + ui.menu = {} + + ui.menu.strategy = {} + + var strategyOn = [ + {name:"O", realname:"zero strategy. judge on an absolute scale.", margin:4}, + {name:"N", realname:"normalize", margin:4}, + {name:"F", realname:"normalize frontrunners only", margin:4}, + {name:"F+", realname:"best frontrunner", margin:4}, + {name:"F-", realname:"not the worst frontrunner"} + ]; + // old ones + // {name:"FL", realname:"justfirstandlast", margin:4}, + // {name:"T", realname:"threshold"}, + // {name:"SNTF", realname:"starnormfrontrunners"} + var onChooseVoterStrategyOn = function(data){ + config.firstStrategy = data.realname; + model.firstStrategy = config.firstStrategy + model.update(); + + }; + ui.menu.strategy.createDOM = function() { + ui.menu.strategy.chooseVoterStrategyOn = new ButtonGroup({ + label: "which strategy?", + width: 42, + data: strategyOn, + onChoose: onChooseVoterStrategyOn + }); + } + + ui.menu.frun = {} + + function _iconButton(id) { + return "<span class='buttonshape'>"+model.icon(id)+"</span>" + } + var frunMakelist = function() { + var a = [] + for (var i=0; i < model.candidates.length; i++) { + var c = model.candidates[i] + a.push({ + name:_iconButton(c.id), + realname:c.id, + margin:4 + }) } + if (a.length > 0) a[a.length-1].margin = 0 + return a + } + + var onChooseFrun = function(data){ + + // update config... + // no reset... + if (data.isOn) { + if (!config.preFrontrunnerIds.includes(data.realname) ) {config.preFrontrunnerIds.push(data.realname)} + } else { + var index = config.preFrontrunnerIds.indexOf(data.realname); + if (index > -1) { + config.preFrontrunnerIds.splice(index, 1); + } + } + model.preFrontrunnerIds = config.preFrontrunnerIds + for (var i=0; i<model.voterGroups.length; i++) { + model.voterGroups[i].preFrontrunnerIds = config.preFrontrunnerIds + } + model.dm.districtsListCandidates() + model.update(); + + }; + ui.menu.frun.createDOM = function() { + // This is an important change. It allows the user to put this whole bindBallotMenu function earlier in the code's logic + // That allows the coder to put more information into the menu item's section of code, + // which makes the code easier to read. + ui.menu.frun.chooseFrun = new ButtonGroup({ + label: "who are the frontrunners?", + width: 42, + makeData: frunMakelist, // this depends on the model + onChoose: onChooseFrun, + isCheckbox: true + }); + } + ui.menu.frun.redraw = function() { + // we redraw these buttons because calls to model.icon might use a placeholder for an image when it's loading + if(ui.showChoiceOfFrontrunners) { + // needed to call model.icon again for placeholders + ui.menu.frun.chooseFrun.redraw() + } + } + + ui.selectMENU = function(){ + if(ui.menu.strategy.chooseVoterStrategyOn) ui.menu.strategy.chooseVoterStrategyOn.highlight("realname", config.firstStrategy); + if(ui.menu.frun.chooseFrun) ui.menu.frun.chooseFrun.highlight("realname", config.preFrontrunnerIds); + }; + +} + +function createMenuB(ui) { + // depends on the model + + if(ui.showChoiceOfStrategy) { + ui.menu.strategy.createDOM() + ui.dom.left.appendChild(ui.menu.strategy.chooseVoterStrategyOn.dom); + } + if(ui.showChoiceOfFrontrunners) { + ui.menu.frun.createDOM() + ui.dom.left.appendChild(ui.menu.frun.chooseFrun.dom); + } +} + +function createUIArenaB(ui,model,config,initialConfig) { + ////////////////////////// + //////// RESET... //////// + ////////////////////////// + + // CREATE A RESET BUTTON + var resetDOM = document.createElement("div"); + resetDOM.id = "reset"; + resetDOM.innerHTML = "reset"; + resetDOM.onclick = function(){ + // LOAD + _copyAttributes(config, initialConfig) + // CREATE, CONFIGURE, INIT + if (ui.preset.update) ui.preset.update() // depends on the ui.menu being done (after createMenuB) // run some extra stuff specified by the preset + model.reset() + // UPDATE + model.update() + // UPDATE + ui.selectMENU() - var selectUI = function(){ - if(window.chooseVoterStrategyOn) chooseVoterStrategyOn.highlight("realname", model.strategy); - if(window.chooseFrun) chooseFrun.highlight("realname", model.preFrontrunnerIds); - }; - selectUI(); - - ////////////////////////// - //////// RESET... //////// - ////////////////////////// - - // CREATE A RESET BUTTON - var resetDOM = document.createElement("div"); - resetDOM.id = "reset"; - resetDOM.innerHTML = "reset"; - resetDOM.onclick = function(){ - - config = JSON.parse(JSON.stringify(initialConfig)); // RESTORE IT! - // Reset manually, coz update LATER. - model.reset(true); - model.onInit(); - //setInPosition(); - model.update() - // Back to ol' UI - selectUI(); - console.log(initialConfig) - }; - document.body.appendChild(resetDOM); + }; + ui.dom.left.appendChild(resetDOM); +} + +function bindBallotModel(ui,model,config) { + + model.planModel = function() { + // LOAD + model.size = 250 + model.border = 2 + model.ballotType = config.ballotType + model.system = config.system + model.newWay = ui.newWay + } + + model.initPlugin = function(){ + + model.initDOM() + // CREATE + model.voterGroups.push(new SingleVoter(model)) + model.candidates.push(new Candidate(model)) + model.candidates.push(new Candidate(model)) + model.candidates.push(new Candidate(model)) + model.voterCenter = new VoterCenter(model) + // CONFIGURE + Object.assign( model.candidates[0],{x: 41, y: 50, icon:"square"} ) + Object.assign( model.candidates[1],{x:153, y: 95, icon:"triangle"} ) + Object.assign( model.candidates[2],{x:216, y:216, icon:"hexagon"} ) + Object.assign( model.voterGroups[0], {x: 81, y: 92, typeVoterModel: model.ballotType, + preFrontrunnerIds: config.preFrontrunnerIds} ) + model.preFrontrunnerIds = config.preFrontrunnerIds; + model.firstStrategy = config.firstStrategy + model.doStarStrategy = config.doStarStrategy; + model.theme = config.theme + model.dimensions = config.dimensions + model.customNames = config.customNames + model.namelist = config.namelist.split("\n") + // INIT + model.candidates[0].init() + model.candidates[1].init() + model.candidates[2].init() + model.initMODEL() + model.voterManager.initVoters() + model.dm.redistrict() + }; - Loader.load([ + model.onInitModel = function() { + + if(ui.showChoiceOfStrategy || ui.showChoiceOfFrontrunners) { + ui.menu.frun.chooseFrun.init() + ui.selectMENU() + } + } + + _insertFunctionAfter( model,"onUpdate", function() { - // the peeps - "img/voter_face.png", - "img/square.png", - "img/triangle.png", - "img/hexagon.png", - // Ballot instructions - "img/ballot_fptp.png", - "img/ballot_ranked.png", - "img/ballot_approval.png", - "img/ballot_range.png", + if (model.voterGroups.length == 0) return + var ballot = model.voterGroups[0].voterPeople[0].stages[model.stage].ballot // just pick the first ballot to show + if (model.newWay) { + // onDraw + } else { + ui.dom.paperBallot.update(ballot); + } + + }) + + + model.onDraw = function(){ + + var ready = model.nLoading == 0 || ! model.placeHolding + // ready to replace + // two different ways to replace the image + + + if ( ready ) { + + // 1 : call draw again + ui.menu.frun.redraw() + } + + + if (model.voterGroups.length == 0) return + if (model.voterGroups[0].voterGroupType == "GaussianVoters") return + if (model.newWay) { + var text = model.voterGroups[0].voterModel.toTextH(model.voterGroups[0].voterPeople[0].stages[model.stage].ballot); + if ( ready ) { + + // 2 : replace the Placeholder + text = text.replace(/\^Placeholder{(.*?)}/g, (match, $1) => { + return model.icon($1) + }); // https://stackoverflow.com/a/49262416 + + + if (ui.way1) { + ui.dom.caption.innerHTML = text + } else { + ui.dom.right.innerHTML = text + } + } + } + } + + +} + + + + +main_ballot.assets = [ + + // the peeps + "play/img/voter_face.png", + "play/img/voter.png", + + // candidate icons - don't need because we dynamically load + // "play/img/square.png", + // "play/img/triangle.png", + // "play/img/hexagon.png", + // "play/img/pentagon.png", + // "play/img/bob.png", + + // "play/img/square.svg", + // "play/img/triangle.svg", + // "play/img/hexagon.svg", + // "play/img/pentagon.svg", + // "play/img/bob.svg", + + // "play/img/blue_bee.png", + // "play/img/yellow_bee.png", + // "play/img/red_bee.png", + // "play/img/green_bee.png", + // "play/img/orange_bee.png", + + // plus - dynamic loading works for these + // "play/img/plusCandidate.png", + // "play/img/plusOneVoter.png", + // "play/img/plusVoterGroup.png", - // The boxes - "img/ballot_box.png", - "img/ballot_rate.png" + // Ballot instructions - old style ballot + // "play/img/ballot5_fptp.png", + // "play/img/ballot5_ranked.png", + // "play/img/ballot5_approval.png", + // "play/img/ballot5_range.png", - ]); + // The boxes - old style ballot + // "play/img/ballot5_box.png", + // "play/img/ballot_rate.png", + // "play/img/ballot_three.png" -} \ No newline at end of file +]; \ No newline at end of file diff --git a/play/js/main_ballot_original.js b/play/js/main_ballot_original.js new file mode 100644 index 00000000..25b9b725 --- /dev/null +++ b/play/js/main_ballot_original.js @@ -0,0 +1,88 @@ + +function main_ballot(ui){ + var preset = ui.preset + var ballotType = preset.config.ballotType + var presetName = ui.presetName + + var BallotType = window[ballotType+"Ballot"]; + + var l = new Loader() + l.onload = function(){ + + // CREATE + var model = new Model(presetName); + model.createDOM() + + // CONFIGURE + model.size = 250 + model.border = 2 + model.startAt1 = true + model.theme = "Nicky" + model.doOriginal = true + + // INIT + model.initDOM() + + var basediv = document.querySelector("#" + presetName) + basediv.appendChild(model.dom); + model.initPlugin = function(){ + // CREATE + model.voterGroups.push(new SingleVoter(model)) + model.candidates.push(new Candidate(model)) + model.candidates.push(new Candidate(model)) + model.candidates.push(new Candidate(model)) + // CONFIGURE + Object.assign( model.voterGroups[0],{x: 81, y: 92, typeVoterModel: ballotType} ) + Object.assign( model.candidates[0],{x: 41, y: 50, icon:"square"} ) + Object.assign( model.candidates[1],{x:153, y: 95, icon:"triangle"} ) + Object.assign( model.candidates[2],{x:216, y:216, icon:"hexagon"} ) + model.firstStrategy = "zero strategy. judge on an absolute scale." + // INIT + model.candidates[0].init() + model.candidates[1].init() + model.candidates[2].init() + model.initMODEL() + model.voterManager.initVoters() + // UPDATE + if(ballotType=="Score") { + model.voterGroups[0].voterModel.minscore = 1 + } + model.dm.redistrict() + model.update() + }; + + + // CREATE A BALLOT + var ballot = new BallotType(model); + basediv.appendChild(ballot.dom); + model.onUpdate = function(){ + ballot.update(model.voterGroups[0].voterPeople[0].stages[model.stage].ballot); + }; + + // UPDATE + model.initPlugin(); + model.update() + + }; + + l.load([ + + // the peeps + "play/img/voter_face.png", + "play/img/square.png", + "play/img/triangle.png", + "play/img/hexagon.png", + + // Ballot instructions + "play/img/ballot_fptp.png", + "play/img/ballot_ranked.png", + "play/img/ballot_approval.png", + "play/img/ballot_range_original.png", + + // The boxes + "play/img/ballot_box.png", + "play/img/ballot_rate_original.png" + + ]); + +} \ No newline at end of file diff --git a/play/js/main_preset.js b/play/js/main_preset.js new file mode 100644 index 00000000..03272730 --- /dev/null +++ b/play/js/main_preset.js @@ -0,0 +1,10 @@ +main_preset() + +function main_preset() { + var url = window.location.pathname; + var htmlname = url.substring(url.lastIndexOf('/') + 1); + var presetName = htmlname.slice(0,htmlname.length-5) + var ui = loadpreset(presetName) + var preset = ui.preset + sandbox(preset) +} diff --git a/play/js/main_sandbox.js b/play/js/main_sandbox.js index 0b9cd38d..0a5ff2e5 100644 --- a/play/js/main_sandbox.js +++ b/play/js/main_sandbox.js @@ -1,1011 +1,8759 @@ -window.FULL_SANDBOX = window.FULL_SANDBOX || false; -window.HACK_BIG_RANGE = true; - -window.ONLY_ONCE = false; - -function main(config){ - - // ONCE. - if(ONLY_ONCE) return; - ONLY_ONCE=true; - - /////////////////////////////////////////////////////////////// - // ACTUALLY... IF THERE'S DATA IN THE QUERY STRING, OVERRIDE // - /////////////////////////////////////////////////////////////// - - var _getParameterByName = function(name, url){ - var url = window.top.location.href; - name = name.replace(/[\[\]]/g, "\\$&"); - var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"), - results = regex.exec(url); - if (!results) return null; - if (!results[2]) return ''; - return decodeURIComponent(results[2].replace(/\+/g, " ")).replace("}/","}"); //not sure how that / got there. - }; - var modelData = _getParameterByName("m"); - if(modelData){ - - // Parse! - var data = JSON.parse(modelData); - - config = data; - - } - - var initialConfig - var allnames = ["systems","voters","candidates","strategy","percentstrategy","unstrategic","frontrunners","poll","yee"] - var doms = {} // for hiding menus, later - var stratsliders = [] // for hiding sliders, later - - var loadDefaults = function() { - // Defaults... - config = config || {}; - config.system = config.system || "FPTP"; - config.candidates = config.candidates || 3; - config.voters = config.voters || 1; - config.snowman = config.snowman || false; - //config.votersRealName = config.votersRealName || "Single Voter"; - config.oneVoter = config.oneVoter || false; - - config.features = config.features || 0; // 1-basic, 2-voters, 3-candidates, 4-save - if ( config.features == 0 && ! config.featurelist) {config.featurelist = ["systems"] // old spec - } else if (config.features == 1) {config.featurelist = ["systems"] - } else if (config.features == 2) {config.featurelist = ["systems","voters"] - } else if (config.features == 3) {config.featurelist = ["systems","voters","candidates"] - } else if (config.features == 4) {config.featurelist = ["systems","voters","candidates"]; config.sandboxsave = true;} - config.sandboxsave = config.sandboxsave || false; - config.featurelist = config.featurelist || ["systems"] - config.doPercentFirst = config.doPercentFirst || false; - if (config.doPercentFirst) config.featurelist = config.featurelist.concat(["percentstrategy"]); - config.doFullStrategyConfig = config.doFullStrategyConfig || false; - if (config.doFullStrategyConfig) config.featurelist = config.featurelist.concat(["strategy","percentstrategy","unstrategic","frontrunners","poll","yee"]) - // clear the grandfathered config settings - config.doPercentFirst = undefined - config.features = undefined - config.doFullStrategyConfig = undefined - config.hidegearconfig = config.hidegearconfig || false; - - config.preFrontrunnerIds = config.preFrontrunnerIds || ["square","triangle"] - config.voterStrategies = config.voterStrategies || [] - config.description = config.description || "" - for (i in [0,1,2]) { - config.voterStrategies[i] = config.voterStrategies[i] || "zero strategy. judge on an absolute scale." - } - config.voterPercentStrategy = config.voterPercentStrategy || [] - for (i in [0,1,2]) { - config.voterPercentStrategy[i] = config.voterPercentStrategy[i] || 0 - } - - config.unstrategic = config.unstrategic || "zero strategy. judge on an absolute scale."; - config.keyyee = config.keyyee || "off"; - config.computeMethod = config.computeMethod || "ez"; - var url = window.location.pathname; - var filename = url.substring(url.lastIndexOf('/')+1); - config.filename = filename - config.presethtmlname = filename; - initialConfig = JSON.parse(JSON.stringify(config)); - - } - loadDefaults() - - Loader.onload = function(){ - - //////////////////////// - // THE FRIGGIN' MODEL // - //////////////////////// - - window.model = new Model(); - document.querySelector("#center").appendChild(model.dom); - model.dom.removeChild(model.caption); - document.querySelector("#right").appendChild(model.caption); - model.caption.style.width = ""; - - // INIT! - model.onInit = function(){ - - // Based on config... what should be what? - model.numOfCandidates = config.candidates; - model.numOfVoters = config.voters; - model.votersRealName = voters.filter( function(x){return (x.num==config.voters && (x.snowman||false)==config.snowman && (x.oneVoter||false) == config.oneVoter) })[0].realname - model.system = config.system; - model.preFrontrunnerIds = config.preFrontrunnerIds; - model.computeMethod = config.computeMethod; - var votingSystem = votingSystems.filter(function(system){ - return(system.name==model.system); - })[0]; - model.voterType = votingSystem.voter; - model.ballotType = window[votingSystem.ballot]; - model.election = votingSystem.election; - - // Voters - var num = model.numOfVoters; - var voterPositions; - if(num==1){ - voterPositions = [[150,150]]; - }else if(num==2){ - voterPositions = [[95,150],[205,150]]; - }else if(num==3){ - voterPositions = [[65,150],[150,150],[235,150]]; - if (config.snowman) { - voterPositions = [[150,83],[150,150],[150,195]] - } - } - for(var i=0; i<num; i++){ - var pos = voterPositions[i]; - if (config.oneVoter) { - var dist1 = SingleVoter - } else { - var dist1 = GaussianVoters - } - model.addVoters({ - dist: dist1, - type: model.voterType, - strategy: config.voterStrategies[i], - percentStrategy: config.voterPercentStrategy[i], - preFrontrunnerIds: config.preFrontrunnerIds, - unstrategic: config.unstrategic, - vid: i, - snowman: config.snowman, - num:(4-num), - x:pos[0], y:pos[1] - }); - } - - // Candidates, in a circle around the center. - var _candidateIDs = ["square","triangle","hexagon","pentagon","bob"]; - var angle = 0; - var num = model.numOfCandidates; - switch(num){ - case 3: angle=Math.TAU/12; break; - case 4: angle=Math.TAU/8; break; - case 5: angle=Math.TAU/6.6; break; - } - for(var i=0; i<num; i++){ - var r = 100; - var x = 150 - r*Math.cos(angle); - var y = 150 - r*Math.sin(angle); - var id = _candidateIDs[i]; - model.addCandidate(id, x, y); - angle += Math.TAU/num; - } - - for (i in [0,1,2]) stratsliders[i].setAttribute("style",(i<config.voters) ? "display:inline": "display:none") - - // Yee diagram - if (config.kindayee == "can") { - model.yeeobject = model.candidatesById[config.keyyee] - } else if (config.kindayee=="voter") { - model.yeeobject = model.voters[config.keyyee] - } else if (config.kindayee=="off") { - model.yeeobject = undefined - } else { // if yeeobject is not defined - model.yeeobject = undefined - } - if (model.yeeobject) {model.yeeon = true} else {model.yeeon = false} - - - // hide some menus - for (i in allnames) if(config.featurelist.includes(allnames[i])) {doms[allnames[i]].hidden = false} else {doms[allnames[i]].hidden = true} - - }; - model.election = Election.plurality; - model.onUpdate = function(){ - model.election(model, {sidebar:true}); - - // CREATE A BALLOT - var myNode = document.querySelector("#right"); - while (myNode.firstChild) { - myNode.removeChild(myNode.firstChild); - } // remove old one, if there was one - // document.querySelector("#ballot").remove() - if (config.oneVoter) { - window.ballot = new model.ballotType(); - document.querySelector("#right").appendChild(ballot.dom); - } else { - document.querySelector("#right").appendChild(model.caption); - } - - if (config.oneVoter) { - ballot.update(model.voters[0].ballot); - } - }; - - // In Position! - var setInPosition = function(){ // runs when we change the config for number of voters or candidates - - var positions; - - // CANDIDATE POSITIONS - positions = config.candidatePositions; - if(positions){ - for(var i=0; i<positions.length; i++){ - var position = positions[i]; - var candidate = model.candidates[i]; - candidate.x = position[0]; - candidate.y = position[1]; - } - } - - // VOTER POSITION - positions = config.voterPositions; - if(positions){ - for(var i=0; i<positions.length; i++){ - var position = positions[i]; - var voter = model.voters[i]; - voter.x = position[0]; - voter.y = position[1]; - } - } - - // update! - model.update(); - - }; - - - ////////////////////////////////// - // BUTTONS - WHAT VOTING SYSTEM // - ////////////////////////////////// - - - // Which voting system? - var votingSystems = [ - {name:"FPTP", voter:PluralityVoter, ballot:"PluralityBallot", election:Election.plurality, margin:4}, - {name:"IRV", voter:RankedVoter, ballot:"RankedBallot", election:Election.irv}, - {name:"Borda", voter:RankedVoter, ballot:"RankedBallot", election:Election.borda, margin:4}, - {name:"Condorcet", voter:RankedVoter, ballot:"RankedBallot", election:Election.condorcet}, - {name:"Approval", voter:ApprovalVoter, ballot:"ApprovalBallot", election:Election.approval, margin:4}, - {name:"Score", voter:ScoreVoter, ballot:"ScoreBallot", election:Election.score}, - {name:"STAR", voter:ScoreVoter, ballot:"ScoreBallot", election:Election.star, margin:4}, - {name:"3-2-1", voter:ThreeVoter, ballot:"ThreeBallot", election:Election.three21} - ]; - var onChooseSystem = function(data){ - - // update config... - config.system = data.name; - - // no reset... - model.voterType = data.voter; - model.ballotType = window[data.ballot]; - - for(var i=0;i<model.voters.length;i++){ - model.voters[i].setType(data.voter); - } - model.election = data.election; - model.update(); - - }; - window.chooseSystem = new ButtonGroup({ - label: "what voting system?", - width: 108, - data: votingSystems, - onChoose: onChooseSystem - }); - document.querySelector("#left").appendChild(chooseSystem.dom); - doms["systems"] = chooseSystem.dom - - - - - // How many voters? - var voters = [ - {realname: "Single Voter", name:"웃", num:1, margin:5, oneVoter:true}, - {realname: "One Group", name:"1", num:1, margin:5}, - {realname: "Two Groups", name:"2", num:2, margin:5}, - {realname: "Three Groups", name:"3", num:3, margin:5}, - {realname: "Different Sized Groups (like a snowman)", name:"☃", num:3, snowman:true}, - ]; - var onChooseVoters = function(data){ - - // update config... - config.voters = data.num; - - config.snowman = data.snowman || false; - config.oneVoter = data.oneVoter || false; - model.votersRealName = data.realname; - // save candidates before switching! - config.candidatePositions = save().candidatePositions; - - // reset! - config.voterPositions = null; - model.reset(); - setInPosition(); - - - for (i in [0,1,2]) stratsliders[i].setAttribute("style",(i<data.num) ? "display:inline": "display:none") - - }; - window.chooseVoters = new ButtonGroup({ - label: "how many groups of voters?", - width: 40, - data: voters, - onChoose: onChooseVoters - }); - document.querySelector("#left").appendChild(chooseVoters.dom); - doms["voters"] = chooseVoters.dom - - - - var candidates = [ - {name:"two", num:2, margin:4}, - {name:"three", num:3, margin:4}, - {name:"four", num:4, margin:4}, - {name:"five", num:5} - ]; - var onChooseCandidates = function(data){ - - // update config... - config.candidates = data.num; - - // save voters before switching! - config.voterPositions = save().voterPositions; - - // reset! - config.candidatePositions = null; - model.reset(); - setInPosition(); - - }; - window.chooseCandidates = new ButtonGroup({ - label: "how many candidates?", - width: 52, - data: candidates, - onChoose: onChooseCandidates - }); - document.querySelector("#left").appendChild(chooseCandidates.dom); - doms["candidates"] = chooseCandidates.dom - - - - // strategy - - var strategyOn = [ - {name:"O", realname:"zero strategy. judge on an absolute scale.", margin:5}, - {name:"N", realname:"normalize", margin:5}, - {name:"F", realname:"normalize frontrunners only", margin:5}, - {name:"B", realname:"best frontrunner", margin:5}, - {name:"W", realname:"not the worst frontrunner"} - ]; - // old ones - // {name:"FL", realname:"justfirstandlast", margin:5}, - // {name:"T", realname:"threshold"}, - // {name:"SNTF", realname:"starnormfrontrunners"} - - - var onChooseVoterStrategyOn = function(data){ - - // update config... - // only the middle percent (for the yellow triangle) - - // no reset... - for(var i=0;i<model.voters.length;i++){ - config.voterStrategies[i] = data.realname; - model.voters[i].strategy = config.voterStrategies[i] - } - model.update(); - - }; - window.chooseVoterStrategyOn = new ButtonGroup({ - label: "do voters strategize?", - width: 40, - data: strategyOn, - onChoose: onChooseVoterStrategyOn - }); - document.querySelector("#left").appendChild(chooseVoterStrategyOn.dom); - doms["strategy"] = chooseVoterStrategyOn.dom - - if(0){ - - var strategyPercent = [ - {name:"0", num:0, margin:4}, - {name:"50", num:50, margin:4}, - {name:"80", num:80, margin:4}, - {name:"100", num:100} - ]; - var onChoosePercentStrategy = function(data){ - - // update config... - config.voterPercentStrategy[0] = data.num; - - // no reset... - for(var i=0;i<model.voters.length;i++){ - model.voters[i].percentStrategy = config.voterPercentStrategy[i] - } - model.update(); - - }; - window.choosePercentStrategy = new ButtonGroup({ - label: "how many strategize? %", - width: 52, - data: strategyPercent, - onChoose: onChoosePercentStrategy - }); - document.querySelector("#left").appendChild(choosePercentStrategy.dom); +function sandbox(ui) { + + // sandbox runs the whole userland experience. + // ui is a mother object for everything related to the User Interface. + // in much the same way that model is the mother for everything simulation related. + + // Search for HOWTO to see notes on making changes. (right now, just for renaming menus) + + + ///////////////// + // INPUT GUIDE // + ///////////////// + + // Example usage: + // sandbox({idScript:"asdf",presetName:"sandbox"}) + // sandbox({idScript:"uiop"}) + // ui = sandbox() + // Just put it inside <div><script id="idScript"> + + // Like so: + // <div> + // <script id="asdf"> + // sandbox({idScript:"asdf",presetName:"sandbox"}) + // </script> + // <div> + + // The input variables are: + // ui.idScript : id of the current <script> div (required) + // ui.idModel : id of any <div> to be the model container, i.e. if you already know it and don't want to create one. feeds into createDOM() + // ui.presetName : the name of the preset (or, for grandfathered cases, also the same as the ui.idModel) + // ui.preset : all the preset data + // .config : the config. It lists all the button settings. + // .uiType : determines the <div> attributes for the model container + // .update : clumsy script to run after loading buttons + // ui.url : paste any saved url in here to load that configuration + + // + + // We can even do a url pasted directly from the address bar + // (only the part after the ? question mark is read into the Config) + // sandbox({id:"uio",url:"http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA41Ty27CMBD8lz374HdizhXH_gBwiGiokNJEDYkqFcG3d8aBhApVKsbs7MO7411zFi2rzSZZFdNObazRqtAEPihTWiATjDJFBApRmaR3OyWGZ0zQCpu6k5XYF1HiZaWVBFkdquZUK4kItOpp4UgBj1ZPC54SHqT--4uYhBhwUf_YpIs7Dv0IPobM5bqVq94K-BK5GRUz2m6nH37krVvX1TD29frYDHV_N-ctwvwW979mDQqb0dZf-6ql5udemKUtJi4QjTAeEtcOECl30IIxjBZ0DQTyWwg3-fxkDJOIkxFpHEQ5aSkfcJytnD7Hqq_BZeiPVfve1KTsTA50NidxbopfyDpkP8vStzuwd-DuwE_gAhOoSP2NSg5sItOnPGWDmTvlUdaTEUfiOYhci4p9VJB54wh8vpIPnLW6Ldpj5uwLNj2f4sMrf2mJGmlBCagprx0RinJEgfOyBC6nCrdnuwwoLAMKrFONQ05QPs42pMUT9QNmmY4R0c6Ir2J_7PcNKUXeSdrqI88ism_d4UBHsfx1WEsuP5a3SEChAwAA"}) + + // + + // Finally, if id is not provided, then generate an id and leave the node dangling + // ui.containerDiv will be generated with this id + // Example: + // <div> + // <script id="later"> + // ui = {presetName:"sandbox"} + // sandbox(ui) + // ui.idScript = "later" + // ui.makeParentDivs() + // </script> + // <div> + + + + + + + ///////////// + // SUMMARY // + ///////////// + + // This sets up the sandbox, with some help from the functions called below during the CREATE phase. + // The main tasks are + // Attach: loads inputs, loads presets, and hooks into the web page + // Model: setting up the model for simulations. + // bindModel: binding functions into the Model simulation routine. And creating the primary divs + // Cypher: decyphering the URL, + // Config: getting the configuration from it. + // menu: Creating all the divs and assigning click events inside the left menu + // uiArena: " " outside the left menu + // Loader: Save some bandwidth + + // There is a pattern to the code: LOAD, CREATE, CONFIGURE, INIT, & UPDATE. + // LOAD loads the input or defaults. + // CREATE makes an empty data structure to be used. + // CONFIGURE adds all the input to the data structure. + // INIT completes the data structure by doing steps that needed to use the data structure as input, and is otherwise similar to CONFIGURE. + // UPDATE runs the actions, now that the data structure is complete. + + // Basic description of main_sandbox.js + // First we load the config, + // Then we update the model and menu. + // Then wait for mouse events. + + // there are two parts, the model and the config. + // load initial config + // bind model and config using menu items (config step for ) + // start/ initialize + // The sandbox is the binder + // we pass around the context, ui + + // See main_ballot.js for a more conceptual view of the data structures ui, model, and config + + // So the basic concept of this user intervace is: + // Load some data into the model, + // Let the user modify it through a controls, + // Describe the controls with a configuration, + // Display the state of the model, + // Save the control data. + + + + // CREATE the data structure + + // handle input + if (ui == undefined) ui = {} + ui.attach = new Attach(ui) + ui.attach.handleInputMain() + + + + var model = new Model(ui.idModel) + ui.model = model + ui.attach.attachDOM(model) + + var config = {} + var initialConfig = {} + ui.config = config + bindModel(ui,model,config) + + var cConfig = new Config(ui,config,initialConfig) + + ui.cypher = new Cypher(ui) + + + var l = new Loader() + l.onload = function(assets){ + + model.assets = assets + + // CREATE the divs! + createDOM(ui,model) + menu(ui,model,config,initialConfig, cConfig) + createMenu(ui) + uiArena(ui,model,config,initialConfig, cConfig) + + // CONNECT : Step 2 of CREATE : make connections between code parts (just one connection here) + ui.cypher.setUpEncode() + + + // INIT + // Read in the config file. + cConfig.loadUrl(afterLoadUrl) + + function afterLoadUrl(urlData) { + + cConfig.setConfig(urlData) + + // UPDATE SANDBOX + ui.menu.theme.init_sandbox(); + _objF(ui.arena,"update") + model.initPlugin(); // this wants to set ui.menu.item.dom.hidden + + ui.initButtons() // for those buttons that depend on the model, like the yeefilter buttons + _objF(ui.menu,"select"); + + // run some extra stuff specified by the preset + if (ui.preset.update) { + ui.preset.update() + } + + + model.update() + + } + }; + l.load(sandbox.assets) + // use loader to save on bandwidth + // alternatively if we call the loader from all the places where we need the images, then we could just do + // s.update() + // for possibly greater speed, since we don't have to wait on the downloads. + + + + + + +} + +function Attach(ui) { + var self = this + + self.handleInputMain = function() { + // handles the input to sandbox() + // e.g. sandbox({idScript:"asdf",presetName:"sandbox"}) + // see main for more + + // Defaults + // a little help filling in the ui (should have been done in html as above) + if (ui == undefined || ui.preset == undefined || ui.preset.config == undefined || ui.presetName == undefined) { + loadpreset(ui) + } + ui.missingModelId = false + ui.danglingScript = false + if (ui.idModel == undefined) { + ui.idModel = "model-" + _rand5() + ui.missingModelId = true + + if (ui.idScript == undefined) { + ui.idScript = "script-" + _rand5() + ui.danglingScript = true + } + + } + if (ui.link) { + var regExp = /\(([^)]+)\)/; + var matches = regExp.exec(ui.link); + //matches[1] contains the value between the parentheses + ui.url = matches[1] + // https://stackoverflow.com/a/17779833 + } + ui.url = ui.url || window.location.href + + } + + function loadpreset (ui) { + + // if we don't already have a ui.presetName, generate one + // then look it up + + // ui.presetName: for the presets + // ui.idModel : for the divs. This is + + if(ui.quick != undefined) { + ui.presetName = ui.quick + ui.idModel = ui.quick + } + + // default presetName + if (ui.presetName == undefined ) { + ui.presetName = "sandbox" + // then we will end up skipping down to the bottom + } + + _lookupPreset(ui) + } + + + self.attachDOM = function(model) { + + // Here are two boolean variables to consider + // ui.missingModelId : needs a new parent div + // ui.danglingScript : can't be appended to a parent div, so will leave dangling + + // Here are the actual strings that these bools refer to + // ui.idModel : for the divs + // ui.idScript: for the divs + + ui.dom = {} + if (ui.missingModelId) { + _makeParentDivs() + } else { + ui.dom.basediv = document.querySelector("#" + model.id) + ui.dom.container = ui.dom.basediv.parentNode + + } + if (ui.uiType == "ballot") { + ui.dom.basediv.classList = "div-ballot div-model" + } else if (ui.uiType == "election-ballot") { + ui.dom.basediv.classList = "div-election div-ballot div-model" + } else if (ui.uiType == "election") { + ui.dom.basediv.classList = "div-election div-ballot-in-sandbox div-model" + } else if (ui.uiType == "sandbox") { + ui.dom.basediv.classList = "div-sandbox div-election div-ballot-in-sandbox div-model" + } + + ui.dom.container.classList = "contain-model" + + function _makeParentDivs() { + + // the model + var md = document.createElement('div'); + md.id = ui.idModel + + + // the contain-model + var cm = document.createElement('div'); + + // the script + if (ui.danglingScript) { + var pa = document.createElement('div'); + } else { + var sc = document.getElementById(ui.idScript); + var pa = sc.parentNode + } + + // connecting + pa.appendChild(cm); + cm.appendChild(md) + + ui.dom.basediv = md + ui.dom.container = cm + ui.dom.parent = pa + + // goes from this + // <div> + // <script id="idScript"> + // sandbox({idScript:"idScript",idModel:"idModel",presetName:"election3",uiType:"election"}) + // </script> + // <div> + + // to this + // <div class="contain-model"> + // <div id="idModel" class="div-sandbox div-election div-ballot-in-sandbox div-model" > + // </div> + // <script id="idScript"> + // sandbox({idScript:"idScript",idModel:"idModel",presetName:"election3",uiType:"election"}) + // </script> + // </div> + // https://stackoverflow.com/a/758683 + // via https://stackoverflow.com/a/1219857 + + // if id is not provided, then uses the generated id and leave the node dangling + + } + + } + self.detach = function() { + _removeSubnodes(ui.dom.basediv) + ui.dom.container.class = "" + } +} + +function bindModel(ui,model,config) { + + model.initPlugin = function(){ + + // LOAD + model.inSandbox = true + + // This "model.initPlugin()" launches the model + // So it is also useful as a template for everything that you might need to do after a button press. + + // CREATE + + if (config.candidatePositions) { + for(var i=0; i<config.candidatePositions.length; i++) model.candidates.push(new Candidate(model)) + } else { + for(var i=0; i<config.numOfCandidates; i++) model.candidates.push(new Candidate(model)) + } + + if (config.voterGroupTypes) { + for(var i=0; i<config.voterGroupTypes.length; i++) { + var vType = window[config.voterGroupTypes[i]] + var n = new vType(model) + model.voterGroups.push(n) + } + } else if (config.oneVoter) { + model.voterGroups.push(new SingleVoter(model)) + } else { + for(var i=0; i<config.numVoterGroups; i++) model.voterGroups.push(new GaussianVoters(model)) + } + model.voterCenter = new VoterCenter(model) + + // PRE CONFIGURE + model.dimensions = config.dimensions + + // CONFIGURE + // expand config to calculate some values to add to the model + // load expanded config into the model + // configure writes to model and reads from config. Sanity rule: configure does not read from model. + _objF(ui.menu,"configure") + ui.strategyOrganizer.configure() + // CONFIGURE DEFAULTS (model) + model.border = config.arena_border + model.HACK_BIG_RANGE = true; + // INIT + model.initDOM() + for (var i=0; i<model.candidates.length; i++) { + model.candidates[i].init() + } + model.initMODEL() + model.voterManager.initVoters() + _pileVoters(model) + model.dm.redistrict() + // INIT (menu) + ui.menu.presetconfig.init_sandbox() + // ui.menu.gearicon.init_sandbox() + ui.arena.desc.init_sandbox() + ui.arena.codeEditor.init_sandbox() + ui.arena.minusControl.init_sandbox() + ui.menu.theme.init_sandbox(); + // UPDATE + ui.menu_update() + ui.showHideSystems() + ui.strategyOrganizer.showOnlyStrategyForTypeOfSystem() + + }; + + model.onUpdate = function() { + + if (model.optionsForElection.sidebar ) { + handleRoundTransition() + } + } + + model.onDraw = function(){ + + // explanation boxes are drawn from bottom to top + + ui.redrawButtons() // make sure the icons show up + + if (model.optionsForElection.sidebar ) { + + sankeyDraw() + + weightChartsDraw() + + roundChartDraw() + + filterMapDraw() + + } + + ballotDraw() + + utilityDraw() + } + + function handleRoundTransition() { + + if (!model.checkSystemWithRoundButtons()) return + + if (model.roundCurrent == undefined) { + // initialize + model.roundCurrent = [] + model.flagFinalRound = [] + for (var i = 0; i < model.district.length; i++) { + var defaultFinal = true + if (defaultFinal) { + var maxRound = model.district[i].result.history.rounds.length + model.roundCurrent[i] = maxRound + model.flagFinalRound[i] = true + } else { + model.roundCurrent[i] = 0 + model.flagFinalRound[i] = false + } + } + } else { + // make sure round is in the right range + for (var i = 0; i < model.district.length; i++) { + var round = model.roundCurrent[i] + var maxRound = model.district[i].result.history.rounds.length + if (model.flagFinalRound[i]) round = maxRound // make sure round stays final. Final round is sticky. Stay in the final round. + round = Math.min(round, maxRound) + model.roundCurrent[i] = round + } + } + } + + function ballotDraw() { + + // CREATE A BALLOT + if (ui.dom.rightBallot) ui.dom.rightBallot.remove() // remove old one, if there was one + ui.dom.rightBallot = undefined + + // decide whether to draw the election explanation + var hideSidebar = ! model.optionsForElection.sidebar + + // decide whether to draw a ballot + var dragging = model.arena.mouse.dragging + if (model.arena.viewMan.active || dragging && dragging.isViewMan) { + var doDrawBallot = true + var voterPerson = model.arena.viewMan.focus + if (voterPerson == null) doDrawBallot = false + } else if (config.oneVoter && model.voterGroups[0].voterGroupType == "SingleVoter") { + hideSidebar = true + var doDrawBallot = true + var voterPerson = model.voterGroups[0].voterPeople[0] + } + + if (hideSidebar) { + _addClass(model.caption,"displayNoneClass") + } else { + _removeClass(model.caption,"displayNoneClass") + } + + if (hideSidebar && ! doDrawBallot) { + _addClass(ui.dom.right,"displayNoneClass") + } else { + _removeClass(ui.dom.right,"displayNoneClass") + } + + + if (doDrawBallot) { + + var doOldBallot = false + + if (doOldBallot) { + + var BallotType = model.BallotType + var ballot = new BallotType(model); + ui.dom.rightBallot = ballot.dom + ui.dom.right.prepend(ui.dom.rightBallot) + ballot.update(voterPerson.stages[model.stage].ballot); + + } else { + + var divBallot = document.createElement("div") + divBallot.className = "div-ballot-parent" + ui.dom.rightBallot = divBallot + ui.dom.right.prepend(ui.dom.rightBallot) + + var currentStage = model.stage + for (var stage of Object.keys(voterPerson.stages)) { + if (stage == "backup") continue // just show the real stages, not this one that I made as a temporary holding place + model.stage = stage + + var divStage = document.createElement("div") + divStage.className = "div-ballot" + divBallot.append(divStage) + + var divPaper = document.createElement("div") + divPaper.id = "paper" + divStage.append(divPaper) + + // text += model.voterGroups[0].voterModel.toTextV(voterPerson.stages[model.stage].ballot); + parts = model.voterGroups[0].voterModel.toTextV(voterPerson); + if (ui.minusControl == undefined) ui.minusControl = {} + if (ui.minusControl.ballotPart == undefined) ui.minusControl.ballotPart = [] + for (var i = 0; i < parts.length; i++) { + if (parts[i] == '') continue + var divPart = document.createElement("div") + divPart.style["margin-bottom"] = "10px" + divPart.innerHTML = parts[i] + divPaper.append(divPart) + if (ui.minusControl.ballotPart[i] == undefined) ui.minusControl.ballotPart[i] = {} + addMinusButtonC(divPart,ui.minusControl.ballotPart[i],{ballot:true}) + + } + + var target = divBallot + if (model.tallyEventsToAssign) { + for (let e of model.tallyEventsToAssign) { + target.querySelector("#" + e.eventID).addEventListener("mouseover", e.f) + target.querySelector("#" + e.eventID).addEventListener("mouseleave", ()=>model.draw()) + } + model.tallyEventsToAssign = undefined + } + } + model.stage = currentStage + } + } + } + + function sankeyDraw() { + var sankeyOn = ["IRV","STV"].includes(model.system) + + // do sankey if any districts have more than 1 person + sankeyOn = sankeyOn && model.district.map(x => x.voterPeople.length).some( x => x > 1) + + sankeyOn = sankeyOn && model.district[0].result.transfers + + if (! sankeyOn) { + if (ui.dom.sankey) ui.dom.sankey.remove() + ui.dom.sankey = undefined + return + } + + var haveCharts = (ui.dom.sankey != undefined) && ui.sankeyDistricts == model.district.length // we already have the number of charts we need. They're ready. + + // turning on + if (! haveCharts) { + + if (ui.sankey == undefined) { + ui.sankey = d3.sankey() + } + if (ui.dom.sankey) { + ui.dom.sankey.remove() + ui.dom.sankey = undefined + } + ui.dom.sankey = document.createElement("div") + + ui.dom.sankey.id = "chart" + + } + ui.dom.right.prepend(ui.dom.sankey) + + ui.sankeyDistricts = model.district.length + + ui.dom.sankey.innerHTML = '<div style="text-align:center;"><span class="small" > Sankey Diagram </span></div>' + + + if (ui.minusControl == undefined) ui.minusControl = {} + if (ui.minusControl.sankey == undefined) ui.minusControl.sankey = {} + addMinusButtonC(ui.dom.sankey,ui.minusControl.sankey) + + var noSankeys = true + + for (var district of model.district) { + + if (model.district.length > 1) { + ui.dom.sankey.innerHTML += `<div style="text-align:center;"><span class="small" > District ${district.i+1} </span></div>` + } + + if (district.voterPeople.length <= 1) continue + + // option + var doSpecialWinColor = true + + var sankey = ui.sankey + + + var numcans = district.candidates.length + var nodewidth = Math.max(10, Math.min(25, 100 / numcans)) // really this is node height, but the original code was horizontal, not vertical + + var outHeight = numcans * 50 + + var margin = {top: 10, right: 10, bottom: 10, left: 10} + var width = 220 - margin.left - margin.right // was 960 + var height = outHeight - margin.top - margin.bottom // was 500 + + sankey + .nodeWidth(nodewidth) // was 15 + .nodePadding(0) // was 10 + .size([width, height]); + + var svg = d3.select(ui.dom.sankey).append("svg") + .attr("width", width + margin.left + margin.right) + .attr("height", height + margin.top + margin.bottom) + .append("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + + + var formatNumber = d3.format(",.0f") + var format = function(d) { return formatNumber(d); } + var color = (cid) => model.candidatesById[cid].fill + var getName = (cid) => model.candidatesById[cid].name + var fPercent = (frac) => Math.round(100 * frac) + "%" + + + var path = sankey.link(); + + var dataSankey = getDataSankey(district) + // var dataSankey = getEnergyData() + + var noLinks = dataSankey.links.length == 0 + if (noLinks) continue + noSankeys = false + + sankey + .nodes(dataSankey.nodes) + .links(dataSankey.links) + .layout(32); // what is this? iterations + + var link = svg.append("g").selectAll(".link") + .data(dataSankey.links) + .enter().append("path") + .attr("class", "link") + .attr("d", path) + .style("stroke-width", function(d) { return Math.max(1, d.dy); }) + .style("stroke", function(d) { return d.source.color = (d.winner && doSpecialWinColor) ? "#fff" : color(d.source.cid); }) + .sort(function(a, b) { return b.dy - a.dy; }); + + link.append("title") + .text(function(d) { return getName(d.source.cid) + " → " + getName(d.target.cid) + "\n" + fPercent(d.value/d.numBallots); }); + // title is an SVG standard way of providing tooltips, up to the browser how to render this, so changing the style is tricky + + var node = svg.append("g").selectAll(".node") + .data(dataSankey.nodes) + .enter().append("g") + .attr("class", "node") + .attr("transform", function(d) { + return "translate(" + d.x + "," + d.y + ")"; + }) + .call(d3.behavior.drag() + .origin(function(d) { return d; }) + .on("dragstart", function() { this.parentNode.appendChild(this); }) + .on("drag", dragmove)); + + var rectNode = node.append("rect") + .attr("height", sankey.nodeWidth()) + .attr("width", function(d) { return d.dy; }) + .style("fill", function(d) { return d.color = color(d.cid); }) + .style("stroke", function(d) { return d3.rgb(d.color).darker(2); }); + + rectNode.append("title") + .text(function(d) { return getName(d.cid) + "\n" + fPercent(d.value/d.numBallots); }); + + node.append("text") + .attr("transform", function(d) { + return "translate(" + d.dy/2 + "," + nodewidth/2 + ")"; + }) + .attr("dominant-baseline","middle") + .attr("text-anchor","middle") + .text(function(d) { return (d.winner) ? "win" : ""; }) + // .style("fill", "#555") + .style("opacity", "50%") + .style("font-size", nodewidth*.9); + /* + node.append("text") + .attr("text-anchor", "middle") + //.attr("transform", "rotate(-20)") + .attr("x", function (d) { return d.dy / 2 }) + .attr("y", sankey.nodeWidth() / 2) + .attr("dy", ".35em") + .text(function(d) { return d.name; }) + //.text(function(d) { if(d.name.length > 8) { return d.name.substring(0, 5) + "..."; } else return d.name; }) + .filter(function(d) { return d.x < width / 2; }); + */ + + function dragmove(d) { + //d3.select(this).attr("transform", "translate(" + d.x + "," + (d.y = Math.max(0, Math.min(height - d.dy, d3.event.y))) + ")"); + d3.select(this).attr("transform", "translate(" + (d.x = Math.max(0, Math.min(width - d.dy, d3.event.x))) + "," + d.y + ")"); + sankey.relayout(); + link.attr("d", path); + } + + + + } + + if (noSankeys) { + ui.dom.sankey.remove() + ui.dom.sankey = undefined + } + + }; + + function getDataSankey(district) { + var nodes = [] + var links = [] + + // each transfer is referred to by transfers [ round ] [ transfer index ] + // we use ids + // transfer.from + // transfer.flows [ to ] + // transfer.flows [ to ] [ first ] -- also shows who was the first choice + + var coalitions = district.result.coalitions + var tallies = district.result.tallies + var continuing = district.result.continuing + var transfers = district.result.transfers + var numRounds = transfers.length + var numBallots = district.voterPeople.length + + var winnersContinue = model.system == "STV" && 1 + if (winnersContinue)var won = district.result.won + if (winnersContinue) var quotaAmount = numBallots / (model.seats + 1) + + + // function to find the sorted position of the candidate + var xpos = cid => model.tarena.modelToArena(model.candidatesById[cid]).x + // var listOfCandidates = Object.keys(transfers[0][0].flows) + // listOfCandidates.push(transfers[0][0].from) + var listOfCandidates = district.candidates.map(c => c.id) + // listOfCandidates.sort((a,b) => a.length - b.length) + var xc = {} + for (var cid of listOfCandidates) { + xc[cid] = xpos(cid) + } + + + // make ids and lookup tool + var idx = 0 + var lookup = {} + for (var rid = 0; rid <= numRounds; rid ++) { + lookup[rid] = {} + if (rid == 0) { + var useList = _jcopy(listOfCandidates) + } else { + var useList = _jcopy(continuing[rid-1]) // continuing from last round + if (winnersContinue) useList = useList.concat(won[rid-1]) + } + useList.sort( (a,b) => xc[a] - xc[b] ) + for( var k = 0; k < useList.length; k++) { + var cid = useList[k] + lookup[rid][cid] = idx + var node = {name:rid + "_" + cid,round:rid,cid:cid,numBallots:numBallots} + if (winnersContinue) { + if (won[rid] && won[rid].includes(cid)) node.winner = true + } else { + if (rid == numRounds) { //last round + if (district.result.winners.includes(cid)) node.winner = true + } + } + nodes.push(node) + idx ++ + } + } + + + for (var rid = 0; rid < numRounds; rid ++) { + var round = transfers[rid] + // var unTransferred = _jcopy(listOfCandidates) + for (var transfer of round) { + var from = transfer.from + // delete unTransferred[from] + for (var to in transfer.flows) { + var lfrom = lookup[rid][from] + var lto = lookup[rid+1][to] + var v = 0 + var allfirst = transfer.flows[to] + for (var first of Object.keys(allfirst) ) { + v += allfirst[first] + } + if (v > 0) { + var link = {"source":lfrom,"target":lto,"value":v,numBallots:numBallots} + links.push(link) + } + + } + } + for (var cid of continuing[rid]) { + var lfrom = lookup[rid][cid] + var lto = lookup[rid+1][cid] + var v = tallies[rid][cid] + if (v > 0) { + var link = {"source":lfrom,"target":lto,"value":v,numBallots:numBallots} + links.push(link) + } + + } + if (winnersContinue) { + for (var cid of won[rid]) { + var lfrom = lookup[rid][cid] + var lto = lookup[rid+1][cid] + var v = quotaAmount + var link = {"source":lfrom,"target":lto,"value":v,"winner":true,numBallots:numBallots} + links.push(link) + } + } + // if (v == 0 && stv && rid > 0 && won[rid-1].includes(from) ) { // you won, and you're not on the tally + // v = quotaAmount + // } + } + + + var data = {nodes:nodes,links:links} + return data + } + + function roundChartDraw() { + + + var roundChartOn = ["IRV","STV"].includes(model.system) + + // turning off + if (! roundChartOn) { + if (ui.dom.roundChart) ui.dom.roundChart.remove() + ui.dom.roundChart = undefined + return + } + + // hmm, might have a problem with changing number of districts. + + var haveCharts = (ui.dom.roundChart != undefined) && ui.roundChartDistricts == model.district.length // we already have the number of charts we need. They're ready. + + // turning on + if (haveCharts) { // we already have the number of charts we need. They're ready. + // nothing + } else { + + // if we're updating the number, remove the old charts + if (ui.dom.roundChart) { + ui.dom.roundChart.remove() + ui.dom.roundChart = undefined + } + + // set up container dom for chart + ui.roundChartDistricts = model.district.length + + ui.dom.roundChart = document.createElement("div") + ui.dom.roundChart.id = "chart" + + ui.dom.roundChart.innerHTML += '<div style="text-align:center;"><span class="small" > Votes by Round </span></div>' + + if (ui.minusControl == undefined) ui.minusControl = {} + if (ui.minusControl.roundChart == undefined) ui.minusControl.roundChart = {} + addMinusButtonC(ui.dom.roundChart,ui.minusControl.roundChart) + + ui.dom.roundChartRoundNumText = [] + ui.dom.roundChartBackButton = [] + ui.dom.roundChartForwardButton = [] + + ui.dom.roundChartSpace = [] + ui.dom.roundChartCaption = [] + ui.dom.roundChartPreCaption = [] + ui.dom.roundChartPreCaptionOuter = [] + for (var i = 0; i < model.district.length; i++) { + if (model.district.length > 1) { + var title = document.createElement("div") + // title.innerText = `District ${i+1}` + title.innerHTML = `<div style="text-align:center;"><span class="small" > District ${i+1} </span></div>` + ui.dom.roundChart.append(title) + // don't use innerHTML on ui.dom.roundChart here. It will cause problems. The bindings to variables will be lost. + // if (model.district.length > 1) ui.dom.roundChart.innerHTML += `<div style="text-align:center;"><span class="small" > District ${i+1} </span></div>` + } + + ui.dom.roundChartPreCaptionOuter[i] = document.createElement("div") + ui.dom.roundChartPreCaptionOuter[i].setAttribute("style","font-size: 12px; margin-left:10px; margin-top: 10px; margin-bottom: 10px;") + ui.dom.roundChartPreCaption[i] = document.createElement("span") + ui.dom.roundChartPreCaption[i].className = "xsmall" + ui.dom.roundChartPreCaptionOuter[i].append(ui.dom.roundChartPreCaption[i]) + ui.dom.roundChart.append(ui.dom.roundChartPreCaptionOuter[i]) + + var buttonDiv = document.createElement("div") + buttonDiv.setAttribute("style","margin-left:10px;") + ui.dom.roundChart.append(buttonDiv) + ui.dom.roundChartBackButton[i] = document.createElement("button") + ui.dom.roundChartBackButton[i].innerText = " < " + ui.dom.roundChartBackButton[i].className = "roundChartButton" + buttonDiv.append(ui.dom.roundChartBackButton[i]) + + ui.dom.roundChartRoundNumText[i] = document.createElement("span") + ui.dom.roundChartRoundNumText[i].className = "small" + buttonDiv.append(ui.dom.roundChartRoundNumText[i]) + // var roundNumberText = document.createTextNode(` Round x `); + // ui.dom.roundChart.appendChild(roundNumberText) + ui.dom.roundChartForwardButton[i] = document.createElement("button") + ui.dom.roundChartForwardButton[i].innerText = " > " + ui.dom.roundChartForwardButton[i].className = "roundChartButton" + buttonDiv.append(ui.dom.roundChartForwardButton[i]) + ui.dom.roundChartSpace[i] = document.createElement("div") + ui.dom.roundChart.append(ui.dom.roundChartSpace[i]) + ui.dom.roundChartCaption[i] = document.createElement("div") + var captionMinHeight = (model.customNames == "Yes") ? 4 : 2 + ui.dom.roundChartCaption[i].setAttribute("style",`margin-left:10px;min-height:${captionMinHeight}em;`) + ui.dom.roundChart.append(ui.dom.roundChartCaption[i]) + } + + } + ui.dom.right.prepend(ui.dom.roundChart) + + var gFlag = model.googleLoaded !== undefined + if (!haveCharts || !gFlag) { + // set up chart + + // Load the Visualization API and the corechart package. + // google.charts.load('current', {'packages':['corechart']}); + google.charts.load('49', {'packages':['corechart']}); + + // Set a callback to run when the Google Visualization API is loaded. + ui.roundChart = [] + for (var i = 0; i < model.district.length; i++) { + + google.charts.setOnLoadCallback( (function(a) { return function() { model.googleLoaded = true; instantiateThenDrawRoundChart(a) }})(i) ) + } + + } else { + // update chart + for (var i = 0; i < model.district.length; i++) { + actualRoundChartDraw(i) + } + } + + } + + function enforceFlagFinalRound(i) { + // keep set to final round, even if the number of rounds changed... TODO + if (model.flagFinalRound[i]) { + model.roundCurrent[i] = district.result.history.rounds.length + } + } + + function instantiateThenDrawRoundChart(i) { + + // Instantiate chart + ui.roundChart[i] = new google.visualization.BarChart(ui.dom.roundChartSpace[i]); + + // Disabling the button while the chart is drawing. + getButtonReady(ui.dom.roundChartBackButton[i],i) + getButtonReady(ui.dom.roundChartForwardButton[i],i) + + ui.dom.roundChartBackButton[i].onclick = (function(i) { return function() { + var district = model.district[i] + model.roundCurrent[i] -- + if (model.roundCurrent[i] < 0) model.roundCurrent[i] = 0 + model.flagFinalRound[i] = false + ui.roundUpdateSet(i) + }})(i) + ui.dom.roundChartForwardButton[i].onclick = (function(i) { return function() { + var district = model.district[i] + model.roundCurrent[i] ++ + if (model.system == "IRV") { + var maxRound = district.result.tallies.length + } else { + var maxRound = district.result.history.rounds.length + } + if (model.roundCurrent[i] > maxRound) model.roundCurrent[i] = maxRound + if (model.roundCurrent[i] == maxRound) model.flagFinalRound[i] = true + ui.roundUpdateSet(i) + }})(i) + + + function getButtonReady(button,i) { + button.disabled = true; + google.visualization.events.addListener(ui.roundChart[i], 'ready', function() { + button.disabled = false; + }); + } + + actualRoundChartDraw(i) + + + } + + function actualRoundChartDraw(iDistrict, opt) { + + opt = opt || {} + opt.ease = opt.ease || false + + opt.showCoalitions = true + + // for (var district of model.district) { + + // + var district = model.district[iDistrict] + var round = model.roundCurrent[iDistrict] + var coalitionInRound = district.result.coalitionInRound + + // future + // ui.dom.roundChartCaption[i].innerHTML = district.result.roundText[round+1] + + if (model.system == "IRV") { // TODO: add result.history.rounds to IRV + var maxRound = district.result.tallies.length + } else { + var maxRound = district.result.history.rounds.length + } + if (round == maxRound) { + var rt = ` Final ` + var flagF = true + round -- // temporary bandage TODO: handle final totals better. right now round 1 annotations are displayed in round 2. + } else { + var rt = ` Round ${round + 1} ` + } + ui.dom.roundChartRoundNumText[iDistrict].innerText = rt + + var dataSankey = getDataSankey(district) + + // get list of all candidates in the same order as the sankey nodes + var nodes0 = dataSankey.nodes.filter( x => x.round == 0) + var cids = nodes0.map( x => x.cid) + + // Create the data table. + var data = new google.visualization.DataTable(); + data.addColumn('string', 'Candidate'); + if (opt.showCoalitions) { + for ( var i = 0; i < cids.length; i++) { + data.addColumn('number', 'Votes'); + } + } + data.addColumn('number', 'Votes'); + data.addColumn({ type:'string', role: 'annotation' }) + data.addColumn({ type:'string', role: 'style' }) + data.addColumn('number', 'Votes'); + data.addColumn({ type:'string', role: 'annotation' }) + data.addColumn({ type:'string', role: 'style' }) + + var color = (cid) => model.candidatesById[cid].fill + var getName = (cid) => model.candidatesById[cid].name + var fPercent = (frac) => Math.round(100 * frac) + "%" + + + var lookup = [] + var rows = [] + for ( var i = 0; i < cids.length; i++) { + var cid = cids[i] + lookup[cid] = i + } + + var numBallots = dataSankey.nodes[0].numBallots + var quotaAmount = numBallots / (model.seats + 1) + + // won : list of all candidates that have won at the end of the round + // continuing : list of candidates still continuing to be tallied at the end of the round + if (district.result.won) { // for STV, not IRV + var won = district.result.won[round] + } else { + if (round == maxRound - 1) { + var won = district.result.winners + } else { + var won = [] + } + } + var continuing = district.result.continuing[round] + if (continuing == undefined) continuing = [] + + if (opt.showCoalitions) { + var eliminationOrder = _jcopy(district.result.loserslist)//.reverse() + if (district.result.loserslist.length < district.candidates.length) { + eliminationOrder = eliminationOrder.concat(_jcopy(district.result.winners).reverse()) + for ( var i = 0; i < cids.length; i++) { + var cid = cids[i] + if ( ! eliminationOrder.includes(cid)) { + eliminationOrder.push(cid) + } + } + } + } + + for ( var i = 0; i < cids.length; i++) { + var cid = cids[i] + var name = getName(cid) + var num = district.result.tallies[round][cid] + if (num === undefined) { + if (won.includes(cid)) { + num = quotaAmount + } else { + num = 0 + } + } + var frac = num / numBallots + var barColor = hslToHex(color(cid)) + if (won.includes(cid)) { + var annotation = "✔️" + } else if (continuing.includes(cid)) { + var annotation = "----" + } else { + var annotation = "✖️" + } + var idx = lookup[cid] + if (opt.showCoalitions) { + var coal = coalitionInRound[round][cid] + if (coal) { + var fracs100 = eliminationOrder.map( x => coal[x] / numBallots * 100) + } else { + if (won.includes(cid)) { + // find round in which they won + var r + for (r = round; coalitionInRound[r][cid] == undefined; r--) { + // keep going + } + coal = coalitionInRound[r][cid] + var fracs100 = eliminationOrder.map( x => coal[x] / numBallots * 100) + var sum = fracs100.reduce( (p,c) => p+c) + var normalize = quotaAmount / (sum / 100 * numBallots) + fracs100 = fracs100.map( x => x * normalize) + } else { + var fracs100 = eliminationOrder.map( () => 0) + } + } + rows[idx] = [name, ...fracs100 ,0,annotation,barColor, 0, "  " + name,barColor] + } else { + rows[idx] = [name,frac*100,annotation,barColor, 0, "  " + name,barColor] + } + } + data.addRows(rows); + + var formatter = new google.visualization.NumberFormat( + {suffix: '%', fractionDigits:0}); + formatter.format(data, 1); // Apply formatter to second column + + // Set chart options + var options = { + "height": 20 * rows.length, + width: 200, + fontSize: 13, + chartArea: { + left: 10, + top: 10, + bottom: 10, + right: 10, + }, + legend: { position: 'none' }, + hAxis: { + minValue: 0, + maxValue: 100, + ticks: [0,10,20,30,40,50,60,70,80,90,100], + gridlines: { + color: '#eee' + }, + + }, + bar: {groupWidth: '100%'}, + annotations: { + // alwaysOutside: true, + textStyle: { + fontSize: 15, + auraColor: 'none', + bold: true, + }, + }, + isStacked: true, + } + + if (opt.showCoalitions) { + var allBarColors = eliminationOrder.map( (cid) => hslToHex(color(cid))) + allBarColors.push('#000') + options.colors = allBarColors + } + + if (opt.ease) { + options.animation = { + duration: 700, + easing: 'out', + } + } + + // draw our chart, passing in some options. + ui.roundChart[iDistrict].draw(data, options); + + // caption + + if (flagF) { + var roundText = district.result.history.afterFinalRound.finalText + } else { + var roundText = district.result.history.rounds[round].roundText + } + roundText = `<span class="small" > ${roundText} </span>` + if (model.placeHolding || model.doPlaceHoldDuringElection) { + if (model.nLoading > 0) { + // will do on next draw + return + } else { + // ready to replace + ui.dom.roundChartCaption[iDistrict].innerHTML = model.replacePlaceholder(roundText) + } + } else { + ui.dom.roundChartCaption[iDistrict].innerHTML = roundText + } + + var startText = model.district[iDistrict].result.history.startText + // startText = startText.replace("/<br>/g","\n") + ui.dom.roundChartPreCaption[iDistrict].innerHTML = startText + + } + + function utilityDraw() { + // make a google chart + + // each data series is a candidate's utility value over all the voters + + + // turning off + if (! model.showUtilityChart) { + if (ui.dom.utilityChart) { + ui.dom.utilityChart.remove() + ui.dom.utilityChart = undefined + } + return + } + + // hmm, might have a problem with changing number of districts. + + var haveCharts = (ui.dom.utilityChart != undefined) && ui.utilityChartDistricts == model.district.length // we already have the number of charts we need. They're ready. + + // turning on + if (haveCharts) { // we already have the number of charts we need. They're ready. + // nothing + } else { + + // if we're updating the number, remove the old charts + if (ui.dom.utilityChart) { + ui.dom.utilityChart.remove() + ui.dom.utilityChart = undefined + } + + // set up container dom for chart + ui.utilityChartDistricts = model.district.length + + ui.dom.utilityChart = document.createElement("div") + ui.dom.utilityChart.id = "chart" + + ui.dom.utilityChart.innerHTML += '<div style="text-align:center;"><span class="small" > Utility </span></div>' + + if (ui.minusControl == undefined) ui.minusControl = {} + if (ui.minusControl.utilityChart == undefined) ui.minusControl.utilityChart = {} + addMinusButtonC(ui.dom.utilityChart,ui.minusControl.utilityChart) + + ui.dom.utilityChartRoundNumText = [] + ui.dom.utilityChartBackButton = [] + ui.dom.utilityChartForwardButton = [] + + ui.dom.utilityChartSpace = [] + ui.dom.utilityChartSpace2 = [] + ui.dom.utilityChartCaption = [] + ui.dom.utilityChartCaptionOuter = [] + ui.dom.utilityChartCaptionButton = [] + for (var i = 0; i < model.district.length; i++) { + if (model.district.length > 1) { + var title = document.createElement("div") + title.innerHTML = `<div style="text-align:center;"><span class="small" > District ${i+1} </span></div>` + ui.dom.utilityChart.append(title) + } + ui.dom.utilityChartSpace[i] = document.createElement("div") + ui.dom.utilityChart.append(ui.dom.utilityChartSpace[i]) + ui.dom.utilityChartSpace2[i] = document.createElement("div") + ui.dom.utilityChart.append(ui.dom.utilityChartSpace2[i]) + ui.dom.utilityChartCaption[i] = document.createElement("span") + ui.dom.utilityChartCaption[i].className = "small" + ui.dom.utilityChartCaptionOuter[i] = document.createElement("div") + ui.dom.utilityChart.append(ui.dom.utilityChartCaptionOuter[i]) + ui.dom.utilityChartCaptionOuter[i].append(ui.dom.utilityChartCaption[i]) + + ui.dom.utilityChartCaptionButton[i] = document.createElement("button") + ui.dom.utilityChartCaptionButton[i].className = "roundChartButton" + ui.dom.utilityChartCaptionButton[i].innerText = "Reset VSE Average" + ui.dom.utilityChartCaptionButton[i].onmouseup = function() { + model.totalNumVSEData = 0 + model.averageVSE = 0 + console.log("clicked") + } + model.totalNumVSEData = 0 + model.averageVSE = 0 + ui.dom.utilityChartCaptionOuter[i].append(ui.dom.utilityChartCaptionButton[i]) + + } + + } + ui.dom.right.prepend(ui.dom.utilityChart) + + var gFlag = model.googleLoaded !== undefined + if (!haveCharts || !gFlag) { + // set up chart + + // Load the Visualization API and the corechart package. + // google.charts.load('current', {'packages':['corechart']}); + google.charts.load('49', {'packages':['corechart']}); + + // Set a callback to run when the Google Visualization API is loaded. + ui.utilityChart = [] + ui.utilityChartAverage = [] + + for (var i = 0; i < model.district.length; i++) { + + google.charts.setOnLoadCallback( (function(a) { return function() { model.googleLoaded = true; instantiateThenDrawUtilityChart(a) }})(i) ) + } + + } else { + // update chart + for (var i = 0; i < model.district.length; i++) { + actualUtilityChartDraw(i) + } + } + + } + + function instantiateThenDrawUtilityChart(i) { + + // Instantiate chart + ui.utilityChart[i] = new google.visualization.LineChart(ui.dom.utilityChartSpace[i]); + ui.utilityChartAverage[i] = new google.visualization.BarChart(ui.dom.utilityChartSpace2[i]) + + actualUtilityChartDraw(i) + + + } + + function actualUtilityChartDraw(iDistrict, opt) { + + opt = opt || {} + + + var district = model.district[iDistrict] + + + + // candidates + var cans = district.stages[model.stage].candidates + if (model.stage == "primary") { + var district = model.district[voterPerson.iDistrict] + cans = district.parties[voterPerson.iParty].candidates + } + + // Create the data table. + var data = new google.visualization.DataTable(); + data.addColumn('number', 'Voter ID'); + cans.forEach( c => data.addColumn('number', c.name) ) + var doMax = true + if (doMax) { + data.addColumn('number', "Max Utility of Winners") + } + + var optDist = {dontSort: true, noBallot:true} + + var rows = [] + var i = 0 + + if (doMax) { + var winnerCans = district.candidates.filter(c => district.result.winners.includes(c.id)) + var winnerIndices = winnerCans.map( c => c.i ) + var rowsPlus = [] + } + + if (model.orderOfVoters) { + var vPeople = [] + var v = model.voterSet.getVoterArray() + for ( var i = 0; i < model.orderOfVoters.length; i++ ) { + var idx = model.orderOfVoters[i] + vPerson = v[idx] + if (vPerson.iDistrict == iDistrict) { + vPeople.push(vPerson) + } + } + } else { + return + // var vPeople = district.voterPeople + } + + for (let voterPerson of vPeople) { + // // voters + // var voterAtStage = voterPerson.stages[model.stage] + + + // // distances + // var distList = makeDistList(model,voterPerson,voterAtStage,cans,optDist) + + // distances + var distList = makeDistList(model,voterPerson,null,cans,optDist) + voterPerson.distList = distList // just pass it along.. maybe do this part better + var utilities = distList.map( c => c.uNorm ) + if (doMax) { + var winnerUtilities = utilities.filter( (x,idx) => winnerIndices.includes(idx)) + var maxUtility = winnerUtilities.reduce( (a,b) => Math.max(a, b) ) + } + utilities.unshift(i) + rows.push(_jcopy(utilities)) + if (doMax) { + utilities.push(maxUtility) + rowsPlus.push(utilities) + } + i++ + } + + + // var voters = model.getSortedVoters() + // for ( var i = 0; i < voters.length; i++) { + + // } + + var color = (cid) => model.candidatesById[cid].fill + + var seriesColors = [] + for (let c of cans) { + seriesColors.push({color: hslToHex(color(c.id))}) + } + if (doMax) { + seriesColors.push({color: "#555", pointSize:2, lineWidth:0}) // max chosen + } + // seriesColors = cans.map( c => { color:hslToHex(color(c.id)) } ) + + // var getName = (cid) => model.candidatesById[cid].name + // var fPercent = (frac) => Math.round(100 * frac) + "%" + + if (doMax) { + data.addRows(rowsPlus); + } else { + data.addRows(rows); + } + + + // var formatter = new google.visualization.NumberFormat( + // {suffix: '%', fractionDigits:0}); + // formatter.format(data, 1); // Apply formatter to second column + + // Set chart options + var options = { + height: 200, + width: 200, + fontSize: 13, + chartArea: { + left: 10, + top: 10, + bottom: 10, + right: 10, + }, + legend: { position: 'none' }, + series: seriesColors, + hAxis: { + gridlines: { + color: '#fff' + }, + }, + vAxis: { + gridlines: { + color: '#fff' + }, + }, + } + + + // draw our chart, passing in some options. + ui.utilityChart[iDistrict].draw(data, options); + // ui.utilityChart.draw(view, options); + + // do an average over the rows + sums = rows[0].map( () => 0) // zeros + for ( var i = 0; i < rows.length; i++) { + for ( var j = 0; j < rows[i].length; j++) { + sums[j] += rows[i][j] + } + } + // remove the first column + sums.shift() + // average + var avg = sums.map( (x) => x / rows.length) + + + // Create the data table. + var dataAverage = new google.visualization.DataTable(); + dataAverage.addColumn('string', 'Candidate'); + dataAverage.addColumn('number', 'Utility'); + dataAverage.addColumn({ type:'string', role: 'style' }) + dataAverage.addColumn({ type:'string', role: 'annotation' }) + + var color = (cid) => model.candidatesById[cid].fill + var getName = (cid) => model.candidatesById[cid].name + var fPercent = (frac) => Math.round(100 * frac) + "%" + + rows = [] + for (var idx = 0; idx < cans.length; idx ++) { + var c = cans[idx] + var barColor = hslToHex(color(c.id)) + rows[idx] = [c.name,avg[idx] * 100,barColor,c.name] + } + dataAverage.addRows(rows); + + var formatter = new google.visualization.NumberFormat( + {suffix: '', fractionDigits:0}); + formatter.format(dataAverage, 1); // Apply formatter to second column + + // Set chart options + var options = { + "height": 20 * rows.length, + width: 200, + fontSize: 13, + chartArea: { + left: 10, + top: 10, + bottom: 10, + right: 10, + }, + legend: { position: 'none' }, + hAxis: { + minValue: 0, + maxValue: 100, + ticks: [0,10,20,30,40,50,60,70,80,90,100], + gridlines: { + color: '#eee' + }, + + }, + bar: {groupWidth: '100%'}, + annotations: { + // alwaysOutside: true, + textStyle: { + fontSize: 15, + auraColor: 'none', + bold: true, + }, + }, + } + + // draw our chart, passing in some options. + + + ui.utilityChartAverage[iDistrict].draw(dataAverage, options); + + // write out the utility + // var winneridx = model.district[iDistrict].result.winners[0] + var winneridx = winnerIndices[0] + var averageUtility = 1 / avg.length * avg.reduce( (p,c) => p + c) + var maxUtility = avg.reduce( (p,c) => Math.max(p , c) ) + var winUtility = avg[winneridx] + var vse = ( winUtility - averageUtility ) / ( maxUtility - averageUtility ) + + if (winnerIndices.length == 1) { // ties aren't good data points right now, but maybe later. + var frac = model.totalNumVSEData / (model.totalNumVSEData+1) + model.averageVSE = vse * (1-frac) + model.averageVSE * frac + model.totalNumVSEData ++ + } + + var vseText = `VSE of winner is ${Math.round(vse*100)} %. \n Average VSE of ${model.totalNumVSEData} past Elections is ${Math.round(model.averageVSE*100)} %.\n` + ui.dom.utilityChartCaption[iDistrict].innerText = vseText + } + + function weightChartsDraw() { + + var weightChartsOn = model.checkDoMultiWinnerBarCharts() + + // turning off + if (! weightChartsOn) { + if (ui.dom.weightCharts) ui.dom.weightCharts.remove() + ui.dom.weightCharts = undefined + return + } + + var old = ui.justFinal + ui.justFinal = (model.system == "equalFacilityLocation" || model.system == "PAV") // only show the final assignments + var matchOpt = old == ui.justFinal + + // redo charts when changing number of districts. + var matchDist = ui.weightChartsDistricts == model.district.length + + var haveCharts = (ui.dom.weightCharts != undefined) && matchDist && matchOpt // we already have the number of charts we need. They're ready. + + // turning on + if (haveCharts) { // we already have the number of charts we need. They're ready. + // nothing + } else { + + // if we're updating the number, remove the old charts + if (ui.dom.weightCharts) ui.dom.weightCharts.remove() + ui.dom.weightCharts = undefined + + // set up container dom for chart + ui.weightChartsDistricts = model.district.length + + ui.dom.weightCharts = document.createElement("div") + ui.dom.weightCharts.id = "chart" + + if (ui.justFinal) { + ui.dom.weightCharts.innerHTML += '<div style="text-align:center;"><span class="small" > Voter Weight by Candidate</span></div>' + } else { + ui.dom.weightCharts.innerHTML += '<div style="text-align:center;"><span class="small" > Voter Weight by Round</span></div>' + } + + if (ui.minusControl == undefined) ui.minusControl = {} + if (ui.minusControl.weightCharts == undefined) ui.minusControl.weightCharts = {} + addMinusButtonC(ui.dom.weightCharts,ui.minusControl.weightCharts) + + ui.dom.weightChartsRoundNumText = [] + ui.dom.weightChartsBackButton = [] + ui.dom.weightChartsForwardButton = [] + + ui.dom.weightChartsSpace = [] + ui.dom.weightChartsCaption = [] + ui.dom.weightChartsPreCaption = [] + for (var i = 0; i < model.district.length; i++) { + if (model.district.length > 1) { + var title = document.createElement("div") + // title.innerText = `District ${i+1}` + title.innerHTML = `<div style="text-align:center;"><span class="small" > District ${i+1} </span></div>` + ui.dom.weightCharts.append(title) + // don't use innerHTML on ui.dom.weightCharts here. It will cause problems. The bindings to variables will be lost. + // if (model.district.length > 1) ui.dom.weightCharts.innerHTML += `<div style="text-align:center;"><span class="small" > District ${i+1} </span></div>` + } + ui.dom.weightChartsPreCaption[i] = document.createElement("div") + ui.dom.weightChartsPreCaption[i].setAttribute("style","font-size: 12px; margin-left:10px; margin-top: 10px; margin-bottom: 10px;") + ui.dom.weightChartsPreCaption[i].innerHTML = `<span class="xsmall" > Sort the voters by similarity. <br> Show candidates near closest voter. <br> Show the weight of each vote.</span>` + ui.dom.weightCharts.append(ui.dom.weightChartsPreCaption[i]) + var buttonDiv = document.createElement("div") + buttonDiv.setAttribute("style","margin-left:10px;") + ui.dom.weightCharts.append(buttonDiv) + ui.dom.weightChartsBackButton[i] = document.createElement("button") + ui.dom.weightChartsBackButton[i].innerText = " < " + ui.dom.weightChartsBackButton[i].className = "weightChartsButton" + buttonDiv.append(ui.dom.weightChartsBackButton[i]) + + ui.dom.weightChartsRoundNumText[i] = document.createElement("span") + ui.dom.weightChartsRoundNumText[i].className = "small" + buttonDiv.append(ui.dom.weightChartsRoundNumText[i]) + // var roundNumberText = document.createTextNode(` Round x `); + // ui.dom.weightCharts.appendChild(roundNumberText) + ui.dom.weightChartsForwardButton[i] = document.createElement("button") + ui.dom.weightChartsForwardButton[i].innerText = " > " + ui.dom.weightChartsForwardButton[i].className = "weightChartsButton" + buttonDiv.append(ui.dom.weightChartsForwardButton[i]) + ui.dom.weightChartsSpace[i] = document.createElement("div") + ui.dom.weightCharts.append(ui.dom.weightChartsSpace[i]) + ui.dom.weightChartsCaption[i] = document.createElement("div") + ui.dom.weightCharts.append(ui.dom.weightChartsCaption[i]) + } + + } + ui.dom.right.prepend(ui.dom.weightCharts) // yes, prepend every time so that we get the order right... maybe not the best solution but it works + + if (!haveCharts) { + // set up chart + + // Load the Visualization API and the corechart package. + // google.charts.load('current', {'packages':['corechart']}); + // google.charts.load('49', {'packages':['corechart']}); + + // Set a callback to run when the Google Visualization API is loaded. + ui.weightCharts = [] + for (var i = 0; i < model.district.length; i++) { + instantiateThenDrawWeightCharts(i) + } + + } else { + // update chart + for (var i = 0; i < model.district.length; i++) { + actualWeightChartsDraw(i) + } + } + + } + + function instantiateThenDrawWeightCharts(i) { + + // Instantiate chart + + var canvas = document.createElement('canvas') + var ctx = canvas.getContext('2d') + canvas.height = 130 + canvas.width = 600 + canvas.style = "height: 43px; width:200px; margin-left: 10px; margin-top: 10px;" + ui.dom.weightChartsSpace[i].append(canvas) + + var canvasW = document.createElement('canvas') + var ctxW = canvasW.getContext('2d') + canvasW.height = 300 + canvasW.width = 600 + canvasW.style = "height: 100px; width:200px; margin-left: 10px;" + ui.dom.weightChartsSpace[i].append(canvasW) + + var canvasU = document.createElement('canvas') + var ctxU = canvasU.getContext('2d') + canvasU.height = 201 + canvasU.width = 600 + canvasU.style = "height: 67px; width:200px; margin-left: 10px;" + ui.dom.weightChartsSpace[i].append(canvasU) + + var canvasP = document.createElement('canvas') + var ctxP = canvasP.getContext('2d') + canvasP.height = 201 + canvasP.width = 600 + canvasP.style = "height: 67px; width:200px; margin-left: 10px;" + ui.dom.weightChartsSpace[i].append(canvasP) + + var canvasS = document.createElement('canvas') + var ctxS = canvasS.getContext('2d') + canvasS.height = 201 + canvasS.width = 600 + canvasS.style = "height: 67px; width:200px; margin-left: 10px;" + ui.dom.weightChartsSpace[i].append(canvasS) + + var canvasWK = document.createElement('canvas') + var ctxWK = canvasWK.getContext('2d') + canvasWK.height = 300 + canvasWK.width = 600 + canvasWK.style = "height: 100px; width:200px; margin-left: 10px;" + ui.dom.weightChartsSpace[i].append(canvasWK) + + var canvasK = document.createElement('canvas') + var ctxK = canvasK.getContext('2d') + canvasK.height = 201 + canvasK.width = 600 + canvasK.style = "height: 67px; width:200px; margin-left: 10px;" + ui.dom.weightChartsSpace[i].append(canvasK) + + + ui.weightCharts[i] = {arena: {canvas:canvas, ctx:ctx} , arenaP: {canvas:canvasP, ctx:ctxP} , arenaU: {canvas:canvasU, ctx:ctxU} , arenaW: {canvas:canvasW, ctx:ctxW} , arenaK: {canvas:canvasK, ctx:ctxK} , arenaWK: {canvas:canvasWK, ctx:ctxWK} , arenaS: {canvas:canvasS, ctx:ctxS}} + + ui.dom.weightChartsBackButton[i].onclick = (function(i) { return function() { + var district = model.district[i] + model.roundCurrent[i] -- + if (model.roundCurrent[i] < 0) model.roundCurrent[i] = 0 + model.flagFinalRound[i] = false + ui.roundUpdateSet(i); + }})(i) + ui.dom.weightChartsForwardButton[i].onclick = (function(i) { return function() { + var district = model.district[i] + model.roundCurrent[i] ++ + var maxRound = district.result.history.rounds.length - 1 + 1 + if (model.roundCurrent[i] > maxRound) model.roundCurrent[i] = maxRound + if (model.roundCurrent[i] == maxRound) model.flagFinalRound[i] = true + ui.roundUpdateSet(i); + }})(i) + + actualWeightChartsDraw(i) + + + } + + function actualWeightChartsDraw(iDistrict, opt) { + + opt = opt || {} + + var round = model.roundCurrent[iDistrict] + var arena = ui.weightCharts[iDistrict].arena + var district = model.district[iDistrict] + + // get only the sorted voters for this district. + var v = model.getSortedVoters() + v = v.filter(x => x.iDistrict == iDistrict) + + var barOptions = {} + barOptions.width = arena.canvas.width + barOptions.widthRectangle = barOptions.width / v.length + barOptions.heightRectangle = 100 + barOptions.baralpha = .9 + barOptions.fontSize = 32 + + barOptions.heightRectangle2 = Math.min(200 / model.candidates.length, 200/5) + + if (model.system == "PAV" || model.system == "equalFacilityLocation") { + var roundOrCan = "Candidate" + } else { + var roundOrCan = "Round" + } + if (round == district.result.history.rounds.length) { + var rt = ` Final ` + } else { + var rt = ` ${roundOrCan} ${round + 1} ` + } + ui.dom.weightChartsRoundNumText[iDistrict].innerText = rt + + arena.ctx.clearRect(0,0,arena.canvas.width,arena.canvas.height) + drawDottedVoterLine(100,barOptions,v,arena.ctx) + var optCDraw = (model.customNames == "Yes") ? {rotate:10} : {} + for(var i=0; i<district.candidates.length; i++){ + var c = district.candidates[i] + c.draw(arena.ctx,model.tarena,optCDraw) // tarena just provides a function that translates coordinates. + } + _drawText("Candidates in Voter Space",10,50,40,arena.ctx,"start") + + var arenaW = ui.weightCharts[iDistrict].arenaW + var barOptionsW = _jcopy(barOptions) + barOptionsW.pos = 80 + arenaW.ctx.clearRect(0,0,arenaW.canvas.width,arenaW.canvas.height) + drawWeight(model,arenaW,barOptionsW,v,round+1) + + if (model.showPowerChart) { + + var arenaU = ui.weightCharts[iDistrict].arenaU + arenaU.canvas.hidden = false + var barOptionsU = _jcopy(barOptions) + barOptionsU.base = 200 + arenaU.ctx.clearRect(0,0,arenaU.canvas.width,arenaU.canvas.height) + drawWeightUsed(model,arenaU,barOptionsU,v,round+1) + + var arenaP = ui.weightCharts[iDistrict].arenaP + arenaP.canvas.hidden = false + var barOptionsP = _jcopy(barOptions) + barOptionsP.doPowerUsed = true + barOptionsP.base = 200 + arenaP.ctx.clearRect(0,0,arenaP.canvas.width,arenaP.canvas.height) + drawWeightUsed(model,arenaP,barOptionsP,v,round+1) + + // show satisfaction + if (model.system != "STV") { + var arenaS = ui.weightCharts[iDistrict].arenaS + arenaS.canvas.hidden = false + var barOptionsS = _jcopy(barOptions) + barOptionsS.base = 200 + barOptionsS.doSatisfaction = true + arenaS.ctx.clearRect(0,0,arenaS.canvas.width,arenaS.canvas.height) + drawWeightUsed(model,arenaS,barOptionsS,v,round+1) + } else { + var arenaS = ui.weightCharts[iDistrict].arenaS + arenaS.canvas.hidden = true + } + + if (model.system == "Phragmen Seq S") { + + var arenaWK = ui.weightCharts[iDistrict].arenaWK + arenaWK.canvas.hidden = false + var barOptionsWK = _jcopy(barOptions) + barOptionsWK.pos = 80 + arenaWK.ctx.clearRect(0,0,arenaWK.canvas.width,arenaWK.canvas.height) + drawKWeight(model,arenaWK,barOptionsWK,v,round+1) + + var arenaK = ui.weightCharts[iDistrict].arenaK + arenaK.canvas.hidden = false + var barOptionsK = _jcopy(barOptions) + barOptionsK.base = 200 + arenaK.ctx.clearRect(0,0,arenaK.canvas.width,arenaK.canvas.height) + drawKWeightUsed(model,arenaK,barOptionsK,v,round+1) + } else { + var arenaWK = ui.weightCharts[iDistrict].arenaWK + arenaWK.canvas.hidden = true + var arenaK = ui.weightCharts[iDistrict].arenaK + arenaK.canvas.hidden = true + } + } else { + // hide canvases + var arenaU = ui.weightCharts[iDistrict].arenaU + arenaU.canvas.hidden = true + + var arenaP = ui.weightCharts[iDistrict].arenaP + arenaP.canvas.hidden = true + + var arenaWK = ui.weightCharts[iDistrict].arenaWK + arenaWK.canvas.hidden = true + + var arenaK = ui.weightCharts[iDistrict].arenaK + arenaK.canvas.hidden = true + + var arenaS = ui.weightCharts[iDistrict].arenaS + arenaS.canvas.hidden = true + } + } + + function filterMapDraw() { + // manage the creation of the canvases + + // redo charts when number of candidates or rounds changes + var useCandidates = model.ballotType === "Approval" || model.ballotType === "Score" || model.ballotType === "Three" + var useRounds = ["IRV","STV"].includes(model.system) // TODO: add more + + var noFilterMaps = ! (useCandidates || useRounds) + if (noFilterMaps) { + if (ui.dom.filterMapDiv) ui.dom.filterMapDiv.remove() + ui.dom.filterMapDiv = undefined + return + } + + // redo charts when changing number of districts. + var matchDist = (ui.weightChartsDistricts == model.district.length) + + if (matchDist) { + var match = true + + if (useCandidates) { + for (var i = 0; i < model.district.length; i++) { + match = match && (ui.chartNotesCandidates !== undefined && ui.chartNotesCandidates[i] == model.district[i].candidates.length) + } + } + if (useRounds) { + for (var i = 0; i < model.district.length; i++) { + match = match && (ui.chartNotesRounds !== undefined && ui.chartNotesRounds[i] == model.district[i].result.history.rounds.length) + } + } + } else { + var match = false + } + + let makeNewMapDivs = (ui.dom.filterMapDraw === undefined) || ! matchDist || ! match + + if(makeNewMapDivs) { + // if we're updating the number, remove the old charts + if (ui.dom.filterMapDiv) ui.dom.filterMapDiv.remove() + ui.dom.filterMapDiv = undefined + + // keep note of the number of districts + ui.weightChartsDistricts = model.district.length + + // keep note of the number of candidates and the number of rounds + if (useCandidates) { + ui.chartNotesCandidates = model.district.map( x => x.candidates.length) + ui.chartNotesRounds = [] + } + if (useRounds) { // useRounds + ui.chartNotesCandidates = model.district.map( x => x.candidates.length) + ui.chartNotesRounds = model.district.map( x => x.result.history.rounds.length) + } + + ui.dom.filterMapDiv = document.createElement("div") + ui.dom.filterMapDiv.id = "chart" + + if (useCandidates) { + var titleText = "Vote Maps By Candidate" + } else if (useRounds) { + var titleText = "Vote Maps By Round" + } + ui.dom.filterMapDiv.innerHTML += `<div style="text-align:center;"><span class="small" > ${titleText} </span></div>` + + if (ui.minusControl == undefined) ui.minusControl = {} + if (ui.minusControl.filterMapDiv == undefined) ui.minusControl.filterMapDiv = {} + addMinusButtonC(ui.dom.filterMapDiv,ui.minusControl.filterMapDiv) + + + ui.dom.filterMaps = [] + for (var i = 0; i < model.district.length; i++) { + if (model.district.length > 1) { + var title = document.createElement("div") + // title.innerText = `District ${i+1}` + title.innerHTML = `<div style="text-align:center;"><span class="small" > District ${i+1} </span></div>` + ui.dom.filterMapDiv.append(title) + } + if (useCandidates) { + var numMaps = model.district[i].candidates.length + } + if (useRounds) { + var numMaps = model.district[i].result.history.rounds.length + } + ui.dom.filterMaps[i] = [] + for (var k = 0; k < numMaps; k++) { + var canvas = document.createElement('CANVAS'); + canvas.className = "filterMap" + var ctx = canvas.getContext('2d'); + canvas.height = 90; + canvas.width = 90; + // canvas.style.height = 500; + // canvas.style.width = 500; + ui.dom.filterMaps[i][k] = {canvas:canvas,ctx:ctx} + ui.dom.filterMapDiv.append(canvas) + } + + } + } + ui.dom.right.prepend(ui.dom.filterMapDiv) // yes, prepend every time so that we get the order right... maybe not the best solution but it works + + // actually draw the filterMaps + + // temporarily change style (so it will be consistent) + var tempCandidateIconsSet = model.candidateIconsSet + model.candidateIconsSet = ["body"] + var tempVoterIcons = model.voterIcons + if (useRounds) { + model.voterIcons = "top" + } else { + model.voterIcons = "circle" + } + + for (var i = 0; i < model.district.length; i++) { + var numMaps = ui.dom.filterMaps[i].length + var district = model.district[i] + if (useRounds) { + var oldRoundCurrent = model.roundCurrent[i] + var oldFlagFinalRound = model.flagFinalRound[i] + model.flagFinalRound[i] = false + } + for (var k = 0; k < numMaps; k++) { + // draw ballots + arena = ui.dom.filterMaps[i][k] + arena.ctx.clearRect(0,0,arena.canvas.width,arena.canvas.height) + arena.ctx.fillStyle = '#333'; + arena.ctx.fillRect(0,0,arena.canvas.width,arena.canvas.height) + arena.ctx.scale(arena.canvas.width/model.size/2,arena.canvas.height/model.size/2) // 50/600 + if (useCandidates) { + for(var m=0; m<model.voterGroups.length; m++){ + var voterGroup = model.voterGroups[m]; + for(var voterPerson of voterGroup.voterPeople){ + voterGroup.voterModel.drawMe(arena.ctx, voterPerson, 1, {onlyCandidate:k}) + } + } + var c = district.candidates[k] + c.draw(arena.ctx,model.arena) + var text = c.name + } else if (useRounds) { + model.roundCurrent[i] = k + for(var m=0; m<model.voterGroups.length; m++){ + var voterGroup = model.voterGroups[m]; + for(var voterPerson of voterGroup.voterPeople){ + voterGroup.voterModel.drawMe(arena.ctx, voterPerson, 1) + } + } + for(var m=0; m<district.candidates.length; m++){ + var c = district.candidates[m] + c.draw(arena.ctx,model.arena) // model.arena just provides a function that translates coordinates. + } + var text = `${k+1}` + + } else { + for(var m=0; m<model.voterGroups.length; m++){ + var voterGroup = model.voterGroups[m]; + for(var voterPerson of voterGroup.voterPeople){ + voterGroup.voterModel.drawMe(arena.ctx, voterPerson, 1) + } + } + for(var m=0; m<district.candidates.length; m++){ + var c = district.candidates[m] + c.draw(arena.ctx,model.arena) // model.arena just provides a function that translates coordinates. + } + var text = "" + } + _drawStrokedColor(text, 300, 80, 80,5, "white", arena.ctx) + } + if (useRounds) { + model.roundCurrent[i] = oldRoundCurrent + model.flagFinalRound[i] = oldFlagFinalRound + } + } + model.candidateIconsSet = tempCandidateIconsSet + model.voterIcons = tempVoterIcons + } + + ui.roundUpdateSet = function() { + if (ui.dom.weightCharts != undefined) { + // update chart + for (var i = 0; i < model.district.length; i++) { + actualWeightChartsDraw(i) + } + } + if (ui.dom.roundChart != undefined) { + // update chart + for (var i = 0; i < model.district.length; i++) { + actualRoundChartDraw(i, {ease:true}); + } + } + model.drawArenas() + } + + model.updateFromModel = function() { + _objF(ui.menu,"updateFromModel") + } + + // helpers + + model.onInitModel = function() { // works for onUpdate, too + ui.initButtons() + } + + model.onAddCandidate = function() { + var n = model.candidates.length + model.numOfCandidates = n + config.numOfCandidates = n + ui.menu.nCandidates.select() + } + + model.voterManager.onDeleteVoterGroup = function() { + config.voterGroupNameList = model.voterGroupNameList.join('\n') + ui.menu.voterGroupNameList.select() + } +} + + +function Config(ui, config, initialConfig) { + // Getting the configuration from a URL or previous version, requiring clean up. + + var self = this + + // see the HOWTO below + + // When you add a new menu item or variable, + // add it to the defaults + // And when you change which variables are used, + // add some code to the cleanConfig. + + + ///////////////////////////// + // LOAD DEFAULTS and INPUT // + ///////////////////////////// + + // some switches + ui.embed = false + ui.maxVoters = 10 + ui.tryNewURL = true + + var all_candidate_names = Object.keys(Candidate.graphicsByIcon["Default"]) // helper + var yes_all_candidates = {} + for (var i = 0; i < all_candidate_names.length; i++) { + yes_all_candidates[i] = true + } + self.defaults = { + configversion:2.5, + sandboxsave: false, + hidegearconfig: false, + description: "", + keyyee: "newcan", + snowman: false, // section + x_voters: false, + oneVoter: false, + system: "FPTP", // section + rbsystem: "Tideman", + seats: 3, + numOfCandidates: 3, + customNames: "No", + namelist: "", + numVoterGroups: 1, + xNumVoterGroups: 4, + nVoterGroupsRealName: "One Group", + spread_factor_voters: 1, + arena_size: 300, + median_mean: 1, + theme: "Default", + utility_shape: "linear", + votersAsCandidates: false, + visSingleBallotsOnly: false, + ballotVis: true, + stepMenu: "geom", + menuVersion: "1", + menuLevel: "normal", + dimensions: "2D", + nDistricts: 1, + colorChooser: "pick and generate", + colorSpace: "hsluv with dark", + arena_border: 2, + preFrontrunnerIds: ["square","triangle"], + autoPoll: "Manual", + // primaries: "No", + firstStrategy: "zero strategy. judge on an absolute scale.", + secondStrategy: "zero strategy. judge on an absolute scale.", + doTwoStrategies: true, + centerPollThreshold: .5, + yeefilter: yes_all_candidates, + computeMethod: "ez", + pixelsize: 60, + featurelist: ['gearconfig',"doFeatureFilter"], + doFeatureFilter: true, + yeeon: false, + beatMap: "auto", + kindayee: "newcan", + ballotConcept: "auto", + roundChart: "auto", + sidebarOn: "on", + lastTransfer: "off", + voterIcons: "circle", + voterCenterIcons: "on", + candidateIconsSet: ["image","note"], + pairwiseMinimaps: "off", + doTextBallots: false, + textBallotInput: "", + behavior: "stand", + showToolbar: "on", + rankedVizBoundary: "atWinner", + useBeatMapForRankedBallotViz: false, + doMedianDistViz: false, + doElectabilityPolls: true, + partyRule: "crowd", + doFilterSystems: false, + filterSystems: [], + doFilterStrategy: true, + includeSystems: ["choice","pair","score","multi","dev"], + showUtilityChart: false, + showPowerChart: true, + putMenuAbove: false, + scoreFirstStrategy: "zero strategy. judge on an absolute scale.", + choiceFirstStrategy: "zero strategy. judge on an absolute scale.", + pairFirstStrategy: "zero strategy. judge on an absolute scale.", + scoreSecondStrategy: "zero strategy. judge on an absolute scale.", + choiceSecondStrategy: "zero strategy. judge on an absolute scale.", + pairSecondStrategy: "zero strategy. judge on an absolute scale.", + codeEditorText: Election.defaultCodeScore, + createStrategyType: "score", + createBallotType: "Score", + minusControl: [], + voterGroupCustomNames: "No", + voterGroupNameList: "", + } + // HOWTO: add to the end here (or anywhere inside) + + + self.loadUrl = function(afterLoadUrl) { + + if (ui.url != undefined) { + var modelData = _getParameterByName("m",ui.url); + if (ui.tryNewURL) var version = _getParameterByName("v",ui.url); + if (modelData === null) { + var shortCode = _getParameterByName("u",ui.url) + if (shortCode !== null) { + // get request + // var listShortLinkUrl = "https://spreadsheets.google.com/tq?&tq=&key=12TYpTVx6WgyNzUTBvXUPLBdplk9Hi1MJVMkGskyh5cs" + // var listShortLinkUrl = "https://spreadsheets.google.com/feeds/list/12TYpTVx6WgyNzUTBvXUPLBdplk9Hi1MJVMkGskyh5cs/1/public/values?alt=json" + // var listShortLinkUrl = "https://spreadsheets.google.com/feeds/list/12TYpTVx6WgyNzUTBvXUPLBdplk9Hi1MJVMkGskyh5cs/1/public/values?alt=json&sq=shortcode=" + shortCode + // var listShortLinkUrl = "https://sheets.googleapis.com/v4/spreadsheets/12TYpTVx6WgyNzUTBvXUPLBdplk9Hi1MJVMkGskyh5cs/values/Sheet1" + // var listShortLinkUrl = "https://spreadsheets.google.com/feeds/cells/12TYpTVx6WgyNzUTBvXUPLBdplk9Hi1MJVMkGskyh5cs/1/public/values?alt=json" + var listShortLinkUrl = 'https://script.google.com/macros/s/AKfycbzMf0eb8jFTPnM7X83RIZwYN783E-xKt0M6RmgI-0AO2yf8BKb3/exec' + _ajax.get(listShortLinkUrl, {shortcode: shortCode, link: "getTable"}, function(res) { + // do the load config again, basically + var resObj = JSON.parse(res) + + var codedUrl = resObj.link + + // example, the third shortcode: resObj.feed.entry[2].gsx$shortcode.$t + + // second try, worked for a while until google changed to version 4 of the sheets API + // var rows = resObj.feed.entry + // search rows for shortcode + // var row = rows.filter( r => r.gsx$shortcode.$t === shortCode) + // get the url + // var codedUrl = row[0].gsx$link.$t + + + // first try + // var rows = resObj.table.rows + // // search rows for shortcode + // var row = rows.filter( r => r.c[0].v === shortCode) + // // get the url + // var codedUrl = row[0].c[1].v + + ui.url = codedUrl + + var modelData = _getParameterByName("m",ui.url); + if (ui.tryNewURL) var version = _getParameterByName("v",ui.url); + + var urlData = {modelData:modelData, version:version} + afterLoadUrl(urlData) + + }) + return + + } + } + } + function _getParameterByName(name,url){ + name = name.replace(/[\[\]]/g, "\\$&"); + var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"), + results = regex.exec(url); + if (!results) return null; + if (!results[2]) return ''; + return decodeURIComponent(results[2].replace(/\+/g, " ")).replace("}/","}"); //not sure how that / got there. + }; + + var urlData = {modelData:modelData, version:version} + afterLoadUrl(urlData) + return + } + + self.setConfig = function(urlData) { + + var modelData = urlData.modelData + var version = urlData.version + + var c = ui.preset.config + // INIT - initialize all data structures + // the data structure for a sandbox is the configuration of the model. Init completes this data structures. + // backwards compatibility + // the data structure for a model is model.<property> + + if(modelData){ + if (ui.tryNewURL) { + if (version) { + // if we have a version number, then we know the data is in this format + c = { + zipped:modelData, + configversion:version + } + } else { + var data = JSON.parse(modelData); + c = data; + } + } else { + var data = JSON.parse(modelData); + c = data; + } + } + self.cleanConfig(c) + // overwrite + if (ui.overwriteConfig != undefined) { + _addAttributes( c, ui.overwriteConfig ) + } + _copyAttributes(config,c) + _copyAttributes(initialConfig,c) + + } + + self.cleanConfig = function(config) { + // Load the defaults. This runs at the start and after loading a preset. + + // HOWTO: + // cleanConfig: + // The only real problem that it's worth keeping track of version numbers for is: + // interpreting old parameters if I replaced them with something else in my config, + // and that is handled in here in cleanConfig. + + // Incrementing: + // Increment the version number when you do something so weird that you can no longer run + // the previous version's update commands. + // For example, for version 2.3, I changed the way a variable is stored, so + // I couldn't run the same script to change it twice. + // An example where we don't need version updating + // is when we stop using one variable name and start using a different one. + + + // FILENAME + // config.presethtmlname = ui.url.substring(ui.url.lastIndexOf('/')+1); + + + if(config.configversion == undefined || config.configversion == 1) { + // GRANDFATHER URL variable names + // change old variable names to new names to preserve backward compatibility with urls and presets + // and clear grandfathered variables + function newname(config,oldname,newname) { + if(config[oldname] != undefined) config[newname] = config[oldname] + delete config[oldname] + } + newname(config,"candidates","numOfCandidates") + newname(config,"voters","numVoterGroups") + newname(config,"s","system") + if(config.c != undefined) { // grandfather in the variables + config.numOfCandidates = config.c.length + config.numVoterGroups = config.v.length + config.features = 4 + } + newname(config,"c","candidatePositions") + newname(config,"v","voterPositions") + newname(config,"d","description") + + // GRANDFATHER OLD NAMES OF VARIABLES + if(config.unstrategic) config.firstStrategy = config.unstrategic + if(config.strategic) config.secondStrategy = config.strategic + if(config.strategy) config.secondStrategy = config.strategy + if(config.voterStrategies) config.secondStrategies = _jcopy(config.voterStrategies) + if(config.voterPercentStrategy) config.percentSecondStrategy = _jcopy(config.voterPercentStrategy) + if(config.second_strategy) config.doTwoStrategies = config.second_strategy + delete config.unstrategic + delete config.strategic + delete config.strategy + delete config.voterStrategies + delete config.voterPercentStrategy + delete config.second_strategy + + // GRANDFATHER Feature List... + // config.features: 1-basic, 2-voters, 3-candidates, 4-save + if ( config.featurelist == undefined) { + config.featurelist = fl() + function fl() { + switch(config.features){ + case 1: return ["systems"] + case 2: return ["systems","voters"] + case 3: return ["systems","voters","candidates"] + case 4: + config.sandboxsave = true + return ["systems","voters","candidates"] + } + } + } + if (config.doPercentFirst) config.featurelist = config.featurelist.concat(["percentStrategy"]); + if (config.doFullStrategyConfig) { + config.doFeatureFilter = false + // The feature filter is off, so all items are displayed. + // basically everything that should be displayed at the start + // This config.doFullStrategyConfig is a shorthand that gets replaced by the featurelist + // The featurelist has it's own control as an item + } + // clear the grandfathered config settings + delete config.doPercentFirst + delete config.features + delete config.doFullStrategyConfig + + // GRANDFATHER featurelist step 2 + // HOWTO: When replacing the name of a menu item, the old configurations with that name still need to work + // So, add the new menu item to the list below. + // replace old names with new names + if (config.featurelist) { + var menuNameTranslator = { + "voters":"nVoterGroups", + "candidates":"nCandidates", + "unstrategic":"firstStrategy", + "second strategy":"doTwoStrategies", + "yee":"yee", + "rbvote":"rbSystems", + "custom_number_voters":"xVoterGroups", + "xHowManyVoterGroups":"xVoterGroups", + "strategy":"secondStrategy", + "percentstrategy":"percentSecondStrategy", + } + // all the current names get translated as themselves + for (var id of Object.keys(ui.menu)) { + menuNameTranslator[id] = id + } + var temp_featurelist = [] + for (var i=0; i<config.featurelist.length; i++) { + var oldName = config.featurelist[i] + var newName = menuNameTranslator[oldName] + temp_featurelist.push(newName) + } + config.featurelist = temp_featurelist + } + config.configversion = 2 // updating to next version + } + + if (config.configversion == 2) { + if (config.yeefilter) { + var oldyeefilter = config.yeefilter + var newyeefilter = {} + var oldcandidates = ["square","triangle","hexagon","pentagon","bob"] + for (var i = 0; i < oldcandidates.length; i++) { + var id = oldcandidates[i] + if (oldyeefilter.includes(id)) { + newyeefilter[id] = true + } else { + newyeefilter[id] = false + } + } + config.yeefilter = newyeefilter + } + + config.configversion = 2.1 // bump to next version + } + // we are now generating a new version of config. We are done with grandfathering + + ui.cypher.decode_config(config) // decodes the config, depending on version number + // so in this case, we're decoding stuff from 2.2, but not 2.1 because 2.1 isn't encoded + if (config.configversion == 2.1) { + config.configversion = 2.2 // 2.1 is now treated the same as 2.2 because both are decoded + } + + if (config.configversion == 2.2) { + if (config.yeefilter) { + var oldy = config.yeefilter + var newy = {} + var oldcandidates = ["square","triangle","hexagon","pentagon","bob"] + var serial = 0 + for (var i = 0; i < oldcandidates.length; i++) { + for (var k=1; k<=20; k++) { + var id = oldcandidates[i] + if (k > 1) id += k + if (id in oldy) { + newy[serial] = oldy[id] + } + serial++ + } + } + config.yeefilter = newy + } + + config.configversion = 2.3 + } + if (config.configversion == 2.3) { + config.configversion = 2.4 + } + // add in features that were not included with the old version + if (config.configversion == 2.4) { + + + + // ctrl-reset solves this problem + + // // There is a problem in going from an old featureset to a new one. + // // The new features are not included in the set. + // // So we add an opton to choose whether to filter features. + // // By default the filter is on. + // // The default in the sandbox preset is off. + // // In this way, it is easy to switch the filter off and update the features. + // // The filter switch is inside the config menu. + + // // if this is a save from before version 2.5 + // // then the featurelist is on and it didn't include the menu items that are hidden in menuVersion 1 + // // so, allow the user to remove the filter + + // // so, if the featurelist is set then we want to turn on the filter + // // if the featurelist is not set, then we don't do the filter + // if ( config.featurelist == undefined) { + // config.doFeatureFilter = false + // } else { + // modifyConfigFeaturelist(config,true, ["doFeatureFilter"]) + // } + + // // Hmm. on the one hand, I want to load an example where I have purposely set the filter + // // and that example is indistinguishable from an example where I want to see the new features + // // because I can't tell the difference (in the old version) between manual and automatic hiding + // // So maybe I should have an upgrade button, to allow the user to decide what to do. + // // or maybe the user could manually change the 2.4 to 2.5 in the URL. + // // 2.4 would not have the upgrade button + // // 2.5 + // // Oh + // // The difference between the locked down version and the updating version is the "config" menu + // // So I put the "Disable filters" button in the config menu, + + if ( config.featurelist == undefined) { + config.doFeatureFilter = false + } + + + + // one more thing + // switch the name for this setting: + if (config.kindayee == "beatCircles") { + config.beatMap = "on" // new setting + + config.yeeon = false // old settings + config.keyyee = "newcan" + config.kindayee = "newcan" + } + var isSomething = x => (x != undefined && x != "off" ) + if (isSomething(config.kindayee) || isSomething(config.keyyee)) { + config.yeeon = true + } + + config.configversion = 2.5 + } + + // now these corrections might have to be done to version 2.5, and they won't hurt the next version + if (config.configversion == 2.5) { + + // if the yee menu was in the featurelist, then make sure the new yee on/off switch is added to the featurelist // and beatMap + if (config.featurelist != undefined && config.featurelist.includes("yee")) modifyConfigFeaturelist(config,true,["yeeon","beatMap"]) + + // so basically, we are getting rid of the "none" button in the yee chooser and making it into a separate control. + + if (config.behavior == undefined && config.theme == "Bees") { + config.behavior = "bounce" + } + + + // strategies section // + + // translate old strategies + var tr = ui.strategyOrganizer.translate + var sys = config.system + var s1 = config.firstStrategy + var s2 = config.secondStrategy + if (s1 !== undefined) { + config.firstStrategy = tr(s1,sys) + } + if (s2 !== undefined) { + config.secondStrategy = tr(s2,sys) + } + + if (config.scoreFirstStrategy == undefined) { + config.scoreFirstStrategy = tr(s1,"Score") + } + + if (config.choiceFirstStrategy == undefined) { + config.choiceFirstStrategy = tr(s1,"FPTP") + } + + if (config.pairFirstStrategy == undefined) { + config.pairFirstStrategy = tr(s1,"Condorcet") + } + + if (config.scoreSecondStrategy == undefined) { + config.scoreSecondStrategy = tr(s2,"Score") + } + + if (config.choiceSecondStrategy == undefined) { + config.choiceSecondStrategy = tr(s2,"FPTP") + } + + if (config.pairSecondStrategy == undefined) { + config.pairSecondStrategy = tr(s2,"Condorcet") + } + + // double check to correct some weird errors + config.scoreFirstStrategy = tr(config.scoreFirstStrategy, "Score") + config.choiceFirstStrategy = tr(config.choiceFirstStrategy, "FPTP") + config.pairFirstStrategy = tr(config.pairFirstStrategy, "Condorcet") + config.scoreSecondStrategy = tr(config.scoreSecondStrategy, "Score") + config.choiceSecondStrategy = tr(config.choiceSecondStrategy, "FPTP") + config.pairSecondStrategy = tr(config.pairSecondStrategy, "Condorcet") + + config.secondStrategies = [] // no longer using this + + // end strategies section // + + if (config.crowdShape == undefined) { + config.crowdShape = config.voterGroupX.map( x => (x) ? "gaussian sunflower" : "Nicky circles" ) + } + + if (config.group_count_vert == undefined) { + config.group_count_vert = config.voterGroupX.map( x => 5) + } + if (config.group_count_h == undefined) { + config.group_count_h = config.voterGroupX.map( x => 5) + } + + // there's no incompatibility problems yet, so no need to increment + // code below this if {} statement are still needed in future versions + } + + // Finally done with old versions! + + + // VOTER DEFAULTS + // we want individual percent strategies to be loaded in, if they are there + config.percentSecondStrategy = config.percentSecondStrategy || [] + config.voter_group_count = config.voter_group_count || [] + config.voter_group_spread = config.voter_group_spread || [] + for (var i = 0; i < ui.maxVoters; i++) { + if(config.percentSecondStrategy[i] == undefined) config.percentSecondStrategy[i] = 0 + config.voter_group_count[i] = config.voter_group_count[i] || 50 + config.voter_group_spread[i] = config.voter_group_spread[i] || 190 + } + + _fillInDefaults(config, self.defaults) + + + } + self.reset = function() { + _copyAttributes(config,initialConfig) + } + + self.save = function() { + _copyAttributes(initialConfig,config) + // UPDATE SAVE URL // + var newURLs = _makeURL(); + // CONSOLE OUTPUT // + console_out(1) // gives a log of settings to copy and paste + return newURLs + + // the old way, here for historical reasons. + // SAVE & PARSE + // ?m={s:[system], v:[voterPositions], c:[candidatePositions], d:[description]} + } + + var _makeURL = function(){ + + // URI ENCODE! + var doEncode = true + if (doEncode) { + var eConfig = ui.cypher.encode_config(config) + } else { + var eConfig = config + } + if (ui.tryNewURL) { + var uri = encodeURIComponent(eConfig) + } else { + var uri = encodeURIComponent(JSON.stringify(eConfig)); + } + + // Put it in the save link box! + + // make link string + + var baseUrl = document.baseURI + // var getUrl = window.location; + // var baseUrl = getUrl.protocol + "//" + getUrl.host; // http://ncase.me/ + // var restofurl = getUrl.pathname.split('/') + // for (var i=1; i < restofurl.length - 1; i++) { // /ballot/ + // if (restofurl[i] != "sandbox") { + // baseUrl += "/" + restofurl[i]; + // } + // } + if (ui.embed) { + var relativePath = "sandbox/embedbox.html?v=" + } else { + var relativePath = "sandbox/?v=" + } + if (ui.tryNewURL) { + var link = baseUrl + relativePath + config.configversion + "&m="+uri; + } else { + var link = baseUrl + relativePath + uri; + } + if (ui.embed) { + var linkText = '<iframe src="' + link + '" scrolling="yes" width="100%" height="650"></iframe>' + } else { + var linkText = link + } + + // var shortCode = _randAlphaNum(7) + var shortCode = _hashCode(link) + var shortLink = baseUrl + "sandbox/?v=" + config.configversion + "&u=" + shortCode + + console.log("(Link length is ",linkText.length,")") + console.log("") + return {link:link, linkText:linkText, shortCode:shortCode, shortLink:shortLink} + }; + + var console_out = function (log){ + // helper function to output the config to the console. + var logtext = '' + for (i in config) { + logtext += i + ": " +JSON.stringify(config[i]) + ',\n' + // logtext += '"' + i + '",\n' // for codebook + } + var aloc = window.location.pathname.split('/') + //logtext += "\n\npaste this JSON into" + aloc[aloc.length-2] + "/" + aloc[aloc.length-1] + logtext += "\n\npaste this JSON into /play/js/Presets.js under option " + aloc[aloc.length-1] + console.log(logtext) + if (log==2) console.log(JSON.stringify(config)) + } + + + +} + +function Cypher(ui) { + // Decyphers the URL + + // See the HOWTO + // I explain that we don't really need to keep track of configversion in the codebooks. + + var self = this + var doFriendlyURI = true + + var encodeFields = {} + + var decodeFields = { + 0:"candidatePositions", + 1:"voterPositions", + 2:"candidates", + 3:"dimensions", + 4:"system", + 5:"hidegearconfig", + // "configversion", + 6:"secondStrategies", + 7:"percentSecondStrategy", + 8:"voter_group_count", + 9:"voter_group_spread", + 10:"sandboxsave", // not needed anymore, but keep it + 11:"featurelist", + 12:"description", + 13:"keyyee", + 14:"snowman", + 15:"x_voters", + 16:"oneVoter", + 17:"rbsystem", + 18:"numOfCandidates", + 19:"numVoterGroups", + 20:"xNumVoterGroups", + 21:"nVoterGroupsRealName", + 22:"spread_factor_voters", + 23:"arena_size", + 24:"median_mean", + 25:"theme", + 26:"utility_shape", + 27:"colorChooser", + 28:"colorSpace", + 29:"arena_border", + 30:"preFrontrunnerIds", + 31:"autoPoll", + 32:"firstStrategy", + 33:"secondStrategy", + 34:"doTwoStrategies", + 35:"yeefilter", + 36:"computeMethod", + 37:"pixelsize", + 38:"optionsForElection", // no longer used, but okay to have + 39:"candidateSerials", + 40:"voterGroupTypes", + 41:"voterGroupX", + 42:"voterGroupSnowman", + 43:"voterGroupDisk", + 44:"seats", + 45:"candidateB", + 46:"nDistricts", + 47:"votersAsCandidates", + 48:"visSingleBallotsOnly", + 49:"ballotVis", + 50:"customNames", + 51:"namelist", + 52:"menuVersion", + 53:"stepMenu", + 54:"menuLevel", + 55:"doFeatureFilter", + 56:"yeeon", + 57:"beatMap", + 58:"kindayee", + 59:"ballotConcept", + 60:"roundChart", + 61:"sidebarOn", + 62:"lastTransfer", + 63:"voterIcons", + 64:"candidateIcons", + 65:"candidateIconsSet", + 66:"pairwiseMinimaps", + 67:"submitTextBallots", + 68:"textBallotInput", + 69:"doTextBallots", + 70:"behavior", + 71:"showToolbar", + 72:"rankedVizBoundary", + 73:"doElectabilityPolls", + 74:"partyRule", + 75:"doFilterSystems", + 76:"filterSystems", + 77:"doFilterStrategy", + 78:"includeSystems", + 79:"showPowerChart", + 80:"putMenuAbove", + 81:"scoreFirstStrategy", + 82:"choiceFirstStrategy", + 83:"pairFirstStrategy", + 84:"scoreSecondStrategy", + 85:"choiceSecondStrategy", + 86:"pairSecondStrategy", + 87:"voterCenterIcons", + 88:"useBeatMapForRankedBallotViz", + 89:"centerPollThreshold", + 90:"doMedianDistViz", + 91:"crowdShape", + 92:"group_count_vert", + 93:"group_count_h", + 94:"createStrategyType", + 95:"createBallotType", + 96:"showUtilityChart", + 97:"minusControl", + 98:"codeEditorText", + 99:"voterGroupRandomSeed", + 100:"voterGroupCustomNames", + 101:"voterGroupNameList", + } + // HOWTO + // add more on to the end ONLY + + + self.setUpEncode = function() { + + _makeEncodeFields() + + function _makeEncodeFields() { + for (var [i,v] of Object.entries(decodeFields)) { + i = Number(i) + encodeFields[v] = i + } + } + // set up encoders + for (var e in ui.menu) { + var item = ui.menu[e] + if (item.codebook) { + for (var page of item.codebook) { + _makeEncode(page) + } + } + } + + for (var page of ui.extraCodeBook) { + _makeEncode(page) + } + + } + + self.encode_config = function(config) { + + // make a copy because we aren't going to modify config + var conf = _jcopy(config) + + step1encode(conf) + // + // encode field names + // console.log(conf) + // console.log(conf) + for (var i in encodeFields) { + var n = encodeFields[i] + conf[n] = conf[i] + delete conf[i] + } + // zip + delete conf["configversion"] + var dataZ = pako.gzip( JSON.stringify(conf) ,{ to: 'string' }) + var dataString = btoa(dataZ) + if (doFriendlyURI) dataString = Base64EncodeUrl(dataString) + if (ui.tryNewURL) { + return dataString + } else { + var co = {} + co["zipped"] = dataString + co["configversion"] = config["configversion"] + return co + } + // be careful to include all the zipped config items and add any new ones or they will appear as extras + } + + + // HOWTO: Add to the end of the list. Don't add to the middle of this list. + // safer: use a dictionary + // These are the fields that show up in the URL, so we shorten them. + + + + + self.decode_config = function(config) { + if (config.configversion == undefined) return config + if (config.configversion <= 2.1) return config + if (! ("zipped" in config) ) return config + + // unzip + var dataString = config["zipped"] + if (doFriendlyURI) dataString = Base64DecodeUrl(dataString) + var data = pako.inflate( atob( dataString)) + var strData = String.fromCharCode.apply(null, new Uint16Array(data)); + var conUnzipped = JSON.parse( strData ) + + Object.assign(config,conUnzipped) + delete config["zipped"] + + // note that we have to decode it in place + var conf = _jcopy(config) + // decode field names + for (var [e,v] of Object.entries(conf)) { + if (decodeFields.hasOwnProperty(e)) { + // if we can decode it, then decode it + delete config[e] + var d = decodeFields[e] + config[d] = v + } + } + + step1decode(config) + return + } + + + + + + function _makeEncode(page) { + var decode = page.decode + + var encode = {} + for (var [i,v] of Object.entries(decode)) { + var value = JSON.stringify(v) + i = Number(i) + encode[value] = i + } + page.encode = encode + } + + + function step1encode(conf) { + for (var e in ui.menu) { + var item = ui.menu[e] + if (item.codebook) { + for (var page of item.codebook) { + _encode(page,conf) + } + } + } + for (var page of ui.extraCodeBook) { + _encode(page,conf) + } + } + + function _encode(page,conf) { + var encode = page.encode + var field = page.field + + var value = conf[field] + if (Array.isArray(value)) { + var temp = [] + for (var i =0; i < value.length; i++) { + temp.push(_lookupE(value[i],encode)) + } + conf[field] = temp + } else { + conf[field] = _lookupE(value,encode) // TODO: it is possible there could be a collision in encoded/decoded values + } + } + function _lookupE(v,encode) { + var vs = JSON.stringify(v) + if (vs in encode) { + return encode[vs] + } else { + return "~" + vs // store as JSON String, and set a flag character + } + } + + function step1decode(config) { + + // console.log("config before decoding",config) + for (var e in ui.menu) { + var item = ui.menu[e] + if (item.codebook) { + for (var page of item.codebook) { + _decode(page,config) + } + } + } + // console.log("config after decoding",config) + + for (var page of ui.extraCodeBook) { + _decode(page,config) + } + } + function _decode(page,config) { + var decode = page.decode + var field = page.field + + var value = config[field] + // console.log(value) + if (Array.isArray(value)) { + var temp = [] + for (var i =0; i < value.length; i++) { + temp.push(_lookup(value[i],decode)) + } + config[field] = temp + } else { + var temp = _lookup(value,decode) + config[field] = temp + } + } + + + function _lookup2(v,decode) { + var f = removeTildas(v) + if (f in decode) { + return decode[f] + } else { + return f + } + } + function _lookup(v,decode) { + var debug = 0 + var c = checkTilda(v) + if (c) { + var f = removeTildas(v) + if (debug >= 2) console.log(v,f) + return f + } + if (v in decode) { + var d = decode[v] + if (debug >= 2) console.log(v,d) + return d + } else { + // encode didn't work right on old versions, + // so this is for grandfathering in the old URLs + if (debug >= 3) console.log("") + if (debug >= 2) console.log(v) + return v + } + } + function removeTildas(v) { + // check if first char is tilda + + if (typeof(v) != "string") return v + var a = v.split("") + if (a[0] == "~") { + // if it is then remove it + var b = v.slice(1) + var c = JSON.parse(b) + // and send it back into removeTildas + var d = removeTildas(c) + // and return the result + return d + } else { + // otherwise, return it + return v + } + } + function checkTilda(v) { + // check if first character is a tilda + if (typeof(v) != "string") return false + var a = v.split("") + if (a[0] == "~") { + return true + } + return false + } + + + // HOWTO: + + // Decoding: + + // Why do we track the version of the encoding? + // So that we run current links in old versions of this web app + // Why do we want to do that? + // Maybe we messed something up and need to use an old version + // but don't want to re-create the link in the old version + + // I guess I don't really need to do version numbers + // It's just too much work for a hypothetical benefit + + // So just set the default version to the current one and don't worry. Its okay. + + // cleanConfig: + // The only real problem that it's worth keeping track of version numbers for is: + // interpreting old parameters if I replaced them with something else in my config, + // and that is handled in cleanConfig. + + + // Oh wait, there is a problem, + // When I open up old links, + // JSON Parse doesn't work on things not in the codebook + // + + // we should make sure no two codebooks are for the same fields + + // maybe in the future + // an example codebook should be + // + // self.codebook = { + // system: { + // 0:"FPTP", + // 1:"+Primary", + // 2:"Top Two", + // 3:"RBVote", + // 4:"IRV", + // 5:"Borda", + // 6:"Minimax", + // 7:"Schulze", + // 8:"RankedPair", + // 9:"Condorcet", + // 10:"Approval", + // 11:"Score", + // 12:"STAR", + // 13:"3-2-1", + // 14:"RRV", + // 15:"RAV", + // 16:"STV", + // 17:"QuotaApproval", + // }, + // } + + // URL Friendly Base 64 + // * use this to make a Base64 encoded string URL friendly, + // * i.e. '+' and '/' are replaced with '-' and '~' also any trailing '=' + // * characters are removed + // https://tools.ietf.org/html/rfc4648 + function Base64EncodeUrl(str){ + return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, ''); + } + + function Base64DecodeUrl(str){ + str = str + '===' // pad + var newlen = str.length - str.length % 4 // round + str = str.slice(0,newlen) // cut + // str = (str + '===').slice(0, str.length + (str.length % 4)); + return str.replace(/-/g, '+').replace(/_/g, '/'); + } + + + +} + +function createDOM(ui,model) { + ui.dom.left = newDivOnBase("left") + ui.dom.center = newDivOnBase("center") + ui.dom.right = newDivOnBase("right") + function newDivOnBase(name) { + var a = document.createElement("div"); + a.setAttribute("id", name); + ui.dom.basediv.appendChild(a); + return a + } + // Details + model.createDOM() + + var centerDiv = ui.dom.center + if (centerDiv.hasChildNodes()){ + var firstNode = centerDiv.childNodes[0] + centerDiv.insertBefore(model.dom,firstNode); + } else { + centerDiv.appendChild(model.dom) + } + model.dom.removeChild(model.caption); + ui.dom.right.appendChild(model.caption); + model.caption.style.width = ""; +} + +function menu(ui,model,config,initialConfig, cConfig) { + + // Each menu item is kind of similar to a mini instance of model.initPlugin. That's because most of the stuff in model.initPlugin has already been done. These small onChoose functions just launch when a button is pressed, which is after the whole sandbox has loaded. + + // HOWTO: Copy and paste a button function below and then search and replace all the mentions of the name + // the other places to look are in Cypher, Config, and Model + // as well as the menu trees at the end of this class. + + ui.menu = {} + + ui.menu_update = function() { + // UPDATE MENU // + + // Make the MENU look correct. The MENU is not part of the "model". + // for (i in ui.menu.percentSecondStrategy.choose.sliders) ui.menu.percentSecondStrategy.choose.sliders[i].setAttribute("style",(i<config.numVoterGroups) ? "display:inline": "display:none") + // for (i in ui.menu.group_count.choose.sliders) ui.menu.group_count.choose.sliders[i].setAttribute("style",(i<config.numVoterGroups) ? "display:inline": "display:none") + // for (i in ui.menu.group_spread.choose.sliders) ui.menu.group_spread.choose.sliders[i].setAttribute("style",(i<config.numVoterGroups) ? "display:inline": "display:none") + + for (i in ui.menu.percentSecondStrategy.choose.sliders) { + if (i < model.voterGroups.length && model.voterGroups[i].voterGroupType == "GaussianVoters") { + var style = "display:inline" + } else { + var style = "display:none" + } + ui.menu.percentSecondStrategy.choose.sliders[i].setAttribute("style",style) + ui.menu.group_count.choose.sliders[i].setAttribute("style",style) + ui.menu.group_spread.choose.sliders[i].setAttribute("style",style) + } + + } + + + ui.initButtons = function() { + + // draw sandbox buttons that depend on candidate images + var m = [ui.menu.yee, ui.menu.yeefilter, ui.menu.frontrunners] + m.forEach(function(m) { + m.choose.init() + m.select() + }) + + } + ui.redrawButtons = function() { + + // draw sandbox buttons that depend on candidate images + var m = [ui.menu.yee, ui.menu.yeefilter, ui.menu.frontrunners] + m.forEach(function(m) { + m.choose.redraw() + }) + + } + + /////////////////// + // SLIDERS CLASS // + /////////////////// + + var makeslider = function(cf) { + var self = this + self.slider = document.createElement("input"); + self.slider.type = "range"; + self.slider.max = cf.max; + self.slider.min = cf.min; + self.slider.value = cf.value; + //self.slider.setAttribute("width","20px"); + self.slider.id = cf.chid; + self.slider.n = cf.n + self.slider.chfn = cf.chfn + self.slider.class = "slider"; + self.slider.addEventListener('input', function() {self.slider.chfn(self.slider,self.slider.n)}, true); + var label = document.createElement('label') + label.htmlFor = cf.chid; + label.appendChild(document.createTextNode(cf.chtext)); + cf.containchecks.appendChild(self.slider); + //cf.containchecks.appendChild(label); + self.slider.innerHTML = cf.chtext; + } // https://stackoverflow.com/a/866249/8210071 + + var sliderSet = function(cf){ + var self = this + self.dom = document.createElement('div') + self.dom.className = "button-group" + if (cf.labelText) { + var button_group_label = document.createElement('div') + button_group_label.className = "button-group-label" + button_group_label.innerHTML = cf.labelText; + self.dom.appendChild(button_group_label) + } + + cf.containchecks = self.dom.appendChild(document.createElement('div')); + cf.containchecks.id="containsliders" + self.sliders = [] + if(cf.num) { + for (var i = 0; i < cf.num; i++) { + cf.n = i + var slider = new makeslider(cf) + self.sliders.push(slider.slider) + } + } else { + cf.n = 0 + var slider = new makeslider(cf) + self.sliders.push(slider.slider) + } + } + + /////////////////// + // Button Widths // + /////////////////// + let bw = (x) => (220 - 4*(x-1)) / x - 2 + + + ////////////////////////////////// + // BUTTONS - WHAT VOTING SYSTEM // + ////////////////////////////////// + + function _smaller(x) { return `<span class="smaller">${x}</span>`} + + ui.menu.systems = new function() { // Which voting system? + // "new function () {code}" means make an object "this", and run "code" in a new scope + // I made a singleton class so we can use "self" instead of saying "systems" (or another button group name). + // This is useful when we want to make another button group and we copy and paste this code. + // It might be better to make a class and then an instance, but I think this singleton class is easier. + // single = function() {stuff}; var systems = new single() + var self = this + + var autoSwitchDim = false + + self.list = [ + {name:"FPTP", value:"FPTP", ballotType:"Plurality", election:Election.plurality, margin:4}, + {name:"+Primary", value:"+Primary", ballotType:"Plurality", election:Election.pluralityWithPrimary}, + {name:"Top Two", value:"Top Two", ballotType:"Plurality", election:Election.toptwo, margin:4}, + {name:"RBVote", value:"RBVote", realname:"Rob Legrand's RBVote (Ranked Ballot Vote)", ballotType:"Ranked", election:Election.rbvote}, + {name:"IRV", value:"IRV", realname:"Instant Runoff Voting. Sometimes called RCV Ranked Choice Voting but I call it IRV because there are many ways to have ranked ballots.", ballotType:"Ranked", election:Election.irv, margin:4}, + {name:"Borda", value:"Borda", ballotType:"Ranked", election:Election.borda}, + {name:"Minimax", value:"Minimax", realname:"Minimax Condorcet method.", ballotType:"Ranked", election:Election.minimax, margin:4}, + {name:"Schulze", value:"Schulze", realname:"Schulze Condorcet method.", ballotType:"Ranked", election:Election.schulze}, + {name:"RankedPair", value:"RankedPair", realname:"Ranked Pairs Condorcet method.", ballotType:"Ranked", election:Election.rankedPairs, margin:4}, + {name:"Condorcet", value:"Condorcet", realname:"Choose the Condorcet Winner, and if there isn't one, Tie", ballotType:"Ranked", election:Election.condorcet}, + {name:"Approval", value:"Approval", ballotType:"Approval", election:Election.approval, margin:4}, + {name:"Score", value:"Score", ballotType:"Score", election:Election.score}, + {name:"STAR", value:"STAR", ballotType:"Score", election:Election.star, margin:4}, + {name:"3-2-1", value:"3-2-1", ballotType:"Three", election:Election.three21}, + {name:"RRV", value:"RRV", ballotType:"Score", election:Election.rrv, margin:4}, + {name:"RAV", value:"RAV", ballotType:"Approval", election:Election.rav}, + {name:"QuotaScore", value:"QuotaScore", realname:"Using a quota with score voting to make proportional representation.",ballotType:"Score", election:Election.quotaScore, margin:4}, + {name:"QuotaApproval", value:"QuotaApproval", realname:"Using a quota with approval voting to make proportional representation.",ballotType:"Approval", election:Election.quotaApproval}, + {name:"STV", value:"STV", ballotType:"Ranked", election:Election.stv, margin:4}, + {name:"STV-Minimax", value:"stvMinimax", realname:"Use STV to form equal clusters of voters. Then use Minimax within the voter clusters to elect candidates.",ballotType:"Ranked", election:Election.stvMinimax}, + {name:_smaller("TestQuotaMinimax"), nameIsHTML:true, value:"QuotaMinimax", realname:"An idea of using a quota with Minimax Condorcet voting to make proportional representation.",ballotType:"Ranked", election:Election.quotaMinimax}, + {name:"Test LP", value:"PhragmenMax", realname:"Phragmen's method of minimizing the maximum representation with assignments.",ballotType:"Score", election:Election.phragmenMax}, + {name:"PAV", value:"PAV", realname:"Proportional Approval Voting",ballotType:"Approval", election:Election.pav}, + {name:_smaller("Equal Facility"), nameIsHTML:true, value:"equalFacilityLocation", realname:"Facility location problem with equal assignments.",ballotType:"Score", election:Election.equalFacilityLocation}, + {name:"Monroe Seq S", value:"Monroe Seq S", realname:"A monroe-like sequential method.", ballotType:"Score",election:Election.monroeSequentialRange}, + {name:"AllocatedScore", value:"Allocated Score", realname:"A proportional sequential method. Monroe Sequential Score is similar.", ballotType:"Score",election:Election.allocatedScore}, + {name:"STAR PR", value:"STAR PR", realname:"A proportional sequential method using STAR to elect. Monroe Sequential Score is similar.", ballotType:"Score",election:Election.starPR}, + {name:"Phragmen Seq S", value:"Phragmen Seq S", realname:"A phragmen-like sequential method for score voting with KP transform.", ballotType:"Score",election:Election.phragmenSequentialRange}, + {name:"Create One", value:"Create",realname:"Write your own javascript code for a voting method.",ballotType:undefined, election:Election.create}, + + ]; + self.systemsCodebook = [ + { + field: "system", + decode: { + 0:"FPTP", + 1:"+Primary", + 2:"Top Two", + 3:"RBVote", + 4:"IRV", + 5:"Borda", + 6:"Minimax", + 7:"Schulze", + 8:"RankedPair", + 9:"Condorcet", + 10:"Approval", + 11:"Score", + 12:"STAR", + 13:"3-2-1", + 14:"RRV", + 15:"RAV", + 16:"STV", + 17:"QuotaApproval", + 18:"QuotaMinimax", + 19:"QuotaScore", + 20:"PhragmenMax", + 21:"equalFacilityLocation", + 22:"Create", + 23:"Monroe Seq S", + 24:"Phragmen Seq S", + 25:"stvMinimax", + 26:"Allocated Score", + 27:"STAR PR", + 28:"PAV", + } + } + ] + self.codebook = self.systemsCodebook + self.listByName = function() { + var votingSystem = self.list.filter(function(system){ + return(system.value==config.system); + })[0]; + return votingSystem; + } + self.onChoose = function(data){ + // LOAD INPUT + config.system = data.value; + config.ballotType = data.ballotType + if (data.value == "Create") config.ballotType = config.createBallotType + // CONFIGURE + self.configure() + ui.strategyOrganizer.configure() + // UPDATE + + // TODO: work this out so that the voters get re initialized in the correct place + if (autoSwitchDim) { + ui.menu.dimensions.onChoose({name:model.dimensions}) + ui.menu.dimensions.select() + } + for (var voter of model.voterGroups) { + voter.initVoterModel() + } + model.dm.redistrict() + model.update(); + ui.menu_update() + ui.strategyOrganizer.showOnlyStrategyForTypeOfSystem() + }; + self.choose = new ButtonGroup({ + label: "what voting system?", + width: bw(2), + data: self.list, + onChoose: self.onChoose + }); + self.configure= function() { + + showMenuItemsIf("divRBVote", config.system === "RBVote") + showMenuItemsIf("divLastTransfer", config.system === "IRV" || config.system === "STV") + showMenuItemsIf("divDoElectabilityPolls", config.system == "+Primary") + showMenuItemsIf("divSeats", model.checkMultiWinner(config.system)) + + + var hideCodeEditor = ! (config.system == "Create") + + showMenuItemsIf("divCreate", ! hideCodeEditor) + setTimeout( () => ui.dom.codeMirror.refresh() , 30); + + _displayNoneIf(ui.arena.codeSave.dom, hideCodeEditor) + ui.arena.codeEditor.dom.hidden = hideCodeEditor + // ui.arena.codeSave.dom.hidden = hideCodeEditor + + model.system = config.system; + + var s = self.listByName() + model.election = s.election + + + model.ballotType = config.ballotType || s.ballotType + + model.BallotType = window[model.ballotType+"Ballot"]; + + if (autoSwitchDim) { + if (model.checkSystemWithBarChart()) { + model.dimensions = "1D+B" + } else { + model.dimensions = "2D" + } + } + + if (model.checkGotoTarena()) { + model.tarena.canvas.hidden = false + } else { + model.tarena.canvas.hidden = true + } + for (var voter of model.voterGroups) { + voter.typeVoterModel = model.ballotType // needs init + } + for (let district of model.district) { + district.pollResults = undefined + } + + + if (model.ballotType == "Ranked") { + var goPairwise = _pickRankedDescription(model).doPairs + } else { + var goPairwise = false + } + showMenuItemsIf("divPairwiseMinimaps", goPairwise) + + } + self.select = function() { + self.choose.highlight("value", config.system) + } + } + + ui.menu.rbSystems = new function() { // Which RB voting system? + var self = this + self.list = [ + {name:"Baldwin", value:"Baldwin",rbelection:rbvote.calcbald, margin:4}, + {name:"Black", value:"Black",rbelection:rbvote.calcblac}, + {name:"Borda", value:"Borda",rbelection:rbvote.calcbord, margin:4}, + {name:"Bucklin", value:"Bucklin",rbelection:rbvote.calcbuck}, + {name:"Carey", value:"Carey",rbelection:rbvote.calccare, margin:4}, + {name:"Coombs", value:"Coombs",rbelection:rbvote.calccoom}, + {name:"Copeland", value:"Copeland",rbelection:rbvote.calccope, margin:4}, + {name:"Dodgson", value:"Dodgson",rbelection:rbvote.calcdodg}, + {name:"Hare", value:"Hare",rbelection:rbvote.calchare, margin:4}, + {name:"Nanson", value:"Nanson",rbelection:rbvote.calcnans}, + {name:"Raynaud", value:"Raynaud",rbelection:rbvote.calcrayn, margin:4}, + {name:"Schulze", value:"Schulze",rbelection:rbvote.calcschu}, + {name:"Simpson", value:"Simpson",rbelection:rbvote.calcsimp, margin:4}, + {name:"Small", value:"Small",rbelection:rbvote.calcsmal}, + {name:"Tideman", value:"Tideman",rbelection:rbvote.calctide} + ] + + self.codebook = [ + { + field: "rbsystem", + decode: { + 0:"Baldwin", + 1:"Black", + 2:"Borda", + 3:"Bucklin", + 4:"Carey", + 5:"Coombs", + 6:"Copeland", + 7:"Dodgson", + 8:"Hare", + 9:"Nanson", + 10:"Raynaud", + 11:"Schulze", + 12:"Simpson", + 13:"Small", + 14:"Tideman" + } + } + ] + self.listByName = function() { + var votingSystem = self.list.filter(function(system){ + return(system.value==config.rbsystem); + })[0]; + return votingSystem; + } + self.onChoose = function(data){ + // LOAD INPUT + config.rbsystem = data.value; + // CONFIGURE + self.configure() + // UPDATE + model.update(); + }; + self.choose = new ButtonGroup({ + label: "which RB voting system?", + width: bw(2), + data: self.list, + onChoose: self.onChoose + }); + self.configure= function() { + model.rbsystem = config.rbsystem + model.rbelection = self.listByName().rbelection + for (let district of model.district) { + district.pollResults = undefined + } + } + self.select = function() { + self.choose.highlight("value", config.rbsystem) + } + } + + ui.menu.dimensions = new function () { + var self = this + self.list = [ + {name:"2D", value:"2D",realname:"Two Position Dimensions",margin:4}, + {name:"1D+B", value:"1D+B",realname:"One Position Dimension Horizontally, Plus Broadness in the Vertical Dimension", margin:4}, + {name:"1D", value:"1D", realname:"One Dimension Horizontally, Vertical Doesn't Matter"} + ] + self.codebook = [ { + field: "dimensions", + decode: { + 0:"2D", + 1:"1D+B", + 2:"1D", + } + } ] + self.onChoose = function(data){ + // LOAD + config.dimensions = data.value + // CONFIGURE + self.configure() + // INIT (LOADER) + model.initDOM() + // INIT + model.voterManager.initVoters() + _pileVoters(model) + model.dm.redistrict() + for (var i=0; i<model.candidates.length; i++) { + model.candidates[i].init() + } + // UPDATE + model.update() + // _objF(ui.arena,"update") + // ui.menu.spread_factor_voters.select() + }; + self.configure = function() { + model.dimensions = config.dimensions + } + self.select = function() { + self.choose.highlight("value", config.dimensions); + } + self.choose = new ButtonGroup({ + label: "Arena Dimensions:", + width: bw(3), + data: self.list, + onChoose: self.onChoose + }); + } + + ui.menu.nDistricts = new function () { + var self = this + self.list = [ + {name:"1", value:1,margin:4}, + {name:"2", value:2,margin:4}, + {name:"3", value:3,margin:4}, + {name:"4", value:4,margin:4}, + {name:"5", value:5,margin:4}, + {name:"6", value:6,margin:4}, + {name:"7", value:7,margin:4}, + {name:"8", value:8,margin:4}, + {name:"9", value:9,margin:4}, + {name:"10", value:10, value:"10"} + ] + self.codebook = [ { + field: "nDistricts", + decode: { + 0:0, + 1:1, + 2:2, + 3:3, + 4:4, + 5:5, + 6:6, + 7:7, + 8:8, + 9:9, + } + } ] + self.onChoose = function(data){ + // LOAD + config.nDistricts = Number(data.value) + // CONFIGURE + self.configure() + // INIT + model.dm.redistrict() + // UPDATE + model.update() + }; + self.configure = function() { + model.nDistricts = config.nDistricts + if (model.checkGotoTarena()) { // TODO: make a visualization for more than 1 district + model.tarena.canvas.hidden = false + } else { + model.tarena.canvas.hidden = true + } + } + self.select = function() { + self.choose.highlight("value", config.nDistricts); + } + self.choose = new ButtonGroup({ + label: "Number of Districts:", + width: bw(10), + data: self.list, + onChoose: self.onChoose + }); + } + + ui.menu.seats = new function () { + var self = this + self.list = [ + {name:"1", value:1,margin:4}, + {name:"2", value:2,margin:4}, + {name:"3", value:3,margin:4}, + {name:"4", value:4,margin:4}, + {name:"5", value:5,margin:4}, + {name:"6", value:6,margin:4}, + {name:"7", value:7,margin:4}, + {name:"8", value:8,margin:4}, + {name:"9", value:9,margin:4}, + {name:"10", value:10} + ] + self.codebook = [ { + field: "seats", + decode: { + 0:0, + 1:1, + 2:2, + 3:3, + 4:4, + 5:5, + 6:6, + 7:7, + 8:8, + 9:9, + 10:10, + } + } ] + self.onChoose = function(data){ + // LOAD + config.seats = data.value + // CONFIGURE + self.configure() + // UPDATE + model.update() + }; + self.configure = function() { + model.seats = config.seats + } + self.select = function() { + self.choose.highlight("value", config.seats); + } + self.choose = new ButtonGroup({ + label: "How many Seats:", + width: bw(10), + data: self.list, + onChoose: self.onChoose + }); + } + + ui.menu.nVoterGroups = new function() { // How many voters? + var self = this + self.list = [ + {realname: "Single Voter", value:"Single Voter", name:"웃", nameIsHTML:true, num:1, margin:4, oneVoter:true}, + {realname: "One Group", value:"One Group", name:"1", num:1, margin:4}, + {realname: "Two Groups", value:"Two Groups", name:"2", num:2, margin:4}, + {realname: "Three Groups", value:"Three Groups", name:"3", num:3, margin:4}, + {realname: "Different Sized Groups (like a snowman)", value:"Different Sized Groups (like a snowman)", name:"☃", nameIsHTML:true, num:3, snowman:true, margin:4}, + {realname: "Custom Number of Voters and Sizes and Spreads", value:"Custom Number of Voters and Sizes and Spreads", name:"X", num:4, x_voters:true}, + ]; + self.codebook = [ + { + decode: { + 0:"Single Voter", + 1:"One Group", + 2:"Two Groups", + 3:"Three Groups", + 4:"Different Sized Groups (like a snowman)", + 5:"Custom Number of Voters and Sizes and Spreads" + }, + field: "nVoterGroupsRealName" + }, + { + decode: { + 0:1, + 1:2, + 2:3, + 3:4, + }, + field: "numVoterGroups" + }, + { + decode: { + 0:"GaussianVoters", + 1:"SingleVoter" + }, + field: "voterGroupTypes" + }, + { + field: "snowman", + decode: { + 0:false, + 1:true, + } + }, + { + field: "x_voters", + decode: { + 0:false, + 1:true, + } + }, + { + field: "voterGroupSnowman", + decode: { + 0:false, + 1:true, + } + }, + { + field: "voterGroupX", + decode: { + 0:false, + 1:true, + } + }, + { + field: "oneVoter", + decode: { + 0:false, + 1:true, + } + }, + { + field: "crowdShape", + decode: { + 0:"Nicky circles", + 1:"gaussian sunflower", + 2:"circles", + } + }, + ] + self.listByName = function() { // when we load from a config + if (config.x_voters) { + return self.list.filter( function(x){return x.x_voters || false})[0] + } else if (config.snowman) { + return self.list.filter( function(x){return x.snowman || false})[0] + } else if (config.oneVoter) { + return self.list.filter( function(x){return x.oneVoter || false})[0] + } else { + return self.list.filter( function(x){return x.num==config.numVoterGroups && (x.oneVoter || false) == false && (x.snowman || false) == false})[0] + } + } + self.onChoose = function(data){ + // LOAD INPUT + // add the configuration for the voter groups when "X" is chosen + + + if(data.x_voters) { + config.numVoterGroups = config.xNumVoterGroups + } else { + config.numVoterGroups = data.num; + } + config.nVoterGroupsRealName = data.value // this set of attributes is calculated based on config + config.snowman = data.snowman || false; + config.x_voters = data.x_voters || false; + config.oneVoter = data.oneVoter || false; + config.voterPositions = null + config.voterGroupTypes = null + // CREATE + model.voterGroups = [] + if (config.oneVoter) { + model.voterGroups.push(new SingleVoter(model)) + } else { + for(var i=0; i<config.numVoterGroups; i++) { + model.voterGroups.push(new GaussianVoters(model)) + } + } + // CONFIGURE + self.configure() + //_objF(ui.menu,"configure") // TODO: do I need this? + ui.strategyOrganizer.configure() + ui.menu.spread_factor_voters.configure() + // INIT + model.initMODEL() + model.voterManager.initVoters() + _pileVoters(model) + model.dm.redistrict() + // UPDATE + model.update() + ui.menu_update() + }; + self.configure = function() { + // MODEL // + + showMenuItemsIf("divXVoterGroups", config.x_voters) + + // MODEL // + model.nVoterGroupsRealName = config.nVoterGroupsRealName + + var s = ui.menu.systems.listByName() + model.VoterType = window[s.ballotType+"Voter"] + + if (config.voterGroupTypes && config.voterPositions) { + // we are reading a config string of version 2.2 or greater + for(var i=0; i<config.voterPositions.length; i++){ + var pos = config.voterPositions[i]; + Object.assign(model.voterGroups[i], { + vid: i, + disk:(4-num), + x:pos[0], + y:pos[1], + snowman: config.voterGroupSnowman[i], + x_voters: config.voterGroupX[i], + disk: config.voterGroupDisk[i], + crowdShape: config.crowdShape[i], + group_count_vert: config.group_count_vert[i], + group_count_h: config.group_count_h[i], + }) + if (config.voterGroupRandomSeeds && config.voterGroupRandomSeeds[i]) { + // if we are reading a config that includes a seed, then assign the seed + Object.assign(model.voterGroups[i], { + randomSeed: config.voterGroupRandomSeeds[i] + }) + } + model.voterGroups[i].typeVoterModel = model.ballotType // needs init + } + } else if (config.voterPositions) { + var num = model.voterGroups.length + for(var i=0; i<config.voterPositions.length; i++){ + var pos = config.voterPositions[i]; + Object.assign(model.voterGroups[i], { + vid: i, + disk:(4-num), + x:pos[0], + y:pos[1], + snowman: config.snowman, + x_voters: config.x_voters + }) + model.voterGroups[i].typeVoterModel = model.ballotType // needs init + } + } else { + var num = model.voterGroups.length + var voterPositions; + if (config.snowman) { + voterPositions = [[150,83],[150,150],[150,195]] + }else if(config.x_voters) { + voterPositions = [[65,150],[150,150],[235,150],[150,65]] + if (1) {//(num > 4) { + + + var points = []; + var angle = 0; + var _radius = 0; + var _radius_norm = 0; + var _spread_factor = 600 * .2 + var theta = Math.TAU * .5 * (3 - Math.sqrt(5)) + for (var count = 0; count < num; count++) { + angle = theta * count + _radius_norm = Math.sqrt((count+.5)/num) + _radius = _radius_norm * _spread_factor + + var x = Math.cos(angle)*_radius + 150 ; + var y = Math.sin(angle)*_radius + 150 ; + points.push([x,y]); + } + voterPositions = points + + } + }else if(num==1){ + voterPositions = [[150,150]]; + }else if(num==2){ + voterPositions = [[95,150],[205,150]]; + }else if(num==3){ + voterPositions = [[65,150],[150,150],[235,150]]; + + } + + for(var i=0; i<num; i++){ + var pos = voterPositions[i]; + Object.assign(model.voterGroups[i], { + vid: i, + disk:(4-num), + x:pos[0] * config.arena_size / 300, //+ (config.arena_size - 300) * .5 + y:pos[1] * config.arena_size / 300, //+ (config.arena_size - 300) * .5 + snowman: config.snowman, + x_voters: config.x_voters + }) + model.voterGroups[i].typeVoterModel = model.ballotType // needs init + + } + } + } + self.select = function() { + self.choose.highlight("value", config.nVoterGroupsRealName); + } + self.choose = new ButtonGroup({ + label: "how many groups of voters?", + width: bw(6), + data: self.list, + onChoose: self.onChoose + }); + } + + ui.menu.xVoterGroups = new function() { // if the last option X is selected, we need a selection for number of voters + var self = this + self.onChoose = function(slider,n) { + // LOAD INPUT + var num = slider.value + config.xNumVoterGroups = num; + config.numVoterGroups = num; + + // CREATE + model.voterGroups = [] + for(var i=0; i<num; i++) { + model.voterGroups.push(new GaussianVoters(model)) + } + config.voterPositions = null + // CONFIGURE + ui.menu.nVoterGroups.configure() // same settings in this other button + ui.strategyOrganizer.configure() + ui.menu.spread_factor_voters.configure() + // INIT + model.initMODEL() + model.voterManager.initVoters() + _pileVoters(model) + model.dm.redistrict() + // UPDATE + model.update() + ui.menu_update() + + } + self.choose = new sliderSet({ + max: ui.maxVoters-1, + min:"1", + value:"4", + chtext:"", + chid:"choose number of voter groups", + chfn:self.onChoose + }) + self.select = function() { + self.choose.sliders[0].value = config.xNumVoterGroups // TODO: load x_voters config somehow + } + } + + ui.menu.group_count = new function() { // group count + var self = this + self.codebook = [ { + field: "voter_group_count", + decode: { + 0:50, + } + },{ + field: "voter_group_spread", + decode: { + 0:190, + } + } ] + self.onChoose = function(slider,n) { + // LOAD INPUT + config.voter_group_count[n] = slider.value; + // CONFIGURE + self.configure() + // INIT + model.voterGroups[n].init() + _pileVoters(model) + model.dm.redistrict() + // UPDATE + model.update() + ui.menu_update() + } + self.configure = function() { + for (var i=0; i<model.voterGroups.length; i++) { + self.configureN(i) + } + } + self.configureN = function(n) { + if (model.voterGroups[n].voterGroupType=="GaussianVoters") { + model.voterGroups[n].group_count =config.voter_group_count[n] + } + } + self.select = function() { + for (i in self.choose.sliders) { + self.choose.sliders[i].value = config.voter_group_count[i] + } + } + self.updateFromModel = function(n) { + for (i in model.voterGroups) { + if (model.voterGroups[i].voterGroupType=="GaussianVoters") { + var s = model.voterGroups[i].group_count + config.voter_group_count[i] = s + self.choose.sliders[i].value = s + } + } + } + self.choose = new sliderSet({ + max: "200", + min:"0", + value:"50", + chtext:"", + chid:"choose number", + chfn:self.onChoose, + num:ui.maxVoters, + labelText: "what # voters in each group?" + }) + } + + ui.menu.group_spread = new function() { // group count + var self = this + self.onChoose = function(slider,n) { + // LOAD INPUT + config.voter_group_spread[n] = slider.value; + // CONFIGURE + self.configureN(n) + // INIT + model.voterGroups[n].init() + _pileVoters(model) + model.dm.redistrict() + // UPDATE + model.update() + ui.menu_update() + } + self.configure = function() { + for (var i=0; i<model.voterGroups.length; i++) { + self.configureN(i) + } + } + self.configureN = function(n) { + if (model.voterGroups[n].voterGroupType=="GaussianVoters") { + model.voterGroups[n].group_spread = config.voter_group_spread[n] + } + } + self.choose = new sliderSet({ + max: "500", + min:"10", + value:"250", + chtext:"", + chid:"choose width in pixels", + chfn:self.onChoose, + num:ui.maxVoters, + labelText: "how spread out is the group?" + }) + self.select = function() { + for (i in self.choose.sliders) { + self.choose.sliders[i].value = config.voter_group_spread[i] + } + } + self.updateFromModel = function(n) { + for (i in model.voterGroups) { + if (model.voterGroups[i].voterGroupType=="GaussianVoters") { + var s = model.voterGroups[i].group_spread + config.voter_group_spread[i] = s + self.choose.sliders[i].value = s + } + } + } + } + + ui.menu.nCandidates = new function() { // how many candidates? + var self = this + self.list = [ + {name:"two", num:2, margin:4}, + {name:"three", num:3, margin:4}, + {name:"four", num:4, margin:4}, + {name:"five", num:5} + ]; + self.onChoose = function(data){ + // LOAD INPUT + config.numOfCandidates = data.num; + config.candidatePositions = null + config.candidateSerials = null + config.candidateB = null + // CREATE + model.candidates = [] + for(var i=0; i<config.numOfCandidates; i++) { + model.candidates.push(new Candidate(model)) + } + // CONFIGURE + self.configure() + + // INIT + for(var i=0; i<model.candidates.length; i++) { + model.candidates[i].init() + } + model.initMODEL() + // UPDATE + + model.dm.redistrictCandidates() + + // update model + model.update() + }; + self.choose = new ButtonGroup({ + label: "how many candidates?", + width: bw(4), + data: self.list, + onChoose: self.onChoose + }); + self.configure = function() { + // expanding upon what the button means for the model + // this handles all the candidate creation every time a set of candidates is loaded + model.numOfCandidates = config.numOfCandidates + // Candidates, in a circle around the center. + var _candidateIcons = Object.keys(Candidate.graphicsByIcon["Default"]) + var num = config.numOfCandidates; + if (config.candidatePositions) { + model.numOfCandidates = config.candidatePositions.length + for(var i=0; i<config.candidatePositions.length; i++){ + var serial = i + if (config.candidateSerials) { + serial = config.candidateSerials[i] + } + var b = 1 + if (config.candidateB) { + b = config.candidateB[i] + } + var icon = _candidateIcons[serial % _candidateIcons.length]; + var instance = Math.floor(serial / _candidateIcons.length) + 1 + Object.assign(model.candidates[i],{ + icon:icon, + instance:instance, + b:b, + x:config.candidatePositions[i][0], + y:config.candidatePositions[i][1] + }) + } + } else { + var angle = 0; + switch(num){ + case 3: angle=Math.TAU/12; break; + case 4: angle=Math.TAU/8; break; + case 5: angle=Math.TAU/6.6; break; + } + for(var i=0; i<num; i++){ + var r = 100; + var x = 150 - r*Math.cos(angle) + (config.arena_size - 300) * .5; // probably replace "model" with "config", but maybe this will cause a bug + var y = 150 - r*Math.sin(angle) + (config.arena_size - 300) * .5; // TODO check for bug + var b = 1 + var icon = _candidateIcons[i]; + Object.assign(model.candidates[i],{ + icon:icon, + x:x, + y:y, + b:b + }) + angle += Math.TAU/num; + } + } + } + self.select = function() { + self.choose.highlight("num", config.numOfCandidates); + } + } + + ui.menu.customNames = new function () { + var self = this + self.list = [ + {name:"Yes", value:"Yes",margin:4}, + {name:"No", value:"No"} + ] + self.codebook = [ { + field: "customNames", + decode: { + 0:"No", + 1:"Yes", + } + } ] + self.onChoose = function(data){ + // LOAD + config.customNames = data.value + // CONFIGURE + self.configure() + // INIT + for(var i=0; i<model.candidates.length; i++) { + model.candidates[i].init() + } + // UPDATE + model.draw() + }; + self.configure = function() { + showMenuItemsIf("divCustomNames", config.customNames === "Yes") + model.customNames = config.customNames + ui.menu.namelist.choose.dom.hidden = (model.customNames == "Yes") ? false : true + } + self.select = function() { + self.choose.highlight("value", config.customNames); + } + self.choose = new ButtonGroup({ + label: "Customize Candidates' Names?", + width: bw(3), + data: self.list, + onChoose: self.onChoose + }); + } + + ui.menu.voterGroupCustomNames = new function () { + var self = this + self.list = [ + {name:"Yes", value:"Yes",margin:4}, + {name:"No", value:"No"} + ] + self.codebook = [ { + field: "voterGroupCustomNames", + decode: { + 0:"No", + 1:"Yes", + } + } ] + self.onChoose = function(data){ + // LOAD + config.voterGroupCustomNames = data.value + // CONFIGURE + self.configure() + // INIT + model.voterManager.initNames() + // UPDATE + model.draw() + }; + self.configure = function() { + showMenuItemsIf("divVoterGroupCustomNames", config.voterGroupCustomNames === "Yes") + model.voterGroupCustomNames = config.voterGroupCustomNames + ui.menu.voterGroupNameList.choose.dom.hidden = (model.voterGroupCustomNames == "Yes") ? false : true + } + self.select = function() { + self.choose.highlight("value", config.voterGroupCustomNames); + } + self.choose = new ButtonGroup({ + label: "Customize Voter Groups' Names?", + width: bw(3), + data: self.list, + onChoose: self.onChoose + }); + } + + ui.menu.namelist = new function () { + var self = this + self.codebook = [ { + field: "namelist", + decode: { + 0:"", + } + } ] + self.onChoose = function(){ + // LOAD + config.namelist = self.choose.dom.value + // CONFIGURE + self.configure() + // INIT + for(var i=0; i<model.candidates.length; i++) { + model.candidates[i].init() + } + // UPDATE + model.draw() + }; + self.configure = function() { + model.namelist = config.namelist.split("\n") + } + self.select = function() { + self.choose.dom.value = config.namelist + } + self.choose = { + dom: document.createElement("textarea") + } + self.choose.dom.addEventListener("input",self.onChoose) + } + + + ui.menu.voterGroupNameList = new function () { + var self = this + self.codebook = [ { + field: "voterGroupNameList", + decode: { + 0:"", + } + } ] + self.onChoose = function(){ + // LOAD + config.voterGroupNameList = self.choose.dom.value + // CONFIGURE + self.configure() + // INIT + model.voterManager.initNames() + // UPDATE + model.draw() + }; + self.configure = function() { + model.voterGroupNameList = config.voterGroupNameList.split("\n") + } + self.select = function() { + self.choose.dom.value = config.voterGroupNameList + } + self.choose = { + dom: document.createElement("textarea") + } + self.choose.dom.addEventListener("input",self.onChoose) + } + + + + ui.strategyOrganizer = new function() { // an organizer for each strategy type's menu items + var self = this + + var choiceType = "choice" + var scoreType = "score" + var pairType = "pair" + self.types = [choiceType,pairType,scoreType] + var lookupStratBySys = { + "FPTP": choiceType, + "+Primary": choiceType, + "Top Two": choiceType, + "RBVote": pairType, + "IRV": choiceType, + "Borda": pairType, + "Minimax": pairType, + "Schulze": pairType, + "RankedPair": pairType, + "Condorcet": pairType, + "Approval": scoreType, + "Score": scoreType, + "STAR": scoreType, + "3-2-1": scoreType, + "RRV": scoreType, + "RAV": scoreType, + "STV": choiceType, + "QuotaApproval": scoreType, + "QuotaMinimax": pairType, + "stvMinimax": pairType, + "QuotaScore": scoreType, + "PhragmenMax": scoreType, + "PAV": scoreType, + "equalFacilityLocation": scoreType, + "Monroe Seq S": scoreType, + "Phragmen Seq S": scoreType, + "Allocated Score": scoreType, + "STAR PR": scoreType, + } + self.stratBySys = function(sys) { + if (sys == "Create") { + return model.createStrategyType + } else { + return lookupStratBySys[sys] + } + } + self.menuNameFirst = { + "choice":"choiceFirstStrategy", + "pair":"pairFirstStrategy", + "score":"scoreFirstStrategy", + } + self.menuNameSecond = { + "choice":"choiceSecondStrategy", + "pair":"pairSecondStrategy", + "score":"scoreSecondStrategy", + } + self.divMenuNameFirst = { + "choice":"divChoiceFirstStrategy", + "pair":"divPairFirstStrategy", + "score":"divScoreFirstStrategy", + } + self.divMenuNameSecond = { + "choice":"divChoiceSecondStrategy", + "pair":"divPairSecondStrategy", + "score":"divScoreSecondStrategy", + } + self.decodeList = { + 0:"zero strategy. judge on an absolute scale.", + 1:"normalize", + 2:"normalize frontrunners only", + 3:"best frontrunner", + 4:"not the worst frontrunner", + } + self.translate = function(strat,sys) { + // type O N F F+ F- + // score O N F F+ F- + // choice O O F F F + // pair O O O O O + var zero = "zero strategy. judge on an absolute scale." + var theType = self.stratBySys(sys) + if (theType == "choice") { + var not_f = [zero,"normalize"] + if (not_f.includes(strat)) { + return zero + } else { + return "normalize frontrunners only" + } + } else if (theType == "pair") { + return zero + } else if (theType == "score") { + return strat + } else { + return strat + } + } + self.onChoose = function(data){ + // CONFIGURE + self.configure() + // UPDATE + model.update(); + ui.menu_update() + }; + self.configure = function() { + // take on the strategy of the relevant system type + var theType = self.stratBySys(config.system) + var theConfigFirst = self.menuNameFirst[theType] + config.firstStrategy = config[theConfigFirst] + var theConfigSecond = self.menuNameSecond[theType] + config.secondStrategy = config[theConfigSecond] + + _showOrHideMenuForStrategy(config) + model.firstStrategy = config.firstStrategy + model.secondStrategy = config.secondStrategy + + var listMenuFirst = ui.menu[theConfigFirst].list + var itemFirst = listMenuFirst.filter(x => x.value == config.firstStrategy) + model.realNameFirstStrategy = itemFirst[0].realname + var listMenuSecond = ui.menu[theConfigSecond].list + var itemSecond = listMenuSecond.filter(x => x.value == config.secondStrategy) + model.realNameSecondStrategy = itemSecond[0].realname + } + + self.showOnlyStrategyForTypeOfSystem = function() { + + var theType = self.stratBySys(model.system) + var types = self.types + + // show only the one that applies + for (var t of types) { + df = self.divMenuNameFirst[t] + ds = self.divMenuNameSecond[t] + showMenuItemsIf(df, t == theType ) + showMenuItemsIf(ds, t == theType ) + } + + } + } + + + ui.menu.loadCode = new function() { // set the type of strategy for the new method we're writing + var self = this + self.list = [ + {name:"Load Code", value:"Load Code", realname:"Load Code from an Existing Voting Method into the Code Editor"}, + ]; + self.onChoose = function(data){ + + // popup : + var msg = "Are you sure? Loading will overwrite any work you've done in the code editor." + var goahead = window.confirm(msg) + if (goahead) { + var whichmsg = "Which Voting Method Would You Like to Load? Type the name." + var nameMethod = prompt(whichmsg, "Score") + var methodList = ui.menu.systems.list.filter(x => x.name==nameMethod)[0] + if (methodList !== undefined) { + var methodFunction = methodList.election + // copy text into box + var stringFunction = methodFunction.toString() + ui.dom.codeMirror.setValue(stringFunction) + setTimeout( () => ui.dom.codeMirror.refresh() , 30); + + config.codeEditorText = stringFunction + model.codeEditorText = stringFunction + + // make sure the strategy and ballot types are updated + var bType = methodList.ballotType + var system = methodList.value + var sType = ui.strategyOrganizer.stratBySys(system) + + + config.createStrategyType = sType + ui.menu.createStrategyType.configure() + for (var voter of model.voterGroups) { + voter.initVoterModel() + } + + ui.menu.createStrategyType.select(sType) + + config.createBallotType = bType + ui.menu.createBallotType.configure() + ui.menu.createBallotType.select(bType) + + for (var voter of model.voterGroups) { + voter.initVoterModel() + } + + showMenuItemsIf("divLastTransfer", system === "IRV" || system === "STV") + showMenuItemsIf("divDoElectabilityPolls", system == "+Primary") + showMenuItemsIf("divSeats", model.checkMultiWinner(system)) + + // ui.menu.systems.onChoose(methodList.value) // run the election // but this might not work + + ui.strategyOrganizer.configure() + model.update(); + ui.menu_update() + ui.strategyOrganizer.showOnlyStrategyForTypeOfSystem() + } else { + window.alert("Typo") + } + } + }; + self.configure = function() { + } + self.choose = new ButtonGroup({ + label: "Load Code from Existing Method?", + width: bw(2), + data: self.list, + onChoose: self.onChoose, + justButton: true, + }); + } + + ui.menu.createStrategyType = new function() { // set the type of strategy for the new method we're writing + var self = this + self.list = [ + {name:_smaller("Choice"), nameIsHTML:true, value:"choice", realname:"Do choice-type strategy", margin:4}, + {name:"Pair", value:"pair", realname:"Do pair-type strategy", margin:4}, + {name:"Score", value:"score", realname:"Do score-type strategy"}, + ]; + self.codebook = [ { + field: "createStrategyType", + decode: { + 0:"choice", + 1:"pair", + 2:"score", + } + } ] + self.onChoose = function(data){ + config.createStrategyType = data.value; + self.configure() + + for (var voter of model.voterGroups) { + voter.initVoterModel() + } + ui.strategyOrganizer.configure() + model.update(); + ui.menu_update() + ui.strategyOrganizer.showOnlyStrategyForTypeOfSystem() + }; + self.configure = function() { + model.createStrategyType = config.createStrategyType + } + self.choose = new ButtonGroup({ + label: "Strategy Type for New Method", + width: bw(5), + data: self.list, + onChoose: self.onChoose, + }); + self.select = function() { + self.choose.highlight("value", config.createStrategyType); + } + } + + + ui.menu.createBallotType = new function() { // set the ballot type for the new method we're making + var self = this + self.list = [ + {name:_smaller("Ranked"), nameIsHTML:true, value:"Ranked", realname:"Ranked", margin:4}, + {name:_smaller("Score"), nameIsHTML:true, value:"Score", realname:"Score", margin:4}, + {name:_smaller("Approval"), nameIsHTML:true, value:"Approval", realname:"Approval", margin:4}, + {name:_smaller("Plurality"), nameIsHTML:true, value:"Plurality", realname:"Plurality"}, + {name:_smaller("Three"), nameIsHTML:true, value:"Three", realname:"Three"}, + ]; + self.codebook = [ { + field: "createBallotType", + decode: { + 0:"Ranked", + 1:"Score", + 2:"Approval", + 3:"Plurality", + 4:"Three", + } + } ] + self.onChoose = function(data){ + config.createBallotType = data.value; + self.configure() + }; + self.configure = function() { + model.createBallotType = config.createBallotType + if (model.system == "Create") { + config.ballotType = config.createBallotType + model.ballotType = config.createBallotType + model.BallotType = window[config.ballotType+"Ballot"]; + + for (var voter of model.voterGroups) { + voter.typeVoterModel = config.ballotType // needs init + } + for (let district of model.district) { + district.pollResults = undefined + } + + + if (model.ballotType == "Ranked") { + var goPairwise = _pickRankedDescription(model).doPairs + } else { + var goPairwise = false + } + showMenuItemsIf("divPairwiseMinimaps", goPairwise) + } + } + self.choose = new ButtonGroup({ + label: "Ballot Type for New Method", + width: bw(4), + data: self.list, + onChoose: self.onChoose, + }); + self.select = function() { + self.choose.highlight("value", config.createBallotType); + } + } + + + ui.menu.scoreFirstStrategy = new function() { // just filling in firstStrategy with a limited set + var self = this + self.list = [ + {name:"J", value:"zero strategy. judge on an absolute scale.", realname:"Judge: Every voter judges the candidates on the same absolute scale of distance.", margin:4}, + {name:"N", value:"normalize", realname:"Normalize all: Highest score for closest, lowest for farthest. Somewhere in the middle for everyone else.", margin:4}, + {name:"F", value:"normalize frontrunners only", realname:"Frontrunners: Normalize only for the set of frontrunners. Highest score for all better. Lowest score for all worse.", margin:4}, + {name:"F+", value:"best frontrunner", realname:"Best frontrunner: Highest score for best frontrunner and all better. Lowest score for all others.", margin:4}, + {name:"F-", value:"not the worst frontrunner", realname:"Not the worst frontrunner: Lowest score for worst frontrunner and all worse. Highest score for all others."} + ]; + self.codebook = [ + { + decode: ui.strategyOrganizer.decodeList, + field: "scoreFirstStrategy" + }, + ] + self.onChoose = function(data){ + config.scoreFirstStrategy = data.value; + ui.strategyOrganizer.onChoose() + }; + self.choose = new ButtonGroup({ + label: "strategy for score-type voter:", + width: bw(5), + data: self.list, + onChoose: self.onChoose + }); + self.configure = function() { + return + } + self.select = function() { + self.choose.highlight("value", config.scoreFirstStrategy); + } + } + + ui.menu.choiceFirstStrategy = new function() { // just filling in firstStrategy with a limited set + var self = this + self.list = [ + {name:"H", value:"zero strategy. judge on an absolute scale.", realname:"Honesty", margin:4}, + {name:"F", value:"normalize frontrunners only", realname:"Pick the Best Frontrunner", margin:4}, + ]; + self.codebook = [ + { + decode: ui.strategyOrganizer.decodeList, + field: "choiceFirstStrategy" + }, + ] + self.onChoose = function(data){ + config.choiceFirstStrategy = data.value; + ui.strategyOrganizer.onChoose() + }; + self.choose = new ButtonGroup({ + label: "Voter Strategy " + _smaller("(choice-type):"), + labelIsHTML:true, + width: bw(5), + data: self.list, + onChoose: self.onChoose + }); + self.configure = function() { + return + } + self.select = function() { + self.choose.highlight("value", config.choiceFirstStrategy); + } + } + + ui.menu.pairFirstStrategy = new function() { // just filling in firstStrategy with a limited set + var self = this + self.list = [ + {name:"H", value:"zero strategy. judge on an absolute scale.", realname:"Honesty", margin:4}, + ]; + self.codebook = [ + { + decode: ui.strategyOrganizer.decodeList, + field: "pairFirstStrategy" + }, + ] + self.onChoose = function(data){ + config.pairFirstStrategy = data.value; + ui.strategyOrganizer.onChoose() + }; + self.choose = new ButtonGroup({ + label: "strategy for pair-type voter:", + width: bw(5), + data: self.list, + onChoose: self.onChoose + }); + self.configure = function() { + return + } + self.select = function() { + self.choose.highlight("value", config.pairFirstStrategy); + } + } + + function _showOrHideMenuForStrategy(config) { + var not_f = ["zero strategy. judge on an absolute scale.","normalize"] + var turnOffFrontrunnerControls = not_f.includes(config.firstStrategy) + if (config.doTwoStrategies) { + if (! not_f.includes(config.secondStrategy) ) { + turnOffFrontrunnerControls = false + } + } + var onFront = ! turnOffFrontrunnerControls + showMenuItemsIf("divPoll", onFront) + var doManual = config.autoPoll == "Manual" + showMenuItemsIf("divManualPoll", doManual) + } + + ui.menu.doTwoStrategies = new function() { // Is there a 2nd strategy? + var self = this + self.list = [ + {realname: "opton for 2nd strategy", name:"2", value:"2"} + ]; + self.codebook = [ + { + decode: ui.strategyOrganizer.decodeList, + field: "secondStrategies" // old. not used anymore, but kept + }, + { + decode: ui.strategyOrganizer.decodeList, + field: "secondStrategy" + }, + { + decode: ui.strategyOrganizer.decodeList, + field: "firstStrategy" + }, + { + field: "doTwoStrategies", + decode: { + 0:false, + 1:true, + }, + }, + ] + self.onChoose = function(data){ + // LOAD INPUT + config.doTwoStrategies = data.isOn + // CONFIGURE + self.configure() + // UPDATE + model.update(); + ui.menu_update() + }; + self.configure = function() { + showMenuItemsIf("divSecondStrategy", config.doTwoStrategies) + _showOrHideMenuForStrategy(config) + model.doTwoStrategies = config.doTwoStrategies + for (var i=0; i<model.voterGroups.length; i++) { + model.voterGroups[i].doTwoStrategies = config.doTwoStrategies + } + } + self.select = function() { + if (config.doTwoStrategies) { + var a = ["2"] + } else { + var a = [] + } + self.choose.highlight("value", a) + } + self.choose = new ButtonGroup({ + label: "", + width: bw(5), + data: self.list, + onChoose: self.onChoose, + isCheckbox: true + }); + } + + ui.menu.scoreSecondStrategy = new function() { // just filling in secondStrategy with a limited set + var self = this + self.list = ui.menu.scoreFirstStrategy.list + self.codebook = [ + { + decode: ui.strategyOrganizer.decodeList, + field: "scoreSecondStrategy" + }, + ] + self.onChoose = function(data){ + config.scoreSecondStrategy = data.value; + ui.strategyOrganizer.onChoose() + }; + self.choose = new ButtonGroup({ + label: "2nd strategy for score-type voter:", + width: bw(5), + data: self.list, + onChoose: self.onChoose + }); + self.configure = function() { + return + } + self.select = function() { + self.choose.highlight("value", config.scoreSecondStrategy); + } + } + + ui.menu.choiceSecondStrategy = new function() { // just filling in secondStrategy with a limited set + var self = this + self.list = ui.menu.choiceFirstStrategy.list + self.codebook = [ + { + decode: ui.strategyOrganizer.decodeList, + field: "choiceSecondStrategy" + }, + ] + self.onChoose = function(data){ + config.choiceSecondStrategy = data.value; + ui.strategyOrganizer.onChoose() + }; + self.choose = new ButtonGroup({ + label: "2nd strategy for choice-type voter:", + width: bw(5), + data: self.list, + onChoose: self.onChoose + }); + self.configure = function() { + return + } + self.select = function() { + self.choose.highlight("value", config.choiceSecondStrategy); + } + } + + ui.menu.pairSecondStrategy = new function() { // just filling in secondStrategy with a limited set + var self = this + self.list = ui.menu.pairFirstStrategy.list + self.codebook = [ + { + decode: ui.strategyOrganizer.decodeList, + field: "pairSecondStrategy" + }, + ] + self.onChoose = function(data){ + config.pairSecondStrategy = data.value; + ui.strategyOrganizer.onChoose() + }; + self.choose = new ButtonGroup({ + label: "2nd strategy for pair-type voter:", + width: bw(5), + data: self.list, + onChoose: self.onChoose + }); + self.configure = function() { + return + } + self.select = function() { + self.choose.highlight("value", config.pairSecondStrategy); + } + } + + ui.menu.percentSecondStrategy = new function() { // group count + var self = this + self.onChoose = function(slider,n) { + // LOAD INPUT + config.percentSecondStrategy[n] = slider.value; + // CONFIGURE + self.configureN(n) + // UPDATE + model.update(); + } + self.configure = function() { + for (var i=0; i<model.voterGroups.length; i++) { + self.configureN(i) + } + } + self.configureN = function(n) { + // _showOrHideMenuForStrategy(config) // not necessary + if (model.voterGroups[n].voterGroupType=="GaussianVoters") { + model.voterGroups[n].percentSecondStrategy = config.percentSecondStrategy[n] + } + } + self.select = function() { + for (i in self.choose.sliders) { + self.choose.sliders[i].value = config.percentSecondStrategy[i] + } + } + self.choose = new sliderSet({ + max: "100", + min:"0", + value:"50", + chtext:"", + chid:"choosepercent", + chfn:self.onChoose, + num:ui.maxVoters, + labelText: "what % use this 2nd strategy?" + }) + } + + if (0) { // are there primaries? + ui.menu.primaries = new function() { + var self = this + self.list = [ + {name:"Yes", value:"Yes",realname:"Yes", margin:4}, + {name:"No", value:"No",realname:"No"} + ]; + self.onChoose = function(data){ + config.primaries = data.value + self.configure() + model.update() + }; + self.configure = function() { + model.primaries = data.value + } + self.select = function() { + self.choose.highlight("value", config.primaries) + } + self.choose = new ButtonGroup({ + label: "Primaries?", + width: bw(3), + data: self.list, + onChoose: self.onChoose + }); + } + } + + ui.menu.autoPoll = new function() { // do a poll to find frontrunner + var self = this + self.list = [ + {name:"Auto", value:"Auto",realname:"Choose frontrunners automatically.", margin:4}, + {name:"Manual", value:"Manual",realname:"Press the poll button to find the frontrunners once."} + ]; + self.codebook = [ + { + field: "autoPoll", + decode: { + 0:"Auto", + 1:"Manual" + } + } + ] + self.onChoose = function(data){ + // LOAD INPUT + config.autoPoll = data.value + // CONFIGURE + self.configure() + // UPDATE + model.update(); + ui.menu_update() + }; + self.configure = function() { + showMenuItemsIf("divManualPoll", config.autoPoll == "Manual") + model.autoPoll = config.autoPoll + } + self.select = function() { + self.choose.highlight("value", config.autoPoll) + } + self.choose = new ButtonGroup({ + label: "AutoPoll to find new frontrunner:", + width: bw(3), + data: self.list, + onChoose: self.onChoose + }); + } + + + function _iconButton(id) { + return "<span class='buttonshape'>"+model.icon(id)+"</span>" + } + + ui.menu.frontrunners = new function() { // frontrunners + var self = this + self.list = undefined + // the makelist menu items are of a different class + // they can't have things be dependent on them + // they have to wait for the update step + // they can depend on a configured model + // maybe the initDOM step woudl be a good place ot put them + self.makelist = function() { + var a = [] + for (var i=0; i < model.candidates.length; i++) { + var c = model.candidates[i] + a.push({ + name:_iconButton(c.id), + nameIsHTML:true, + realname:c.id, + margin:4 + }) + } + if (a.length > 0) a[a.length-1].margin = 0 + return a + } + self.codebook = [ { + field: "preFrontrunnerIds", + decode: { + 0:"square", + 1:"triangle", + 2:"hexagon", + 3:"pentagon", + 4:"bob", + } + } ] + self.onChoose = function(data){ + // LOAD INPUT + var preFrontrunnerSet = new Set(config.preFrontrunnerIds) + if (data.isOn) { + preFrontrunnerSet.add(data.realname) + } else { + preFrontrunnerSet.delete(data.realname) + } + config.preFrontrunnerIds = Array.from(preFrontrunnerSet) + // CONFIGURE + self.configure() + // UPDATE + model.dm.districtsListCandidates() + model.update(); + }; + self.configure = function() { + model.preFrontrunnerIds = config.preFrontrunnerIds + for (var i=0; i<model.voterGroups.length; i++) { + model.voterGroups[i].preFrontrunnerIds = config.preFrontrunnerIds + } + } + self.select = function() { + self.choose.highlight("realname", config.preFrontrunnerIds); + } + self.choose = new ButtonGroup({ + label: "who are the frontrunners?", + width: bw(5), + makeData: self.makelist, + onChoose: self.onChoose, + isCheckbox: true + }); + } + + ui.menu.poll = new function() { // do a poll to find frontrunner + var self = this + self.list = [ + {name:"Poll",value:"Poll",margin:4}, + {name:"Poll 2",value:"Poll 2",realname:"Find the top 2 frontrunners."} + ]; + self.onChoose = function(data){ + // DO CALCULATIONS // + // get poll results + if (data.value == "Poll") { // get last poll + var won = model.result.winners + } else { // do new poll + model.doTop2 = true + model.update() + model.doTop2 = false + var won = model.result.theTop2 + } + // UPDATE CONFIG // + config.preFrontrunnerIds = won + // UPDATE MENU // + ui.menu.frontrunners.select() + // CONFIGURE AND UPDATE MODEL // + ui.menu.frontrunners.configure() + model.dm.districtsListCandidates() + model.update(); + }; + self.choose = new ButtonGroup({ + label: "Poll to find new frontrunner:", + width: bw(4), + data: self.list, + onChoose: self.onChoose, + justButton: true + }); + } + + + ui.menu.centerPollThreshold = new function() { // do a poll to find frontrunner + var self = this + self.list = [ + {name:".5", value:.5, margin:4}, + {name:".7", value:.7, margin:4}, + {name:".9", value:.9,} + ]; + // self.codebook = [ + // { + // field: "centerPollThreshold", + // decode: { + // 0:.5, + // 1:.7, + // 2:.9, + // } + // } + // ] + self.onChoose = function(data){ + // LOAD INPUT + config.centerPollThreshold = data.value + // CONFIGURE + self.configure() + // UPDATE + model.update() + }; + self.configure = function() { + model.centerPollThreshold = config.centerPollThreshold + } + self.select = function() { + self.choose.highlight("value", config.centerPollThreshold) + } + self.choose = new ButtonGroup({ + label: "What fraction of leading frontrunner's votes is viable?", + width: bw(4), + data: self.list, + onChoose: self.onChoose + }); + } + + + ui.menu.yee = new function() { // yee + var self = this + self.list = undefined + self.makelist = function() { + var a = [] + for (var i=0; i < model.voterGroups.length; i++) { + var v = model.voterGroups[i] + a.push({ + name:i+1, + realname:"voter group #"+(i+1), + keyyee:i, + kindayee:"voter", + margin:4 + }) + } + var calcWidth = 6 * bw(10) + 5 * 4 + a.push({ + name:"Voter Center", + realname:"all voters", + keyyee:"mean", + kindayee:"center", + width: calcWidth, + margin:220-calcWidth, + }) + for (var i=0; i < model.candidates.length; i++) { + var c = model.candidates[i] + a.push({ + name:_iconButton(c.id), + nameIsHTML:true, + realname:c.id, + keyyee:c.id, + kindayee:"can", + margin:4 + }) + } + a.push({ + name:"+", + realname:"Additional Candidate", + keyyee: "newcan", + kindayee:"newcan", + margin:4 + }) + + + return a + } + self.codebook = [ { + field: "kindayee", + decode: { + 0:"voter", + 1:"center", + 2:"can", + 3:"newcan", + } + },{ + field: "keyyee", + decode: { + 0:"newcan", + 1:"mean", + 2:"square", + 3:"triangle", + 4:"hexagon", + 5:"pentagon", + 6:"bob", + } + } ] + self.onChoose = function(data){ + // LOAD INPUT + config.kindayee = data.kindayee + config.keyyee = data.keyyee + // CONFIGURE + self.configure() + // INIT + model.initMODEL() + // UPDATE + model.update(); + ui.menu_update() + }; + self.configure = function() { + showMenuItemsIf("divYee", true) // kind of a holdover from a previous version + model.kindayee = config.kindayee + model.keyyee = config.keyyee + } + self.select = function() { + self.choose.highlight("keyyee", config.keyyee); + } + self.choose = new ButtonGroup({ + label: "which object for yee map?", + width: bw(9), + makeData: self.makelist, + onChoose: self.onChoose, + }); + self.choose.dom.setAttribute("id","yee") // interesting + } + + ui.menu.yeefilter = new function() { // yee filter + var self = this + self.list = undefined + self.makelist = function() { + var a = [] + var newListSerial = [] + var newListId = [] + for (var i=0; i < model.candidates.length; i++) { + var c = model.candidates[i] + a.push({ + name:_iconButton(c.id), + nameIsHTML:true, + realname:c.id, + keyyee:c.id, + serial: c.serial, + margin:4 + }) + if (c.serial in config.yeefilter) { + // if (Object.keys(config.yeefilter).includes(c.serial)) { + newListSerial[c.serial] = config.yeefilter[c.serial] + newListId[c.id] = config.yeefilter[c.serial] + } else { + newListSerial[c.serial] = true + newListId[c.id] = true + } + } + + // update yeefilter + config.yeefilter = newListSerial + model.yeefilter = newListId + return a + } + self.codebook = [ { + field: "yeefilter", + decode: { + 0:false, + 1:true, + } + } ] + self.onChoose = function(data){ + // LOAD CONFIG // + config.yeefilter[data.serial] = data.isOn + // CONFIGURE + self.configure() + // UPDATE + model.drawArenas(); + }; + self.configure = function() { + for (var serial in config.yeefilter) { + var id = Candidate.idFromSerial(serial,config.theme) + model.yeefilter[id] = config.yeefilter[serial] + } + return + // model.yeefilter = config.yeefilter + } + self.select = function() { + self.choose.highlight("serial", config.yeefilter); + } + self.choose = new ButtonGroup({ + label: "filter yee map?", + width: bw(9), + makeData: self.makelist, + onChoose: self.onChoose, + isCheckboxBool: true + }); + self.choose.dom.setAttribute("id","yeefilter") + } + + ui.menu.gearconfig = new function() { // gear config - decide which menu items to do + var self = this + self.initSpecial = function() { + // list all the menu items + self.list = [] + var n=1 + for (var name in ui.menu) { + var mar = (n % 10 == 0) ? 0 : 4 + self.list.push({name:n,realname:name,margin:mar}) + n++ + } + self.choose = new ButtonGroup({ + label: "which menu options are displayed?", + width: bw(10), + data: self.list, + onChoose: self.onChoose, + isCheckbox: true + }); + } + self.codebook = [ + { + field: "featurelist", + decode: { + 0: "systems", + 1: "rbSystems", + 2: "dimensions", // still need to keep this even though it isn't here anymore + 3: "nVoterGroups", + 4: "xVoterGroups", + 5: "group_count", + 6: "group_spread", + 7: "nCandidates", + 8: "firstStrategy", + 9: "doTwoStrategies", + 10: "secondStrategy", + 11: "percentSecondStrategy", + 12: "autoPoll", + 13: "frontrunners", + 14: "poll", + 15: "yee", + 16: "yeefilter", + 17: "gearicon", + 18: "seats", + 19: "nDistricts", + 20: "customNames", + 21: "namelist", + 22: "gearconfig", + 23: "presetconfig", + 24: "choose_pixel_size", + 25: "computeMethod", + 26: "colorChooser", + 27: "colorSpace", + 28: "spread_factor_voters", + 29: "arena_size", + 30: "median_mean", + 31: "theme", + 32: "utility_shape", + 33: "votersAsCandidates", + 34: "ballotVis", + 35: "visSingleBallotsOnly", + 36: "gearoff", + 37: "menuVersion", + 38: "menuLevel", + 39: "stepMenu", + 40: "doFeatureFilter", + 41: "spacer", + 42: "yeeon", + 43: "beatMap", + 44: "ballotConcept", + 45: "roundChart", + 46: "sidebarOn", + 47: "lastTransfer", + 48: "voterIcons", + 49: "candidateIcons", + 50: "pairwiseMinimaps", + 51: "textBallotInput", + 52: "doTextBallots", + 53: "behavior", + 54: "submitTextBallots", + 55: "showToolbar", + 56: "rankedVizBoundary", + 57: "showDescription", + 58: "doElectabilityPolls", + 59: "partyRule", + 60: "doFilterSystems", + 61: "filterSystems", + 62: "doFilterStrategy", + 63: "includeSystems", + 64: "showUtilityChart", + 65: "putMenuAbove", + 66: "scoreFirstStrategy", + 67: "choiceFirstStrategy", + 68: "pairFirstStrategy", + 69: "scoreSecondStrategy", + 70: "choiceSecondStrategy", + 71: "pairSecondStrategy", + 72: "voterIcons", + 74: "useBeatMapForRankedBallotViz", + 75: "centerPollThreshold", + 76: "doMedianDistViz", + 77: "createStrategyType", + 78: "createBallotType", + 79: "voterGroupCustomNames", + 80: "namelist", + }, + } + ] + // HOWTO: This is another place to update when adding a new menu item + + self.onChoose = function(data){ + // LOAD INPUT + modifyConfigFeaturelist(config,data.isOn, [data.realname]) + // UPDATE + self.configure() + }; + self.configure = function() { + _hideOrShowFeatures() + } + self.select = function() { + self.choose.highlight("realname", config.featurelist); + } + } + + function _hideOrShowFeatures() { + + var noneShow = _showFeatures() + _hideFeatures() + + if (noneShow) { + _addClass(ui.dom.left,"displayNoneClass") + } else { + _removeClass(ui.dom.left,"displayNoneClass") + } + + if ( !noneShow && config.putMenuAbove && ! model.devOverrideShowAllFeatures) { + _addClass(ui.dom.left,"displayAboveClass") + } else { + _removeClass(ui.dom.left,"displayAboveClass") + } + + return + } + + function _showFeatures() { + var noneShow = true + for (i in ui.menu) { + // go through all the menu items + // if the feature is listed or the feature filter is off, show the feature + // or if the Override is on + // and , show the feature + if(model.devOverrideShowAllFeatures || !config.doFeatureFilter || config.featurelist.includes(i)) { + ui.menu[i].choose.dom.hidden = false + noneShow = false + } + } + return noneShow + } + + function _hideFeatures() { + for (i in ui.menu) { + // go through all the menu items + // if the feature is listed or the feature filter is off, show the feature + // or if the Override is on + // and , show the feature + if(model.devOverrideShowAllFeatures || !config.doFeatureFilter || config.featurelist.includes(i)) { + continue + } else { + ui.menu[i].choose.dom.hidden = true + } + } + } + + + ui.menu.presetconfig = new function() { // pick a preset + var self = this + self.list = _buildPresetConfig({nElection:31,nBallot:17}) + function _buildPresetConfig(c) { + // var presetnames = ["O","SA"] + // var presetModelNames = [config.filename,"sandbox.html"] + // var presetdescription = ["original intended preset","sandbox"] + var presetnames = ["S"] + var presetModelNames = ["sandbox"] + var presetdescription = ["sandbox"] + + // and fill in the rest + for (var i=1;i<=c.nElection;i++) { + presetnames.push("e"+i) + presetModelNames.push("election"+i) + presetdescription.push("election"+i) + } + presetnames.push("O") + presetModelNames.push(model.id) + presetdescription.push("original intended preset") + // TODO + for (var i=1;i<=c.nBallot;i++) { + presetnames.push("b"+i) + presetModelNames.push("ballot"+i) + presetdescription.push("ballot"+i) + } + + var presetconfig = [] + for (i in presetnames) { + var mar = ((Number(i)+1) % 5 == 0) ? 0 : 4 + presetconfig.push({name:presetnames[i],realname:presetdescription[i],presetName:presetModelNames[i],margin:mar}) + } + return presetconfig + } + self.onChoose = function(data){ + if (data.isOn) { + // LOAD MAIN + var ui2 = loadpreset(data.presetName) + var firstletter = data.presetName[0] + + // here's where I should make use of ui2.uiType + // if (ui2.uiType == "election" || ui2.uiType == "sandbox" || ui2.uiType == "sandbox-original") + if (firstletter == 'e' || firstletter == 's') { + + // LOAD Preset + _copyAttributes(config, ui2.preset.config) + + // } else if (ui2.uiType == "ballot") + } else if (firstletter == 'b') { + //document.location.replace(data.htmlname); + // LOAD Defaults + var ballotconfig = { + system: "Plurality", + voterPositions: [[81,92]], + candidatePositions: [[41,50],[153,95],[216,216]], + firstStrategy: "zero strategy. judge on an absolute scale.", + scoreFirstStrategy: "zero strategy. judge on an absolute scale.", + choiceFirstStrategy: "zero strategy. judge on an absolute scale.", + pairFirstStrategy: "zero strategy. judge on an absolute scale.", + secondStrategy: "zero strategy. judge on an absolute scale.", + scoreSecondStrategy: "zero strategy. judge on an absolute scale.", + choiceSecondStrategy: "zero strategy. judge on an absolute scale.", + pairSecondStrategy: "zero strategy. judge on an absolute scale.", + preFrontrunnerIds: ["square","triangle"], + showChoiceOfStrategy: false, + showChoiceOfFrontrunners: false, + doStarStrategy: false + } + // LOAD Preset + Object.assign(ballotconfig, ui2.preset.config ) + // get config from ballotconfig + var systemTranslator = {Plurality:"FPTP",Ranked:"Condorcet",Approval:"Approval",Score:"Score",Three:"3-2-1"} + + _copyAttributes(config, { + system: systemTranslator[ballotconfig.system], + voterPositions: ballotconfig.voterPositions, + candidatePositions: ballotconfig.candidatePositions, + firstStrategy: ballotconfig.firstStrategy, + scoreFirstStrategy: ballotconfig.scoreFirstStrategy, + choiceFirstStrategy: ballotconfig.choiceFirstStrategy, + pairFirstStrategy: ballotconfig.pairFirstStrategy, + secondStrategy: ballotconfig.secondStrategy, + scoreSecondStrategy: ballotconfig.scoreSecondStrategy, + choiceSecondStrategy: ballotconfig.choiceSecondStrategy, + pairSecondStrategy: ballotconfig.pairSecondStrategy, + preFrontrunnerIds: ballotconfig.preFrontrunnerIds, + // these are not based on the ballot config + oneVoter: true, + arena_size: 300 + }) + config.featurelist = [] + if (ballotconfig.showChoiceOfFrontrunners) {config.featurelist.push("frontrunners")} + if (ballotconfig.showChoiceOfStrategy) {config.featurelist.push("scoreFirstStrategy")} + if (ballotconfig.showChoiceOfStrategy) {config.featurelist.push("choiceFirstStrategy")} + if (ballotconfig.showChoiceOfStrategy) {config.featurelist.push("pairFirstStrategy")} + if (ballotconfig.showChoiceOfStrategy) {config.featurelist.push("scoreSecondStrategy")} + if (ballotconfig.showChoiceOfStrategy) {config.featurelist.push("choiceSecondStrategy")} + if (ballotconfig.showChoiceOfStrategy) {config.featurelist.push("pairSecondStrategy")} + } + // CONFIGURE MAIN + cConfig.cleanConfig(config) + config.sandboxsave = true // we're in a sandbox + config.featurelist = Array.from((new Set(config.featurelist)).add("gearicon").add("presetconfig")) + _copyAttributes(initialConfig,config) + // CONFIGURE (LOADER) + model.size = config.arena_size + // INIT (LOADER) + model.initDOM() + // RESET = CREATE, CONFIGURE, INIT (FOR MODEL) + model.reset() + model.update() + // UPDATE (MENU AND ARENA) + _objF(ui.arena,"update") + _objF(ui.menu,"select"); + } + }; + self.choose = new ButtonGroup({ + label: "pick a preset:", + width: bw(5), + data: self.list, + onChoose: self.onChoose + }); + self.init_sandbox = function() { + self.choose.highlight("presetName", model.id); // only do this once. Otherwise it would be in updateUI } - - - - // percentstrategy - - var aba = document.createElement('div') - aba.className = "button-group" - document.querySelector("#left").appendChild(aba) - var aba2 = document.createElement('div') - aba2.className = "button-group-label" - aba2.innerHTML = "how many voters strategize?"; - aba.appendChild(aba2) - - var makeslider = function(chtext,chid,chfn,containchecks,n) { - var slider = document.createElement("input"); - slider.type = "range"; - slider.max = "100"; - slider.min = "0"; - slider.value = "50"; - //slider.setAttribute("width","20px"); - slider.id = chid; - slider.class = "slider"; - slider.addEventListener('input', function() {chfn(slider,n)}, true); - var label = document.createElement('label') - label.htmlFor = chid; - label.appendChild(document.createTextNode(chtext)); - containchecks.appendChild(slider); - //containchecks.appendChild(label); - slider.innerHTML = chtext; - return slider - } // https://stackoverflow.com/a/866249/8210071 - - var containchecks = aba.appendChild(document.createElement('div')); - containchecks.id="containsliders" - var slfn = function(slider,n) { - // update config... - config.voterPercentStrategy[n] = slider.value; - if (n<model.numOfVoters) { - model.voters[n].percentStrategy = config.voterPercentStrategy[n] - model.update(); - } - } - stratsliders.push(makeslider("","choosepercent",slfn,containchecks,0)) - stratsliders.push(makeslider("","choosepercent",slfn,containchecks,1)) - stratsliders.push(makeslider("","choosepercent",slfn,containchecks,2)) - doms["percentstrategy"] = aba - - - // unstrategic - - var strategyOff = [ - {name:"O", realname:"zero strategy. judge on an absolute scale.", margin:5}, - {name:"N", realname:"normalize", margin:5}, - {name:"F", realname:"normalize frontrunners only", margin:5}, - {name:"B", realname:"best frontrunner", margin:5}, - {name:"W", realname:"not the worst frontrunner"} - ]; - // old ones - // {name:"FL", realname:"justfirstandlast", margin:5}, - // {name:"T", realname:"threshold"}, - // {name:"SNTF", realname:"starnormfrontrunners"} - var onChooseVoterStrategyOff = function(data){ - - // update config... - // only the middle percent (for the yellow triangle) - - // no reset... - for(var i=0;i<model.voters.length;i++){ - config.unstrategic = data.realname; - model.voters[i].unstrategic = config.unstrategic - } - model.update(); - - }; - window.chooseVoterStrategyOff = new ButtonGroup({ - label: "what do the rest do?", - width: 40, - data: strategyOff, - onChoose: onChooseVoterStrategyOff - }); - document.querySelector("#left").appendChild(chooseVoterStrategyOff.dom); - doms["unstrategic"] = chooseVoterStrategyOff.dom - - - - // frontrunners - - var h1 = function(x) {return "<span class='buttonshape'>"+_icon(x)+"</span>"} - var frun = [ - {name:h1("square"),realname:"square",margin:5}, - {name:h1("triangle"),realname:"triangle",margin:5}, - {name:h1("hexagon"),realname:"hexagon",margin:5}, - {name:h1("pentagon"),realname:"pentagon",margin:5}, - {name:h1("bob"),realname:"bob"} - ]; - var onChooseFrun = function(data){ - - // update config... - // no reset... - var preFrontrunnerSet = new Set(config.preFrontrunnerIds) - if (data.isOn) { - preFrontrunnerSet.add(data.realname) - } else { - preFrontrunnerSet.delete(data.realname) - } - config.preFrontrunnerIds = Array.from(preFrontrunnerSet) - model.preFrontrunnerIds = config.preFrontrunnerIds - model.update(); - - }; - window.chooseFrun = new ButtonGroup({ - label: "who are the frontrunners?", - width: 40, - data: frun, - onChoose: onChooseFrun, - isCheckbox: true - }); - document.querySelector("#left").appendChild(chooseFrun.dom); - doms["frontrunners"] = chooseFrun.dom - - - - // do a poll to find frontrunner - - var poll = [ - {name:"Poll",margin:5}, - {name:"Poll 2",realname:"Find the top 2 frontrunners."} - ]; - var onChoosePoll = function(data){ - if (data.name == "Poll") { - var won = model.winners - config.preFrontrunnerIds = won - } else { - model.dotop2 = true // not yet implemented - model.update() - model.dotop2 = false - config.preFrontrunnerIds = model.top2 - model.top2 = [] - } - - model.preFrontrunnerIds = config.preFrontrunnerIds - if(window.chooseFrun) chooseFrun.highlight("realname", model.preFrontrunnerIds); - model.update(); - - }; - window.choosePoll = new ButtonGroup({ - label: "Poll to find new frontrunner:", - width: 52, - data: poll, - onChoose: onChoosePoll, - justButton: true - }); - document.querySelector("#left").appendChild(choosePoll.dom); - doms["poll"] = choosePoll.dom - - - - // yee - - var h1 = function(x) {return "<span class='buttonshape'>"+_icon(x)+"</span>"} - var yeeobject = [ - {name:h1("square"),realname:"square",keyyee:"square",kindayee:"can",margin:4}, - {name:h1("triangle"),realname:"triangle",keyyee:"triangle",kindayee:"can",margin:4}, - {name:h1("hexagon"),realname:"hexagon",keyyee:"hexagon",kindayee:"can",margin:4}, - {name:h1("pentagon"),realname:"pentagon",keyyee:"pentagon",kindayee:"can",margin:4}, - {name:h1("bob"),realname:"bob",keyyee:"bob",kindayee:"can",margin:8}, - {name:"1",realname:"first voter group",kindayee:"voter",keyyee:0,margin:4}, - {name:"2",realname:"second voter group",kindayee:"voter",keyyee:1,margin:4}, - {name:"3",realname:"third voter group",kindayee:"voter",keyyee:2,margin:8}, - {name:"off",realname:"turn off",keyyee:"off",kindayee:"off"} - ]; - var onChooseyeeobject = function(data){ - - config.kindayee = data.kindayee - if (data.kindayee == "can") { - model.yeeobject = model.candidatesById[data.keyyee] - } else if (data.kindayee=="voter") { - model.yeeobject = model.voters[data.keyyee] - } else if (data.kindayee=="off") { - model.yeeobject = undefined - } - if (model.yeeobject) {model.yeeon = true} else {model.yeeon = false} - config.keyyee = data.keyyee - model.update(); - - }; - window.chooseyeeobject = new ButtonGroup({ - label: "which object for yee map?", - width: 20, - data: yeeobject, - onChoose: onChooseyeeobject - }); - chooseyeeobject.dom.setAttribute("id","yee") - document.querySelector("#left").appendChild(chooseyeeobject.dom); - doms["yee"] = chooseyeeobject.dom - - - - - // gear config - decide which menu items to do - - // var allnames = ["systems","voters","candidates","strategy","percentstrategy","unstrategic","frontrunners","poll","yee"] // not "save" - var gearconfig = [] - for (i in allnames) gearconfig.push({name:i,realname:allnames[i],margin:4}) - - var onChoosegearconfig = function(data){ - var featureset = new Set(config.featurelist) - if (data.isOn) { - featureset.add(data.realname) - doms[data.realname].hidden = false - } else { - featureset.delete(data.realname) - doms[data.realname].hidden = true - } - config.featurelist = Array.from(featureset) - - }; - window.choosegearconfig = new ButtonGroup({ - label: "which menu options are displayed?", - width: 18, - data: gearconfig, - onChoose: onChoosegearconfig, - isCheckbox: true - }); - choosegearconfig.dom.hidden = true - document.querySelector("#left").insertBefore(choosegearconfig.dom,doms["systems"]); - - // get current filename, in order to go back to the original intended preset - - - // var presetnames = ["O","SA"] - // var presethtmlnames = [config.filename,"sandbox.html"] - // var presetdescription = ["original intended preset","sandbox"] - - var presetnames = ["S"] - var presethtmlnames = ["sandbox.html"] - var presetdescription = ["sandbox"] - - // and fill in the rest - for (var i=1;i<=14;i++) {presetnames.push("e"+i) ; presethtmlnames.push("election"+i+".html") ; presetdescription.push("election"+i+".html")} - presetnames.push("O") ; presethtmlnames.push(filename) ; presetdescription.push("original intended preset") - // TODO - for (var i=1;i<=12;i++) {presetnames.push("b"+i) ; presethtmlnames.push("ballot"+i+".html") ; presetdescription.push("ballot"+i+".html")} - - - - var presetconfig = [] - for (i in presetnames) presetconfig.push({name:presetnames[i],realname:presetdescription[i],htmlname:presethtmlnames[i],margin:4}) - - var onChoosepresetconfig = function(data){ - if (data.isOn) { - var firstletter = data.htmlname[0] - if (firstletter == 'e' || firstletter == 's') { - config = loadpreset(data.htmlname) - loadDefaults() - model.reset(true); - model.onInit(); - setInPosition(); - selectUI(); - } else if (firstletter == 'b') { - //document.location.replace(data.htmlname); - ballotconfig = loadpreset(data.htmlname) - var systemTranslator = {Plurality:"FPTP",Ranked:"Condorcet",Approval:"Approval",Score:"Score",Three:"3-2-1"} - config = {} - config.system = systemTranslator[ballotconfig.system] - var s = ballotconfig.strategy || "zero strategy. judge on an absolute scale." - config.voterStrategies = [s,s,s] - config.preFrontrunnerIds = ballotconfig.preFrontrunnerIds - config.featurelist = [] - if (ballotconfig.showChoiceOfFrontrunners) {config.featurelist.push("frontrunners")} - if (ballotconfig.showChoiceOfStrategy) {config.featurelist.push("strategy")} - config.oneVoter = true - loadDefaults() - model.reset(true); - model.onInit(); - setInPosition(); - selectUI(); - } - } - }; - window.choosepresetconfig = new ButtonGroup({ - label: "pick a preset:", - width: 38, - data: presetconfig, - onChoose: onChoosepresetconfig - }); - choosepresetconfig.dom.hidden = true - document.querySelector("#left").insertBefore(choosepresetconfig.dom,doms["systems"]); - - if(window.choosepresetconfig) choosepresetconfig.highlight("htmlname", config.presethtmlname); - // only do this once. Otherwise it would be in SelectUI - - - var computeMethod = [{name:"gpu",margin:4},{name:"js",margin:4},{name:"ez"}] - var onChooseComputeMethod = function(data){ - config.computeMethod = data.name - model.computeMethod = data.name - if (model.yeeon) {model.calculateYee(); model.update()} - }; - window.chooseComputeMethod = new ButtonGroup({ - label: "method of computing yee diagram:", - width: 38, - data: computeMethod, - onChoose: onChooseComputeMethod - }); - chooseComputeMethod.dom.hidden = true - document.querySelector("#left").insertBefore(chooseComputeMethod.dom,doms["systems"]); - - // gear button (combines with above) - - var gearicon = [{name:"config"}] - var onChoosegearicon = function(data){ - if (data.isOn) { - choosegearconfig.dom.hidden = false - choosepresetconfig.dom.hidden = false - chooseComputeMethod.dom.hidden = false - } else { - choosegearconfig.dom.hidden = true - choosepresetconfig.dom.hidden = true - chooseComputeMethod.dom.hidden = true - } - }; - window.choosegearicon = new ButtonGroup({ - label: "", - width: 60, - data: gearicon, - onChoose: onChoosegearicon, - isCheckbox: true - }); - document.querySelector("#left").insertBefore(choosegearicon.dom,choosegearconfig.dom); - - if(config.hidegearconfig) choosegearicon.dom.hidden = true - - - /////////////////////// - //////// INIT! //////// - /////////////////////// - - model.onInit(); // NOT init, coz don't update yet... - setInPosition(); - - // Select the UI! - var selectUI = function(){ - if(window.chooseSystem) chooseSystem.highlight("name", model.system); - if(window.chooseCandidates) chooseCandidates.highlight("num", model.numOfCandidates); - if(window.chooseVoters) chooseVoters.highlight("realname", model.votersRealName); - if(window.choosePercentStrategy) choosePercentStrategy.highlight("num", model.voters[0].percentStrategy); - if(window.chooseVoterStrategyOn) { - if (model.voters[0].strategy != "starnormfrontrunners") { // kind of a hack for now, but I don't really want another button - chooseVoterStrategyOn.highlight("realname", model.voters[0].strategy); - } - } - if(window.chooseVoterStrategyOff) chooseVoterStrategyOff.highlight("realname", model.voters[0].unstrategic); - if(window.chooseFrun) chooseFrun.highlight("realname", model.preFrontrunnerIds); - if(stratsliders) { - for (i in stratsliders) { - stratsliders[i].value = config.voterPercentStrategy[i] - } - } - if(window.chooseyeeobject) chooseyeeobject.highlight("keyyee", config.keyyee); - if(window.choosegearconfig) choosegearconfig.highlight("realname", config.featurelist); - if(window.chooseComputeMethod) chooseComputeMethod.highlight("name", config.computeMethod); - - }; - selectUI(); - - - ////////////////////////// - //////// RESET... //////// - ////////////////////////// - - // CREATE A RESET BUTTON - var resetDOM = document.createElement("div"); - resetDOM.id = "reset"; - resetDOM.innerHTML = "reset"; - resetDOM.style.top = "340px"; - resetDOM.style.left = "350px"; - resetDOM.onclick = function(){ - - config = JSON.parse(JSON.stringify(initialConfig)); // RESTORE IT! - // Reset manually, coz update LATER. - model.reset(true); - model.onInit(); - setInPosition(); - - // Back to ol' UI - selectUI(); - - }; - document.body.appendChild(resetDOM); - - - /////////////////////////// - ////// SAVE POSITION ////// - /////////////////////////// - - window.save = function(log){ - - // Candidate positions - var positions = []; - for(var i=0; i<model.candidates.length; i++){ - var candidate = model.candidates[i]; - positions.push([ - Math.round(candidate.x), - Math.round(candidate.y) - ]); - } - if(log) console.log("candidatePositions: "+JSON.stringify(positions)); - var candidatePositions = positions; - - // Voter positions - positions = []; - for(var i=0; i<model.voters.length; i++){ - var voter = model.voters[i]; - positions.push([ - Math.round(voter.x), - Math.round(voter.y) - ]); - } - if(log) console.log("voterPositions: "+JSON.stringify(positions)); - var voterPositions = positions; - - // positions! - return { - candidatePositions: candidatePositions, - voterPositions: voterPositions - }; - - }; - - window.jsave = function(log){ - var sofar = window.save() - - // Description - var description = document.getElementById("description_text") || {value:""}; - config.description = description.value; - - var logtext = '' - for (i in sofar) logtext += i + ": " +JSON.stringify(sofar[i]) + ',\n' - for (i in config) { - if (i == "candidatePositions" || i == "voterPositions") { - // skip - } else { - logtext += i + ": " +JSON.stringify(config[i]) + ',\n' - } - } - var aloc = window.location.pathname.split('/') - logtext += "\n\npaste this JSON into" + aloc[aloc.length-2] + "/" + aloc[aloc.length-1] - console.log(logtext) - if (log==2) console.log(JSON.stringify(config)) - - for (i in sofar) config[i] = sofar[i] // for some weird reason config doesn't have the correct positions, hope i'm not introducing a bug - return config + } + + ui.menu.choose_pixel_size = new function() { + var self = this + self.list = [ + {name:"60",val:60,margin:4}, + {name:"30",val:30,margin:4}, + {name:"12",val:12,margin:4}, + {name:"6",val:6} + ] + self.codebook = [ { + field: "pixelsize", + decode: { + 0:6, + 1:12, + 2:30, + 3:60, + } + } ] + self.onChoose = function(data){ + // LOAD + config.pixelsize = data.val + // CONFIGURE + self.configure() + // UPDATE + model.update() + }; + self.configure = function() { + model.pixelsize = config.pixelsize + } + self.select = function() { + self.choose.highlight("val", config.pixelsize); + } + self.choose = new ButtonGroup({ + label: "size of pixels in yee diagram:", + width: bw(5), + data: self.list, + onChoose: self.onChoose + }); + } + + ui.menu.computeMethod = new function () { + var self = this + self.list = [ + {name:"gpu", value:"gpu",margin:4}, + {name:"js", value:"js",margin:4}, + {name:"ez", value:"ez"} + ] + self.codebook = [ { + field: "computeMethod", + decode: { + 0:"gpu", + 1:"js", + 2:"ez", + } + } ] + self.onChoose = function(data){ + // LOAD + config.computeMethod = data.value + // CONFIGURE + self.configure() + // UPDATE + model.update() + }; + self.configure = function() { + model.computeMethod = config.computeMethod + } + self.select = function() { + self.choose.highlight("value", config.computeMethod); + } + self.choose = new ButtonGroup({ + label: "method of computing yee diagram:", + width: bw(5), + data: self.list, + onChoose: self.onChoose + }); + } + + ui.menu.colorChooser = new function () { + var self = this + self.list = [ + {name: "pr", realname:"pick and repeat", value:"pick and repeat",margin:4}, + {name: "pro", realname:"pick and repeat w/ offset", value:"pick and repeat w/ offset",margin:4}, + {name: "g", realname:"generate all", value:"generate all",margin:4}, + {name: "pg", realname:"pick and generate", value:"pick and generate"} + ] + self.codebook = [ + { + field: "colorChooser", + decode: { + 0:"pick and repeat", + 1:"pick and repeat w/ offset", + 2:"generate all", + 3:"pick and generate" + } + } + ] + self.onChoose = function(data){ + // LOAD + config.colorChooser = data.value + // CONFIGURE + self.configure() + // INIT + for(var i=0; i<model.candidates.length; i++) { + model.candidates[i].init() + } + // UPDATE + model.draw() + }; + self.configure = function() { + model.colorChooser = config.colorChooser + } + self.select = function() { + self.choose.highlight("value", config.colorChooser); + } + self.choose = new ButtonGroup({ + label: "Method of Choosing Colors:", + width: bw(4), + data: self.list, + onChoose: self.onChoose + }); + } + + ui.menu.colorSpace = new function () { + var self = this + self.list = [ + {name:"hsluv with dark", value:"hsluv with dark"} + // ,margin:4}, + // {name:"hsluv light", value:"hsluv light",margin:4}, + // {name:"cie sampling", value:"cie sampling",margin:4}, + // {name:"hsl with dark", value:"hsl with dark"} + ] + self.codebook = [ + { + field: "colorSpace", + decode: { + 0:"hsluv with dark" + } + } + ] + self.onChoose = function(data){ + // LOAD + config.colorSpace = data.value + // CONFIGURE + self.configure() + // INIT + for(var i=0; i<model.candidates.length; i++) { + model.candidates[i].init() + } + // UPDATE + model.draw() + }; + self.configure = function() { + model.colorSpace = config.colorSpace + } + self.select = function() { + self.choose.highlight("value", config.colorSpace); + } + self.choose = new ButtonGroup({ + label: "Color Space:", + width: bw(1), + data: self.list, + onChoose: self.onChoose + }); + } + + ui.menu.spread_factor_voters = new function () { + var self = this + self.list = [ + {name:"1",val:1,margin:4}, + {name:"2",val:2,margin:4}, + {name:"5",val:5} + ] + self.codebook = [ { + field: "spread_factor_voters", + decode: { + 0:0, + 1:1, + 2:2, + 3:5, + } + } ] + self.onChoose = function(data){ + // LOAD + config.spread_factor_voters = data.val + // CONFIGURE + self.configure() + // INIT + model.voterManager.initVoters() + _pileVoters(model) + model.dm.redistrict() + // UPDATE + model.update() + }; + self.configure = function() { + model.spread_factor_voters = config.spread_factor_voters + for (var i=0; i<model.voterGroups.length; i++) { + model.voterGroups[i].spread_factor_voters = config.spread_factor_voters + } + } + self.select = function() { + self.choose.highlight("val", config.spread_factor_voters); + } + self.choose = new ButtonGroup({ + label: "Voter Spread:", + width: bw(5), + data: self.list, + onChoose: self.onChoose + }); + } + + ui.menu.arena_size = new function () { + var self = this + self.list = [ + {name:"300",val:300,margin:4}, + {name:"600",val:600} + ] + self.codebook = [ + { + field: "arena_size", + decode: { + 0:300, + 1:600 + } + } + ] + self.onChoose = function(data){ + // LOAD + var ratio = data.val / config.arena_size + config.arena_size = data.val + if ("300" == data.val) config.spread_factor_voters = 1 + if ("600" == data.val) config.spread_factor_voters = 2 + // CONFIGURE + self.configure() + for (var i=0; i<model.voterGroups.length; i++) { + model.voterGroups[i].x *= ratio + model.voterGroups[i].y *= ratio + } + for (var i=0; i<model.candidates.length; i++) { + model.candidates[i].x *= ratio + model.candidates[i].y *= ratio + } + // INIT (LOADER) + model.initDOM() + // INIT + model.voterManager.initVoters() + _pileVoters(model) + model.dm.redistrict() + for (var i=0; i<model.candidates.length; i++) { + model.candidates[i].init() + } + // UPDATE + model.update() + _objF(ui.arena,"update") + ui.menu.spread_factor_voters.select() + }; + self.configure = function() { + // CONFIGURE (LOADER) + model.size = config.arena_size + // CONFIGURE + model.spread_factor_voters = config.spread_factor_voters + for (var i=0; i<model.voterGroups.length; i++) { + model.voterGroups[i].spread_factor_voters = config.spread_factor_voters + } + } + self.select = function() { + self.choose.highlight("val", config.arena_size); + } + self.choose = new ButtonGroup({ + label: "Arena size:", + width: bw(5), + data: self.list, + onChoose: self.onChoose + }); + } + + ui.menu.median_mean = new function () { + var self = this + self.list = [ + {name:"median",val:2,margin:4}, + {name:"mean",val:1} + ] + self.codebook = [ { + field: "median_mean", + decode: { + 0:0, + 1:1, + 2:2, + } + } ] + self.onChoose = function(data){ + // LOAD + config.median_mean = data.val + // CONFIGURE + self.configure() + // UPDATE + model.update() + }; + self.configure = function() { + model.median_mean = config.median_mean + } + self.select = function() { + self.choose.highlight("val", config.median_mean); + } + self.choose = new ButtonGroup({ + label: "Median or Mean:", + width: bw(3), + data: self.list, + onChoose: self.onChoose + }); + } + + ui.menu.theme = new function () { + var self = this + self.list = [ + {name:"Letters", value:"Letters",realname:"Default",margin:4}, + {name:"Shapes", value:"Default",realname:"Default",margin:4}, + {name:"Nicky", value:"Nicky",realname:"The original style theme by Nicky Case",margin:0}, + {name:"Bees", value:"Bees",realname:"The Bee mode style for Unsplit."}, + ] + + self.codebook = [ + { + field: "theme", + decode: { + 0:"Default", + 1:"Nicky", + 2:"Bees", + 3:"Letters", + } + } + ] + self.onChoose = function(data){ + // LOAD + config.theme = data.value + // CONFIGURE + self.configure() + + // some configurations might have updated, so update the ui selections + if (config.theme == "Nicky" || config.theme == "Bees") { + config.colorChooser = "pick and repeat" + } else { + config.colorChooser = "pick and generate" + } + + ui.menu.colorChooser.configure() + + if (config.theme == "Bees") { + config.behavior = "bounce" + } else { + config.behavior = "stand" + } + ui.menu.behavior.configure() + + // INIT MODEL + model.arena.initARENA() + for(var i=0; i<model.candidates.length; i++) { + model.candidates[i].init() + } + model.initMODEL() + // INIT SANDBOX + self.init_sandbox() // sets the class of the div + // UPDATE + model.draw() + }; + self.configure = function() { + model.theme = config.theme + if (config.theme == "Bees") { + model.showVoters = false + } else { + model.showVoters = true + } + + if (config.theme == "Nicky") { + model.useBorderColor = true + model.drawSliceMethod = "circleNicky" + model.allCan = false + model.voterCenterIcons = "off" + model.showToolbar = "off" + } else { + model.useBorderColor = false + model.drawSliceMethod = "barChart" + model.allCan = true + model.voterCenterIcons = "on" + model.showToolbar = "on" + } + + } + self.init_sandbox = function() { + for (var i = 0; i < self.list.length; i++) { + ui.dom.basediv.classList.remove("div-model-theme-" + self.list[i].value) + } + ui.dom.basediv.classList.add("div-model-theme-" + model.theme) + } + self.select = function() { + self.choose.highlight("value", config.theme); + } + self.choose = new ButtonGroup({ + label: "Theme:", + width: bw(3), + data: self.list, + onChoose: self.onChoose + }); + } + + ui.menu.utility_shape = new function () { + var self = this + self.list = [ + {name:"linear", value:"linear",margin:4}, + {name:"quadratic", value:"quadratic",margin:4}, + {name:"log", value:"log"} + ] + self.codebook = [ + { + field: "utility_shape", + decode: { + 0:"linear", + 1:"quadratic", + 2:"log" + } + } + ] + self.onChoose = function(data){ + // LOAD + config.utility_shape = data.value + // CONFIGURE + self.configure() + // UPDATE + model.update() + }; + self.configure = function() { + model.utility_shape = config.utility_shape + } + self.select = function() { + self.choose.highlight("value", config.utility_shape); + } + self.choose = new ButtonGroup({ + label: "Utility Shape:", + width: bw(3), + data: self.list, + onChoose: self.onChoose + }); + } + + ui.menu.votersAsCandidates = new function () { + var self = this + self.list = [ + {name:"yes",value:true,margin:4}, + {name:"no",value:false} + ] + self.codebook = [ + { + field: "votersAsCandidates", + decode: { + 0:false, + 1:true, + } + } + ] + self.onChoose = function(data){ + // LOAD + config.votersAsCandidates = data.value + // CONFIGURE + self.configure() + + config.votersAsCandidates = false // turn it off // just one press + setTimeout(self.select,800) + // UPDATE + + // show warning + var string = 'Turned off Sidebar and Power Chart to save computer time.' + var textNode = document.createElement('div') + textNode.className = "button-group" + self.choose.dom.after(textNode) + var subNode = document.createElement('div') + subNode.className = "button-group-label" + textNode.appendChild(subNode) + subNode.innerHTML = string + setTimeout(() => textNode.remove(),4000) + + if ( model.doTextBallots) return // text ballots are not relevant here + // virtually press some buttons + // pretend we did onChoose and select for the following options to make things run more smoothly + config.roundChart = "off" + ui.menu.roundChart.configure() + ui.menu.roundChart.select() + config.sidebarOn = "off" + ui.menu.sidebarOn.configure() + ui.menu.sidebarOn.select() + + model.update() + }; + self.configure = function() { + model.votersAsCandidates = config.votersAsCandidates + } + self.select = function() { + self.choose.highlight("value", config.votersAsCandidates); + } + self.choose = new ButtonGroup({ + label: "Voters as Candidates:", + width: bw(3), + data: self.list, + onChoose: self.onChoose + }); + } + + ui.menu.ballotVis = new function () { + var self = this + self.list = [ + {name:"yes",value:true,margin:4}, + {name:"no",value:false} + ] + self.codebook = [ + { + field: "ballotVis", + decode: { + 0:false, + 1:true, + } + } + ] + self.onChoose = function(data){ + // LOAD + config.ballotVis = data.value + // CONFIGURE + self.configure() + // UPDATE + model.update() + }; + self.configure = function() { + model.ballotVis = config.ballotVis + } + self.select = function() { + self.choose.highlight("value", config.ballotVis); + } + self.choose = new ButtonGroup({ + label: "Show Ballot Visuals:", + width: bw(3), + data: self.list, + onChoose: self.onChoose + }); + } + + ui.menu.visSingleBallotsOnly = new function () { + var self = this + self.list = [ + {name:"yes",value:true,margin:4}, + {name:"no",value:false} + ] + self.codebook = [ + { + field: "visSingleBallotsOnly", + decode: { + 0:false, + 1:true, + } + } + ] + self.onChoose = function(data){ + // LOAD + config.visSingleBallotsOnly = data.value + // CONFIGURE + self.configure() + // UPDATE + model.update() + }; + self.configure = function() { + model.visSingleBallotsOnly = config.visSingleBallotsOnly + } + self.select = function() { + self.choose.highlight("value", config.visSingleBallotsOnly); + } + self.choose = new ButtonGroup({ + label: "Only Visualize Single Voters:", + width: bw(3), + data: self.list, + onChoose: self.onChoose + }); + } + + ui.menu.gearicon = new function () { + // gear button (combines with above) + var self = this + self.list = [ + {name:"config"} + ] + self.isOn = false // by default + self.onChoose = function(data){ + // LOAD + self.isOn = data.isOn + // no config.gearicon because we don't want to save with the config menu open + // UPDATE + self.configure() + // gear means + }; + self.configure = function() { + if (self.isOn) { + ui.m1.menuNameDivs["gearList"][0].hidden = false + } else { + ui.m1.menuNameDivs["gearList"][0].hidden = true + } + } + // no select because we don't want to save with the config menu open + self.choose = new ButtonGroup({ + label: "", + width: bw(3), + data: self.list, + onChoose: self.onChoose, + isCheckbox: true + }) + } + + ui.menu.gearoff = new function () { + var self = this + self.list = [ + {name:"are you sure?",realname:"You won't be able to get the config back, so I'd recommend saving first and then disabling the config and then saving again. That way you still have a copy that you can edit later if you made a mistake."} + ] + self.onChoose = function(data){ + // LOAD INPUT + var hit = data.isOn + if (hit) { + var response = window.confirm("Press cancel and we can forget this ever happened. Or press the other button and the config options will be removed.") + config.hidegearconfig = response + self.configure() + } + } + self.configure = function() { + if (config.hidegearconfig) { + modifyConfigFeaturelist(config,false, ["gearicon"]) + ui.menu.gearicon.choose.dom.hidden = true + ui.menu.gearicon.onChoose({isOn:false}) + + } + } + // no select because it's either hidden or off + self.choose = new ButtonGroup({ + label: "Disable Config Button !no", + width: bw(1), + data: self.list, + onChoose: self.onChoose, + isCheckbox: true + }); + } + + + + // if there is nothing in the menu, then don't show the option. + + // menuLevel select + + // submenu select + + // button to switch between old and new menus + + // menu version + // set the default config to the old menu + + // set the preset config for the sandbox to the new menu + + + // menuLevel select + + ui.menu.menuVersion = new function () { + var self = this + self.list = [ + {name:"1",value:"1",margin:4}, + {name:"2",value:"2"} + ] + self.codebook = [ { + field: "menuVersion", + decode: { + 0:"0", + 1:"1", + 2:"2", + } + } ] + self.onChoose = function(data){ + // LOAD + config.menuVersion = data.value + if (config.menuVersion !== "1") { + config.doFeatureFilter = false + } + // CONFIGURE + self.configure() + + if (config.menuVersion !== "1") { + ui.menu.doFeatureFilter.select() + } + }; + self.configure = function() { + + // remove nodes from menu + // clearMenus() + + // reattach nodes + if (config.menuVersion === "1") { + ui.m1.buildSubMenus() + } else { + ui.m2.buildSubMenus() // place menu items into dom structure + } + _hideOrShowFeatures() + ui.menu_update() // actually hide/show things + } + self.choose = new ButtonGroup({ + label: "Menu Version:", + width: bw(3), + data: self.list, + onChoose: self.onChoose + }); + self.select = function() { + self.choose.highlight("value", config.menuVersion); + } + } + + ui.menu.menuLevel = new function () { + var self = this + self.list = [ + // {name:"basic",value:"basic",margin:4}, + {name:"stock",value:"normal",margin:4}, + {name:"more",value:"advanced"} + ] + self.codebook = [ { + field: "menuLevel", + decode: { + 0:"normal", // stock? + 1:"advanced" // epic? + } + } ] + self.onChoose = function(data){ + // LOAD + config.menuLevel = data.value + // CONFIGURE + self.configure() + }; + self.configure = function() { + + showMenuItemsIf( "advanced", config.menuLevel === "advanced" ) + + var hideButton = ui.menu.stepMenu.choose.buttonHidden // stepMenu options + + if (config.menuLevel === "normal") { // hide advanced features + hideButton["ui"] = true + hideButton["dev"] = true // these options only have advanced features + } else if (config.menuLevel === "advanced") { + hideButton["ui"] = false + hideButton["dev"] = false + } + ui.menu.stepMenu.choose.configureHidden() + // this update step is actually okay to have inside .configure() + } + self.choose = new ButtonGroup({ + label: "Options:", // Level of Expertise + width: bw(4), + data: self.list, + onChoose: self.onChoose + }); + self.select = function() { + self.choose.highlight("value", config.menuLevel); + } + } + + ui.menu.stepMenu = new function () { + var self = this + self.list = [ + {name:"geom",value:"geom",margin:4}, + {name:"style",value:"style",margin:4}, + {name:"vote",value:"vote",margin:4}, + {name:"viz",value:"viz",margin:0}, + {name:"ui",value:"ui",margin:4}, + {name:"dev",value:"dev"} + ] + self.codebook = [ { + field: "stepMenu", + decode: { + 0:"geom", + 1:"style", + 2:"vote", + 3:"viz", + 4:"ui", + 5:"dev" + } + } ] + self.onChoose = function(data){ + // LOAD + config.stepMenu = data.value + // CONFIGURE + self.configure() + }; + self.configure = function() { + for (var e of self.list) { + var name = e.value + showMenuItemsIf( name, name == config.stepMenu) + } + } + self.choose = new ButtonGroup({ + label: "Steps:", // Sub Menu + width: bw(4), + data: self.list, + onChoose: self.onChoose + }); + self.select = function() { + self.choose.highlight("value", config.stepMenu); + } + } + + + ui.menu.doFeatureFilter = new function () { + var self = this + self.list = [ + {name:"yes",value:true,margin:4}, + {name:"no",value:false}, + ] + self.codebook = [ { + field: "doFeatureFilter", + decode: { + 0:false, + 1:true, + } + } ] + self.onChoose = function(data){ + // LOAD + config.doFeatureFilter = data.value + // CONFIGURE + self.configure() + }; + self.configure = function() { + ui.menu.gearconfig.configure() + } + self.choose = new ButtonGroup({ + label: "Enable filtering of menu items?:", // Sub Menu + width: bw(2), + data: self.list, + onChoose: self.onChoose + }); + self.select = function() { + self.choose.highlight("value", config.doFeatureFilter); + } + } + + ui.menu.spacer = new function () { + var self = this + self.choose = {} + self.choose.dom = document.createElement("div") + self.choose.dom.className = "topMenuSpacer" + } + + + ui.menu.yeeon = new function () { + var self = this + self.list = [ + {name:"on",value:true,margin:4}, + {name:"off",value:false}, + ] + self.codebook = [ { + field: "yeeon", + decode: { + 0:false, + 1:true, + } + } ] + self.onChoose = function(data){ + // LOAD + config.yeeon = data.value + // CONFIGURE + self.configure() + model.initMODEL() + // UPDATE + if (config.yeeon) { + model.update() // need to calculate + } else { + model.draw() + } + }; + self.configure = function() { + showMenuItemsIf("divWinMap", config.yeeon) + model.yeeon = config.yeeon + } + self.choose = new ButtonGroup({ + label: "Draw Win Map (Yee's Diagram)?", // Sub Menu + width: bw(2), + data: self.list, + onChoose: self.onChoose + }); + self.select = function() { + self.choose.highlight("value", config.yeeon); + } + } + + + ui.menu.beatMap = new function () { + var self = this + self.list = [ + {name:"auto",value:"auto",margin:4}, + {name:"on",value:"on",margin:4}, + {name:"off",value:"off"}, + ] + self.codebook = [ { + field: "beatMap", + decode: { + 0:"off", + 1:"on", + 2:"auto", + } + } ] + self.onChoose = function(data){ + // LOAD + config.beatMap = data.value + // CONFIGURE + self.configure() + // UPDATE + if (config.beatMap == "off") { + model.draw() + } else { + model.update() // might need to calculate + } + }; + self.configure = function() { + model.beatMap = config.beatMap + } + self.choose = new ButtonGroup({ + label: "Beat Map", // Sub Menu + width: bw(4), + data: self.list, + onChoose: self.onChoose + }); + self.select = function() { + self.choose.highlight("value", config.beatMap); + } + } + + + ui.menu.ballotConcept = new function () { + var self = this + self.list = [ + {name:"auto",value:"auto",margin:4}, + {name:"on",value:"on",margin:4}, + {name:"off",value:"off"}, + ] + self.codebook = [ { + field: "ballotConcept", + decode: { + 0:"off", + 1:"on", + 2:"auto", + } + } ] + self.onChoose = function(data){ + // LOAD + config.ballotConcept = data.value + // CONFIGURE + self.configure() + // UPDATE + if (config.ballotConcept == "off") { + model.draw() + } else { + model.update() // might need to calculate + } + }; + self.configure = function() { + model.ballotConcept = config.ballotConcept + } + self.choose = new ButtonGroup({ + label: "Ballot conceptualization:", // Sub Menu + width: bw(4), + data: self.list, + onChoose: self.onChoose + }); + self.select = function() { + self.choose.highlight("value", config.ballotConcept); + } + } + + + ui.menu.roundChart = new function () { + var self = this + self.list = [ + {name:"auto",value:"auto",margin:4}, + // {name:"on",value:"on",margin:4}, + {name:"off",value:"off"}, + ] + self.codebook = [ { + field: "roundChart", + decode: { + 0:"off", + 1:"on", + 2:"auto", + } + } ] + self.onChoose = function(data){ + // LOAD + config.roundChart = data.value + // CONFIGURE + self.configure() + // INIT + if (config.sidebarOn == "on") { + model.update() + } + }; + self.configure = function() { + model.roundChart = config.roundChart + if (model.checkGotoTarena()) { // TODO: make a visualization for more than 1 district + model.tarena.canvas.hidden = false + } else { + model.tarena.canvas.hidden = true + } + } + self.choose = new ButtonGroup({ + label: "Power Chart:", // Sub Menu + width: bw(4), + data: self.list, + onChoose: self.onChoose + }); + self.select = function() { + self.choose.highlight("value", config.roundChart); + } + } + + ui.menu.showPowerChart = new function () { + var self = this + self.list = [ + {name:"yes",value:true,margin:4}, + {name:"no",value:false}, + ] + self.codebook = [ { + field: "showPowerChart", + decode: { + 0:false, + 1:true, + } + } ] + self.onChoose = function(data){ + // LOAD + config.showPowerChart = data.value + // CONFIGURE + self.configure() + // INIT + if (config.sidebarOn = "on") { + model.update() + } + }; + self.configure = function() { + model.showPowerChart = config.showPowerChart + } + self.choose = new ButtonGroup({ + label: "Show Power Chart:", // Sub Menu + width: bw(4), + data: self.list, + onChoose: self.onChoose + }); + self.select = function() { + self.choose.highlight("value", config.showPowerChart); + } + } + + ui.menu.showUtilityChart = new function () { + var self = this + self.list = [ + {name:"yes",value:true,margin:4}, + {name:"no",value:false}, + ] + self.codebook = [ { + field: "showUtilityChart", + decode: { + 0:false, + 1:true, + } + } ] + self.onChoose = function(data){ + // LOAD + config.showUtilityChart = data.value + // CONFIGURE + self.configure() + // INIT + if (config.sidebarOn = "on") { + model.update() + } + }; + self.configure = function() { + model.showUtilityChart = config.showUtilityChart + } + self.choose = new ButtonGroup({ + label: "Show Utility Chart:", // Sub Menu + width: bw(4), + data: self.list, + onChoose: self.onChoose + }); + self.select = function() { + self.choose.highlight("value", config.showUtilityChart); + } + } + + ui.menu.sidebarOn = new function () { + var self = this + self.list = [ + // {name:"auto",value:"auto",margin:4}, + {name:"on",value:"on",margin:4}, + {name:"off",value:"off"}, + ] + self.codebook = [ { + field: "sidebarOn", + decode: { + 0:"off", + 1:"on", + 2:"auto", + } + } ] + self.onChoose = function(data){ + // LOAD + config.sidebarOn = data.value + // CONFIGURE + self.configure() + // UPDATE + if (config.sidebarOn == "on") { + model.update() + } else { + model.onDraw() + } + }; + self.configure = function() { + model.optionsForElection.sidebar = (config.sidebarOn == "on") + } + self.choose = new ButtonGroup({ + label: "Written Results:", // Sub Menu + width: bw(4), + data: self.list, + onChoose: self.onChoose + }); + self.select = function() { + self.choose.highlight("value", config.sidebarOn); + } + } + + + ui.menu.lastTransfer = new function () { + var self = this + self.list = [ + // {name:"auto",value:"auto",margin:4}, + {name:"on",value:"on",margin:4}, + {name:"off",value:"off"}, + ] + self.codebook = [ { + field: "lastTransfer", + decode: { + 0:"off", + 1:"on", + } + } ] + self.onChoose = function(data){ + // LOAD + config.lastTransfer = data.value + // CONFIGURE + self.configure() + // INIT + model.update() // must re-run election + }; + self.configure = function() { + model.opt.irv100 = (config.lastTransfer == "on") + } + self.choose = new ButtonGroup({ + label: "Show Last Transfer for Transferable Methods?", // Sub Menu + width: bw(4), + data: self.list, + onChoose: self.onChoose + }); + self.select = function() { + self.choose.highlight("value", config.lastTransfer); + } + } + + + ui.menu.voterIcons = new function () { + var self = this + self.list = [ + {name:"circle",value:"circle",realname:"circle",margin:4}, + {name:"top",value:"top",realname:"circles with the top preference only",margin:4}, + {name:"dots",value:"dots",realname:"dots",margin:4}, + {name:"body",value:"body",realname:"People"}, + {name:"off",value:"off",realname:"off"}, + ] + self.codebook = [ { + field: "voterIcons", + decode: { + 0:"circle", + 1:"top", + 2:"dots", + 3:"off", + 4:"body", + } + } ] + self.onChoose = function(data){ + // LOAD + config.voterIcons = data.value + // CONFIGURE + self.configure() + model.draw() + }; + self.configure = function() { + model.voterIcons = config.voterIcons + } + self.choose = new ButtonGroup({ + label: "Voter Icons:", // Sub Menu + width: bw(4), + data: self.list, + onChoose: self.onChoose + }); + self.select = function() { + self.choose.highlight("value", config.voterIcons); + } + } + + ui.menu.voterCenterIcons = new function () { + var self = this + self.list = [ + {name:"on",value:"on",realname:"on",margin:4}, + {name:"off",value:"off",realname:"off"}, + ] + self.codebook = [ { + field: "voterCenterIcons", + decode: { + 0:"off", + 1:"on", + } + } ] + self.onChoose = function(data){ + // LOAD + config.voterCenterIcons = data.value + // CONFIGURE + self.configure() + model.draw() + }; + self.configure = function() { + model.voterCenterIcons = config.voterCenterIcons + } + self.choose = new ButtonGroup({ + label: "Voter Center Icons:", // Sub Menu + width: bw(4), + data: self.list, + onChoose: self.onChoose + }); + self.select = function() { + self.choose.highlight("value", config.voterCenterIcons); + } + } + + + ui.menu.candidateIcons = new function () { + var self = this + self.list = [ + {name:"image",value:"image",realname:"image",margin:4}, + // {name:"both",value:"both",realname:"both image and name",margin:4}, + {name:"body",value:"body",realname:"People",margin:4}, + {name:"name",value:"name",realname:"name",margin:4}, + // {name:"off",value:"off",realname:"off"}, + {name:"dots",value:"dots",realname:"dots",margin:0}, + {name:"note",value:"note",realname:"annotation",margin:0}, + ] + self.codebook = [ { + field: "candidateIconsSet", + decode: { + 0:"image", + 1:"name", + 2:"dots", + 3:"note", + 4:"body", + } + } ] + var decoder = ["image","name","dots"] // be careful to only add to the end of this list + var encoder = _simpleMakeEncode(decoder) + self.onChoose = function(data){ + // LOAD + // config.candidateIcons = loadDec(encoder,data,config.candidateIcons) + config.candidateIconsSet = loadSet(config.candidateIconsSet, data) + + // config.candidateIcons = data.value + // CONFIGURE + self.configure() + + for (var i=0; i<model.candidates.length; i++) { + model.candidates[i].init() + } + model.initMODEL() + model.draw() + }; + self.configure = function() { + // model.candidateIconsSet = decodeDec(decoder,config.candidateIcons) + model.candidateIconsSet = config.candidateIconsSet + } + self.choose = new ButtonGroup({ + label: "Candidate Icons:", // Sub Menu + width: bw(4), + data: self.list, + onChoose: self.onChoose, + isCheckbox: true + }); + self.select = function() { + // var d = decodeDec(decoder,config.candidateIcons) + var d = config.candidateIconsSet + self.choose.highlight("value", d); + } + } + + function loadSet(featurelist, data) { + // e.g. var xlist = ["choose_pixel_size","yeefilter"] + var featureset = new Set(featurelist) + if (data.isOn) { + featureset.add(data.value) + } else { + featureset.delete(data.value) + } + return Array.from(featureset) + } + + // Went back to more normal config setting for candidate set, + // but kept the old code there just in case. + // I had been encoding the combination of settings as a decimal number, + // but it was too likely to break in the future. + var loadDec = function(encoder, data, a) { + var b = _decStringToBinaryString(a) + // pad with 0's + var b = b.padStart(Object.keys(encoder).length, '0') + var c = loadBinary(encoder, data, b) + var d = _binStringToDecString(c) + return d + } + var decodeDec = function(decoder, a) { + var b = _decStringToBinaryString(a) + // pad with 0's + var b = b.padStart(decoder.length, '0') + var c = decodeBinary(decoder, b) + return c + } + + + var loadBinary = function(encoder, data, a) { + + // example config.candidateIcons is 110 + var b = _stringToArray(a) + // b is [1,1,0] + // which index to set? + var c = data.value + var d = encoder[c] + // d is the index to set + // what to set to? + e = (data.isOn) ? 1 : 0 + b[d] = e + // great! + // now save the config + var f = _arrayToString(b) + return f + } + var decodeBinary = function(decoder, a) { + var b = _stringToArray(a) + // lets decode each element to a new array + var c = [] + for (var [d,e] of Object.entries(b)) { + if (e == 1) { + // add the decoded value to the list + c.push(decoder[d]) + } + } + return c + } + function _stringToArray(string) { + return string.split("") + } + function _arrayToString(array) { + return array.join("") + } + function _decStringToBinaryString(dec) { + return parseInt(dec).toString(2) + } + function _binStringToDecString(bin) { + return parseInt( bin, 2 ) + } + + + + ui.menu.pairwiseMinimaps = new function () { + var self = this + self.list = [ + // {name:"auto",value:"auto",margin:4}, + {name:"auto",value:"auto",margin:4}, + {name:"off",value:"off"}, + ] + self.codebook = [ { + field: "pairwiseMinimaps", + decode: { + 0:"off", + 1:"on", + 2:"auto", + } + } ] + self.onChoose = function(data){ + // LOAD + config.pairwiseMinimaps = data.value + // CONFIGURE + self.configure() + // INIT + model.update() // must re-run election + }; + self.configure = function() { + model.pairwiseMinimaps = config.pairwiseMinimaps + } + self.choose = new ButtonGroup({ + label: "Show Pairwise Minimaps?", // Sub Menu + width: bw(4), + data: self.list, + onChoose: self.onChoose + }); + self.select = function() { + self.choose.highlight("value", config.pairwiseMinimaps); + } + } + + + ui.menu.doTextBallots = new function () { + var self = this + self.list = [ + {name:"Yes",value:true,margin:4}, + {name:"No",value:false} + ] + self.codebook = [ { + field: "doTextBallots", + decode: { + 0:false, + 1:true, + } + } ] + self.onChoose = function(data){ + // LOAD + config.doTextBallots = data.value + // CONFIGURE + self.configure() + // INIT AND UPDATE + model.update() + }; + self.configure = function() { + showMenuItemsIf("divDoTextBallots", config.doTextBallots) + model.doTextBallots = config.doTextBallots + } + self.select = function() { + self.choose.highlight("value", config.doTextBallots); + } + self.choose = new ButtonGroup({ + label: "Text Ballot Input?", + width: bw(4), + data: self.list, + onChoose: self.onChoose + }); + } + + + ui.menu.textBallotInput = new function () { + var self = this + self.onChoose = function(){ + // LOAD + config.textBallotInput = self.choose.dom.value + // CONFIGURE + self.configure() + }; + self.codebook = [ { + field: "textBallotInput", + decode: { + 0:"", + } + } ] + self.configure = function() { + model.textBallotInput = config.textBallotInput + } + self.select = function() { + self.choose.dom.value = config.textBallotInput + } + self.choose = { + dom: document.createElement("textarea") + } + self.choose.dom.addEventListener("input",self.onChoose) + } + + ui.menu.submitTextBallots = new function () { + var self = this + self.list = [ + {name:"Submit"} + ] + self.onChoose = function(){ + // INIT AND UPDATE + model.update() + }; + self.choose = new ButtonGroup({ + label: "", + width: bw(4), + data: self.list, + onChoose: self.onChoose, + justButton: true + }); + } + + + ui.menu.behavior = new function () { + var self = this + self.list = [ + {name:"stand",value:"stand",realname:"stand still",margin:4}, + {name:"bounce",value:"bounce",realname:"run and bounce off the walls",margin:4}, + {name:"buzz",value:"buzz",realname:"buzz around randomly",margin:4}, + {name:"goal",value:"goal",realname:"seek the goal, try to win, using perfect information",margin:0}, + ] + self.codebook = [ { + field: "behavior", + decode: { + 0:"stand", + 1:"bounce", + 2:"buzz", + 3:"goal", + } + } ] + self.onChoose = function(data){ + // LOAD + config.behavior = data.value + // CONFIGURE + self.configure() + // INIT AND UPDATE + model.update() + }; + self.configure = function() { + model.behavior = config.behavior + if (config.behavior != "stand") { + model.doBuzz = true + model.buzzInterval = setInterval(function(){ + if (model.doBuzz) { + model.buzz() + model.update() + } + },60) + } else { + model.doBuzz = false + clearInterval(model.buzzInterval) + } + } + self.select = function() { + self.choose.highlight("value", config.behavior); + } + self.choose = new ButtonGroup({ + label: "Candidate behavior over time:", + width: bw(4), + data: self.list, + onChoose: self.onChoose + }); + } + + ui.menu.switcher = new function () { + var self = this + self.list = [ + {name:"sandbox",value:"sandbox",realname:"sandbox",margin:4}, + {name:"ballot",value:"ballot",realname:"ballot",margin:4}, + {name:"election",value:"election",realname:"election",margin:4}, + {name:"election-ballot",value:"election-ballot",realname:"election-ballot",margin:0}, + ] + // self.codebook = [ { + // field: "", + // decode: { + // 0:"sandbox", + // 1:"ballot", + // } + // } ] + self.onChoose = function(data){ + // LOAD & CONFIGURE + if(ui.uiType != data.value || 1) { + ui.uiType = data.value + ui.switchedUI = true + // send the config over to the new ui + ui.updateConfig() + ui.preset.config = config + ui.preset.presetName = "switch" + // I guess the config will stay the same... + // UPDATE + model.update() + } + }; + self.configure = function() { + } + self.select = function() { + self.choose.highlight("value", ui.uiType); + } + self.choose = new ButtonGroup({ + label: "Switch UI", + width: bw(4), + data: self.list, + onChoose: self.onChoose + }); + } + + + ui.menu.showToolbar = new function () { + var self = this + self.list = [ + {name:"on",value:"on",realname:"on",margin:4}, + {name:"off",value:"off",realname:"off"}, + ] + self.codebook = [ { + field: "showToolbar", + decode: { + 0:"off", + 1:"on", + } + } ] + self.onChoose = function(data){ + // LOAD + config.showToolbar = data.value + // CONFIGURE + self.configure() + model.initMODEL() + model.draw() + }; + self.configure = function() { + model.showToolbar = config.showToolbar + } + self.choose = new ButtonGroup({ + label: "Show Toolbar?", // Sub Menu + width: bw(4), + data: self.list, + onChoose: self.onChoose + }); + self.select = function() { + self.choose.highlight("value", config.showToolbar); + } + } + + ui.menu.rankedVizBoundary = new function () { + var self = this + self.list = [ + {name:"atWinner",value:"atWinner",margin:4}, + {name:"atMidpoint",value:"atMidpoint",margin:0}, + {name:"atLoser",value:"atLoser",margin:4}, + {name:"beforeWinner",value:"beforeWinner",margin:0}, + ] + self.codebook = [ { + field: "rankedVizBoundary", + decode: { + 0:"atLoser", + 1:"atMidpoint", + 2:"atWinner", + 3:"beforeWinner", + } + } ] + self.onChoose = function(data){ + // LOAD + config.rankedVizBoundary = data.value + // CONFIGURE + self.configure() + // UPDATE + if (model.doVoterMapGPU) { + model.update() + } else { + model.draw() + } + }; + self.configure = function() { + model.rankedVizBoundary = config.rankedVizBoundary + } + self.choose = new ButtonGroup({ + label: "Boundaries for Ranked Viz", // Sub Menu + width: bw(2), + data: self.list, + onChoose: self.onChoose + }); + self.select = function() { + self.choose.highlight("value", config.rankedVizBoundary); + } + } + + ui.menu.useBeatMapForRankedBallotViz = new function () { + var self = this + self.list = [ + {name:"Yes",value:true,margin:4}, + {name:"No",value:false,margin:4}, + ] + self.codebook = [ { + field: "useBeatMapForRankedBallotViz", + decode: { + 0:false, + 1:true, + } + } ] + self.onChoose = function(data){ + // LOAD + config.useBeatMapForRankedBallotViz = data.value + // CONFIGURE + self.configure() + // UPDATE + model.draw() + }; + self.configure = function() { + model.useBeatMapForRankedBallotViz = config.useBeatMapForRankedBallotViz + } + self.choose = new ButtonGroup({ + label: "Use Beat Map for Ranked Viz", // Sub Menu + width: bw(4), + data: self.list, + onChoose: self.onChoose + }); + self.select = function() { + self.choose.highlight("value", config.useBeatMapForRankedBallotViz); + } + } + + ui.menu.doMedianDistViz = new function () { + var self = this + self.list = [ + {name:"Yes",value:true,margin:4}, + {name:"No",value:false,margin:4}, + ] + self.codebook = [ { + field: "doMedianDistViz", + decode: { + 0:false, + 1:true, + } + } ] + self.onChoose = function(data){ + // LOAD + config.doMedianDistViz = data.value + // CONFIGURE + self.configure() + // UPDATE + model.draw() + }; + self.configure = function() { + model.doMedianDistViz = config.doMedianDistViz + } + self.choose = new ButtonGroup({ + label: "Show Special Median Viz", + width: bw(4), + data: self.list, + onChoose: self.onChoose + }); + self.select = function() { + self.choose.highlight("value", config.doMedianDistViz); + } + } + + ui.menu.showDescription = new function () { + var self = this + self.list = [ + {name:"on",value:"on",realname:"on",margin:4}, + {name:"off",value:"off",realname:"off"}, + ] + self.onChoose = function(data){ + // LOAD + var show = data.value + config.sandboxsave = (show == "on") + // CONFIGURE + self.configure() + }; + self.configure = function() { + ui.arena.desc.configure() + } + self.choose = new ButtonGroup({ + label: "Show Description?", // Sub Menu + width: bw(4), + data: self.list, + onChoose: self.onChoose + }); + self.select = function() { + if (config.sandboxsave) { + var show = "on" + } else { + var show = "off" + } + self.choose.highlight("value", show); + } + } + + ui.menu.doElectabilityPolls = new function () { + var self = this + self.list = [ + {name:"Yes",value:true,margin:4}, + {name:"No",value:false} + ] + self.codebook = [ { + field: "doElectabilityPolls", + decode: { + 0:false, + 1:true, + } + } ] + self.onChoose = function(data){ + // LOAD + config.doElectabilityPolls = data.value + // CONFIGURE + self.configure() + // INIT AND UPDATE + model.update() + }; + self.configure = function() { + model.doElectabilityPolls = config.doElectabilityPolls + } + self.choose = new ButtonGroup({ + label: "Do electability Polls?", // Sub Menu + width: bw(4), + data: self.list, + onChoose: self.onChoose + }); + self.select = function() { + self.choose.highlight("value", config.doElectabilityPolls); + } + } + + ui.menu.partyRule = new function () { + var self = this + self.list = [ + {name:"crowd",value:"crowd",margin:4}, + {name:"left-right",value:"leftright",margin:0}, + ] + self.codebook = [ { + field: "partyRule", + decode: { + 0:"crowd", + 1:"leftright", + } + } ] + self.onChoose = function(data){ + // LOAD + config.partyRule = data.value + // CONFIGURE + self.configure() + // INIT AND UPDATE + model.dm.redistrict() + model.update() + }; + self.configure = function() { + model.partyRule = config.partyRule + } + self.choose = new ButtonGroup({ + label: "How do we register parties?", // Sub Menu + width: bw(2), + data: self.list, + onChoose: self.onChoose + }); + self.select = function() { + self.choose.highlight("value", config.partyRule); + } + } + + ui.menu.doFilterSystems = new function () { + var self = this + self.list = [ + {name:"Yes",value:true,margin:4}, + {name:"No",value:false} + ] + self.codebook = [ { + field: "doFilterSystems", + decode: { + 0:false, + 1:true, + } + } ] + self.onChoose = function(data){ + // LOAD + config.doFilterSystems = data.value + // CONFIGURE + self.configure() + ui.showHideSystems() + }; + self.configure = function() { + return + } + self.choose = new ButtonGroup({ + label: "Filter Voting Systems?", // Sub Menu + width: bw(4), + data: self.list, + onChoose: self.onChoose + }); + self.select = function() { + self.choose.highlight("value", config.doFilterSystems); + } + } + + ui.menu.filterSystems = new function() { + var self = this + self.list = ui.menu.systems.list + for (var entry of self.list) entry.margin = 2 + + self.codebook = ui.menu.systems.systemsCodebook + self.onChoose = function(data){ + // LOAD CONFIG // + modifyConfigFilterSystems(config,data.isOn, [data.value]) + // CONFIGURE + self.configure() + ui.showHideSystems() + }; + self.configure = function() { + return + } + self.select = function() { + self.choose.highlight("value", config.filterSystems); + } + self.choose = new ButtonGroup({ + label: "Systems to Keep:", + width: bw(2), + data: self.list, + onChoose: self.onChoose, + isCheckbox: true + }); + } + + ui.showHideSystems = function() { + + // categories for systems + + includeIf = { + choice: [ + "FPTP", + "+Primary", + "Top Two", + "IRV", + "STV", + "Create", + ], + pair: [ + "Minimax", + "Schulze", + "RankedPair", + "Condorcet", + "STAR", + "3-2-1", + "RBVote", + "QuotaMinimax", + "stvMinimax", + "Create", + ], + score: [ + "Approval", + "Score", + "Borda", + "STAR", + "3-2-1", + "RRV", + "RAV", + "QuotaApproval", + "QuotaScore", + "PhragmenMax", + "PAV", + "equalFacilityLocation", + "Create", + "Monroe Seq S", + "Phragmen Seq S", + "Allocated Score", + "STAR PR", + ] + } + includeOnlyIf = { + multi: [ + "RRV", + "RAV", + "STV", + "QuotaApproval", + "QuotaMinimax", + "stvMinimax", + "QuotaScore", + "Monroe Seq S", + "Allocated Score", + "STAR PR", + "Phragmen Seq S", + "PhragmenMax", + "PAV", + "equalFacilityLocation", + ], + dev: [ + "QuotaApproval", + "QuotaMinimax", + "stvMinimax", + "QuotaScore", + "Monroe Seq S", + "Allocated Score", + "STAR PR", + "Phragmen Seq S", + "PhragmenMax", + "PAV", + "equalFacilityLocation", + "Create", + ] + } + + // just for reference + all = [ + "FPTP", + "+Primary", + "Top Two", + "RBVote", + "IRV", + "Borda", + "Minimax", + "Schulze", + "RankedPair", + "Condorcet", + "STAR", + "3-2-1", + "Approval", + "Score", + "STAR", + "3-2-1", + "RRV", + "RAV", + "STV", + "QuotaApproval", + "QuotaMinimax", + "stvMinimax", + "QuotaScore", + "Monroe Seq S", + "Allocated Score", + "STAR PR", + "Phragmen Seq S", + "PhragmenMax", + "PAV", + "equalFacilityLocation", + "Create", + ] + + // helper + var getDom = x => ui.menu.systems.choose.buttonDOMByValue[x] + + // default: hide all + for (var entry of ui.menu.filterSystems.list) { + var dom = getDom(entry.value) + dom.hidden = true + } + + // show if + for (let catName in includeIf) { + // is the button selected? + if (config.includeSystems.includes(catName)) { + // show all systems in category + let systems = includeIf[catName] + for(let sys of systems) { + let dom = getDom(sys) + dom.hidden = false + } + } + } + + // show only if + for (let catName in includeOnlyIf) { + // is the button NOT selecte? + if (! config.includeSystems.includes(catName)) { + // hide all systems in category + let systems = includeOnlyIf[catName] + for(let sys of systems) { + let dom = getDom(sys) + dom.hidden = true + } + } + } + + // PART 2 + + // hide individual systems + for (var entry of ui.menu.filterSystems.list) { + var dom = getDom(entry.value) + var show = !config.doFilterSystems || config.filterSystems.includes(entry.value) + if( show) { + continue + } else { + dom.hidden = true + } + } + + + } + + ui.menu.doFilterStrategy = new function () { // this is actually the old way of doing things... it doesn't do anything anymore + var self = this + self.list = [ + {name:"Yes",value:true,margin:4}, + {name:"No",value:false} + ] + self.codebook = [ { + field: "doFilterStrategy", + decode: { + 0:false, + 1:true, + } + } ] + self.onChoose = function(data){ + // LOAD + config.doFilterStrategy = data.value + // CONFIGURE + self.configure() + }; + self.configure = function() { + return + } + self.choose = new ButtonGroup({ + label: "Filter Voting Strategies by System?", // Sub Menu + width: bw(4), + data: self.list, + onChoose: self.onChoose + }); + self.select = function() { + self.choose.highlight("value", config.doFilterStrategy); + } + } + + + + ui.menu.includeSystems = new function () { + var self = this + self.list = [ + {name:_smaller("choice"), nameIsHTML:true,value:"choice",realname:"choice",margin:4}, + {name:"pair",value:"pair",margin:4}, + {name:"score",value:"score",margin:4}, + {name:"multi",value:"multi",margin:4}, + {name:"dev",value:"dev"} + ] + self.codebook = [ { + field: "includeSystems", + decode: { + 0:"choice", + 1:"pair", + 2:"score", + 3:"multi", + 4:"dev", + } + } ] + self.onChoose = function(data){ + // LOAD CONFIG // + config.includeSystems = modifyArrayAsSet(config.includeSystems,data.isOn, [data.value]) + // CONFIGURE + self.configure() + ui.showHideSystems() + }; + self.configure = function() { + return + } + self.choose = new ButtonGroup({ + label: "Voting systems by type:", // Sub Menu + width: bw(5), + data: self.list, + onChoose: self.onChoose, + isCheckbox: true + }); + self.select = function() { + self.choose.highlight("value", config.includeSystems); + } + } + + ui.menu.putMenuAbove = new function () { + var self = this + self.list = [ + {name:"Yes",value:true,margin:4}, + {name:"No",value:false} + ] + self.codebook = [ { + field: "putMenuAbove", + decode: { + 0:false, + 1:true, + } + } ] + self.onChoose = function(data){ + // LOAD + config.putMenuAbove = data.value + // CONFIGURE + self.configure() + }; + self.configure = function() { + _hideOrShowFeatures() + } + self.choose = new ButtonGroup({ + label: "Put menu above arena?", // Sub Menu + width: bw(4), + data: self.list, + onChoose: self.onChoose + }); + self.select = function() { + self.choose.highlight("value", config.putMenuAbove); + } + } + + + // helper + function showMenuItemsIf(name,condition) { + // show or hide all menu items with the given name + + var hideIt = ! (condition === true) + + for (var m of [ ui.m1, ui.m2 ]) { // both menus + var divs = m.menuNameDivs[name] + if (divs == undefined) continue + for (var div of divs) { // all divs + div.hidden = hideIt + } + } + } + +} + +function createMenu(ui) { + + // run this after loading the whole menu + ui.menu.gearconfig.initSpecial() + + // rebuild menu + + // function to make menu items visible/hidden + + // but there are some submenus that are dependent on selections from other menus, + // so we need to run the choose operations for those menu items + // well, visibility is hard to compute. There are a series of checks that must all be positive. + // There is a list of checks. Each triggering menu item needs to add a check to the list. + // Or, otoh, a tree would be simpler. Each menu item sets the visibility flag on a visibility tree. + // Updating the menu consists of traversing the tree and checking visibility at each node. + // Decide whether to go to subnodes. + // Some menu items, like "advanced", affect multiple nodes belonging to the advanced class. + + + // Put all in order + + + // In version 1, I had a single list, featurelist, that would be controlled by many buttons. + // In version 2, I skip the featurelist and control the divs directly from the buttons. + // That means version 2 has + // CONFIGURE is the right step to control the divs + // I also have a leftover menu where I can add another control to the divs. So I'm going to put two divs, + // which is kind of redundant but works. + + + + + ///////////////// + // BUILD MENUS // + ///////////////// + + + menu1 = [ + ["gearicon", [ + "gearicon", + ]], + [ "gearList", [ + "menuVersion", + "doFeatureFilter", + "gearconfig", // start of hidden list + "presetconfig", + "computeMethod", + "colorChooser", + "colorSpace", + "spread_factor_voters", + "arena_size", + "median_mean", + "utility_shape", + "votersAsCandidates", + "partyRule", + "ballotVis", + "visSingleBallotsOnly", + "rankedVizBoundary", + "useBeatMapForRankedBallotViz", + "doMedianDistViz", + "sidebarOn", + ["divLastTransfer", [ + "lastTransfer", + ]], + ["divPairwiseMinimaps", [ + "pairwiseMinimaps", + ]], + "voterIcons", + "voterCenterIcons", + "candidateIcons", + "showToolbar", + "showDescription", + "switcher", + "gearoff", + "doFilterSystems", + "filterSystems" , + "putMenuAbove", + "centerPollThreshold", + ]], + [ "main", [ + "includeSystems", + "systems", // start of normal list + [ "divRBVote", [ + "rbSystems", + ]], + [ "divCreate", [ + "loadCode", + "createStrategyType", + "createBallotType", + ]], + "dimensions", + "nDistricts", + ["divSeats", [ + "seats", + ]], + "nVoterGroups", + [ "divXVoterGroups", [ + "xVoterGroups", + "group_count", + "group_spread", + ]], + "nCandidates", + "theme", + "customNames", + ["divCustomNames", [ + "namelist", + ]], + "voterGroupCustomNames", + ["divVoterGroupCustomNames", [ + "voterGroupNameList", + ]], + ["divDoElectabilityPolls", [ + "doElectabilityPolls", + ]], + [ "divChoiceFirstStrategy", [ + "choiceFirstStrategy", + ]], + [ "divPairFirstStrategy", [ + "pairFirstStrategy", + ]], + [ "divScoreFirstStrategy", [ + "scoreFirstStrategy", + ]], + "doTwoStrategies", + [ "divSecondStrategy", [ + [ "divChoiceSecondStrategy", [ + "choiceSecondStrategy", + ]], + [ "divPairSecondStrategy", [ + "pairSecondStrategy", + ]], + [ "divScoreSecondStrategy", [ + "scoreSecondStrategy", + ]], + "percentSecondStrategy", + ]], + // "primaries", // not doing this one, comment out + [ "divPoll", [ + "autoPoll", + [ "divManualPoll", [ + "frontrunners", + "poll", + ]], + ]], + "yeeon", + ["divWinMap", [ + "yee", + ["divYee", [ + "yeefilter" , + "choose_pixel_size" + ]], + ]], + "beatMap", + "ballotConcept", + "roundChart", + "showUtilityChart", + "showPowerChart", + "behavior", + "doTextBallots", + ["divDoTextBallots", [ + "textBallotInput", + "submitTextBallots", + ]], + ]], + [ "hidden", [ // hidden menu - for things that don't fit into the other spots + "stepMenu", + "menuLevel", + "spacer", + ]], + ["obsolete", [ + "doFilterStrategy", + ]], + ] + + + // organize into submenus + + menu2 = [ + ["topmenu", [ + "stepMenu", + "menuLevel", + "spacer", + ]], + [ "submenu", [ + [ "geom", [ + [ "normal", [ + "dimensions", + "nDistricts", + "nVoterGroups", + [ "divXVoterGroups", [ + "xVoterGroups", + "group_count", + "group_spread", + ]], + "nCandidates", + ]], + [ "advanced", [ + "spread_factor_voters", + "arena_size", + "median_mean", + "utility_shape", + "votersAsCandidates", + "partyRule", + ]], + ]], + ["style", [ + ["normal", [ + "theme", + "customNames", + ["divCustomNames", [ + "namelist", + ]], + "voterGroupCustomNames", + ["divVoterGroupCustomNames", [ + "voterGroupNameList", + ]], + "candidateIcons", + "voterIcons", + ]], + ["advanced", [ + "voterCenterIcons", + "colorChooser", + "colorSpace", + "behavior", + ]], + ]], + ["vote", [ + ["normal", [ + "includeSystems", + "systems", + [ "divRBVote", [ + "rbSystems", + ]], + [ "divCreate", [ + "loadCode", + "createStrategyType", + "createBallotType", + ]], + ["divSeats", [ + "seats", + ]], + ["divDoElectabilityPolls", [ + "doElectabilityPolls", + ]], + [ "divChoiceFirstStrategy", [ + "choiceFirstStrategy", + ]], + [ "divPairFirstStrategy", [ + "pairFirstStrategy", + ]], + [ "divScoreFirstStrategy", [ + "scoreFirstStrategy", + ]], + "doTwoStrategies", + [ "divSecondStrategy", [ + [ "divChoiceSecondStrategy", [ + "choiceSecondStrategy", + ]], + [ "divPairSecondStrategy", [ + "pairSecondStrategy", + ]], + [ "divScoreSecondStrategy", [ + "scoreSecondStrategy", + ]], + "percentSecondStrategy", + ]], + [ "divPoll", [ + "autoPoll", + [ "divManualPoll", [ + "frontrunners", + "poll", + ]], + ]], + // "primaries", // not doing this one, comment out + ]], + ["advanced", [ + "centerPollThreshold", + "doTextBallots", + ["divDoTextBallots", [ + "textBallotInput", + "submitTextBallots", + ]], + + ]], + ]], + ["viz", [ + ["normal", [ + "yeeon", + ["divWinMap", [ + "yee", + ["divYee", [ + "yeefilter" , + "choose_pixel_size" + ]], + ]], + "beatMap", + "ballotConcept", + ["divLastTransfer", [ + "lastTransfer", + ]], + ["divPairwiseMinimaps", [ + "pairwiseMinimaps", + ]], + ]], + ["advanced", [ + "roundChart", + "showUtilityChart", + "showPowerChart", + "sidebarOn", + "ballotVis", + "visSingleBallotsOnly", + "rankedVizBoundary", + "useBeatMapForRankedBallotViz", + "doMedianDistViz", + ]], + ]], + ["ui", [ + ["normal", [ + ]], + ["advanced", [ + "menuVersion", + "doFeatureFilter", + "showDescription", + "showToolbar", + "gearconfig", + "presetconfig", + "doFilterSystems", + "filterSystems" , + "putMenuAbove", + ]], + ]], + ["dev", [ + ["normal", [ + + ]], + ["advanced", [ + "computeMethod", + "switcher", + ]], + ]], + ]], + ["hidden", [ + "gearoff", + "gearicon", + ]], + ["obsolete", [ + "doFilterStrategy", + ]], + ] + + // TODO + // basic = [ + // "systems", + // "nCandidates", + // "nVoterGroups", + // ] + + + ui.m1 = new MenuTree(ui) + ui.m1.assignMenu( menu1 , ui.dom.left, "basediv" ) + // detail: seems harmless, but the basediv gets reattached. + + ui.m1.menuNameDivs["gearList"][0].hidden = true + ui.m1.menuNameDivs["hidden"][0].hidden = true + ui.m1.menuNameDivs["obsolete"][0].hidden = true + + ui.m1.buildSubMenus() + + + ui.m2 = new MenuTree(ui) + ui.m2.assignMenu( menu2 , ui.dom.left, "basediv" ) + + ui.m2.menuNameDivs["hidden"][0].hidden = true + ui.m2.menuNameDivs["obsolete"][0].hidden = true + + + +} + +function MenuTree(ui) { + var self = this + + // Loop through and collect nodes with the same name into a list + // Later, during build, use that list to turn divs on and off + + // append all the menu dom elements to the menu + + + self.menuNameDivs = {} + // Here are all the divs that correspond to names to toggle on and off + // list of submenu doms to turn on/off + + var ap = [] + // Here are all the attachments points for the nodes + // Now I just need a list of the child doms for each one + // so that later I can loop through all the attachment points and attach the children. + // ap[i].parent: parent div + // ap[i].children: [array of divs to attach] + + self.assignMenu = function(m,parent,parentName) { + var children = [] + var childrenNames = [] + for (var a of m) { + if (typeof a === "string") { // child + var div = ui.menu[a].choose.dom + var aName = a + } else { // parent + var div = document.createElement("div") + parent.appendChild(div) + var aName = a[0] + if (self.menuNameDivs[aName] == undefined) { + self.menuNameDivs[aName] = [] + } + self.menuNameDivs[aName].push(div) + self.assignMenu(a[1],div,aName) // recursion + } + children.push(div) + childrenNames.push(aName) + } + ap.push({parentName:parentName,childrenNames:childrenNames,parent:parent,children:children}) + } + + // To build the menu, it's really easy, just attach the divs for the menu items to the submenu div structure + self.buildSubMenus = function() { + for (var a of ap) { + for(var child of a.children) { + a.parent.appendChild(child) + } + } + } + +} + +function uiArena(ui,model,config,initialConfig, cConfig) { + + ui.arena = {} + + ////////////////////////// + //////// RESET... //////// + ////////////////////////// + + ui.arena.reset = new function() { + var self = this + var resetDOM = document.createElement("div"); + resetDOM.id = "reset"; + resetDOM.innerHTML = "reset"; + resetDOM.onclick = function(event){ + // special keypress to get menu back + if (event.ctrlKey) { + model.devOverrideShowAllFeatures = ! model.devOverrideShowAllFeatures + ui.menu.doFeatureFilter.configure() + return + } + // LOAD INITIAL CONFIG + cConfig.reset() + // RESET = CREATE, CONFIGURE, INIT + model.reset() + model.update() + // UPDATE MENU // + _objF(ui.menu,"select"); + }; + ui.dom.center.appendChild(resetDOM); + self.dom = resetDOM + } + + ////////////////////////////////// + /////// SAVE & SHARE, YO! //////// + ////////////////////////////////// + + ui.arena.desc = new function() { // Create a description up top + var self = this + var descDOM = document.createElement("div"); + descDOM.id = "description_container"; + var refNode = ui.dom.left; + ui.dom.basediv.insertBefore(descDOM, refNode); + ui.dom.descText = document.createElement("textarea"); + var descText = ui.dom.descText + descText.id = "description_text"; + + var containText = document.createElement("div"); + containText.id = "double_description_container"; + descDOM.appendChild(containText); + containText.appendChild(descText); + // yay. + self.init_sandbox = function() { + descText.value = initialConfig.description; } + self.configure = function () { + + if (config.sandboxsave) { + ui.dom.center.style.width = config.arena_size + model.border*2 + "px" + descDOM.hidden = false + descText.hidden = false + } else { + descDOM.hidden = true + descText.hidden = true + } + if (config.theme == "Nicky") { + descText.placeholder = "[type a description for your model here. for example...]\n\nLook, it's the whole shape gang! Steven Square, Tracy Triangle, Henry Hexagon, Percival Pentagon, and last but not least, Bob." + } else { + descText.placeholder = "[type a description for your model here.]" + } + } + self.dom = descDOM + self.text = {} + self.text.dom = descText + } + ui.arena.codeEditor = new function() { // Create a description up top + var self = this + var codeEditorDOM = document.createElement("div"); + codeEditorDOM.id = "codeEditorText_container"; + ui.dom.basediv.appendChild(codeEditorDOM); + ui.dom.codeEditorText = document.createElement("textarea"); + var codeEditorText = ui.dom.codeEditorText + codeEditorText.id = "codeEditorText"; - ////////////////////////////////// - /////// SAVE & SHARE, YO! //////// - ////////////////////////////////// - - var descText, linkText; - if(1){ // SAVE & SHARE as feature. - - - if (config.sandboxsave) { - // Create a description up top - var descDOM = document.createElement("div"); - descDOM.id = "description_container"; - var refNode = document.getElementById("left"); - document.body.insertBefore(descDOM, refNode); - descText = document.createElement("textarea"); - descText.id = "description_text"; - descDOM.appendChild(descText); - - // yay. - descText.value = initialConfig.description; - } - // Move that reset button - if (config.sandboxsave) { - resetDOM.style.top = "470px"; - resetDOM.style.left = "235px"; - } else { - resetDOM.style.top = "340px"; - resetDOM.style.left = "245px"; - } - // Create a "save" button - var saveDOM = document.createElement("div"); - saveDOM.id = "save"; - saveDOM.innerHTML = "save:"; - if (config.sandboxsave) { - saveDOM.style.top = "470px"; - saveDOM.style.left = "350px"; - } else { - saveDOM.style.top = "340px"; - saveDOM.style.left = "350px"; - } - saveDOM.onclick = function(){ - _saveModel(); - }; - document.body.appendChild(saveDOM); - - // The share link textbox - linkText = document.createElement("input"); - linkText.id = "savelink"; - linkText.placeholder = "[when you save your model, a link you can copy will show up here]"; - linkText.setAttribute("readonly", true); - linkText.onclick = function(){ - linkText.select(); - }; - if (config.sandboxsave) { - //skip - } else { - linkText.style.position = "absolute"; - linkText.style.top = "340px"; - linkText.style.left = "460px"; - linkText.style.height = "30px"; - linkText.style.width = "90px"; - } - document.body.appendChild(linkText); - - // Create a URL... (later, PARSE!) - // save... ?d={s:[system], v:[voterPositions], c:[candidatePositions], d:[description]} + var containText = document.createElement("div"); + containText.id = "double_codeEditorText_container"; + codeEditorDOM.appendChild(containText); + containText.appendChild(codeEditorText); + ui.dom.codeMirror = CodeMirror.fromTextArea(ui.dom.codeEditorText, {lineNumbers: true, mode: "javascript", autorefresh: true}) // https://stackoverflow.com/a/42619796 + // yay. + self.init_sandbox = function() { + ui.dom.codeMirror.setValue(initialConfig.codeEditorText) + setTimeout( () => ui.dom.codeMirror.refresh() , 30); + model.codeEditorText = initialConfig.codeEditorText } + self.configure = function () { + + codeEditorText.placeholder = Election.defaultCodeScore + } + self.dom = codeEditorDOM + self.text = {} + self.text.dom = codeEditorText + } + + ui.arena.minusControl = new function() { + var self = this + self.init_sandbox = function() { + if (model.minusControl == undefined) model.minusControl = {} + if (ui.minusControl == undefined) ui.minusControl = {} + + model.minusControl.caption = readConfigControl(0) + ui.minusControl.sankey = readConfigControl(1) + ui.minusControl.weightCharts = readConfigControl(2) + ui.minusControl.roundChart = readConfigControl(3) + ui.minusControl.ballotPart = [] + ui.minusControl.ballotPart[0] = readConfigControl(4) + ui.minusControl.ballotPart[1] = readConfigControl(5) + ui.minusControl.ballotPart[2] = readConfigControl(6) + ui.minusControl.ballotPart[3] = readConfigControl(7) + ui.minusControl.ballotPart[4] = readConfigControl(8) + ui.minusControl.utilityChart = readConfigControl(9) + ui.minusControl.filterMapDiv = readConfigControl(10) + + function readConfigControl(x) { return {show: defaultTrue(config.minusControl[x])} } + function defaultTrue(x) { return (x == undefined) ? true : x} + } + } + + ui.arena.codeSave = new function() { // Create a "save" button for code + var self = this + var codeSaveDOM = document.createElement("div"); + codeSaveDOM.className = "codeSave"; + codeSaveDOM.innerHTML = "Save Code"; + codeSaveDOM.onclick = function(){ + + ui.arena.save.dom.onclick() + + // Open a new page in the background with the saved code and configuration + // window.open(ui.arena.save.link, '_blank'); + // window.focus(); + + // rerun the election + model.codeEditorText = config.codeEditorText + model.update() + + }; + ui.dom.basediv.appendChild(codeSaveDOM); + self.dom = codeSaveDOM + } + + ui.updateConfig = function() { + // UPDATE CONFIG // + var pos = savePositions() // saves the candidate and voter positions in the config. + for (i in pos) config[i] = pos[i] // for some weird reason config doesn't have the correct positions, hope i'm not introducing a bug + // Description + config.description = ui.dom.descText.value || ""; + config.codeEditorText = ui.dom.codeMirror.getValue() || ""; + // Minuses + // if the minus is not defined, then it defaults to true. Maybe I should insert a step into the setup to set these variables to default to true with the others. + var bp = ui.minusControl.ballotPart + if (bp == undefined) ui.minusControl.ballotPart = [] + var mc = { + 0: defaultTrue(model.minusControl.caption.show), + 1: defaultTrue(ui.minusControl.sankey.show), + 2: defaultTrue(ui.minusControl.weightCharts.show), + 3: defaultTrue(ui.minusControl.roundChart.show), + 4: defaultTrue(ui.minusControl.ballotPart[0].show), + 5: defaultTrue(ui.minusControl.ballotPart[1].show), + 6: defaultTrue(ui.minusControl.ballotPart[2].show), + 7: defaultTrue(ui.minusControl.ballotPart[3].show), + 8: defaultTrue(ui.minusControl.ballotPart[4].show), + 9: defaultTrue(ui.minusControl.utilityChart.show), + 10: defaultTrue(ui.minusControl.filterMapDiv.show), + } + function defaultTrue(x) { return (x == undefined) ? true : x} + // convert to array + config.minusControl = [] + for (const [key, value] of Object.entries(mc)) { + config.minusControl[key] = value + } + config.voterGroupRandomSeeds = model.voterGroups.map( x => x.randomSeed) + + } + + ui.arena.save = new function() { // Create a "save" button + var self = this + self.dom = document.createElement("div"); + self.dom.id = "save"; + self.dom.innerHTML = "save:"; + self.dom.onclick = function(){ + // UPDATE CONFIG // + // config.sandboxsave = true // this seems to fix a bug + ui.updateConfig() + // UPDATE MAIN // + newURLs = cConfig.save() + + var doTinyURL = true + if (doTinyURL) { + var goTiny='https://tinyurl.com/create.php?url='+encodeURIComponent(newURLs.link) + tinyLink.setAttribute("href",goTiny) + tinyLink.innerHTML = `TinyURL<img src="play/img/external_link.svg">` + embedLink.innerHTML = "<embed>"; + } + + self.link = newURLs.link + self.shortCode = newURLs.shortCode + self.shortLink = newURLs.shortLink + + ui.dom.linkText.value = "saving..."; + setTimeout(function(){ + ui.dom.linkText.value = newURLs.linkText; + },750); + + ui.arena.shortLink.dom.hidden = true + // ui.arena.saveShortLink.dom.hidden = false + _displayNoneIf(ui.arena.saveShortLink.dom,false) + + ui.arena.shortLink.dom.value = "saving..."; + setTimeout(function(){ + ui.arena.shortLink.dom.value = newURLs.shortLink; + },750); + + }; + ui.dom.center.appendChild(self.dom); + } + + + ui.arena.linkText = new function() { // The share link textbox + var self = this + ui.dom.linkText = document.createElement("input"); + var linkText = ui.dom.linkText + linkText.id = "savelink"; + linkText.placeholder = "[when you save your model, a link you can copy will show up here]"; + linkText.setAttribute("readonly", true); + linkText.onclick = function(){ + linkText.select(); + }; + ui.dom.center.appendChild(linkText); + self.dom = linkText + } + + var tinyLink = document.createElement("a") + ui.dom.center.appendChild(tinyLink) + tinyLink.setAttribute("target", "_blank") + tinyLink.setAttribute("class", "tinyURL") + + var embedLink = document.createElement("span") + ui.dom.center.appendChild(embedLink) + embedLink.setAttribute("class", "tinyURL") + embedLink.setAttribute("style", "text-decoration: underline;") + embedLink.onclick = function(){ + ui.embed = ! ui.embed + ui.arena.save.dom.onclick() + } + + var shortLinkDatabaseUrl = 'https://script.google.com/macros/s/AKfycbzMf0eb8jFTPnM7X83RIZwYN783E-xKt0M6RmgI-0AO2yf8BKb3/exec' + ui.arena.saveShortLink = new function() { // Create a "save" button + var self = this + self.dom = document.createElement("div"); + self.dom.id = "save"; // todo: make into className + self.dom.innerHTML = "publish:"; + // self.dom.hidden = true + _displayNoneIf(self.dom,true) + self.dom.onclick = function(e){ + + ui.arena.save.dom.onclick() + + ui.arena.shortLink.dom.hidden = false + + e.preventDefault(); + + _ajax.get(shortLinkDatabaseUrl, {shortcode: ui.arena.save.shortCode, link: ui.arena.save.link}, function(res) { + + // extra stuff to handle errors + + if (res != '') { + var resObj = JSON.parse(res) + if (resObj.result == "success") { + return // silent + } + } + window.alert("Bad. The link didn't publish. The short link won't work.") + ui.arena.shortLink.dom.hidden = true + return // alert + }) + } + ui.dom.center.appendChild(self.dom); + } + + ui.arena.shortLink = new function() { // The share link textbox + var self = this + self.dom = document.createElement("input"); + self.dom.hidden = true + self.dom.id = "savelink"; // todo: change to className + self.dom.placeholder = "[when you save your model, a link you can copy will show up here]"; + self.dom.setAttribute("readonly", true); + self.dom.onclick = function(){ + self.dom.select(); + }; + ui.dom.center.appendChild(self.dom); + } + + // additional codebooks + ui.extraCodeBook = [ + { + decode:{ + 0:"", + 1:"[type a description for your model here. for example...]\n\nLook, it's the whole shape gang! Steven Square, Tracy Triangle, Henry Hexagon, Percival Pentagon, and last but not least, Bob.", + }, + field: "description" + }, + { + decode:{ + 0:"", + 1:Election.defaultCodeScore, + }, + field: "codeEditorText" + }, + { + field: "minusControl", + decode:{ + 0:false, + 1:true, + }, + }, + { + field: "hidegearconfig", + decode:{ + 0:false, + 1:true, + }, + }, + { + field: "sandboxsave", + decode:{ + 0:false, + 1:true, + }, + } + ] + + /////////////////////////// + ////// SAVE POSITION ////// + /////////////////////////// + + var savePositions = function(log){ + // The positions of voters and candidates are not held in config. + // So, we need to save them occasionally. + + // Candidate positions + var positions = []; + var serials = []; + var b = [] + for(var i=0; i<model.candidates.length; i++){ + var candidate = model.candidates[i]; + positions.push([ + Math.round(candidate.x), + Math.round(candidate.y) + ]); + serials.push(candidate.serial) + b.push(candidate.b) + } + if(log) console.log("candidatePositions: "+JSON.stringify(positions)); + var candidatePositions = positions; + + // Voter positions + positions = []; + vTypes = [] + x = [] + snowman = [] + disk = [] + crowdShape = [] + group_count_vert = [] + group_count_h = [] + // voter types are varied in style + for(var i=0; i<model.voterGroups.length; i++){ + var voter = model.voterGroups[i]; + positions.push([ + Math.round(voter.x), + Math.round(voter.y) + ]); + vTypes.push(voter.voterGroupType) + x.push(voter.x_voters) + snowman.push(voter.snowman) + disk.push(voter.disk) + crowdShape.push(voter.crowdShape) + group_count_vert.push(voter.group_count_vert) + group_count_h.push(voter.group_count_h) + } + if(log) console.log("voterPositions: "+JSON.stringify(positions)); + var voterPositions = positions; + + // positions! + return { + candidatePositions: candidatePositions, + voterPositions: voterPositions, + candidateSerials: serials, + candidateB: b, + voterGroupTypes: vTypes, + voterGroupX: x, + voterGroupSnowman: snowman, + voterGroupDisk: disk, + crowdShape: crowdShape, + group_count_vert:group_count_vert, + group_count_h:group_count_h, + }; + + }; + + +} + +sandbox.assets = [ + + // the peeps + "play/img/voter_face.png", + "play/img/voter.png", + + // candidate images - now we dynamically load them + + // "play/img/square.png", + // "play/img/triangle.png", + // "play/img/hexagon.png", + // "play/img/pentagon.png", + // "play/img/bob.png", + + // "play/img/square.svg", + // "play/img/triangle.svg", + // "play/img/hexagon.svg", + // "play/img/pentagon.svg", + // "play/img/bob.svg", + + // "play/img/blue_bee.png", + // "play/img/yellow_bee.png", + // "play/img/red_bee.png", + // "play/img/green_bee.png", + // "play/img/orange_bee.png", + + // plus - dynamic loading works for these + // "play/img/plusCandidate.png", + // "play/img/plusOneVoter.png", + // "play/img/plusVoterGroup.png", + + // Ballot instructions - old style ballot + // "play/img/ballot5_fptp.png", + // "play/img/ballot5_ranked.png", + // "play/img/ballot5_approval.png", + // "play/img/ballot5_range.png", + + // The boxes - old style ballot + // "play/img/ballot5_box.png", + // "play/img/ballot_rate.png", + // "play/img/ballot_three.png" + +]; + +// helpers + +function modifyConfigFeaturelist(config, condition, xlist) { + // e.g. var xlist = ["choose_pixel_size","yeefilter"] + var featureset = new Set(config.featurelist) + for (var i in xlist){ + var xi = xlist[i] + if (condition) { + featureset.add(xi) + } else { + featureset.delete(xi) + } + } + config.featurelist = Array.from(featureset) +} + +function modifyConfigFilterSystems(config, condition, xlist) { + // e.g. var xlist = ["FPTP","IRV"] + var filterSet = new Set(config.filterSystems) + for (var i in xlist){ + var xi = xlist[i] + if (condition) { + filterSet.add(xi) + } else { + filterSet.delete(xi) + } + } + config.filterSystems = Array.from(filterSet) +} + +function modifyArrayAsSet(a, condition, xlist) { + // e.g. var xlist = ["FPTP","IRV"] + var s = new Set(a) + for (var i in xlist){ + var xi = xlist[i] + if (condition) { + s.add(xi) + } else { + s.delete(xi) + } + } + return Array.from(s) +} +function _simpleMakeEncode(decode) { + var encode = {} + for (var [i,v] of Object.entries(decode)) { + // var value = JSON.stringify(v) + i = Number(i) + encode[v] = i + } + return encode +} - }; - - Loader.load([ - - // the peeps - "img/voter_face.png", - "img/square.png", - "img/triangle.png", - "img/hexagon.png", - "img/pentagon.png", - "img/bob.png", - - // Ballot instructions - "img/ballot5_fptp.png", - "img/ballot5_ranked.png", - "img/ballot5_approval.png", - "img/ballot5_range.png", - - // The boxes - "img/ballot5_box.png", - "img/ballot_rate.png", - "img/ballot_three.png" - - ]); - - //if(config.sandboxsave) resetDOM.onclick(); - - // SAVE & PARSE - // ?m={s:[system], v:[voterPositions], c:[candidatePositions], d:[description]} - - - var _saveModel = function(){ - - jsave(1) // updates config with positions and gives a log of settings to copy and paste - - // URI ENCODE! - var uri = encodeURIComponent(JSON.stringify(config)); - - // ALSO TURN IT INTO INITIAL CONFIG. _parseModel - - initialConfig = JSON.parse(JSON.stringify(config)); // RESTORE IT! - - // Put it in the save link box! - - // make link string - var getUrl = window.location; - var baseUrl = getUrl.protocol + "//" + getUrl.host; //http://ncase.me/ballot - var restofurl = getUrl.pathname.split('/') - for (var i=1; i < restofurl.length - 2; i++) {baseUrl += "/" + restofurl[i];} - var link = baseUrl + "/sandbox/?m="+uri; - - var savelink = document.getElementById("savelink"); - savelink.value = "saving..."; - setTimeout(function(){ - savelink.value = link; - },750); - - }; - -}; diff --git a/play/js/main_sandbox_original.js b/play/js/main_sandbox_original.js new file mode 100644 index 00000000..a1360b61 --- /dev/null +++ b/play/js/main_sandbox_original.js @@ -0,0 +1,555 @@ +function sandbox(ui){ + + var presetName = ui.presetName + var preset = ui.preset + var config = preset.config + var basediv = document.querySelector("#" + presetName) + // CREATE div stuff for sandbox + function newDivOnBase(name) { + var a = document.createElement("div"); + a.setAttribute("id", name); + basediv.appendChild(a); + } + newDivOnBase("left") + newDivOnBase("center") + newDivOnBase("right") + + + // Big update: Added pattern to the code: LOAD, CREATE, CONFIGURE, INIT, & UPDATE. LOAD loads the input or defaults. CREATE makes an empty data structure to be used. CONFIGURE adds all the input to the data structure. INIT completes the data structure by doing steps that needed to use the data structure as input, and is otherwise similar to CONFIGURE. UPDATE runs the actions, now that the data structure is complete. + + // LOAD DEFAULTS and INPUT + var defaults = { + system: "FPTP", + candidates: 3, + voters: 1, + features: 1 // 1-basic, 2-voters, 3-candidates, 4-save + } + var url = window.top.location.href; + + + // CONFIGURE - configure data structures based on LOAD to be ready for INIT + // in this case: config, initialConfig are the data structures. + + _fillInDefaults(config,defaults) + /////////////////////////////////////////////////////////////// + // ACTUALLY... IF THERE'S DATA IN THE QUERY STRING, OVERRIDE // + /////////////////////////////////////////////////////////////// + var _getParameterByName = function(name,url){ + name = name.replace(/[\[\]]/g, "\\$&"); + var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"), + results = regex.exec(url); + if (!results) return null; + if (!results[2]) return ''; + return decodeURIComponent(results[2].replace(/\+/g, " ")); + }; + var modelData = _getParameterByName("m",url); + if(modelData){ + // Parse! + var data = JSON.parse(modelData); + // Turn into initial config + config = { + features: 4, + system: data.s, + candidates: data.c.length, + candidatePositions: data.c, + voters: data.v.length, + voterPositions: data.v, + description: data.d + }; + } + var initialConfig = JSON.parse(JSON.stringify(config)); + + + var l = new Loader() + // INIT - initialize all data structures + l.onload = function(){ + + //////////////////////// + // THE FRIGGIN' MODEL // + //////////////////////// + + // CREATE + + var ui = {} + + var model = new Model(presetName); + ui.model = model + model.createDOM() + + // CONFIGURE DEFAULTS + model.optionsForElection = {sidebar:true, originalCaption:true} + model.HACK_BIG_RANGE = true; + model.theme = "Nicky" + model.doOriginal = true + model.ballotConcept = "off" + + // INIT + ui.model = model + model.initDOM() + basediv.querySelector("#center").appendChild(model.dom); + model.dom.removeChild(model.caption); + basediv.querySelector("#right").appendChild(model.caption); + model.caption.style.width = ""; + model.initPlugin = function(){ + // CREATE + for(var i=0; i<config.candidates; i++) model.candidates.push(new Candidate(model)) + for(var i=0; i<config.voters; i++) model.voterGroups.push(new GaussianVoters(model)) + // CONFIGURE + // expand config to calculate some values to add to the model + // load expanded config into the model + // configure writes to model and reads from config. Sanity rule: configure does not read from model. + _menuF("configure") + // INIT + for (var i=0; i<model.candidates.length; i++) { + model.candidates[i].init() + } + model.initMODEL() + model.voterManager.initVoters() + model.dm.redistrict() + model.update() + }; + + // helper + function _menuF(f) { // run function if it exists for each menu item + for(var item in ui.menu ) { + if (ui.menu[item][f]) ui.menu[item][f]() + } + } + + ////////////////////////////////// + // BUTTONS - WHAT VOTING SYSTEM // + ////////////////////////////////// + + + ui.menu = {} + + // Which voting system? + ui.menu.systems = new function() { // "new function () {code}" means make an object "this", and run "code" in a new scope + // I made a singleton class so we can use "self" instead of saying "systems" (or another button group name). + // This is useful when we want to make another button group and we copy and paste this code. + // It might be better to make a class and then an instance, but I think this singleton class is easier. + // single = function() {stuff}; var systems = new single() + var self = this + self.name = "systems" + + self.list = [ + {name:"FPTP", ballotType:"Plurality", election:Election.plurality, margin:4}, + {name:"IRV", ballotType:"Ranked", election:Election.irv}, + {name:"Borda", ballotType:"Ranked", election:Election.borda, margin:4}, + {name:"Condorcet", ballotType:"Ranked", election:Election.condorcet}, + {name:"Approval", ballotType:"Approval", election:Election.approval, margin:4}, + {name:"Score", ballotType:"Score", election:Election.score} + ]; + self.listByName = function() { + var votingSystem = self.list.filter(function(system){ + return(system.name==config.system); + })[0]; + return votingSystem; + } + self.onChoose = function(data){ + // LOAD INPUT + config.system = data.name; + // CONFIGURE + self.configure() + // UPDATE + model.voterManager.initVoters() + model.dm.redistrict() + model.update(); + }; + self.configure = function() { + var s = self.listByName() + model.election = s.election + model.system = config.system; + model.ballotType = s.ballotType + for(var i=0;i<model.voterGroups.length;i++){ + model.voterGroups[i].typeVoterModel = s.ballotType + } + } + self.select = function() { + self.choose.highlight("name", config.system) + } + self.choose = new ButtonGroup({ + label: "what voting system?", + width: 108, + data: self.list, + onChoose: self.onChoose + }); + basediv.querySelector("#left").appendChild(self.choose.dom); + } + + // How many voters? + ui.menu.voters = new function() { + var self = this + self.list = [ + {name:"one", num:1, margin:5}, + {name:"two", num:2, margin:5}, + {name:"three", num:3}, + ] + self.onChoose = function(data){ + // LOAD INPUT + config.voters = data.num; + config.voterPositions = null + // CREATE + model.voterGroups = [] + for(var i=0; i<config.voters; i++) { + model.voterGroups.push(new GaussianVoters(model)) + } + // CONFIGURE + self.configure() + // INIT + model.initMODEL() + model.voterManager.initVoters() + // UPDATE + model.dm.redistrict() + model.update() + }; + self.configure = function() { + model.numOfVoters = config.voters; + var num = config.voters; + if (config.voterPositions) { + for(var i=0; i<num; i++){ + var pos = config.voterPositions[i]; + Object.assign(model.voterGroups[i], { + num:(4-num), + x:pos[0], y:pos[1] + }) + model.voterGroups[i].typeVoterModel = ui.menu.systems.listByName(config).ballotType // needs init + } + } else { + var voterPositions; + if(num==1){ + voterPositions = [[150,150]]; + }else if(num==2){ + voterPositions = [[150,100],[150,200]]; + }else if(num==3){ + voterPositions = [[150,115],[115,180],[185,180]]; + } + + var votingSystem = ui.menu.systems.list.filter(function(system){ + return(system.name==config.system); + })[0]; + for(var i=0; i<num; i++){ + var pos = voterPositions[i]; + Object.assign(model.voterGroups[i], { + disk:(4-num), + x:pos[0], y:pos[1] + }) + model.voterGroups[i].typeVoterModel = ui.menu.systems.listByName(config).ballotType // needs init + } + } + } + self.select = function() { + self.choose.highlight("num", config.voters) + } + self.choose = new ButtonGroup({ + label: "how many groups of voters?", + width: 70, + data: self.list, + onChoose: self.onChoose + }); + basediv.querySelector("#left").appendChild(self.choose.dom); + } + + if(initialConfig.features<2){ // VOTERS as feature. + ui.menu.voters.choose.dom.hidden = true + } + + // How many voters? + ui.menu.candidates = new function() { + var self = this + self.list = [ + {name:"two", num:2, margin:4}, + {name:"three", num:3, margin:4}, + {name:"four", num:4, margin:4}, + {name:"five", num:5} + ]; + self.onChoose = function(data){ + // LOAD INPUT + config.candidates = data.num; + config.candidatePositions = null + // CREATE + model.candidates = [] + for(var i=0; i<config.candidates; i++) { + model.candidates.push(new Candidate(model)) + } + // CONFIGURE + self.configure() + // INIT + for(var i=0; i<model.candidates.length; i++) { + model.candidates[i].init() + } + model.initMODEL() + // UPDATE + model.dm.redistrict() + model.update() + }; + self.configure = function() { + model.numOfCandidates = config.candidates; + // Candidates, in a circle around the center. + var _candidateIcons = Object.keys(Candidate.graphicsByIcon["Default"]) + var num = config.candidates; + if (config.candidatePositions) { + for(var i=0; i<num; i++){ + var icon = _candidateIcons[i]; + Object.assign(model.candidates[i],{ + icon:icon, + x:config.candidatePositions[i][0], + y:config.candidatePositions[i][1] + }) + } + } else { + var angle = 0; + switch(num){ + case 3: angle=Math.TAU/12; break; + case 4: angle=Math.TAU/8; break; + case 5: angle=Math.TAU/6.6; break; + } + for(var i=0; i<num; i++){ + var r = 100; + var x = 150 - r*Math.cos(angle); + var y = 150 - r*Math.sin(angle); + var icon = _candidateIcons[i]; + Object.assign(model.candidates[i],{ + icon:icon, + x:x, + y:y + }) + angle += Math.TAU/num; + } + + } + } + self.select = function() { + self.choose.highlight("num", config.candidates) + } + self.choose = new ButtonGroup({ + label: "how many candidates?", + width: 52, + data: self.list, + onChoose: self.onChoose + }); + basediv.querySelector("#left").appendChild(self.choose.dom); + } + + if(initialConfig.features<3){ // CANDIDATES as feature. + ui.menu.candidates.choose.dom.hidden = true + } + + ////////////////////////// + //////// RESET... //////// + ////////////////////////// + + ui.arena = {} + ui.arena.reset = new function() { + var self = this + var resetDOM = document.createElement("div"); + basediv.appendChild(resetDOM); + resetDOM.id = "reset"; + resetDOM.innerHTML = "reset"; + resetDOM.style.top = "340px"; + resetDOM.style.left = "350px"; + resetDOM.onclick = function(){ + + // LOAD INITIAL CONFIG + config = JSON.parse(JSON.stringify(initialConfig)); + // RESET = CREATE, CONFIGURE, INIT, & UPDATE + model.reset() + model.update() + + // Back to ol' UI + updateUI(); + + }; + self.dom = resetDOM + } + + ////////////////////////////////// + /////// SAVE & SHARE, YO! //////// + ////////////////////////////////// + + ui.arena.desc = new function() { // Create a description up top + var self = this + var descDOM = document.createElement("div"); + descDOM.id = "description_container"; + var refNode = basediv.querySelector("#left"); + basediv.insertBefore(descDOM, refNode); + var descText = document.createElement("textarea"); + descText.id = "description_text"; + + containText = document.createElement("div"); + containText.id = "double_description_container"; + descDOM.appendChild(containText); + containText.appendChild(descText); + // yay. + + // yay. + descText.value = initialConfig.description; + self.dom = descDOM + self.text = {} + self.text.dom = descText + } + + + ui.arena.save = new function() { // Create a "save" button + var self = this + var saveDOM = document.createElement("div"); + saveDOM.id = "save"; + saveDOM.innerHTML = "save:"; + saveDOM.style.top = "460px"; + saveDOM.style.left = "120px"; + saveDOM.onclick = function(){ + _saveModel(); + }; + basediv.appendChild(saveDOM); + self.dom = saveDOM + } + + ui.arena.linkText = new function() { // The share link textbox + var self = this + var linkText = document.createElement("input"); + linkText.id = "savelink"; + linkText.placeholder = "[when you save your model, a link you can copy will show up here]"; + linkText.setAttribute("readonly", true); + linkText.onclick = function(){ + linkText.select(); + }; + basediv.appendChild(linkText); + self.dom = linkText + } + + if(initialConfig.features<4){ // SAVE & SHARE as feature. + // hide menus + ui.arena.desc.dom.hidden = true + ui.arena.desc.text.dom.hidden = true + ui.arena.save.dom.hidden = true + ui.arena.save.dom.style.display = "none" + ui.arena.linkText.dom.hidden = true + } else { + // Move that reset button + ui.arena.reset.dom.style.top = "460px"; + ui.arena.reset.dom.style.left = "0px"; + } + + /////////////////////////// + ////// SAVE POSITION ////// + /////////////////////////// + + // helpers + ui.saveDrags = function(log){ + + // Candidate positions + var positions = []; + for(var i=0; i<model.candidates.length; i++){ + var candidate = model.candidates[i]; + positions.push([ + Math.round(candidate.x), + Math.round(candidate.y) + ]); + } + if(log) console.log("candidatePositions: "+JSON.stringify(positions)); + var candidatePositions = positions; + + // Voter positions + positions = []; + for(var i=0; i<model.voterGroups.length; i++){ + var voter = model.voterGroups[i]; + positions.push([ + Math.round(voter.x), + Math.round(voter.y) + ]); + } + if(log) console.log("voterPositions: "+JSON.stringify(positions)); + var voterPositions = positions; + + // positions! + return { + candidatePositions: candidatePositions, + voterPositions: voterPositions + }; + + }; + + // SAVE & PARSE + // ?m={s:[system], v:[voterPositions], c:[candidatePositions], d:[description]} + var _saveModel = function(){ + + // Data! + var data = {}; + + // System? + data.s = config.system; + console.log("voting system: "+data.s); + + // Positions... + var positions = ui.saveDrags(true); + data.v = positions.voterPositions; + data.c = positions.candidatePositions; + + // Description + var description = basediv.querySelector("#description_text"); + data.d = description.value; + console.log("description: "+data.d); + + // URI ENCODE! + var uri = encodeURIComponent(JSON.stringify(data)); + + // ALSO TURN IT INTO INITIAL CONFIG. _parseModel + initialConfig = { + features: 4, + system: data.s, + candidates: data.c.length, + candidatePositions: data.c, + voters: data.v.length, + voterPositions: data.v + }; + + // Put it in the save link box! + var baseUrl = document.baseURI + // var getUrl = window.location; + // var baseUrl = getUrl.protocol + "//" + getUrl.host; //http://ncase.me/ballot + // var restofurl = getUrl.pathname.split('/') + // for (var i=1; i < restofurl.length - 1; i++) { // /ballot/ + // if (restofurl[i] != "sandbox") { + // baseUrl += "/" + restofurl[i]; + // } + // } + var link = baseUrl + "sandbox/original?m="+uri; + var savelink = basediv.querySelector("#savelink"); + savelink.value = "saving..."; + setTimeout(function(){ + savelink.value = link; + },750); + + }; + + // UPDATE + model.initPlugin(); + model.update() + updateUI(); // Select the UI! + function updateUI() { + _menuF("select") + }; + }; + + + + // UPDATE - run actions + l.load([ + "play/img/voter_face.png", + "play/img/square.png", + "play/img/triangle.png", + "play/img/hexagon.png", + "play/img/pentagon.png", + "play/img/bob.png" + ]); + // FUNNY HACK. + setInterval(function(){ + var ohno = basediv.querySelector("#ohno"); + if(!ohno) return; + var x = Math.round(Math.random()*10-5); + var y = Math.round(Math.random()*10)+10; + ohno.style.top = y+"px"; + ohno.style.left = x+"px"; + },10); + +}; \ No newline at end of file diff --git a/play/js/model0.js b/play/js/model0.js new file mode 100644 index 00000000..76ab1c15 --- /dev/null +++ b/play/js/model0.js @@ -0,0 +1,16 @@ +// Do nothing model. + +// CREATE +var model = new Model("nothing") +model.createDOM() +// CONFIGURE +model.preFrontrunnerIds = [] +// INIT +model.initDOM() +// INIT +model.initPlugin() +model.voterSet.init() +model.dm.redistrict() +// UPDATE +model.update() +console.log(model) \ No newline at end of file diff --git a/play/js/model1.js b/play/js/model1.js new file mode 100644 index 00000000..de17d092 --- /dev/null +++ b/play/js/model1.js @@ -0,0 +1,65 @@ +var l = new Loader() +l.onload = function(assets){ + + // CREATE + var presetName = "model1" + var model = new Model(presetName); + model.assets = assets + model.createDOM() + // CONFIGURE + model.border = 2 + // INIT + model.initDOM() + + var basediv = document.querySelector("#" + presetName) + basediv.appendChild(model.dom); + + model.initPlugin = function(){ + // CREATE + model.voterGroups.push(new SingleVoter(model)) + model.candidates.push(new Candidate(model)) + model.candidates.push(new Candidate(model)) + // CONFIGURE + Object.assign( model.voterGroups[0], {x:125, y:200} ) + Object.assign( model.candidates[0],{x: 50, y:125, icon:"square"} ) + Object.assign( model.candidates[1],{x:250, y:125, icon:"triangle"} ) + model.theme = "Letters" + model.ballotConcept = "on" + // INIT + model.candidates[0].init() + model.candidates[1].init() + model.initMODEL() + model.voterManager.initVoters() + model.dm.redistrict() + }; + model.onDraw = function(){ + if (model.voterGroups.length == 0) return + if (model.voterGroups[0].voterGroupType == "GaussianVoters") return + var id = model.voterGroups[0].voterPeople[0].stages[model.stage].ballot.vote; + var color = model.candidatesById[id].fill; + var text = "VOTES FOR <b style='color:"+color+"'>"+model.nameUpper(id)+"</b>"; + model.caption.innerHTML = text; + }; + + // INIT + model.initPlugin(); + + // UPDATE + model.update() + +}; +l.load([ +"play/img/voter_face.png", +"play/img/square.png", +"play/img/triangle.png", +"play/img/hexagon.png", +"play/img/square.svg", +"play/img/triangle.svg", +"play/img/hexagon.svg", +"play/img/pentagon.svg", +"play/img/bob.svg", +// plus +"play/img/plusCandidate.png", +"play/img/plusOneVoter.png", +"play/img/plusVoterGroup.png" +]); diff --git a/play/js/model1_original.js b/play/js/model1_original.js new file mode 100644 index 00000000..2476c108 --- /dev/null +++ b/play/js/model1_original.js @@ -0,0 +1,59 @@ +var l = new Loader() +l.onload = function(assets){ + + // CREATE + var presetName = "model1" + var model = new Model(presetName); + model.assets = assets + model.theme = "Nicky" + model.createDOM() + // CONFIGURE + model.border = 2 + // INIT + model.initDOM() + + var basediv = document.querySelector("#" + presetName) + basediv.appendChild(model.dom); + + model.initPlugin = function(){ + // CREATE + model.voterGroups.push(new SingleVoter(model)) + model.candidates.push(new Candidate(model)) + model.candidates.push(new Candidate(model)) + // CONFIGURE + Object.assign( model.voterGroups[0], {x:125, y:200} ) + Object.assign( model.candidates[0],{x: 50, y:125, icon:"square"} ) + Object.assign( model.candidates[1],{x:250, y:125, icon:"triangle"} ) + model.ballotConcept = "off" + // INIT + model.candidates[0].init() + model.candidates[1].init() + model.initMODEL() + model.voterManager.initVoters() + model.dm.redistrict() + // UPDATE + model.update() + }; + model.onUpdate = function(){ + var id = model.voterGroups[0].voterPeople[0].stages[model.stage].ballot.vote; + var color = model.candidatesById[id].fill; + var text = "VOTES FOR <b style='color:"+color+"'>"+id.toUpperCase()+"</b>"; + model.caption.innerHTML = text; + }; + + // UPDATE + model.initPlugin(); + model.update() + +}; +l.load([ +"play/img/voter_face.png", +"play/img/square.png", +"play/img/triangle.png", +"play/img/hexagon.png", +"play/img/square.svg", +"play/img/triangle.svg", +"play/img/hexagon.svg", +"play/img/pentagon.svg", +"play/img/bob.svg" +]); diff --git a/play/js/model2.js b/play/js/model2.js new file mode 100644 index 00000000..68e68723 --- /dev/null +++ b/play/js/model2.js @@ -0,0 +1,54 @@ +var l = new Loader() +l.onload = function(assets){ + + // CREATE + var presetName = "model2" + var model = new Model(presetName); + model.assets = assets + model.createDOM() + // INIT + model.initDOM() + var basediv = document.querySelector("#" + presetName) + basediv.appendChild(model.dom); + + model.initPlugin = function(){ + // CREATE + model.voterGroups.push(new GaussianVoters(model)) + model.candidates.push(new Candidate(model)) + model.candidates.push(new Candidate(model)) + // CONFIGURE + Object.assign( model.voterGroups[0], {x:150, y:150} ) + Object.assign( model.candidates[0],{x: 50, y:125, icon:"square"} ) + Object.assign( model.candidates[1],{x:250, y:125, icon:"triangle"} ) + model.theme = "Letters" + // INIT + model.candidates[0].init() + model.candidates[1].init() + model.initMODEL() + model.voterManager.initVoters() + model.election = Election.plurality + model.optionsForElection = {sidebar:true,verbose:true,originalCaption:true} + model.dm.redistrict() + // UPDATE + model.update() + + }; + model.initPlugin(); + model.update() + +}; +l.load([ +"play/img/voter_face.png", +"play/img/square.png", +"play/img/triangle.png", +"play/img/hexagon.png", +"play/img/square.svg", +"play/img/triangle.svg", +"play/img/hexagon.svg", +"play/img/pentagon.svg", +"play/img/bob.svg", +// plus +"play/img/plusCandidate.png", +"play/img/plusOneVoter.png", +"play/img/plusVoterGroup.png" +]); diff --git a/play/js/model2_original.js b/play/js/model2_original.js new file mode 100644 index 00000000..99b52b9f --- /dev/null +++ b/play/js/model2_original.js @@ -0,0 +1,51 @@ +var l = new Loader() +l.onload = function(assets){ + + // CREATE + var presetName = "model2" + var model = new Model(presetName); + model.assets = assets + model.theme = "Nicky" + model.createDOM() + // INIT + model.initDOM() + var basediv = document.querySelector("#" + presetName) + basediv.appendChild(model.dom); + + model.initPlugin = function(){ + // CREATE + model.voterGroups.push(new GaussianVoters(model)) + model.candidates.push(new Candidate(model)) + model.candidates.push(new Candidate(model)) + // CONFIGURE + Object.assign( model.voterGroups[0], {x:150, y:150} ) + Object.assign( model.candidates[0],{x: 50, y:125, icon:"square"} ) + Object.assign( model.candidates[1],{x:250, y:125, icon:"triangle"} ) + model.ballotConcept = "off" + // INIT + model.candidates[0].init() + model.candidates[1].init() + model.initMODEL() + model.voterManager.initVoters() + model.election = Election.plurality + model.optionsForElection = {sidebar:true,verbose:true,originalCaption:true} + model.dm.redistrict() + // UPDATE + model.update() + + }; + model.initPlugin(); + model.update() + +}; +l.load([ +"play/img/voter_face.png", +"play/img/square.png", +"play/img/triangle.png", +"play/img/hexagon.png", +"play/img/square.svg", +"play/img/triangle.svg", +"play/img/hexagon.svg", +"play/img/pentagon.svg", +"play/img/bob.svg" +]); diff --git a/play/js/model3.js b/play/js/model3.js new file mode 100644 index 00000000..768946b4 --- /dev/null +++ b/play/js/model3.js @@ -0,0 +1,73 @@ +var l = new Loader() +l.onload = function(assets){ + + // CREATE + + var presetName = "model3" + var model = new Model(presetName); + model.assets = assets + model.createDOM() + + // INIT + model.initDOM() + var basediv = document.querySelector("#" + presetName) + basediv.appendChild(model.dom); + + model.initPlugin = function(){ + // CREATE + model.voterGroups.push(new GaussianVoters(model)) + model.candidates.push(new Candidate(model)) + model.candidates.push(new Candidate(model)) + model.candidates.push(new Candidate(model)) + // CONFIGURE + Object.assign( model.voterGroups[0], {x:155, y:125} ) + Object.assign( model.candidates[0],{x: 50, y:125, icon:"square"} ) + Object.assign( model.candidates[1],{x:250, y:125, icon:"triangle"} ) + Object.assign( model.candidates[2],{x:280, y:280, icon:"hexagon"} ) + model.theme = "Letters" + // INIT + model.candidates[0].init() + model.candidates[1].init() + model.candidates[2].init() + model.initMODEL() + model.voterManager.initVoters() + model.election = Election.plurality; + model.optionsForElection = {sidebar:true, original:true, originalCaption:true} + model.dm.redistrict() + // UPDATE + model.update() + }; + model.onUpdate = function(){ + Election.plurality(model.district[0],model); + }; + + var resetDOM = document.createElement("div"); + basediv.appendChild(resetDOM); + resetDOM.id = "reset"; + resetDOM.innerHTML = "reset"; + resetDOM.style.top = "415px"; + resetDOM.style.left = "110px"; + resetDOM.onclick = function(){ + model.reset(); + model.update() + }; + + // UPDATE + model.initPlugin(); + model.update() +}; +l.load([ + "play/img/voter_face.png", + "play/img/square.png", + "play/img/triangle.png", + "play/img/hexagon.png", + "play/img/square.svg", + "play/img/triangle.svg", + "play/img/hexagon.svg", + "play/img/pentagon.svg", + "play/img/bob.svg", + // plus + "play/img/plusCandidate.png", + "play/img/plusOneVoter.png", + "play/img/plusVoterGroup.png" +]); diff --git a/play/js/model3_original.js b/play/js/model3_original.js new file mode 100644 index 00000000..ced243cb --- /dev/null +++ b/play/js/model3_original.js @@ -0,0 +1,70 @@ +var l = new Loader() +l.onload = function(assets){ + + // CREATE + + var presetName = "model3" + var model = new Model(presetName); + model.assets = assets + model.theme = "Nicky" + model.createDOM() + + // INIT + model.initDOM() + var basediv = document.querySelector("#" + presetName) + basediv.appendChild(model.dom); + + model.initPlugin = function(){ + // CREATE + model.voterGroups.push(new GaussianVoters(model)) + model.candidates.push(new Candidate(model)) + model.candidates.push(new Candidate(model)) + model.candidates.push(new Candidate(model)) + // CONFIGURE + Object.assign( model.voterGroups[0], {x:155, y:125} ) + Object.assign( model.candidates[0],{x: 50, y:125, icon:"square"} ) + Object.assign( model.candidates[1],{x:250, y:125, icon:"triangle"} ) + Object.assign( model.candidates[2],{x:280, y:280, icon:"hexagon"} ) + model.ballotConcept = "off" + // INIT + model.candidates[0].init() + model.candidates[1].init() + model.candidates[2].init() + model.initMODEL() + model.voterManager.initVoters() + model.election = Election.plurality; + model.optionsForElection = {sidebar:true, original:true, originalCaption:true} + model.dm.redistrict() + // UPDATE + model.update() + }; + model.onUpdate = function(){ + Election.plurality(model.district[0],model); + }; + + var resetDOM = document.createElement("div"); + basediv.appendChild(resetDOM); + resetDOM.id = "reset"; + resetDOM.innerHTML = "reset"; + resetDOM.style.top = "415px"; + resetDOM.style.left = "110px"; + resetDOM.onclick = function(){ + model.reset(); + model.update() + }; + + // UPDATE + model.initPlugin(); + model.update() +}; +l.load([ + "play/img/voter_face.png", + "play/img/square.png", + "play/img/triangle.png", + "play/img/hexagon.png", + "play/img/square.svg", + "play/img/triangle.svg", + "play/img/hexagon.svg", + "play/img/pentagon.svg", + "play/img/bob.svg" +]); diff --git a/play/js/pako.min.js b/play/js/pako.min.js new file mode 100644 index 00000000..ba397319 --- /dev/null +++ b/play/js/pako.min.js @@ -0,0 +1 @@ +!function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).pako=t()}}(function(){return function r(s,o,l){function h(e,t){if(!o[e]){if(!s[e]){var a="function"==typeof require&&require;if(!t&&a)return a(e,!0);if(d)return d(e,!0);var i=new Error("Cannot find module '"+e+"'");throw i.code="MODULE_NOT_FOUND",i}var n=o[e]={exports:{}};s[e][0].call(n.exports,function(t){return h(s[e][1][t]||t)},n,n.exports,r,s,o,l)}return o[e].exports}for(var d="function"==typeof require&&require,t=0;t<l.length;t++)h(l[t]);return h}({1:[function(t,e,a){"use strict";var s=t("./zlib/deflate"),o=t("./utils/common"),l=t("./utils/strings"),n=t("./zlib/messages"),r=t("./zlib/zstream"),h=Object.prototype.toString,d=0,f=-1,_=0,u=8;function c(t){if(!(this instanceof c))return new c(t);this.options=o.assign({level:f,method:u,chunkSize:16384,windowBits:15,memLevel:8,strategy:_,to:""},t||{});var e=this.options;e.raw&&0<e.windowBits?e.windowBits=-e.windowBits:e.gzip&&0<e.windowBits&&e.windowBits<16&&(e.windowBits+=16),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new r,this.strm.avail_out=0;var a=s.deflateInit2(this.strm,e.level,e.method,e.windowBits,e.memLevel,e.strategy);if(a!==d)throw new Error(n[a]);if(e.header&&s.deflateSetHeader(this.strm,e.header),e.dictionary){var i;if(i="string"==typeof e.dictionary?l.string2buf(e.dictionary):"[object ArrayBuffer]"===h.call(e.dictionary)?new Uint8Array(e.dictionary):e.dictionary,(a=s.deflateSetDictionary(this.strm,i))!==d)throw new Error(n[a]);this._dict_set=!0}}function i(t,e){var a=new c(e);if(a.push(t,!0),a.err)throw a.msg||n[a.err];return a.result}c.prototype.push=function(t,e){var a,i,n=this.strm,r=this.options.chunkSize;if(this.ended)return!1;i=e===~~e?e:!0===e?4:0,"string"==typeof t?n.input=l.string2buf(t):"[object ArrayBuffer]"===h.call(t)?n.input=new Uint8Array(t):n.input=t,n.next_in=0,n.avail_in=n.input.length;do{if(0===n.avail_out&&(n.output=new o.Buf8(r),n.next_out=0,n.avail_out=r),1!==(a=s.deflate(n,i))&&a!==d)return this.onEnd(a),!(this.ended=!0);0!==n.avail_out&&(0!==n.avail_in||4!==i&&2!==i)||("string"===this.options.to?this.onData(l.buf2binstring(o.shrinkBuf(n.output,n.next_out))):this.onData(o.shrinkBuf(n.output,n.next_out)))}while((0<n.avail_in||0===n.avail_out)&&1!==a);return 4===i?(a=s.deflateEnd(this.strm),this.onEnd(a),this.ended=!0,a===d):2!==i||(this.onEnd(d),!(n.avail_out=0))},c.prototype.onData=function(t){this.chunks.push(t)},c.prototype.onEnd=function(t){t===d&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=o.flattenChunks(this.chunks)),this.chunks=[],this.err=t,this.msg=this.strm.msg},a.Deflate=c,a.deflate=i,a.deflateRaw=function(t,e){return(e=e||{}).raw=!0,i(t,e)},a.gzip=function(t,e){return(e=e||{}).gzip=!0,i(t,e)}},{"./utils/common":3,"./utils/strings":4,"./zlib/deflate":8,"./zlib/messages":13,"./zlib/zstream":15}],2:[function(t,e,a){"use strict";var f=t("./zlib/inflate"),_=t("./utils/common"),u=t("./utils/strings"),c=t("./zlib/constants"),i=t("./zlib/messages"),n=t("./zlib/zstream"),r=t("./zlib/gzheader"),b=Object.prototype.toString;function s(t){if(!(this instanceof s))return new s(t);this.options=_.assign({chunkSize:16384,windowBits:0,to:""},t||{});var e=this.options;e.raw&&0<=e.windowBits&&e.windowBits<16&&(e.windowBits=-e.windowBits,0===e.windowBits&&(e.windowBits=-15)),!(0<=e.windowBits&&e.windowBits<16)||t&&t.windowBits||(e.windowBits+=32),15<e.windowBits&&e.windowBits<48&&0==(15&e.windowBits)&&(e.windowBits|=15),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new n,this.strm.avail_out=0;var a=f.inflateInit2(this.strm,e.windowBits);if(a!==c.Z_OK)throw new Error(i[a]);if(this.header=new r,f.inflateGetHeader(this.strm,this.header),e.dictionary&&("string"==typeof e.dictionary?e.dictionary=u.string2buf(e.dictionary):"[object ArrayBuffer]"===b.call(e.dictionary)&&(e.dictionary=new Uint8Array(e.dictionary)),e.raw&&(a=f.inflateSetDictionary(this.strm,e.dictionary))!==c.Z_OK))throw new Error(i[a])}function o(t,e){var a=new s(e);if(a.push(t,!0),a.err)throw a.msg||i[a.err];return a.result}s.prototype.push=function(t,e){var a,i,n,r,s,o=this.strm,l=this.options.chunkSize,h=this.options.dictionary,d=!1;if(this.ended)return!1;i=e===~~e?e:!0===e?c.Z_FINISH:c.Z_NO_FLUSH,"string"==typeof t?o.input=u.binstring2buf(t):"[object ArrayBuffer]"===b.call(t)?o.input=new Uint8Array(t):o.input=t,o.next_in=0,o.avail_in=o.input.length;do{if(0===o.avail_out&&(o.output=new _.Buf8(l),o.next_out=0,o.avail_out=l),(a=f.inflate(o,c.Z_NO_FLUSH))===c.Z_NEED_DICT&&h&&(a=f.inflateSetDictionary(this.strm,h)),a===c.Z_BUF_ERROR&&!0===d&&(a=c.Z_OK,d=!1),a!==c.Z_STREAM_END&&a!==c.Z_OK)return this.onEnd(a),!(this.ended=!0);o.next_out&&(0!==o.avail_out&&a!==c.Z_STREAM_END&&(0!==o.avail_in||i!==c.Z_FINISH&&i!==c.Z_SYNC_FLUSH)||("string"===this.options.to?(n=u.utf8border(o.output,o.next_out),r=o.next_out-n,s=u.buf2string(o.output,n),o.next_out=r,o.avail_out=l-r,r&&_.arraySet(o.output,o.output,n,r,0),this.onData(s)):this.onData(_.shrinkBuf(o.output,o.next_out)))),0===o.avail_in&&0===o.avail_out&&(d=!0)}while((0<o.avail_in||0===o.avail_out)&&a!==c.Z_STREAM_END);return a===c.Z_STREAM_END&&(i=c.Z_FINISH),i===c.Z_FINISH?(a=f.inflateEnd(this.strm),this.onEnd(a),this.ended=!0,a===c.Z_OK):i!==c.Z_SYNC_FLUSH||(this.onEnd(c.Z_OK),!(o.avail_out=0))},s.prototype.onData=function(t){this.chunks.push(t)},s.prototype.onEnd=function(t){t===c.Z_OK&&("string"===this.options.to?this.result=this.chunks.join(""):this.result=_.flattenChunks(this.chunks)),this.chunks=[],this.err=t,this.msg=this.strm.msg},a.Inflate=s,a.inflate=o,a.inflateRaw=function(t,e){return(e=e||{}).raw=!0,o(t,e)},a.ungzip=o},{"./utils/common":3,"./utils/strings":4,"./zlib/constants":6,"./zlib/gzheader":9,"./zlib/inflate":11,"./zlib/messages":13,"./zlib/zstream":15}],3:[function(t,e,a){"use strict";var i="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Int32Array;a.assign=function(t){for(var e,a,i=Array.prototype.slice.call(arguments,1);i.length;){var n=i.shift();if(n){if("object"!=typeof n)throw new TypeError(n+"must be non-object");for(var r in n)e=n,a=r,Object.prototype.hasOwnProperty.call(e,a)&&(t[r]=n[r])}}return t},a.shrinkBuf=function(t,e){return t.length===e?t:t.subarray?t.subarray(0,e):(t.length=e,t)};var n={arraySet:function(t,e,a,i,n){if(e.subarray&&t.subarray)t.set(e.subarray(a,a+i),n);else for(var r=0;r<i;r++)t[n+r]=e[a+r]},flattenChunks:function(t){var e,a,i,n,r,s;for(e=i=0,a=t.length;e<a;e++)i+=t[e].length;for(s=new Uint8Array(i),e=n=0,a=t.length;e<a;e++)r=t[e],s.set(r,n),n+=r.length;return s}},r={arraySet:function(t,e,a,i,n){for(var r=0;r<i;r++)t[n+r]=e[a+r]},flattenChunks:function(t){return[].concat.apply([],t)}};a.setTyped=function(t){t?(a.Buf8=Uint8Array,a.Buf16=Uint16Array,a.Buf32=Int32Array,a.assign(a,n)):(a.Buf8=Array,a.Buf16=Array,a.Buf32=Array,a.assign(a,r))},a.setTyped(i)},{}],4:[function(t,e,a){"use strict";var l=t("./common"),n=!0,r=!0;try{String.fromCharCode.apply(null,[0])}catch(t){n=!1}try{String.fromCharCode.apply(null,new Uint8Array(1))}catch(t){r=!1}for(var h=new l.Buf8(256),i=0;i<256;i++)h[i]=252<=i?6:248<=i?5:240<=i?4:224<=i?3:192<=i?2:1;function d(t,e){if(e<65534&&(t.subarray&&r||!t.subarray&&n))return String.fromCharCode.apply(null,l.shrinkBuf(t,e));for(var a="",i=0;i<e;i++)a+=String.fromCharCode(t[i]);return a}h[254]=h[254]=1,a.string2buf=function(t){var e,a,i,n,r,s=t.length,o=0;for(n=0;n<s;n++)55296==(64512&(a=t.charCodeAt(n)))&&n+1<s&&56320==(64512&(i=t.charCodeAt(n+1)))&&(a=65536+(a-55296<<10)+(i-56320),n++),o+=a<128?1:a<2048?2:a<65536?3:4;for(e=new l.Buf8(o),n=r=0;r<o;n++)55296==(64512&(a=t.charCodeAt(n)))&&n+1<s&&56320==(64512&(i=t.charCodeAt(n+1)))&&(a=65536+(a-55296<<10)+(i-56320),n++),a<128?e[r++]=a:(a<2048?e[r++]=192|a>>>6:(a<65536?e[r++]=224|a>>>12:(e[r++]=240|a>>>18,e[r++]=128|a>>>12&63),e[r++]=128|a>>>6&63),e[r++]=128|63&a);return e},a.buf2binstring=function(t){return d(t,t.length)},a.binstring2buf=function(t){for(var e=new l.Buf8(t.length),a=0,i=e.length;a<i;a++)e[a]=t.charCodeAt(a);return e},a.buf2string=function(t,e){var a,i,n,r,s=e||t.length,o=new Array(2*s);for(a=i=0;a<s;)if((n=t[a++])<128)o[i++]=n;else if(4<(r=h[n]))o[i++]=65533,a+=r-1;else{for(n&=2===r?31:3===r?15:7;1<r&&a<s;)n=n<<6|63&t[a++],r--;1<r?o[i++]=65533:n<65536?o[i++]=n:(n-=65536,o[i++]=55296|n>>10&1023,o[i++]=56320|1023&n)}return d(o,i)},a.utf8border=function(t,e){var a;for((e=e||t.length)>t.length&&(e=t.length),a=e-1;0<=a&&128==(192&t[a]);)a--;return a<0?e:0===a?e:a+h[t[a]]>e?a:e}},{"./common":3}],5:[function(t,e,a){"use strict";e.exports=function(t,e,a,i){for(var n=65535&t|0,r=t>>>16&65535|0,s=0;0!==a;){for(a-=s=2e3<a?2e3:a;r=r+(n=n+e[i++]|0)|0,--s;);n%=65521,r%=65521}return n|r<<16|0}},{}],6:[function(t,e,a){"use strict";e.exports={Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_TREES:6,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_BUF_ERROR:-5,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,Z_BINARY:0,Z_TEXT:1,Z_UNKNOWN:2,Z_DEFLATED:8}},{}],7:[function(t,e,a){"use strict";var o=function(){for(var t,e=[],a=0;a<256;a++){t=a;for(var i=0;i<8;i++)t=1&t?3988292384^t>>>1:t>>>1;e[a]=t}return e}();e.exports=function(t,e,a,i){var n=o,r=i+a;t^=-1;for(var s=i;s<r;s++)t=t>>>8^n[255&(t^e[s])];return-1^t}},{}],8:[function(t,e,a){"use strict";var l,_=t("../utils/common"),h=t("./trees"),u=t("./adler32"),c=t("./crc32"),i=t("./messages"),d=0,f=4,b=0,g=-2,m=-1,w=4,n=2,p=8,v=9,r=286,s=30,o=19,k=2*r+1,y=15,x=3,z=258,B=z+x+1,S=42,E=113,A=1,Z=2,R=3,C=4;function N(t,e){return t.msg=i[e],e}function O(t){return(t<<1)-(4<t?9:0)}function D(t){for(var e=t.length;0<=--e;)t[e]=0}function I(t){var e=t.state,a=e.pending;a>t.avail_out&&(a=t.avail_out),0!==a&&(_.arraySet(t.output,e.pending_buf,e.pending_out,a,t.next_out),t.next_out+=a,e.pending_out+=a,t.total_out+=a,t.avail_out-=a,e.pending-=a,0===e.pending&&(e.pending_out=0))}function U(t,e){h._tr_flush_block(t,0<=t.block_start?t.block_start:-1,t.strstart-t.block_start,e),t.block_start=t.strstart,I(t.strm)}function T(t,e){t.pending_buf[t.pending++]=e}function F(t,e){t.pending_buf[t.pending++]=e>>>8&255,t.pending_buf[t.pending++]=255&e}function L(t,e){var a,i,n=t.max_chain_length,r=t.strstart,s=t.prev_length,o=t.nice_match,l=t.strstart>t.w_size-B?t.strstart-(t.w_size-B):0,h=t.window,d=t.w_mask,f=t.prev,_=t.strstart+z,u=h[r+s-1],c=h[r+s];t.prev_length>=t.good_match&&(n>>=2),o>t.lookahead&&(o=t.lookahead);do{if(h[(a=e)+s]===c&&h[a+s-1]===u&&h[a]===h[r]&&h[++a]===h[r+1]){r+=2,a++;do{}while(h[++r]===h[++a]&&h[++r]===h[++a]&&h[++r]===h[++a]&&h[++r]===h[++a]&&h[++r]===h[++a]&&h[++r]===h[++a]&&h[++r]===h[++a]&&h[++r]===h[++a]&&r<_);if(i=z-(_-r),r=_-z,s<i){if(t.match_start=e,o<=(s=i))break;u=h[r+s-1],c=h[r+s]}}}while((e=f[e&d])>l&&0!=--n);return s<=t.lookahead?s:t.lookahead}function H(t){var e,a,i,n,r,s,o,l,h,d,f=t.w_size;do{if(n=t.window_size-t.lookahead-t.strstart,t.strstart>=f+(f-B)){for(_.arraySet(t.window,t.window,f,f,0),t.match_start-=f,t.strstart-=f,t.block_start-=f,e=a=t.hash_size;i=t.head[--e],t.head[e]=f<=i?i-f:0,--a;);for(e=a=f;i=t.prev[--e],t.prev[e]=f<=i?i-f:0,--a;);n+=f}if(0===t.strm.avail_in)break;if(s=t.strm,o=t.window,l=t.strstart+t.lookahead,h=n,d=void 0,d=s.avail_in,h<d&&(d=h),a=0===d?0:(s.avail_in-=d,_.arraySet(o,s.input,s.next_in,d,l),1===s.state.wrap?s.adler=u(s.adler,o,d,l):2===s.state.wrap&&(s.adler=c(s.adler,o,d,l)),s.next_in+=d,s.total_in+=d,d),t.lookahead+=a,t.lookahead+t.insert>=x)for(r=t.strstart-t.insert,t.ins_h=t.window[r],t.ins_h=(t.ins_h<<t.hash_shift^t.window[r+1])&t.hash_mask;t.insert&&(t.ins_h=(t.ins_h<<t.hash_shift^t.window[r+x-1])&t.hash_mask,t.prev[r&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=r,r++,t.insert--,!(t.lookahead+t.insert<x)););}while(t.lookahead<B&&0!==t.strm.avail_in)}function j(t,e){for(var a,i;;){if(t.lookahead<B){if(H(t),t.lookahead<B&&e===d)return A;if(0===t.lookahead)break}if(a=0,t.lookahead>=x&&(t.ins_h=(t.ins_h<<t.hash_shift^t.window[t.strstart+x-1])&t.hash_mask,a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart),0!==a&&t.strstart-a<=t.w_size-B&&(t.match_length=L(t,a)),t.match_length>=x)if(i=h._tr_tally(t,t.strstart-t.match_start,t.match_length-x),t.lookahead-=t.match_length,t.match_length<=t.max_lazy_match&&t.lookahead>=x){for(t.match_length--;t.strstart++,t.ins_h=(t.ins_h<<t.hash_shift^t.window[t.strstart+x-1])&t.hash_mask,a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart,0!=--t.match_length;);t.strstart++}else t.strstart+=t.match_length,t.match_length=0,t.ins_h=t.window[t.strstart],t.ins_h=(t.ins_h<<t.hash_shift^t.window[t.strstart+1])&t.hash_mask;else i=h._tr_tally(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++;if(i&&(U(t,!1),0===t.strm.avail_out))return A}return t.insert=t.strstart<x-1?t.strstart:x-1,e===f?(U(t,!0),0===t.strm.avail_out?R:C):t.last_lit&&(U(t,!1),0===t.strm.avail_out)?A:Z}function K(t,e){for(var a,i,n;;){if(t.lookahead<B){if(H(t),t.lookahead<B&&e===d)return A;if(0===t.lookahead)break}if(a=0,t.lookahead>=x&&(t.ins_h=(t.ins_h<<t.hash_shift^t.window[t.strstart+x-1])&t.hash_mask,a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart),t.prev_length=t.match_length,t.prev_match=t.match_start,t.match_length=x-1,0!==a&&t.prev_length<t.max_lazy_match&&t.strstart-a<=t.w_size-B&&(t.match_length=L(t,a),t.match_length<=5&&(1===t.strategy||t.match_length===x&&4096<t.strstart-t.match_start)&&(t.match_length=x-1)),t.prev_length>=x&&t.match_length<=t.prev_length){for(n=t.strstart+t.lookahead-x,i=h._tr_tally(t,t.strstart-1-t.prev_match,t.prev_length-x),t.lookahead-=t.prev_length-1,t.prev_length-=2;++t.strstart<=n&&(t.ins_h=(t.ins_h<<t.hash_shift^t.window[t.strstart+x-1])&t.hash_mask,a=t.prev[t.strstart&t.w_mask]=t.head[t.ins_h],t.head[t.ins_h]=t.strstart),0!=--t.prev_length;);if(t.match_available=0,t.match_length=x-1,t.strstart++,i&&(U(t,!1),0===t.strm.avail_out))return A}else if(t.match_available){if((i=h._tr_tally(t,0,t.window[t.strstart-1]))&&U(t,!1),t.strstart++,t.lookahead--,0===t.strm.avail_out)return A}else t.match_available=1,t.strstart++,t.lookahead--}return t.match_available&&(i=h._tr_tally(t,0,t.window[t.strstart-1]),t.match_available=0),t.insert=t.strstart<x-1?t.strstart:x-1,e===f?(U(t,!0),0===t.strm.avail_out?R:C):t.last_lit&&(U(t,!1),0===t.strm.avail_out)?A:Z}function M(t,e,a,i,n){this.good_length=t,this.max_lazy=e,this.nice_length=a,this.max_chain=i,this.func=n}function P(){this.strm=null,this.status=0,this.pending_buf=null,this.pending_buf_size=0,this.pending_out=0,this.pending=0,this.wrap=0,this.gzhead=null,this.gzindex=0,this.method=p,this.last_flush=-1,this.w_size=0,this.w_bits=0,this.w_mask=0,this.window=null,this.window_size=0,this.prev=null,this.head=null,this.ins_h=0,this.hash_size=0,this.hash_bits=0,this.hash_mask=0,this.hash_shift=0,this.block_start=0,this.match_length=0,this.prev_match=0,this.match_available=0,this.strstart=0,this.match_start=0,this.lookahead=0,this.prev_length=0,this.max_chain_length=0,this.max_lazy_match=0,this.level=0,this.strategy=0,this.good_match=0,this.nice_match=0,this.dyn_ltree=new _.Buf16(2*k),this.dyn_dtree=new _.Buf16(2*(2*s+1)),this.bl_tree=new _.Buf16(2*(2*o+1)),D(this.dyn_ltree),D(this.dyn_dtree),D(this.bl_tree),this.l_desc=null,this.d_desc=null,this.bl_desc=null,this.bl_count=new _.Buf16(y+1),this.heap=new _.Buf16(2*r+1),D(this.heap),this.heap_len=0,this.heap_max=0,this.depth=new _.Buf16(2*r+1),D(this.depth),this.l_buf=0,this.lit_bufsize=0,this.last_lit=0,this.d_buf=0,this.opt_len=0,this.static_len=0,this.matches=0,this.insert=0,this.bi_buf=0,this.bi_valid=0}function Y(t){var e;return t&&t.state?(t.total_in=t.total_out=0,t.data_type=n,(e=t.state).pending=0,e.pending_out=0,e.wrap<0&&(e.wrap=-e.wrap),e.status=e.wrap?S:E,t.adler=2===e.wrap?0:1,e.last_flush=d,h._tr_init(e),b):N(t,g)}function q(t){var e,a=Y(t);return a===b&&((e=t.state).window_size=2*e.w_size,D(e.head),e.max_lazy_match=l[e.level].max_lazy,e.good_match=l[e.level].good_length,e.nice_match=l[e.level].nice_length,e.max_chain_length=l[e.level].max_chain,e.strstart=0,e.block_start=0,e.lookahead=0,e.insert=0,e.match_length=e.prev_length=x-1,e.match_available=0,e.ins_h=0),a}function G(t,e,a,i,n,r){if(!t)return g;var s=1;if(e===m&&(e=6),i<0?(s=0,i=-i):15<i&&(s=2,i-=16),n<1||v<n||a!==p||i<8||15<i||e<0||9<e||r<0||w<r)return N(t,g);8===i&&(i=9);var o=new P;return(t.state=o).strm=t,o.wrap=s,o.gzhead=null,o.w_bits=i,o.w_size=1<<o.w_bits,o.w_mask=o.w_size-1,o.hash_bits=n+7,o.hash_size=1<<o.hash_bits,o.hash_mask=o.hash_size-1,o.hash_shift=~~((o.hash_bits+x-1)/x),o.window=new _.Buf8(2*o.w_size),o.head=new _.Buf16(o.hash_size),o.prev=new _.Buf16(o.w_size),o.lit_bufsize=1<<n+6,o.pending_buf_size=4*o.lit_bufsize,o.pending_buf=new _.Buf8(o.pending_buf_size),o.d_buf=1*o.lit_bufsize,o.l_buf=3*o.lit_bufsize,o.level=e,o.strategy=r,o.method=a,q(t)}l=[new M(0,0,0,0,function(t,e){var a=65535;for(a>t.pending_buf_size-5&&(a=t.pending_buf_size-5);;){if(t.lookahead<=1){if(H(t),0===t.lookahead&&e===d)return A;if(0===t.lookahead)break}t.strstart+=t.lookahead,t.lookahead=0;var i=t.block_start+a;if((0===t.strstart||t.strstart>=i)&&(t.lookahead=t.strstart-i,t.strstart=i,U(t,!1),0===t.strm.avail_out))return A;if(t.strstart-t.block_start>=t.w_size-B&&(U(t,!1),0===t.strm.avail_out))return A}return t.insert=0,e===f?(U(t,!0),0===t.strm.avail_out?R:C):(t.strstart>t.block_start&&(U(t,!1),t.strm.avail_out),A)}),new M(4,4,8,4,j),new M(4,5,16,8,j),new M(4,6,32,32,j),new M(4,4,16,16,K),new M(8,16,32,32,K),new M(8,16,128,128,K),new M(8,32,128,256,K),new M(32,128,258,1024,K),new M(32,258,258,4096,K)],a.deflateInit=function(t,e){return G(t,e,p,15,8,0)},a.deflateInit2=G,a.deflateReset=q,a.deflateResetKeep=Y,a.deflateSetHeader=function(t,e){return t&&t.state?2!==t.state.wrap?g:(t.state.gzhead=e,b):g},a.deflate=function(t,e){var a,i,n,r;if(!t||!t.state||5<e||e<0)return t?N(t,g):g;if(i=t.state,!t.output||!t.input&&0!==t.avail_in||666===i.status&&e!==f)return N(t,0===t.avail_out?-5:g);if(i.strm=t,a=i.last_flush,i.last_flush=e,i.status===S)if(2===i.wrap)t.adler=0,T(i,31),T(i,139),T(i,8),i.gzhead?(T(i,(i.gzhead.text?1:0)+(i.gzhead.hcrc?2:0)+(i.gzhead.extra?4:0)+(i.gzhead.name?8:0)+(i.gzhead.comment?16:0)),T(i,255&i.gzhead.time),T(i,i.gzhead.time>>8&255),T(i,i.gzhead.time>>16&255),T(i,i.gzhead.time>>24&255),T(i,9===i.level?2:2<=i.strategy||i.level<2?4:0),T(i,255&i.gzhead.os),i.gzhead.extra&&i.gzhead.extra.length&&(T(i,255&i.gzhead.extra.length),T(i,i.gzhead.extra.length>>8&255)),i.gzhead.hcrc&&(t.adler=c(t.adler,i.pending_buf,i.pending,0)),i.gzindex=0,i.status=69):(T(i,0),T(i,0),T(i,0),T(i,0),T(i,0),T(i,9===i.level?2:2<=i.strategy||i.level<2?4:0),T(i,3),i.status=E);else{var s=p+(i.w_bits-8<<4)<<8;s|=(2<=i.strategy||i.level<2?0:i.level<6?1:6===i.level?2:3)<<6,0!==i.strstart&&(s|=32),s+=31-s%31,i.status=E,F(i,s),0!==i.strstart&&(F(i,t.adler>>>16),F(i,65535&t.adler)),t.adler=1}if(69===i.status)if(i.gzhead.extra){for(n=i.pending;i.gzindex<(65535&i.gzhead.extra.length)&&(i.pending!==i.pending_buf_size||(i.gzhead.hcrc&&i.pending>n&&(t.adler=c(t.adler,i.pending_buf,i.pending-n,n)),I(t),n=i.pending,i.pending!==i.pending_buf_size));)T(i,255&i.gzhead.extra[i.gzindex]),i.gzindex++;i.gzhead.hcrc&&i.pending>n&&(t.adler=c(t.adler,i.pending_buf,i.pending-n,n)),i.gzindex===i.gzhead.extra.length&&(i.gzindex=0,i.status=73)}else i.status=73;if(73===i.status)if(i.gzhead.name){n=i.pending;do{if(i.pending===i.pending_buf_size&&(i.gzhead.hcrc&&i.pending>n&&(t.adler=c(t.adler,i.pending_buf,i.pending-n,n)),I(t),n=i.pending,i.pending===i.pending_buf_size)){r=1;break}T(i,r=i.gzindex<i.gzhead.name.length?255&i.gzhead.name.charCodeAt(i.gzindex++):0)}while(0!==r);i.gzhead.hcrc&&i.pending>n&&(t.adler=c(t.adler,i.pending_buf,i.pending-n,n)),0===r&&(i.gzindex=0,i.status=91)}else i.status=91;if(91===i.status)if(i.gzhead.comment){n=i.pending;do{if(i.pending===i.pending_buf_size&&(i.gzhead.hcrc&&i.pending>n&&(t.adler=c(t.adler,i.pending_buf,i.pending-n,n)),I(t),n=i.pending,i.pending===i.pending_buf_size)){r=1;break}T(i,r=i.gzindex<i.gzhead.comment.length?255&i.gzhead.comment.charCodeAt(i.gzindex++):0)}while(0!==r);i.gzhead.hcrc&&i.pending>n&&(t.adler=c(t.adler,i.pending_buf,i.pending-n,n)),0===r&&(i.status=103)}else i.status=103;if(103===i.status&&(i.gzhead.hcrc?(i.pending+2>i.pending_buf_size&&I(t),i.pending+2<=i.pending_buf_size&&(T(i,255&t.adler),T(i,t.adler>>8&255),t.adler=0,i.status=E)):i.status=E),0!==i.pending){if(I(t),0===t.avail_out)return i.last_flush=-1,b}else if(0===t.avail_in&&O(e)<=O(a)&&e!==f)return N(t,-5);if(666===i.status&&0!==t.avail_in)return N(t,-5);if(0!==t.avail_in||0!==i.lookahead||e!==d&&666!==i.status){var o=2===i.strategy?function(t,e){for(var a;;){if(0===t.lookahead&&(H(t),0===t.lookahead)){if(e===d)return A;break}if(t.match_length=0,a=h._tr_tally(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++,a&&(U(t,!1),0===t.strm.avail_out))return A}return t.insert=0,e===f?(U(t,!0),0===t.strm.avail_out?R:C):t.last_lit&&(U(t,!1),0===t.strm.avail_out)?A:Z}(i,e):3===i.strategy?function(t,e){for(var a,i,n,r,s=t.window;;){if(t.lookahead<=z){if(H(t),t.lookahead<=z&&e===d)return A;if(0===t.lookahead)break}if(t.match_length=0,t.lookahead>=x&&0<t.strstart&&(i=s[n=t.strstart-1])===s[++n]&&i===s[++n]&&i===s[++n]){r=t.strstart+z;do{}while(i===s[++n]&&i===s[++n]&&i===s[++n]&&i===s[++n]&&i===s[++n]&&i===s[++n]&&i===s[++n]&&i===s[++n]&&n<r);t.match_length=z-(r-n),t.match_length>t.lookahead&&(t.match_length=t.lookahead)}if(t.match_length>=x?(a=h._tr_tally(t,1,t.match_length-x),t.lookahead-=t.match_length,t.strstart+=t.match_length,t.match_length=0):(a=h._tr_tally(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++),a&&(U(t,!1),0===t.strm.avail_out))return A}return t.insert=0,e===f?(U(t,!0),0===t.strm.avail_out?R:C):t.last_lit&&(U(t,!1),0===t.strm.avail_out)?A:Z}(i,e):l[i.level].func(i,e);if(o!==R&&o!==C||(i.status=666),o===A||o===R)return 0===t.avail_out&&(i.last_flush=-1),b;if(o===Z&&(1===e?h._tr_align(i):5!==e&&(h._tr_stored_block(i,0,0,!1),3===e&&(D(i.head),0===i.lookahead&&(i.strstart=0,i.block_start=0,i.insert=0))),I(t),0===t.avail_out))return i.last_flush=-1,b}return e!==f?b:i.wrap<=0?1:(2===i.wrap?(T(i,255&t.adler),T(i,t.adler>>8&255),T(i,t.adler>>16&255),T(i,t.adler>>24&255),T(i,255&t.total_in),T(i,t.total_in>>8&255),T(i,t.total_in>>16&255),T(i,t.total_in>>24&255)):(F(i,t.adler>>>16),F(i,65535&t.adler)),I(t),0<i.wrap&&(i.wrap=-i.wrap),0!==i.pending?b:1)},a.deflateEnd=function(t){var e;return t&&t.state?(e=t.state.status)!==S&&69!==e&&73!==e&&91!==e&&103!==e&&e!==E&&666!==e?N(t,g):(t.state=null,e===E?N(t,-3):b):g},a.deflateSetDictionary=function(t,e){var a,i,n,r,s,o,l,h,d=e.length;if(!t||!t.state)return g;if(2===(r=(a=t.state).wrap)||1===r&&a.status!==S||a.lookahead)return g;for(1===r&&(t.adler=u(t.adler,e,d,0)),a.wrap=0,d>=a.w_size&&(0===r&&(D(a.head),a.strstart=0,a.block_start=0,a.insert=0),h=new _.Buf8(a.w_size),_.arraySet(h,e,d-a.w_size,a.w_size,0),e=h,d=a.w_size),s=t.avail_in,o=t.next_in,l=t.input,t.avail_in=d,t.next_in=0,t.input=e,H(a);a.lookahead>=x;){for(i=a.strstart,n=a.lookahead-(x-1);a.ins_h=(a.ins_h<<a.hash_shift^a.window[i+x-1])&a.hash_mask,a.prev[i&a.w_mask]=a.head[a.ins_h],a.head[a.ins_h]=i,i++,--n;);a.strstart=i,a.lookahead=x-1,H(a)}return a.strstart+=a.lookahead,a.block_start=a.strstart,a.insert=a.lookahead,a.lookahead=0,a.match_length=a.prev_length=x-1,a.match_available=0,t.next_in=o,t.input=l,t.avail_in=s,a.wrap=r,b},a.deflateInfo="pako deflate (from Nodeca project)"},{"../utils/common":3,"./adler32":5,"./crc32":7,"./messages":13,"./trees":14}],9:[function(t,e,a){"use strict";e.exports=function(){this.text=0,this.time=0,this.xflags=0,this.os=0,this.extra=null,this.extra_len=0,this.name="",this.comment="",this.hcrc=0,this.done=!1}},{}],10:[function(t,e,a){"use strict";e.exports=function(t,e){var a,i,n,r,s,o,l,h,d,f,_,u,c,b,g,m,w,p,v,k,y,x,z,B,S;a=t.state,i=t.next_in,B=t.input,n=i+(t.avail_in-5),r=t.next_out,S=t.output,s=r-(e-t.avail_out),o=r+(t.avail_out-257),l=a.dmax,h=a.wsize,d=a.whave,f=a.wnext,_=a.window,u=a.hold,c=a.bits,b=a.lencode,g=a.distcode,m=(1<<a.lenbits)-1,w=(1<<a.distbits)-1;t:do{c<15&&(u+=B[i++]<<c,c+=8,u+=B[i++]<<c,c+=8),p=b[u&m];e:for(;;){if(u>>>=v=p>>>24,c-=v,0===(v=p>>>16&255))S[r++]=65535&p;else{if(!(16&v)){if(0==(64&v)){p=b[(65535&p)+(u&(1<<v)-1)];continue e}if(32&v){a.mode=12;break t}t.msg="invalid literal/length code",a.mode=30;break t}k=65535&p,(v&=15)&&(c<v&&(u+=B[i++]<<c,c+=8),k+=u&(1<<v)-1,u>>>=v,c-=v),c<15&&(u+=B[i++]<<c,c+=8,u+=B[i++]<<c,c+=8),p=g[u&w];a:for(;;){if(u>>>=v=p>>>24,c-=v,!(16&(v=p>>>16&255))){if(0==(64&v)){p=g[(65535&p)+(u&(1<<v)-1)];continue a}t.msg="invalid distance code",a.mode=30;break t}if(y=65535&p,c<(v&=15)&&(u+=B[i++]<<c,(c+=8)<v&&(u+=B[i++]<<c,c+=8)),l<(y+=u&(1<<v)-1)){t.msg="invalid distance too far back",a.mode=30;break t}if(u>>>=v,c-=v,(v=r-s)<y){if(d<(v=y-v)&&a.sane){t.msg="invalid distance too far back",a.mode=30;break t}if(z=_,(x=0)===f){if(x+=h-v,v<k){for(k-=v;S[r++]=_[x++],--v;);x=r-y,z=S}}else if(f<v){if(x+=h+f-v,(v-=f)<k){for(k-=v;S[r++]=_[x++],--v;);if(x=0,f<k){for(k-=v=f;S[r++]=_[x++],--v;);x=r-y,z=S}}}else if(x+=f-v,v<k){for(k-=v;S[r++]=_[x++],--v;);x=r-y,z=S}for(;2<k;)S[r++]=z[x++],S[r++]=z[x++],S[r++]=z[x++],k-=3;k&&(S[r++]=z[x++],1<k&&(S[r++]=z[x++]))}else{for(x=r-y;S[r++]=S[x++],S[r++]=S[x++],S[r++]=S[x++],2<(k-=3););k&&(S[r++]=S[x++],1<k&&(S[r++]=S[x++]))}break}}break}}while(i<n&&r<o);i-=k=c>>3,u&=(1<<(c-=k<<3))-1,t.next_in=i,t.next_out=r,t.avail_in=i<n?n-i+5:5-(i-n),t.avail_out=r<o?o-r+257:257-(r-o),a.hold=u,a.bits=c}},{}],11:[function(t,e,a){"use strict";var Z=t("../utils/common"),R=t("./adler32"),C=t("./crc32"),N=t("./inffast"),O=t("./inftrees"),D=1,I=2,U=0,T=-2,F=1,i=852,n=592;function L(t){return(t>>>24&255)+(t>>>8&65280)+((65280&t)<<8)+((255&t)<<24)}function r(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new Z.Buf16(320),this.work=new Z.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function s(t){var e;return t&&t.state?(e=t.state,t.total_in=t.total_out=e.total=0,t.msg="",e.wrap&&(t.adler=1&e.wrap),e.mode=F,e.last=0,e.havedict=0,e.dmax=32768,e.head=null,e.hold=0,e.bits=0,e.lencode=e.lendyn=new Z.Buf32(i),e.distcode=e.distdyn=new Z.Buf32(n),e.sane=1,e.back=-1,U):T}function o(t){var e;return t&&t.state?((e=t.state).wsize=0,e.whave=0,e.wnext=0,s(t)):T}function l(t,e){var a,i;return t&&t.state?(i=t.state,e<0?(a=0,e=-e):(a=1+(e>>4),e<48&&(e&=15)),e&&(e<8||15<e)?T:(null!==i.window&&i.wbits!==e&&(i.window=null),i.wrap=a,i.wbits=e,o(t))):T}function h(t,e){var a,i;return t?(i=new r,(t.state=i).window=null,(a=l(t,e))!==U&&(t.state=null),a):T}var d,f,_=!0;function H(t){if(_){var e;for(d=new Z.Buf32(512),f=new Z.Buf32(32),e=0;e<144;)t.lens[e++]=8;for(;e<256;)t.lens[e++]=9;for(;e<280;)t.lens[e++]=7;for(;e<288;)t.lens[e++]=8;for(O(D,t.lens,0,288,d,0,t.work,{bits:9}),e=0;e<32;)t.lens[e++]=5;O(I,t.lens,0,32,f,0,t.work,{bits:5}),_=!1}t.lencode=d,t.lenbits=9,t.distcode=f,t.distbits=5}function j(t,e,a,i){var n,r=t.state;return null===r.window&&(r.wsize=1<<r.wbits,r.wnext=0,r.whave=0,r.window=new Z.Buf8(r.wsize)),i>=r.wsize?(Z.arraySet(r.window,e,a-r.wsize,r.wsize,0),r.wnext=0,r.whave=r.wsize):(i<(n=r.wsize-r.wnext)&&(n=i),Z.arraySet(r.window,e,a-i,n,r.wnext),(i-=n)?(Z.arraySet(r.window,e,a-i,i,0),r.wnext=i,r.whave=r.wsize):(r.wnext+=n,r.wnext===r.wsize&&(r.wnext=0),r.whave<r.wsize&&(r.whave+=n))),0}a.inflateReset=o,a.inflateReset2=l,a.inflateResetKeep=s,a.inflateInit=function(t){return h(t,15)},a.inflateInit2=h,a.inflate=function(t,e){var a,i,n,r,s,o,l,h,d,f,_,u,c,b,g,m,w,p,v,k,y,x,z,B,S=0,E=new Z.Buf8(4),A=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15];if(!t||!t.state||!t.output||!t.input&&0!==t.avail_in)return T;12===(a=t.state).mode&&(a.mode=13),s=t.next_out,n=t.output,l=t.avail_out,r=t.next_in,i=t.input,o=t.avail_in,h=a.hold,d=a.bits,f=o,_=l,x=U;t:for(;;)switch(a.mode){case F:if(0===a.wrap){a.mode=13;break}for(;d<16;){if(0===o)break t;o--,h+=i[r++]<<d,d+=8}if(2&a.wrap&&35615===h){E[a.check=0]=255&h,E[1]=h>>>8&255,a.check=C(a.check,E,2,0),d=h=0,a.mode=2;break}if(a.flags=0,a.head&&(a.head.done=!1),!(1&a.wrap)||(((255&h)<<8)+(h>>8))%31){t.msg="incorrect header check",a.mode=30;break}if(8!=(15&h)){t.msg="unknown compression method",a.mode=30;break}if(d-=4,y=8+(15&(h>>>=4)),0===a.wbits)a.wbits=y;else if(y>a.wbits){t.msg="invalid window size",a.mode=30;break}a.dmax=1<<y,t.adler=a.check=1,a.mode=512&h?10:12,d=h=0;break;case 2:for(;d<16;){if(0===o)break t;o--,h+=i[r++]<<d,d+=8}if(a.flags=h,8!=(255&a.flags)){t.msg="unknown compression method",a.mode=30;break}if(57344&a.flags){t.msg="unknown header flags set",a.mode=30;break}a.head&&(a.head.text=h>>8&1),512&a.flags&&(E[0]=255&h,E[1]=h>>>8&255,a.check=C(a.check,E,2,0)),d=h=0,a.mode=3;case 3:for(;d<32;){if(0===o)break t;o--,h+=i[r++]<<d,d+=8}a.head&&(a.head.time=h),512&a.flags&&(E[0]=255&h,E[1]=h>>>8&255,E[2]=h>>>16&255,E[3]=h>>>24&255,a.check=C(a.check,E,4,0)),d=h=0,a.mode=4;case 4:for(;d<16;){if(0===o)break t;o--,h+=i[r++]<<d,d+=8}a.head&&(a.head.xflags=255&h,a.head.os=h>>8),512&a.flags&&(E[0]=255&h,E[1]=h>>>8&255,a.check=C(a.check,E,2,0)),d=h=0,a.mode=5;case 5:if(1024&a.flags){for(;d<16;){if(0===o)break t;o--,h+=i[r++]<<d,d+=8}a.length=h,a.head&&(a.head.extra_len=h),512&a.flags&&(E[0]=255&h,E[1]=h>>>8&255,a.check=C(a.check,E,2,0)),d=h=0}else a.head&&(a.head.extra=null);a.mode=6;case 6:if(1024&a.flags&&(o<(u=a.length)&&(u=o),u&&(a.head&&(y=a.head.extra_len-a.length,a.head.extra||(a.head.extra=new Array(a.head.extra_len)),Z.arraySet(a.head.extra,i,r,u,y)),512&a.flags&&(a.check=C(a.check,i,u,r)),o-=u,r+=u,a.length-=u),a.length))break t;a.length=0,a.mode=7;case 7:if(2048&a.flags){if(0===o)break t;for(u=0;y=i[r+u++],a.head&&y&&a.length<65536&&(a.head.name+=String.fromCharCode(y)),y&&u<o;);if(512&a.flags&&(a.check=C(a.check,i,u,r)),o-=u,r+=u,y)break t}else a.head&&(a.head.name=null);a.length=0,a.mode=8;case 8:if(4096&a.flags){if(0===o)break t;for(u=0;y=i[r+u++],a.head&&y&&a.length<65536&&(a.head.comment+=String.fromCharCode(y)),y&&u<o;);if(512&a.flags&&(a.check=C(a.check,i,u,r)),o-=u,r+=u,y)break t}else a.head&&(a.head.comment=null);a.mode=9;case 9:if(512&a.flags){for(;d<16;){if(0===o)break t;o--,h+=i[r++]<<d,d+=8}if(h!==(65535&a.check)){t.msg="header crc mismatch",a.mode=30;break}d=h=0}a.head&&(a.head.hcrc=a.flags>>9&1,a.head.done=!0),t.adler=a.check=0,a.mode=12;break;case 10:for(;d<32;){if(0===o)break t;o--,h+=i[r++]<<d,d+=8}t.adler=a.check=L(h),d=h=0,a.mode=11;case 11:if(0===a.havedict)return t.next_out=s,t.avail_out=l,t.next_in=r,t.avail_in=o,a.hold=h,a.bits=d,2;t.adler=a.check=1,a.mode=12;case 12:if(5===e||6===e)break t;case 13:if(a.last){h>>>=7&d,d-=7&d,a.mode=27;break}for(;d<3;){if(0===o)break t;o--,h+=i[r++]<<d,d+=8}switch(a.last=1&h,d-=1,3&(h>>>=1)){case 0:a.mode=14;break;case 1:if(H(a),a.mode=20,6!==e)break;h>>>=2,d-=2;break t;case 2:a.mode=17;break;case 3:t.msg="invalid block type",a.mode=30}h>>>=2,d-=2;break;case 14:for(h>>>=7&d,d-=7&d;d<32;){if(0===o)break t;o--,h+=i[r++]<<d,d+=8}if((65535&h)!=(h>>>16^65535)){t.msg="invalid stored block lengths",a.mode=30;break}if(a.length=65535&h,d=h=0,a.mode=15,6===e)break t;case 15:a.mode=16;case 16:if(u=a.length){if(o<u&&(u=o),l<u&&(u=l),0===u)break t;Z.arraySet(n,i,r,u,s),o-=u,r+=u,l-=u,s+=u,a.length-=u;break}a.mode=12;break;case 17:for(;d<14;){if(0===o)break t;o--,h+=i[r++]<<d,d+=8}if(a.nlen=257+(31&h),h>>>=5,d-=5,a.ndist=1+(31&h),h>>>=5,d-=5,a.ncode=4+(15&h),h>>>=4,d-=4,286<a.nlen||30<a.ndist){t.msg="too many length or distance symbols",a.mode=30;break}a.have=0,a.mode=18;case 18:for(;a.have<a.ncode;){for(;d<3;){if(0===o)break t;o--,h+=i[r++]<<d,d+=8}a.lens[A[a.have++]]=7&h,h>>>=3,d-=3}for(;a.have<19;)a.lens[A[a.have++]]=0;if(a.lencode=a.lendyn,a.lenbits=7,z={bits:a.lenbits},x=O(0,a.lens,0,19,a.lencode,0,a.work,z),a.lenbits=z.bits,x){t.msg="invalid code lengths set",a.mode=30;break}a.have=0,a.mode=19;case 19:for(;a.have<a.nlen+a.ndist;){for(;m=(S=a.lencode[h&(1<<a.lenbits)-1])>>>16&255,w=65535&S,!((g=S>>>24)<=d);){if(0===o)break t;o--,h+=i[r++]<<d,d+=8}if(w<16)h>>>=g,d-=g,a.lens[a.have++]=w;else{if(16===w){for(B=g+2;d<B;){if(0===o)break t;o--,h+=i[r++]<<d,d+=8}if(h>>>=g,d-=g,0===a.have){t.msg="invalid bit length repeat",a.mode=30;break}y=a.lens[a.have-1],u=3+(3&h),h>>>=2,d-=2}else if(17===w){for(B=g+3;d<B;){if(0===o)break t;o--,h+=i[r++]<<d,d+=8}d-=g,y=0,u=3+(7&(h>>>=g)),h>>>=3,d-=3}else{for(B=g+7;d<B;){if(0===o)break t;o--,h+=i[r++]<<d,d+=8}d-=g,y=0,u=11+(127&(h>>>=g)),h>>>=7,d-=7}if(a.have+u>a.nlen+a.ndist){t.msg="invalid bit length repeat",a.mode=30;break}for(;u--;)a.lens[a.have++]=y}}if(30===a.mode)break;if(0===a.lens[256]){t.msg="invalid code -- missing end-of-block",a.mode=30;break}if(a.lenbits=9,z={bits:a.lenbits},x=O(D,a.lens,0,a.nlen,a.lencode,0,a.work,z),a.lenbits=z.bits,x){t.msg="invalid literal/lengths set",a.mode=30;break}if(a.distbits=6,a.distcode=a.distdyn,z={bits:a.distbits},x=O(I,a.lens,a.nlen,a.ndist,a.distcode,0,a.work,z),a.distbits=z.bits,x){t.msg="invalid distances set",a.mode=30;break}if(a.mode=20,6===e)break t;case 20:a.mode=21;case 21:if(6<=o&&258<=l){t.next_out=s,t.avail_out=l,t.next_in=r,t.avail_in=o,a.hold=h,a.bits=d,N(t,_),s=t.next_out,n=t.output,l=t.avail_out,r=t.next_in,i=t.input,o=t.avail_in,h=a.hold,d=a.bits,12===a.mode&&(a.back=-1);break}for(a.back=0;m=(S=a.lencode[h&(1<<a.lenbits)-1])>>>16&255,w=65535&S,!((g=S>>>24)<=d);){if(0===o)break t;o--,h+=i[r++]<<d,d+=8}if(m&&0==(240&m)){for(p=g,v=m,k=w;m=(S=a.lencode[k+((h&(1<<p+v)-1)>>p)])>>>16&255,w=65535&S,!(p+(g=S>>>24)<=d);){if(0===o)break t;o--,h+=i[r++]<<d,d+=8}h>>>=p,d-=p,a.back+=p}if(h>>>=g,d-=g,a.back+=g,a.length=w,0===m){a.mode=26;break}if(32&m){a.back=-1,a.mode=12;break}if(64&m){t.msg="invalid literal/length code",a.mode=30;break}a.extra=15&m,a.mode=22;case 22:if(a.extra){for(B=a.extra;d<B;){if(0===o)break t;o--,h+=i[r++]<<d,d+=8}a.length+=h&(1<<a.extra)-1,h>>>=a.extra,d-=a.extra,a.back+=a.extra}a.was=a.length,a.mode=23;case 23:for(;m=(S=a.distcode[h&(1<<a.distbits)-1])>>>16&255,w=65535&S,!((g=S>>>24)<=d);){if(0===o)break t;o--,h+=i[r++]<<d,d+=8}if(0==(240&m)){for(p=g,v=m,k=w;m=(S=a.distcode[k+((h&(1<<p+v)-1)>>p)])>>>16&255,w=65535&S,!(p+(g=S>>>24)<=d);){if(0===o)break t;o--,h+=i[r++]<<d,d+=8}h>>>=p,d-=p,a.back+=p}if(h>>>=g,d-=g,a.back+=g,64&m){t.msg="invalid distance code",a.mode=30;break}a.offset=w,a.extra=15&m,a.mode=24;case 24:if(a.extra){for(B=a.extra;d<B;){if(0===o)break t;o--,h+=i[r++]<<d,d+=8}a.offset+=h&(1<<a.extra)-1,h>>>=a.extra,d-=a.extra,a.back+=a.extra}if(a.offset>a.dmax){t.msg="invalid distance too far back",a.mode=30;break}a.mode=25;case 25:if(0===l)break t;if(u=_-l,a.offset>u){if((u=a.offset-u)>a.whave&&a.sane){t.msg="invalid distance too far back",a.mode=30;break}u>a.wnext?(u-=a.wnext,c=a.wsize-u):c=a.wnext-u,u>a.length&&(u=a.length),b=a.window}else b=n,c=s-a.offset,u=a.length;for(l<u&&(u=l),l-=u,a.length-=u;n[s++]=b[c++],--u;);0===a.length&&(a.mode=21);break;case 26:if(0===l)break t;n[s++]=a.length,l--,a.mode=21;break;case 27:if(a.wrap){for(;d<32;){if(0===o)break t;o--,h|=i[r++]<<d,d+=8}if(_-=l,t.total_out+=_,a.total+=_,_&&(t.adler=a.check=a.flags?C(a.check,n,_,s-_):R(a.check,n,_,s-_)),_=l,(a.flags?h:L(h))!==a.check){t.msg="incorrect data check",a.mode=30;break}d=h=0}a.mode=28;case 28:if(a.wrap&&a.flags){for(;d<32;){if(0===o)break t;o--,h+=i[r++]<<d,d+=8}if(h!==(4294967295&a.total)){t.msg="incorrect length check",a.mode=30;break}d=h=0}a.mode=29;case 29:x=1;break t;case 30:x=-3;break t;case 31:return-4;case 32:default:return T}return t.next_out=s,t.avail_out=l,t.next_in=r,t.avail_in=o,a.hold=h,a.bits=d,(a.wsize||_!==t.avail_out&&a.mode<30&&(a.mode<27||4!==e))&&j(t,t.output,t.next_out,_-t.avail_out)?(a.mode=31,-4):(f-=t.avail_in,_-=t.avail_out,t.total_in+=f,t.total_out+=_,a.total+=_,a.wrap&&_&&(t.adler=a.check=a.flags?C(a.check,n,_,t.next_out-_):R(a.check,n,_,t.next_out-_)),t.data_type=a.bits+(a.last?64:0)+(12===a.mode?128:0)+(20===a.mode||15===a.mode?256:0),(0===f&&0===_||4===e)&&x===U&&(x=-5),x)},a.inflateEnd=function(t){if(!t||!t.state)return T;var e=t.state;return e.window&&(e.window=null),t.state=null,U},a.inflateGetHeader=function(t,e){var a;return t&&t.state?0==(2&(a=t.state).wrap)?T:((a.head=e).done=!1,U):T},a.inflateSetDictionary=function(t,e){var a,i=e.length;return t&&t.state?0!==(a=t.state).wrap&&11!==a.mode?T:11===a.mode&&R(1,e,i,0)!==a.check?-3:j(t,e,i,i)?(a.mode=31,-4):(a.havedict=1,U):T},a.inflateInfo="pako inflate (from Nodeca project)"},{"../utils/common":3,"./adler32":5,"./crc32":7,"./inffast":10,"./inftrees":12}],12:[function(t,e,a){"use strict";var D=t("../utils/common"),I=[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,0,0],U=[16,16,16,16,16,16,16,16,17,17,17,17,18,18,18,18,19,19,19,19,20,20,20,20,21,21,21,21,16,72,78],T=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,0,0],F=[16,16,16,16,17,17,18,18,19,19,20,20,21,21,22,22,23,23,24,24,25,25,26,26,27,27,28,28,29,29,64,64];e.exports=function(t,e,a,i,n,r,s,o){var l,h,d,f,_,u,c,b,g,m=o.bits,w=0,p=0,v=0,k=0,y=0,x=0,z=0,B=0,S=0,E=0,A=null,Z=0,R=new D.Buf16(16),C=new D.Buf16(16),N=null,O=0;for(w=0;w<=15;w++)R[w]=0;for(p=0;p<i;p++)R[e[a+p]]++;for(y=m,k=15;1<=k&&0===R[k];k--);if(k<y&&(y=k),0===k)return n[r++]=20971520,n[r++]=20971520,o.bits=1,0;for(v=1;v<k&&0===R[v];v++);for(y<v&&(y=v),w=B=1;w<=15;w++)if(B<<=1,(B-=R[w])<0)return-1;if(0<B&&(0===t||1!==k))return-1;for(C[1]=0,w=1;w<15;w++)C[w+1]=C[w]+R[w];for(p=0;p<i;p++)0!==e[a+p]&&(s[C[e[a+p]]++]=p);if(0===t?(A=N=s,u=19):1===t?(A=I,Z-=257,N=U,O-=257,u=256):(A=T,N=F,u=-1),w=v,_=r,z=p=E=0,d=-1,f=(S=1<<(x=y))-1,1===t&&852<S||2===t&&592<S)return 1;for(;;){for(c=w-z,s[p]<u?(b=0,g=s[p]):s[p]>u?(b=N[O+s[p]],g=A[Z+s[p]]):(b=96,g=0),l=1<<w-z,v=h=1<<x;n[_+(E>>z)+(h-=l)]=c<<24|b<<16|g|0,0!==h;);for(l=1<<w-1;E&l;)l>>=1;if(0!==l?(E&=l-1,E+=l):E=0,p++,0==--R[w]){if(w===k)break;w=e[a+s[p]]}if(y<w&&(E&f)!==d){for(0===z&&(z=y),_+=v,B=1<<(x=w-z);x+z<k&&!((B-=R[x+z])<=0);)x++,B<<=1;if(S+=1<<x,1===t&&852<S||2===t&&592<S)return 1;n[d=E&f]=y<<24|x<<16|_-r|0}}return 0!==E&&(n[_+E]=w-z<<24|64<<16|0),o.bits=y,0}},{"../utils/common":3}],13:[function(t,e,a){"use strict";e.exports={2:"need dictionary",1:"stream end",0:"","-1":"file error","-2":"stream error","-3":"data error","-4":"insufficient memory","-5":"buffer error","-6":"incompatible version"}},{}],14:[function(t,e,a){"use strict";var l=t("../utils/common"),o=0,h=1;function i(t){for(var e=t.length;0<=--e;)t[e]=0}var d=0,s=29,f=256,_=f+1+s,u=30,c=19,g=2*_+1,m=15,n=16,b=7,w=256,p=16,v=17,k=18,y=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0],x=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],z=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7],B=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],S=new Array(2*(_+2));i(S);var E=new Array(2*u);i(E);var A=new Array(512);i(A);var Z=new Array(256);i(Z);var R=new Array(s);i(R);var C,N,O,D=new Array(u);function I(t,e,a,i,n){this.static_tree=t,this.extra_bits=e,this.extra_base=a,this.elems=i,this.max_length=n,this.has_stree=t&&t.length}function r(t,e){this.dyn_tree=t,this.max_code=0,this.stat_desc=e}function U(t){return t<256?A[t]:A[256+(t>>>7)]}function T(t,e){t.pending_buf[t.pending++]=255&e,t.pending_buf[t.pending++]=e>>>8&255}function F(t,e,a){t.bi_valid>n-a?(t.bi_buf|=e<<t.bi_valid&65535,T(t,t.bi_buf),t.bi_buf=e>>n-t.bi_valid,t.bi_valid+=a-n):(t.bi_buf|=e<<t.bi_valid&65535,t.bi_valid+=a)}function L(t,e,a){F(t,a[2*e],a[2*e+1])}function H(t,e){for(var a=0;a|=1&t,t>>>=1,a<<=1,0<--e;);return a>>>1}function j(t,e,a){var i,n,r=new Array(m+1),s=0;for(i=1;i<=m;i++)r[i]=s=s+a[i-1]<<1;for(n=0;n<=e;n++){var o=t[2*n+1];0!==o&&(t[2*n]=H(r[o]++,o))}}function K(t){var e;for(e=0;e<_;e++)t.dyn_ltree[2*e]=0;for(e=0;e<u;e++)t.dyn_dtree[2*e]=0;for(e=0;e<c;e++)t.bl_tree[2*e]=0;t.dyn_ltree[2*w]=1,t.opt_len=t.static_len=0,t.last_lit=t.matches=0}function M(t){8<t.bi_valid?T(t,t.bi_buf):0<t.bi_valid&&(t.pending_buf[t.pending++]=t.bi_buf),t.bi_buf=0,t.bi_valid=0}function P(t,e,a,i){var n=2*e,r=2*a;return t[n]<t[r]||t[n]===t[r]&&i[e]<=i[a]}function Y(t,e,a){for(var i=t.heap[a],n=a<<1;n<=t.heap_len&&(n<t.heap_len&&P(e,t.heap[n+1],t.heap[n],t.depth)&&n++,!P(e,i,t.heap[n],t.depth));)t.heap[a]=t.heap[n],a=n,n<<=1;t.heap[a]=i}function q(t,e,a){var i,n,r,s,o=0;if(0!==t.last_lit)for(;i=t.pending_buf[t.d_buf+2*o]<<8|t.pending_buf[t.d_buf+2*o+1],n=t.pending_buf[t.l_buf+o],o++,0===i?L(t,n,e):(L(t,(r=Z[n])+f+1,e),0!==(s=y[r])&&F(t,n-=R[r],s),L(t,r=U(--i),a),0!==(s=x[r])&&F(t,i-=D[r],s)),o<t.last_lit;);L(t,w,e)}function G(t,e){var a,i,n,r=e.dyn_tree,s=e.stat_desc.static_tree,o=e.stat_desc.has_stree,l=e.stat_desc.elems,h=-1;for(t.heap_len=0,t.heap_max=g,a=0;a<l;a++)0!==r[2*a]?(t.heap[++t.heap_len]=h=a,t.depth[a]=0):r[2*a+1]=0;for(;t.heap_len<2;)r[2*(n=t.heap[++t.heap_len]=h<2?++h:0)]=1,t.depth[n]=0,t.opt_len--,o&&(t.static_len-=s[2*n+1]);for(e.max_code=h,a=t.heap_len>>1;1<=a;a--)Y(t,r,a);for(n=l;a=t.heap[1],t.heap[1]=t.heap[t.heap_len--],Y(t,r,1),i=t.heap[1],t.heap[--t.heap_max]=a,t.heap[--t.heap_max]=i,r[2*n]=r[2*a]+r[2*i],t.depth[n]=(t.depth[a]>=t.depth[i]?t.depth[a]:t.depth[i])+1,r[2*a+1]=r[2*i+1]=n,t.heap[1]=n++,Y(t,r,1),2<=t.heap_len;);t.heap[--t.heap_max]=t.heap[1],function(t,e){var a,i,n,r,s,o,l=e.dyn_tree,h=e.max_code,d=e.stat_desc.static_tree,f=e.stat_desc.has_stree,_=e.stat_desc.extra_bits,u=e.stat_desc.extra_base,c=e.stat_desc.max_length,b=0;for(r=0;r<=m;r++)t.bl_count[r]=0;for(l[2*t.heap[t.heap_max]+1]=0,a=t.heap_max+1;a<g;a++)c<(r=l[2*l[2*(i=t.heap[a])+1]+1]+1)&&(r=c,b++),l[2*i+1]=r,h<i||(t.bl_count[r]++,s=0,u<=i&&(s=_[i-u]),o=l[2*i],t.opt_len+=o*(r+s),f&&(t.static_len+=o*(d[2*i+1]+s)));if(0!==b){do{for(r=c-1;0===t.bl_count[r];)r--;t.bl_count[r]--,t.bl_count[r+1]+=2,t.bl_count[c]--,b-=2}while(0<b);for(r=c;0!==r;r--)for(i=t.bl_count[r];0!==i;)h<(n=t.heap[--a])||(l[2*n+1]!==r&&(t.opt_len+=(r-l[2*n+1])*l[2*n],l[2*n+1]=r),i--)}}(t,e),j(r,h,t.bl_count)}function X(t,e,a){var i,n,r=-1,s=e[1],o=0,l=7,h=4;for(0===s&&(l=138,h=3),e[2*(a+1)+1]=65535,i=0;i<=a;i++)n=s,s=e[2*(i+1)+1],++o<l&&n===s||(o<h?t.bl_tree[2*n]+=o:0!==n?(n!==r&&t.bl_tree[2*n]++,t.bl_tree[2*p]++):o<=10?t.bl_tree[2*v]++:t.bl_tree[2*k]++,r=n,(o=0)===s?(l=138,h=3):n===s?(l=6,h=3):(l=7,h=4))}function W(t,e,a){var i,n,r=-1,s=e[1],o=0,l=7,h=4;for(0===s&&(l=138,h=3),i=0;i<=a;i++)if(n=s,s=e[2*(i+1)+1],!(++o<l&&n===s)){if(o<h)for(;L(t,n,t.bl_tree),0!=--o;);else 0!==n?(n!==r&&(L(t,n,t.bl_tree),o--),L(t,p,t.bl_tree),F(t,o-3,2)):o<=10?(L(t,v,t.bl_tree),F(t,o-3,3)):(L(t,k,t.bl_tree),F(t,o-11,7));r=n,(o=0)===s?(l=138,h=3):n===s?(l=6,h=3):(l=7,h=4)}}i(D);var J=!1;function Q(t,e,a,i){var n,r,s,o;F(t,(d<<1)+(i?1:0),3),r=e,s=a,o=!0,M(n=t),o&&(T(n,s),T(n,~s)),l.arraySet(n.pending_buf,n.window,r,s,n.pending),n.pending+=s}a._tr_init=function(t){J||(function(){var t,e,a,i,n,r=new Array(m+1);for(i=a=0;i<s-1;i++)for(R[i]=a,t=0;t<1<<y[i];t++)Z[a++]=i;for(Z[a-1]=i,i=n=0;i<16;i++)for(D[i]=n,t=0;t<1<<x[i];t++)A[n++]=i;for(n>>=7;i<u;i++)for(D[i]=n<<7,t=0;t<1<<x[i]-7;t++)A[256+n++]=i;for(e=0;e<=m;e++)r[e]=0;for(t=0;t<=143;)S[2*t+1]=8,t++,r[8]++;for(;t<=255;)S[2*t+1]=9,t++,r[9]++;for(;t<=279;)S[2*t+1]=7,t++,r[7]++;for(;t<=287;)S[2*t+1]=8,t++,r[8]++;for(j(S,_+1,r),t=0;t<u;t++)E[2*t+1]=5,E[2*t]=H(t,5);C=new I(S,y,f+1,_,m),N=new I(E,x,0,u,m),O=new I(new Array(0),z,0,c,b)}(),J=!0),t.l_desc=new r(t.dyn_ltree,C),t.d_desc=new r(t.dyn_dtree,N),t.bl_desc=new r(t.bl_tree,O),t.bi_buf=0,t.bi_valid=0,K(t)},a._tr_stored_block=Q,a._tr_flush_block=function(t,e,a,i){var n,r,s=0;0<t.level?(2===t.strm.data_type&&(t.strm.data_type=function(t){var e,a=4093624447;for(e=0;e<=31;e++,a>>>=1)if(1&a&&0!==t.dyn_ltree[2*e])return o;if(0!==t.dyn_ltree[18]||0!==t.dyn_ltree[20]||0!==t.dyn_ltree[26])return h;for(e=32;e<f;e++)if(0!==t.dyn_ltree[2*e])return h;return o}(t)),G(t,t.l_desc),G(t,t.d_desc),s=function(t){var e;for(X(t,t.dyn_ltree,t.l_desc.max_code),X(t,t.dyn_dtree,t.d_desc.max_code),G(t,t.bl_desc),e=c-1;3<=e&&0===t.bl_tree[2*B[e]+1];e--);return t.opt_len+=3*(e+1)+5+5+4,e}(t),n=t.opt_len+3+7>>>3,(r=t.static_len+3+7>>>3)<=n&&(n=r)):n=r=a+5,a+4<=n&&-1!==e?Q(t,e,a,i):4===t.strategy||r===n?(F(t,2+(i?1:0),3),q(t,S,E)):(F(t,4+(i?1:0),3),function(t,e,a,i){var n;for(F(t,e-257,5),F(t,a-1,5),F(t,i-4,4),n=0;n<i;n++)F(t,t.bl_tree[2*B[n]+1],3);W(t,t.dyn_ltree,e-1),W(t,t.dyn_dtree,a-1)}(t,t.l_desc.max_code+1,t.d_desc.max_code+1,s+1),q(t,t.dyn_ltree,t.dyn_dtree)),K(t),i&&M(t)},a._tr_tally=function(t,e,a){return t.pending_buf[t.d_buf+2*t.last_lit]=e>>>8&255,t.pending_buf[t.d_buf+2*t.last_lit+1]=255&e,t.pending_buf[t.l_buf+t.last_lit]=255&a,t.last_lit++,0===e?t.dyn_ltree[2*a]++:(t.matches++,e--,t.dyn_ltree[2*(Z[a]+f+1)]++,t.dyn_dtree[2*U(e)]++),t.last_lit===t.lit_bufsize-1},a._tr_align=function(t){var e;F(t,2,3),L(t,w,S),16===(e=t).bi_valid?(T(e,e.bi_buf),e.bi_buf=0,e.bi_valid=0):8<=e.bi_valid&&(e.pending_buf[e.pending++]=255&e.bi_buf,e.bi_buf>>=8,e.bi_valid-=8)}},{"../utils/common":3}],15:[function(t,e,a){"use strict";e.exports=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}},{}],"/":[function(t,e,a){"use strict";var i={};(0,t("./lib/utils/common").assign)(i,t("./lib/deflate"),t("./lib/inflate"),t("./lib/zlib/constants")),e.exports=i},{"./lib/deflate":1,"./lib/inflate":2,"./lib/utils/common":3,"./lib/zlib/constants":6}]},{},[])("/")}); diff --git a/play/js/sankey.js b/play/js/sankey.js new file mode 100644 index 00000000..3f19b3de --- /dev/null +++ b/play/js/sankey.js @@ -0,0 +1,322 @@ +d3.sankey = function() { + var sankey = {}, + nodeWidth = 24, + nodePadding = 8, // was 8, needs to be much bigger. these numbers are actually overwritten in the html when we instantiate the viz! + size = [1, 1], + nodes = [], + links = []; + + sankey.nodeWidth = function(_) { + if (!arguments.length) return nodeWidth; + nodeWidth = +_; + return sankey; + }; + + sankey.nodePadding = function(_) { + if (!arguments.length) return nodePadding; + nodePadding = +_; + return sankey; + }; + + sankey.nodes = function(_) { + if (!arguments.length) return nodes; + nodes = _; + return sankey; + }; + + sankey.links = function(_) { + if (!arguments.length) return links; + links = _; + return sankey; + }; + + sankey.size = function(_) { + if (!arguments.length) return size; + size = _; + return sankey; + }; + + sankey.layout = function(iterations) { + computeNodeLinks(); + computeNodeValues(); + + // big changes here + // change the order and depths (y pos) won't need iterations + computeNodeDepths(); + computeNodeBreadths(iterations); + + computeLinkDepths(); + return sankey; + }; + + sankey.relayout = function() { + computeLinkDepths(); + return sankey; + }; + + sankey.link = function() { + var curvature = .5; + + // x0 = line start X + // y0 = line start Y + + // x1 = line end X + // y1 = line end Y + + // y2 = control point 1 (Y pos) + // y3 = control point 2 (Y pos) + + function link(d) { + + // big changes here obviously, more comments to follow + var x0 = d.source.x + d.sy + d.dy / 2, + x1 = d.target.x + d.ty + d.dy / 2, + y0 = d.source.y + nodeWidth, + y1 = d.target.y, + yi = d3.interpolateNumber(y0, y1), + y2 = yi(curvature), + y3 = yi(1 - curvature); + + // ToDo - nice to have - allow flow up or down! Plenty of use cases for starting at the bottom, + // but main one is trickle down (economics, budgets etc), not up + + return "M" + x0 + "," + y0 // start (of SVG path) + + "C" + x0 + "," + y2 // CP1 (curve control point) + + " " + x1 + "," + y3 // CP2 + + " " + x1 + "," + y1; // end + } + + link.curvature = function(_) { + if (!arguments.length) return curvature; + curvature = +_; + return link; + }; + + return link; + }; + + // Populate the sourceLinks and targetLinks for each node. + // Also, if the source and target are not objects, assume they are indices. + function computeNodeLinks() { + nodes.forEach(function(node) { + node.sourceLinks = []; + node.targetLinks = []; + }); + links.forEach(function(link) { + var source = link.source, + target = link.target; + if (typeof source === "number") source = link.source = nodes[link.source]; + if (typeof target === "number") target = link.target = nodes[link.target]; + source.sourceLinks.push(link); + target.targetLinks.push(link); + }); + } + + // Compute the value (size) of each node by summing the associated links. + function computeNodeValues() { + nodes.forEach(function(node) { + node.value = Math.max( + d3.sum(node.sourceLinks, value), + d3.sum(node.targetLinks, value) + ); + }); + } + + // take a grouping of the nodes - the vertical columns + // there shouldnt be 8 - there will be more, the total number of 1st level sources + // then iterate over them and give them an incrementing x + // because the data structure is ALL nodes, just flattened, don't just apply at the top level + // then everything should have an X + // THEN, for the Y + // do the same thing, this time on the grouping of 8! i.e. 8 different Y values, not loads of different ones! + function computeNodeBreadths(iterations) { + var nodesByBreadth = d3.nest() + .key(function(d) { return d.y; }) + .sortKeys(d3.ascending) + .entries(nodes) + .map(function(d) { return d.values; }); // values! we are using the values also as a way to seperate nodes (not just stroke width)? + + // this bit is actually the node sizes (widths) + //var ky = (size[1] - (nodes.length - 1) * nodePadding) / d3.sum(nodes, value) + // this should be only source nodes surely (level 1) + var ky = (size[0] - (nodesByBreadth[0].length - 1) * nodePadding) / d3.sum(nodesByBreadth[0], value); + // I'd like them to be much bigger, this calc doesn't seem to fill the space!? + + nodesByBreadth.forEach(function(nodes) { + nodes.forEach(function(node, i) { + node.x = i; + node.dy = node.value * ky; + }); + }); + + links.forEach(function(link) { + link.dy = link.value * ky; + }); + + resolveCollisions(); + + for (var alpha = 1; iterations > 0; --iterations) { + relaxLeftToRight(alpha); + resolveCollisions(); + + relaxRightToLeft(alpha *= .99); + resolveCollisions(); + } + + // these relax methods should probably be operating on one level of the nodes, not all!? + + function relaxLeftToRight(alpha) { + nodesByBreadth.forEach(function(nodes, breadth) { + nodes.forEach(function(node) { + if (node.targetLinks.length) { + var y = d3.sum(node.targetLinks, weightedSource) / d3.sum(node.targetLinks, value); + node.x += (y - center(node)) * alpha; + } + }); + }); + + function weightedSource(link) { + return center(link.source) * link.value; + } + } + + function relaxRightToLeft(alpha) { + nodesByBreadth.slice().reverse().forEach(function(nodes) { + nodes.forEach(function(node) { + if (node.sourceLinks.length) { + var y = d3.sum(node.sourceLinks, weightedTarget) / d3.sum(node.sourceLinks, value); + node.x += (y - center(node)) * alpha; + } + }); + }); + + function weightedTarget(link) { + return center(link.target) * link.value; + } + } + + function resolveCollisions() { + nodesByBreadth.forEach(function(nodes) { + var node, + dy, + x0 = 0, + n = nodes.length, + i; + + // Push any overlapping nodes right. + // nodes.sort(ascendingDepth); // commenting this out preserved the order of the nodes! + for (i = 0; i < n; ++i) { + node = nodes[i]; + dy = x0 - node.x; + if (dy > 0) node.x += dy; + x0 = node.x + node.dy + nodePadding; + } + + // If the rightmost node goes outside the bounds, push it left. + dy = x0 - nodePadding - size[0]; // was size[1] + if (dy > 0) { + x0 = node.x -= dy; + + // Push any overlapping nodes left. + for (i = n - 2; i >= 0; --i) { + node = nodes[i]; + dy = node.x + node.dy + nodePadding - x0; // was y0 + if (dy > 0) node.x -= dy; + x0 = node.x; + } + } + }); + } + + function ascendingDepth(a, b) { + //return a.y - b.y; // flows go up + return b.x - a.x; // flows go down + //return a.x - b.x; + } + } + + // this moves all end points (sinks!) to the most extreme bottom + function moveSinksDown(y) { + nodes.forEach(function(node) { + if (!node.sourceLinks.length) { + node.y = y - 1; + } + }); + } + + // shift their locations out to occupy the screen + function scaleNodeBreadths(kx) { + nodes.forEach(function(node) { + node.y *= kx; + }); + } + + function computeNodeDepths() { + var remainingNodes = nodes, + nextNodes, + y = 0; + + while (remainingNodes.length) { + nextNodes = []; + remainingNodes.forEach(function(node) { + node.y = y; + //node.dx = nodeWidth; + node.sourceLinks.forEach(function(link) { + if (nextNodes.indexOf(link.target) < 0) { + nextNodes.push(link.target); + } + }); + }); + remainingNodes = nextNodes; + ++y; + } + + // move end points to the very bottom + moveSinksDown(y); + + scaleNodeBreadths((size[1] - nodeWidth) / (y - 1)); + } + + // .ty is the offset in terms of node position of the link (target) + function computeLinkDepths() { + nodes.forEach(function(node) { + node.sourceLinks.sort(ascendingTargetDepth); + node.targetLinks.sort(ascendingSourceDepth); + }); + nodes.forEach(function(node) { + var sy = 0, ty = 0; + //ty = node.dy; + node.sourceLinks.forEach(function(link) { + link.sy = sy; + sy += link.dy; + }); + node.targetLinks.forEach(function(link) { + // this is simply saying, for each target, keep adding the width of the link + // so what if it was the other way round. start with full width then subtract? + link.ty = ty; + ty += link.dy; + //ty -= link.dy; + }); + }); + + function ascendingSourceDepth(a, b) { + //return a.source.y - b.source.y; + return a.source.x - b.source.x; + } + + function ascendingTargetDepth(a, b) { + //return a.target.y - b.target.y; + return a.target.x - b.target.x; + } + } + + function center(node) { + return node.y + node.dy / 2; + } + + function value(link) { + return link.value; + } + + return sankey; +}; \ No newline at end of file diff --git a/play/model1.html b/play/model1.html deleted file mode 100644 index c5d22088..00000000 --- a/play/model1.html +++ /dev/null @@ -1,53 +0,0 @@ -<!doctype html> -<html lang="en"> -<head> - <meta charset="utf-8"> - <link rel="stylesheet" type="text/css" href="css/model.css"> -</head> -<body></body> -</html> - -<script src="js/helpers.js"></script> -<script src="js/minpubsub.js"></script> -<script src="js/Loader.js"></script> -<script src="js/Mouse.js"></script> -<script src="js/Draggable.js"></script> -<script src="js/Model.js"></script> -<script src="js/Candidate.js"></script> -<script src="js/Voters.js"></script> -<script src="js/Election.js"></script> -<script src="js/seedrandom.min.js"></script> - -<script> -Loader.onload = function(){ - - // SELF CONTAINED MODEL - window.model = new Model({border:2}); - document.body.appendChild(model.dom); - model.onInit = function(){ - model.addVoters({ - dist: SingleVoter, - type: PluralityVoter, - x:125, y:200 - }); - model.addCandidate("square", 50, 125); - model.addCandidate("triangle", 250, 125); - }; - model.onUpdate = function(){ - var id = model.voters[0].ballot.vote; - var color = Candidate.graphics[id].fill; - var text = "VOTES FOR <b style='color:"+color+"'>"+id.toUpperCase()+"</b>"; - model.caption.innerHTML = text; - }; - - // Init! - model.init(); - -}; -Loader.load([ - "img/voter_face.png", - "img/square.png", - "img/triangle.png", - "img/hexagon.png" -]); -</script> \ No newline at end of file diff --git a/play/model2.html b/play/model2.html deleted file mode 100644 index bc864c5a..00000000 --- a/play/model2.html +++ /dev/null @@ -1,51 +0,0 @@ -<!doctype html> -<html lang="en"> -<head> - <meta charset="utf-8"> - <link rel="stylesheet" type="text/css" href="css/model.css"> -</head> -<body></body> -</html> - -<script src="js/helpers.js"></script> -<script src="js/minpubsub.js"></script> -<script src="js/Loader.js"></script> -<script src="js/Mouse.js"></script> -<script src="js/Draggable.js"></script> -<script src="js/Model.js"></script> -<script src="js/Candidate.js"></script> -<script src="js/Voters.js"></script> -<script src="js/Election.js"></script> -<script src="js/seedrandom.min.js"></script> - -<script> -Loader.onload = function(){ - - // SELF CONTAINED MODEL - window.model = new Model(); - document.body.appendChild(model.dom); - model.onInit = function(){ - - // Voters & Candidates - model.addVoters({ - dist: GaussianVoters, - type: PluralityVoter, - x:150, y:150 - }); - model.addCandidate("square", 50, 125); - model.addCandidate("triangle", 250, 125); - - }; - model.onUpdate = function(){ - Election.plurality(model, {verbose:true}); - }; - model.init(); - -}; -Loader.load([ - "img/voter_face.png", - "img/square.png", - "img/triangle.png", - "img/hexagon.png" -]); -</script> \ No newline at end of file diff --git a/play/model3.html b/play/model3.html deleted file mode 100644 index 940f7b55..00000000 --- a/play/model3.html +++ /dev/null @@ -1,63 +0,0 @@ -<!doctype html> -<html lang="en"> -<head> - <meta charset="utf-8"> - <link rel="stylesheet" type="text/css" href="css/model.css"> -</head> -<body></body> -</html> - -<script src="js/helpers.js"></script> -<script src="js/minpubsub.js"></script> -<script src="js/Loader.js"></script> -<script src="js/Mouse.js"></script> -<script src="js/Draggable.js"></script> -<script src="js/Model.js"></script> -<script src="js/Candidate.js"></script> -<script src="js/Voters.js"></script> -<script src="js/Election.js"></script> -<script src="js/seedrandom.min.js"></script> - -<script> -Loader.onload = function(){ - - // SELF CONTAINED MODEL - window.model = new Model(); - document.body.appendChild(model.dom); - model.onInit = function(){ - - // Voters & Candidates - model.addVoters({ - dist: GaussianVoters, - type: PluralityVoter, - x:155, y:125 - }); - model.addCandidate("square", 50, 125); - model.addCandidate("triangle", 250, 125); - model.addCandidate("hexagon", 280, 280); - - }; - model.onUpdate = function(){ - Election.plurality(model); - }; - model.init(); - - // CREATE A RESET BUTTON - var resetDOM = document.createElement("div"); - resetDOM.id = "reset"; - resetDOM.innerHTML = "reset"; - resetDOM.style.top = "415px"; - resetDOM.style.left = "110px"; - resetDOM.onclick = function(){ - model.reset(); - }; - document.body.appendChild(resetDOM); - -}; -Loader.load([ - "img/voter_face.png", - "img/square.png", - "img/triangle.png", - "img/hexagon.png" -]); -</script> \ No newline at end of file diff --git a/play/tsp/salesman.js b/play/tsp/salesman.js new file mode 100644 index 00000000..249f2eb7 --- /dev/null +++ b/play/tsp/salesman.js @@ -0,0 +1,516 @@ +// this handles moving a 2D set of points and ballots into a 1D list. And it keeps similar voters together. + +var breakRingTSP = true + +/* Sample of a city */ +function Sample() { + this.x = 0.0; + this.y = 0.0; +}; + +Sample.prototype.draw = function(canvas) { + var centerX = this.x * canvas.width; + var centerY = canvas.height - this.y * canvas.height; + var ctx = canvas.ctx; + ctx.fillStyle = "#C06060"; + ctx.beginPath(); + ctx.arc(centerX, centerY, 3, 0, Math.PI*2, true); + ctx.closePath(); + ctx.fill(); +}; + +/* a node in the neural network */ +function Node(x, y, b) { + this.x = x; + this.y = y; + this.b = []; + for (var i=0; i< b.length; i++) { + this.b[i] = b[i] + } + this.right = this; + this.left = this; + this.life = 3; + this.inhibitation = 0; + this.isWinner = 0; +} + +/* the distance of the euclidian points */ +Node.prototype.potential = function(sample) { + var dx = sample.x - this.x + var dy = sample.y - this.y + var db2 = 0 + for (var i = 0; i < sample.b.length; i++) { + db = sample.b[i] - this.b[i] + db2 += db * db * 739 * 739 + } + return dx * dx + dy * dy + db2 +}; + +/* moves a single node in direction to the sample */ +Node.prototype.move = function(city, value) { + this.x += value * (city.x - this.x); + this.y += value * (city.y - this.y); + for (var i = 0; i < city.b.length; i++) { + this.b[i] += value * (city.b[i] - this.b[i]); + } +}; + +/* computes the number of nodes between the to nodes on the ring */ +Node.prototype.distance = function(other, length) { + var right = 0; + var left = 0; + var current = other; + + if (breakRingTSP) { + var notleft = false + while (current != this) { + if (current.isStart) notleft = true + current = current.left; + left++; + } + if (notleft) { + current = other + while (current != this) { + current = current.right; + right++; + } + return right + } else { + return left + } + } else { + while (current != this) { + current = current.left; + left++; + } + right = length - left; + return (left < right) ? left : right; + + } +}; + +Node.prototype.draw = function(canvas) { + var centerX = this.x * canvas.width + 0.5; + var centerY = canvas.height - this.y * canvas.height + 0.5; + var ctx = canvas.ctx; + ctx.fillStyle = "#208020"; + ctx.fillRect(centerX, centerY, 1, 1); + if (this.right != null) { + ctx.lineTo(this.right.x * canvas.width + 0.5, canvas.height - this.right.y * canvas.height + 0.5); + } +}; + +/* the neural network as a ring of neurons */ +function Ring(start) { + this.start = start; + this.length = 1; + if (breakRingTSP) start.isStart = true; +} + +/* moves all nodes to in direction of the sample */ +Ring.prototype.moveAllNodes = function(city, gain) { + var current = this.start; + var best = this.findMinimum(city); + + for (var i=0; i<this.length; i++) { + current.move(city, this.f(gain, best.distance(current, this.length))); + current = current.right; + } +}; + +/* finds the node with the least distance to the sample */ +Ring.prototype.findMinimum = function(city) { + var actual; + var node = this.start; + var best = node; + var min = node.potential(city); + for (var i=1; i<this.length; i++) { + node = node.right; + actual = node.potential(city); + if (actual < min) { + min = actual; + best = node; + } + } + best.isWinner++; + return best; +}; + +/* deletes a node */ +Ring.prototype.deleteNode = function(node) { + var previous = node.left; + var next = node.right; + + if (previous != null) { + previous.right = next; + } + if (next != null) { + next.left = previous; + } + if (next == node) { + next = null; + } + if (this.start == node) { + this.start = next; + if (breakRingTSP) this.start.isStart = true + } + this.length--; +}; + +/* a node is duplicated & inserted into the ring */ +Ring.prototype.duplicateNode = function(node) { + var newNode = new Node(node.x, node.y, node.b); + var next = node.right; + next.left = newNode; + node.right = newNode; + node.inhibitation = 1; + newNode.right = next; + newNode.left = node; + newNode.inhibitation = 1; + this.length++; +}; + +/* length of tour */ +Ring.prototype.tourLength = function() { + var dist = 0.0; + var current = this.start; + var previous = current.left; + + for (var i=0; i<this.length; i++) { + + var db2 = 0 + for (var k = 0; k < current.b.length; k++) { + var db = current.b[k] - previous.b[k] + db2 += db * db * 739 * 739 + } + var dx = current.x - previous.x + var dy = current.y - previous.y + dist += Math.sqrt( dx * dx + dy * dy + db2 ); + current = previous; + previous = previous.left; + } + return dist; +}; + +Ring.prototype.f = function(gain, n) { + return (0.70710678 * Math.exp(-(n * n) / (gain * gain))); +}; + +/* the simulator containing all the data */ +function TravelingSalesman() { + this.N = 100; /* Number of cities. */ + this.cycle = 0; /* Number of complete survey done */ + this.maxCycles = 100; /* Number of complete suerveys */ + this.cities = null; /* the samples */ + this.neurons = null; /* the neurons */ + this.alpha = 0.04; /* learning rate */ + this.gain = 50.0; /* gain */ + this.lastLength = null; /* length of tour */ + this.isRunning = false; + this.update = 5; /* screen update */ + this.justplugin = false; +} + +/* creates the first node (ring) */ +TravelingSalesman.prototype.createFirstNeuron = function() { + if (this.justplugin) { + // find average + bnew = [] + for (var k = 0; k < this.cities[0].b.length; k++) { + bnew.push(0.5); + } + var start = new Node(0.5, 0.5, bnew); + } else { + var start = new Node(0.5, 0.5); + } + this.neurons = new Ring(start); +}; + +/* deletes all nodes */ +TravelingSalesman.prototype.deleteAllNeurons = function() { + if (this.neurons != null) { + while (this.neurons.start != null) { + this.neurons.deleteNode(this.neurons.start); + } + this.neurons = null; + } +}; + +/* prints positions of cities & nodes */ +TravelingSalesman.prototype.print = function() { + console.log("TSP: N= " + this.N + ", cycle=" + this.cycle + ", lastLength=" + this.lastLength); + for (var i=0; i<this.cities.length; i++) { + var c = this.cities[i]; + console.log("City: " + i + " (" + c.x + "," + c.y + ")"); + } + var n = this.neurons.start; + for (i=0; i<this.neurons.length; i++) { + console.log("Node: " + i + "(" + n.x + "," + n.y + ")"); + n = n.right; + } +}; + +/* creates & displaces randomly a given number of cities, returns the first */ +TravelingSalesman.prototype.createRandomCities = function() { + this.cities = new Array(this.N); + for (var i=0; i<this.N; i++) { + var c = new Sample(); + c.x = Math.random(); + c.y = Math.random(); + this.cities[i] = c; + } +}; + +TravelingSalesman.prototype.stop = function() { + this.isRunning = false; + if (this.justplugin != true) { + this.repaint(); + this.cities = null; + } + this.deleteAllNeurons(); +}; + +TravelingSalesman.prototype.start = function() { + this.stop(); + this.init(); + this.isRunning = true; + this.run(); +}; + +TravelingSalesman.prototype.init = function() { + this.cycle = 0; + this.lastLength = null; + this.createFirstNeuron(); + if (this.justplugin != true) { + this.createRandomCities(); + this.canvas = new Canvas(document.getElementById('canvas')); + this.repaint(); + } +}; + +TravelingSalesman.prototype.run = function() { + if (this.neurons != null) { + if (this.cycle < this.maxCycles && this.isRunning) { + var done = this.surveyRun(); + if (!done) { + var self = this; + if (this.justplugin != true) { + window.setInterval(function() { self.run(); }, 100); + } + self.run(); + return; + } + } + if (this.isRunning) { + //this.print(); + this.isRunning = false; + if (this.justplugin != true) { + this.repaint(); + } + } + } +}; + +/* one cycle in the simulation */ +TravelingSalesman.prototype.surveyRun = function() { + var done = false; + if (this.neurons != null) { + for (var i=0; i<this.cities.length; i++) { + this.neurons.moveAllNodes(this.cities[i], this.gain); + } + } + this.surveyFinish(); + this.gain = this.gain * (1 - this.alpha); + if (this.cycle++ % this.update == 0) { + var length = this.neurons.tourLength(); + //this.print(); + this.repaint(); + if (length == this.lastLength) { + done = true; + } else { + this.lastLength = length; + } + } + return done; +}; + +/* after moving creating & deleting is done */ +TravelingSalesman.prototype.surveyFinish = function() { + if (this.neurons == null) { + return; + } + var node = this.neurons.start; + for (var i=0; i<this.neurons.length; i++) { + node.inhibitation = 0; + switch (node.isWinner) { + case 0: + node.life--; + if (node.life == 0) { + this.neurons.deleteNode(node); + } + break; + case 1: + node.life = 3; + break; + default: + node.life = 3; + this.neurons.duplicateNode(node); + break; + } + node.isWinner = 0; + node = node.right; + } +}; + +TravelingSalesman.prototype.repaint = function() { + if (!this.canvas) { + return; + } + this.canvas.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + + if (this.cities) { + for (var i=0; i<this.cities.length; i++) { + var c = this.cities[i]; + c.draw(this.canvas); + } + } + + if (this.neurons) { + this.canvas.ctx.strokeStyle = "#80D080"; + this.canvas.ctx.beginPath(); + var n = this.neurons.start; + this.canvas.ctx.moveTo(n.x * this.canvas.width + 0.5, this.canvas.height - n.y * this.canvas.height + 0.5); + for (i=0; i<this.neurons.length; i++) { + n.draw(this.canvas); + n = n.right; + } + this.canvas.ctx.lineWidth = 1; + this.canvas.ctx.stroke(); + this.canvas.ctx.closePath(); + } + $('#cycle').val(this.cycle); + $('#length').val(this.lastLength); + $('#done').prop('checked' , !this.isRunning); + if (this.isRunning) { + $('#run').attr('disabled', 'disabled'); + $('#stop').removeAttr('disabled'); + } else { + $('#stop').attr('disabled', 'disabled'); + $('#run').removeAttr('disabled'); + } +}; + +TravelingSalesman.prototype.setupForm = function() { + var self = this; + $('#cities').val(this.N); + $('#maxCycles').val(this.maxCycles); + $('#alpha').val(this.alpha); + $('#gain').val(this.gain); + $('#run').bind("click", function(event) { + self.N = $('#cities').val(); + self.maxCycles = $('#maxCycles').val(); + self.alpha = $('#alpha').val(); + self.gain = $('#gain').val(); + self.start(); + }); + $('#stop').bind("click", function(event) { + self.stop(); + }); +}; + +function Canvas(elem) { + this.elem = elem; + this.ctx = elem.getContext('2d'); + this.height = elem.height; + this.width= elem.width; +} + + +TravelingSalesman.prototype.runOnSet = function (cities) { + this.justplugin = true; + this.cities = cities; + this.start(); +} + +TravelingSalesman.prototype.getOrder = function() { + // get a list of id's of points in order of how they should appear. + + // the neurons are in order, so lets put the cities in order by finding which neuron each city is closest to + + var next = this.neurons.start + for (var k=0; k < this.neurons.length; k++) { + next.c = [] + next = next.right + } + + // find nearest node to each city + // add the city's index to the node + for (var n=0; n < this.cities.length; n++) { // city n + var next = this.neurons.start + var closest = next + var min = Infinity + for (var k=0; k < this.neurons.length; k++) { // neuron k + var dx = this.cities[n].x - next.x + var dy = this.cities[n].y - next.y + var dist2 = dx*dx + dy*dy + if (dist2 < min) { + closest = next + min = dist2 + } + next = next.right + } + closest.c.push(n) + } + + // find order of each city by going through each node and listing the cities + var next = this.neurons.start + var order = [] + for (var k=0; k < this.neurons.length; k++) { // neuron k + for (var n=0; n < next.c.length; n++) { + order.push(next.c[n]) + } + next = next.right + } + return order +} + + +function dostuff() { + points = new Array(100); + for (var i=0; i<points.length; i++) { + var c = new Sample(); + c.x = Math.random(); + c.y = Math.random(); + c.i = i; + points[i] = c; + } + + var tsp = new TravelingSalesman(); + tsp.runOnSet(points) + var order = tsp.getOrder() + console.log(order) +} + +// dostuff() + +/* + +https://github.com/kifj/tsp-js +https://observablehq.com/@fil/som-tsp + +order = { + const D = d3.Delaunay.from(som.neurons); + const order = points.map(d => D.find(d[0], d[1])); + // return the new order — if something goes wrong (we receive -1), use the previous value + return order[0] === -1 ? this : order; + + /* + + // Quadtree version + const D = d3 + .quadtree() + .addAll(som.neurons.map((d, i) => Object.assign(d, { index: i }))); + const order = points.map(d => D.find(d[0], d[1]).index); + return order[0] === -1 ? this : order; + + */ diff --git a/play/tsp/salesman2.js b/play/tsp/salesman2.js new file mode 100644 index 00000000..ea72a25d --- /dev/null +++ b/play/tsp/salesman2.js @@ -0,0 +1,161 @@ +function order_by_distance(nodes, distance, opts) { + const n = nodes.length; + + if (n < 3) return nodes + + const m = new Map(), + connect = []; + + if (distance === undefined) distance = euclidian2; + + function D(i, j) { + return distance(nodes[i], nodes[j]); + } + + const links = []; + for (var i = 0; i < nodes.length; i++) { + for (var j = i + 1; j < nodes.length; j++) { + const d = distance(nodes[i], nodes[j]); + links.push([i, j, d]); + } + } + links.sort((a, b) => a[2] - b[2]); + + for (const l of links) { + const [i, j, dist] = l, + s0 = m.get(i), + s1 = m.get(j); + if (s0 && s0 === s1) continue; // i and j are already linked + + if (s0 && !s1) { + const s = s0, + a = j; + m.set(a, s); + if (D(a, s[0]) < D(a, s[s.length - 1])) { + connect.push({ left: [a, a], right: [s[0], s[s.length - 1]], dist }); + s.unshift(a); + } else { + connect.push({ left: [s[0], s[s.length - 1]], right: [a, a], dist }); + s.push(a); + } + } else if (s1 && !s0) { + const s = s1, + a = i; + m.set(a, s); + if (D(a, s[0]) < D(a, s[s.length - 1])) { + connect.push({ left: [a, a], right: [s[0], s[s.length - 1]], dist }); + s.unshift(a); + } else { + connect.push({ left: [s[0], s[s.length - 1]], right: [a, a], dist }); + s.push(a); + } + } else if (!s0 && !s1) { + const s = [i, j]; + m.set(i, s); + m.set(j, s); + connect.push({ left: [i, i], right: [j, j], dist }); + } else { + // join the two segments by the shortest link between their extremities + const d00 = D(s0[0], s1[0]), + d01 = D(s0[0], s1[s1.length - 1]), + d10 = D(s0[s0.length - 1], s1[0]), + d11 = D(s0[s0.length - 1], s1[s1.length - 1]), + k = Math.min(d00, d01, d10, d11); + var s; + if (k === d00) s = [s1.reverse(), s0]; + else if (k === d01) s = [s1, s0]; + else if (k === d10) s = [s0, s1]; + else if (k === d11) s = [s0, s1.reverse()]; + + connect.push({ + left: [s[0][0], s[0][s[0].length - 1]], + right: [s[1][0], s[1][s[1].length - 1]], + dist + }); + + s = s.flat(); + for (const a of s) m.set(a, s); + } + } + + nodes = m.get(0).map(i => nodes[i]); + + opts = opts || {} + if (opts.crossover || opts.points) { + crosssOverAndPoints(nodes, distance, opts) + } + + nodes.connect = connect; + + // yield nodes; + + return nodes; + + function euclidian2(a, b) { + return a.map((_, i) => (a[i] - b[i]) ** 2).reduce((a, b) => a + b); + } + } + +function crosssOverAndPoints(nodes, distance, opts) { + for (var counter = 0; counter < 200; counter ++ ) { // put some limit on iterations + // yield nodes; + var gain = 0; + for (var i = 0; i < nodes.length - 2; i++) { + for (var j = i + 2; j < nodes.length - 1; j++) { + // no-crossings optimization [i,i+1] vs [j, j+1] + if (opts.crossover) { + const ii1 = distance(nodes[i], nodes[i + 1]), + jj1 = distance(nodes[j], nodes[j + 1]), + ij = distance(nodes[i], nodes[j]), + i1j1 = distance(nodes[j + 1], nodes[i + 1]), + diff = ii1 + jj1 - ij - i1j1; + if (diff > 0) { + gain += diff; + nodes = nodes + .slice(0, i + 1) + .concat(nodes.slice(i + 1, j + 1).reverse()) + .concat(nodes.slice(j + 1, Infinity)); + } + } + + if (opts.points && j < nodes.length - 3) { + const ii1 = distance(nodes[i], nodes[i + 1]), + i1i2 = distance(nodes[i + 1], nodes[i + 2]), + ij1 = distance(nodes[i], nodes[j + 1]), + i1j = distance(nodes[i + 1], nodes[j]), + ii2 = distance(nodes[i], nodes[i + 2]), + i1j1 = distance(nodes[i + 1], nodes[j + 1]), + jj1 = distance(nodes[j], nodes[j + 1]), + j1j2 = distance(nodes[j + 1], nodes[j + 2]), + jj2 = distance(nodes[j], nodes[j + 2]), + diff0 = ii1 + jj1 + j1j2 - (ij1 + i1j1 + jj2), + diff1 = ii1 + jj1 + i1i2 - (i1j + i1j1 + ii2); + if (diff0 > 0) { + /* + i j + >j1 + i1 j2 */ + gain += diff0; + nodes = nodes + .slice(0, i + 1) + .concat([nodes[j + 1]]) + .concat(nodes.slice(i + 1, j + 1)) + .concat(nodes.slice(j + 2, Infinity)); + } else if (diff1 > 0) { + /* + j i + >i1 + j1 i2 */ + gain += diff1; + nodes = nodes + .slice(0, i + 1) + .concat(nodes.slice(i + 2, j + 1)) + .concat([nodes[i + 1]]) + .concat(nodes.slice(j + 1, Infinity)); + } + } + } + } + if (gain == 0) break + } +} \ No newline at end of file diff --git a/primaries.md b/primaries.md new file mode 100644 index 00000000..6a851cd2 --- /dev/null +++ b/primaries.md @@ -0,0 +1,415 @@ +--- +permalink: /primaries/ +layout: page-3 +title: Primaries +description: An Interactive Guide to Voting +byline: 'by Paretoman, May 2020' +--- +{% include letters.html %} + +We Don’t Need Primaries +======================= + + +I’m going to talk about what we're trying to achieve when we vote. And I'd like to talk about how using primaries and choosing only one candidate on our ballots will only achieve accurate representation in the best-case scenario. In many cases, a lot of votes are wasted, and a lot of voters receive no consideration. + +We're going to go over each aspect of voting and see how other methods of voting can work in more scenarios. And I’m going to try to make it easier to think about voting by using diagrams. Voting is not an easy subject because there are many voters and each have different perspectives. There are also a lot of strategic considerations you have to make. Voting is hard, but it could be easier. You should walk away from this tutorial wanting to change how we vote. + +Primaries were created to allow a party to deal with vote splitting, but there are other ways to vote that beat primaries on this original design point. Additionally, better systems can have better competition which produces better candidates. They also can encourage voters to vote honestly without having to watch the polls to help them vote strategically. Most fundamentally, they avoid splitting the vote so voters don't have to coordinate on who they should support and who they will encourage to drop out. + +| Method of Voting | Outcome in Lots of Scenarios | Outcome in Best-Case Scenario | +| ----------------------------------------------- | ---------------------------- | ----------------------------- | +| Primaries with Choose Only One Plurality Voting | Bad | Good | +| Alternative Voting Methods without Primaries | Good | Good | + +The Original Design of a Primary is to Deal with Vote Splitting +--------------------------------------------------------------- + +There are ways to vote that beat primaries at their own game. Specifically the original purpose of having a primary is to solve the problems of only being able to vote for one candidate. A primary allows a party with many potential contenders to unite behind one. Primaries don’t entirely solve this problem. They are only an incremental improvement. + +### Basics + +Let’s back up a little here and ask what the purpose of voting systems is. You use voting systems when you have a large group of people and have to come to a decision. This can be a referendum or a ballot initiative, or you could elect a representative to make the decision for you.  + +A referendum is actually pretty simple; you just say yes or no. Really, the *hard *part of the referendum is to decide what the wording is going to be for the question. The wording is not up to you; that's up to the group that puts the initiative together or the legislators that wrote the referendum. This group tries to consider what the voters want, and that is the hard part. It’s part of the mechanism for the idea that you are represented. + +{% include sim.html title="Referendum - Your Ballot" caption="Yes or No?" id="yesno_sim" gif = "gif/yesno.gif" link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSu04DQQz8lZPrFVo_9nH5CERBg-5SBCkF0gkQhCJC8O14PQoUKLpi1h7v7Ni-T8q0W5bSEhfZp0WsJra-3yfiQbD2xFVGrLTLiYx29J0pUYmwepFzzSGnf58z_SozX2U4hzYzxFkQwgAbAO-zG2AHN8Dm6M-JwxykuI4nhRFJcAIZsbgoLqMOFcmGqCOa44LmMMpjBFBSGFIFD0PqSgujrIKAms6_921IxWG0Nlo1uRxcbNFxsLhlf3KGHq1hATBncyRLBvBYy0rTw_F9Wtfn6fZlWmlsCU0XDdGCpksBoOnSUNJRgqZrBnBUVjRcMbpaoqFhtkKiwlPF2BvW1zCsJiHRFIBhNWyvXX6fBhI_CydJmmzk0WSHYOfw1GGmw0w3JAsAfjr0Omz1YeumJHo8bNvL6f78evRp3W0fb4ft6XSmrx9CIcEjBQMAAA)" %} + +And let's see a whole group of people. + +{% include sim.html title="Referendum - Result" caption="Everybody Votes" id="yesno_many_sim" gif = "gif/yesno_many.gif" link="[link](https://paretoman.github.io/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRsWpDMQz8leDZgyVL8nM-onToUt7LkEKGwqMpbTKE0n57ZR1phxAMPktnnU_yVyppO8_aMinv8sximWTa7XKiQZCwx33ENW1LThK7xm5-g_PN8rvNmZJvljPTXabfZajEczQcjZARwhDBEcESGcANkDj6c-zQI8uu40l2HXLg4BgyLEi6THWADDdEE6IeBbWEURojoSAqDNUKHoaqK82EawYCarX_1UtIjQNdD3w91FE-DhJV8i8nFlal4UNgTnoktQBcL_0s6fnwuSxvm4fjZknJ02hZa8xB0bIqAC1rw5UpXlW0bAWAwRnaNQzONNqpbswgYXBkGHrD5zXUNg542a_r8fR0eT-4z8f1_LFfX0-X9P0LH8D-m48CAAA)" %} + +For an election, the hard part happens in the primary. The general election is easy: there's two choices (at least in the United States, where I'm from, there's two choices). There's not a lot to think about. Each party has put up their nominee, and you pick the one closest to you. + +{% capture cap3 %}{{ A }} or {{ B }}?{% endcapture %} + +{% include sim.html title="General Election" caption=cap3 id="ab_sim_general" gif = "gif/ab_general.gif" link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRu2oEMQz8F9curKfX-xUp0i1bXCDFwcKFcCmOkH-PrOGS4jgWPJJHGo-036WVddusVzLe68bqlXTZ91poEqQc-Zi5lLXVonlanh4VXB--qO3BtPrwBbM8ZcZThlo-R9PRTBkpDBEcESyRA8IAaWA8xwEjbzl04pJDhwI4OYYMKy5DRgIgwx3Zgmxkg7Q0SnMllITAkAh4GJJQ2ghlDgJqMv76NaVmQPeA74HM9hlodum_nHpa1Y4fAnM68tKwKoMxw4gmObdhRDMARrSOkiVfMYzoDYBFOcZzLMot7UsYcUg4HDiW3OGgo7dzwtvpOC7X19vHe1nLy_H1eTrO11v5-QXpTWN6fwIAAA)" %} + +The hard part happens in the primary. That's where you have many more competitors. There are two primaries going on at the same time, and in each one there is the same decision process, where one party wants to put up a candidate that can beat the other party.  + +{% capture cap4 %}{{ A }} or {{ B }}? Which would beat the other party?{% endcapture %} + +{% include sim.html title="Primary Election" caption=cap4 id="ab_sim_primary" gif = "gif/ab_primary.gif" link="[link](http://localhost:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRMWoEMQz8i2sXlizJ3n1FinTLFhdIEVhICJfiCPl7JM2FIxyHixntaMcj-bu0sm6bjkrKe92IyZkFm8GmM7alkrV9r4WiecqfwOyMQuhlbbVIWakWTW7eyvXueO9wpdW748p8qCwPFWp5HUW0KBklApEAEIkM4AFIHP26gCW_csuKKWdgt2EH2DAGY7fpDrDhgWqiWvKH3jIoxUoohY5AvUNHoO5Om-83TrQaRDh2DEu-ru6iwDJmFbpRvtEOs6CSDvLfXizjy8AjIbAseCysTxFWMbb23IVe31MBGFsHWmbepBjbGgDLM4xsWJ5pJo1RDBaGBIbFDyQY-HdwwsvpON7Pz5eP17KWp-Pr83S8nS_l5xcgCQz0sAIAAA)" %} + +### Pairwise Ranking + +I’m going to tell you about another way we could vote, and it’s easier. Instead of posing this question, “What candidate do you think would be best overall?”, we can ask voters, “Who is best for you?”. That is a much easier question to ask. You don't have to make people guess. The best part is that you still get the same benefit: you get to unite a whole side behind the person that is best for all voters.  + +What does that look like? The easiest example of this is a pairwise ranking, because in a pairwise ranking, for every pair, your full support goes to the candidate you prefer. If there's a great candidate that's in the middle, and there's another great candidate that's more towards your side, you’re not going to be in a dilemma of which one to vote for. You can throw all your support to the candidate that's on your side when they are matched against the other side, so he gets the full support of his side. and also, your full weight will count towards the middle candidate over candidates from the other side, and that completely solves the problem that the primary was trying to solve. The whole purpose of the primary was to say that we want our single vote to matter the most. Now, when you count by pairs, your vote - your full vote - counts for the candidate you prefer over any other candidate. + +{% include sim.html +title='Pairwise Ranking' +caption='Your full support always counts for the better candidate in each pair of candidates.' +comment='three candidates, two on one side, one on other' +id='pairwise_intro' +gif = "gif/pairwise_intro.gif" +link='[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRvU6DMQx8lcpzhvgvafoQDIgFfV-HIjpRtUh0QQieHccnJASqMtiO7cvd5YMq7ZaFXYtU2ZeFR2QyM1EpomO_L8Q5o71wq7NW2tVCRrtRyDNvMRHjf0_M9ujU8u9EZ3uzM252uOZzPBnNUlCCEBsCKHFw4ghBgC1iPKcRRjYlcOJSGFXASATAiOWieC5Iw2VHtUU1ckFrEuVpCZAUSKoIIKSBtHBhDDa0gKeQymmVTbg5Y1PelGvykwTgojOx3LPfkAal1vEtoGgjLx2GeSDS10qPx7d1PW_uLpuVKK5B1zXdcAh3R4BwB6rDPofwVhE4Jxt-ocG-5ilpkm2AaGDUYH0How7DuiTE0-F0ulwf3l-PwfP-cH45PtPnN_ziP1OdAgAA)' +%} + +It’s much clearer what happens when you think of the candidate’s perspective. All their supporters are behind them. They get the full weight of their supporters when they face the other party. The candidate doesn't need to worry about splitting votes with anybody. + +{% include sim.html +title='Pairwise Election' +caption='Each candidate gets the full weight of their side against the other party. Mouse over each pair to see the support.' +comment='mouse over pairs to see support' +id='pairwise_election' +gif='gif/pairwise_election.gif' +link='[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRMU4EMQz8ysl1ijiOk80-ggLRoN0tDrEVpzskaBCCt2N7ihM6nVKMnbEnY-ebMs3Lojmxji0tzD1xE48GJ-5sUelqbNm2ROzFk1iJOiEabUYIzTlRpXkk0oiblZZ0c6y2G5PTzTFmusuMuwzneI7dmqcFKQxxBcASmyc2MANcDe05hxFkyZEVRmYyxQAypUZjMRkxaLjsyCZkIxokh1H2lUBJoCQCgCExpcX268dLG0goCoZlW5d9BFVI-qyVr2G5hgIxD2so1P_yFXPXjk-C4TriUrE-NWX6Xel5_1jX8-HhcliJ7BrmVVCLNagCsAaFqk7xsmINLQM4Khv-pGGZTcO5j9Yg0eCo4SM6HHX09hLwcjydLp9PX--7-Xw8nt_2V_r5A7EXqU-9AgAA)' +%} + +So, this solves the problem of splitting votes, which is caused by limiting your vote to choose only one candidate. The head-to-head pairwise ranked ballot achieves the goal of the primary in a better way than the primary could. It does better because each voter knows for themself, who is better for themself. The voter knows their own preferences better than they can guess the preferences of the entire population (or watch polls). There is no more hard part of guessing what nominees to put forward. You're asking more from the voter, and it's information that the voter has: what their own preferences are.   + +If, in a primary, the voters are not engaged and not paying attention to the head-to-head polls, then their vote can be manipulated by whatever media they are consuming. The polling data itself could even be inaccurate. A better voting system wouldn’t ask you to find the middle from the polling data. It would find the middle for you. + +Now, the voting system is smarter, and the voter also needs to get smarter. The voter needs to do more work because now they have to consider every candidate. With primaries, we got away with doing our civic duty pretty easily: we voted for one candidate and said, “Well I voted for that guy, that's all I need to do.” Now it’s your duty to consider all the candidates. + +### A Brief Intro to the World of Voting Methods + +{% comment %} Maybe move this to Vote splitting discussion {% endcomment %} + +There are also other ways to count ranked ballots and there are other kinds of ballots that avoid vote-splitting. Scored ballots avoid vote splitting. A scored ballot allows you to give a rating from 0 to 5 on a candidate. A specific kind of scored ballot is an approval ballot, which allows you to rate on a very simple scale: do you approve or not? And simply because you are no longer limited to vote for a single candidate, you can vote for that candidate without feeling like you’re splitting with other groups of voters that support other candidates. Why would you split if you are not forced into the dilemma of choosing one? + +I think scored ballots aren't quite as good as pairwise because the voter has to watch the polls to decide which candidates need their vote. This is a little more work for the voter, but in the end, it works out pretty much the same as counting by pairs. + +Also, up to now, we haven't called our voting system by its proper English name: First Past the Post (FPTP). FPTP is another name for our choose only one voting method. Let's compare these methods in the simulation below. + +{% comment %} How do I actually show vote splitting? {% endcomment %} + +{% comment %} Todo: Need to add +Primaries {% endcomment %} + +{% capture cap10 %}drag {{ C }} under {{ A }} to create a spoiler effect.<br> then compare these four different voting methods:{% endcapture %} + +{% include sim.html +title='More Voting Methods' +caption=cap10 +id='election31' +id='img/election31_spoilers.gif' +link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSXWsbMRD8K4eelaLVt_0WFwJ5KDi28cvZD6orHCcXy1zsgCntb-9KU0OhBINHe7uam5m9n0KJad8TkSQz2cqeguNTrCftpTa03UpBbcYaSVbV2oipksKKqfithBSulZ6HuBcYlPzvx534aWfyaYdU46aqoNUaNRSQBUAAeQArIMvI7zMMTE5SaCbih5qJNIMGgEZbjLh2QYNGB1QR1aRdMKoppZoBtYaBIGPQhyDDTD0nikGPFvgMvJLU3LLqry1782f17cCEfZux7br9l9L6JtgG7AESLYw6JOaobmcjFuWahm6RXjab4-wy7vPYzdJ45OpbTudu-Zx2r1ys835_yN36MAyJy_l4eCvdbPbE59Vz7h7KZeyWOb2X4_tG1JUjQAfbDgE6B0CADuoc1uAQoFcAapMe4XmswbsWjWGDHhQezvykQYCzgLtBAwwAwQcoCPVbFF_L8UcZd_nMeh_mqznD42LN__en01g-0sBHc6fviHG5ul80qP3lroxZ1G85gC7ediaNtPU5oo4KgG8qwkyEmYi1RQeAnwi-CFux2voSpPiehqGcV9dT5p3Nh8uYhsP5Kn79ARKuovWdAwAA)" +%} + +Another method you've likely heard of if you are reading this page is Ranked Choice Voting (RCV), which is a new name (as of the past 20 years) for Instant Runoff Voting (IRV). It is also a name that is used for Single Transferable Voting (STV), which is different than IRV, and so you have to tell from the context which method people are talking about. IRV avoids some vote splitting by using a process of elimination. It's worth getting into on its own page. + +Read more about: + +* [Approval Voting](approval) +* [STAR Voting](star) +* [Instant Runoff Voting, IRV (Single-Winner RCV)](irv) +* [The Single Transferable Vote, STV (Multi-Winner RCV)](stv) +* [Condorcet Methods](condorcet) + +This is just a brief introduction to the world of voting methods. After you're finished with this page, see the other pages on this site for more explorable explanations of voting methods. + +Game Theory: The Competitive Pressure that the Primary Relies on has a Flaw and Only Works in the Best-Case Scenario +-------------------------------------------------------------------------------------------------------------------- + +Primaries rely on competitive pressure to get good results for the voters. If the scenario changes so that the competitive pressure breaks down, then the results are not good. + +### Game Rules + +Say we have a set of candidates in two primaries, {{ A }}{{ B }}{{ C }}{{ D }}, arranged in a line, like in the drawing below. There are some candidates more towards the middle that would better represent all the voters: that’s {{ B }} and {{ C }}. Ideally, in the best-case scenario, {{ B }} or {{ C }} would win. That's what we want the voting system to do. We also want {{ B }}&{{ C }} to actually run. We want them to not think that they will cause any problems for their party by splitting votes or in any other way.  + +This is kind of a game. There's two players: the {{ A }}{{ B }} party and the {{ C }}{{ D }} party. A party takes an action by nominating a candidate. The party is happier when their candidate wins . They are the least happy when an extreme candidate of the other party wins. This defines what is known in game theory as a normal form game. + +{% capture cap20 %}The {{ A }}{{ B }} party and the {{ C }}{{ D }} party are the players. The action is nominating a candidate in the primary. The outcome is who wins the general election.{% endcapture %} + +{% include sim.html +id='game_setup_sim' +gif='gif/game_setup.gif' +link='[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRMWoEMQz8i2sXlizJ3n1FinTLFhdIEVhICJfiCPl7JM2FIxyHixntaMcj-bu0sm6bjkrKe92IyZkFm8GmM7alkrV9r4WiecqfwOyMQuhlbbVIWakWTW7eyvXueO9wpdW748p8qCwPFWp5HUW0KBklApEAEIkM4AFIHP26gCW_csuKKWdgt2EH2DAGY7fpDrDhgWqiWvKH3jIoxUoohY5AvUNHoO5Om-83TrQaRDh2DEu-ru6iwDJmFbpRvtEOs6CSDvLfXizjy8AjIbAseCysTxFWMbb23IVe31MBGFsHWmbepBjbGgDLM4xsWJ5pJo1RDBaGBIbFDyQY-HdwwsvpON7Pz5eP17KWp-Pr83S8nS_l5xcgCQz0sAIAAA)' +title='Game with Two Players' +caption=cap20 +comment='basically, four candidates, ABCD, in a line with +Primaries as the voting system. A is center. D is center. B and C are moderates' +%} + +### Strategy + +Let's focus on the {{ A }}{{ B }} party. Let's draw a table that shows how happy the {{ A }}{{ B }} party is with each outcome. These numbers are called the payoff or the utility. For this example, we have assigned these numbers for the {{ A }}{{ B }} party: {{ A }} {{ B }} {{ C }} {{ D }} -\> 4 3 2 1 . We're assuming that all these candidates are considered based on their position rather than any other qualities that would make them appeal to a broader base.   + +The payoffs only exist in the way that they affect the decisions each group makes. It would be easy to add these numbers up, but it’s hard to make sense of them. To really get into this idea, you’d have to think about things like how society should work, and that you’d like all voters to be treated equally. It’s an idea worth getting back to. For now, you can just understand that a voter would like the outcome to be a bigger number. + +**{{ A }}{{ B }} Party's Utility (or Happiness) for Each Outcome** + +| Outcome **→** Utility | +| --------------------- | +| {{ A }} **→** 4 | +| {{ B }} **→** 3 | +| {{ C }} **→** 2 | +| {{ D }} **→** 1 | + +Now, let's take an action (in the language of game theory). Let's have each party pick a nominee. What happens if the parties pick {{ A }} and {{ C }} as the nominees? {{ C }} is closer to the middle so {{ C }} wins in the general election.   + +{% capture cap13 %}The {{ A }}{{ B }} party nominates {{ A }}. The {{ C }}{{ D }} party nominates {{ C }}. {{ C }} wins the general election.{% endcapture %} + +{% include sim.html +id='action_1_sim' +gif='gif/action_1.gif' +link='[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRMWoEMQz8i2sXlmTJ3n1FinTLFhe44sAkIdwVR0jeHlkT2OI4XIykkccj-TuVtG6btkzKe96ok0d933OiSfQaad6Y2SOahKS15FTTSjlpxOatnB-O9zZnSn44zvSnzPKUoRLP0bQ2U0YKQ1QBsEQGcANUHf05dliiyq7jRaaYgTk4hgxjMHYZcYAMN2Qd2RIXZK5u2hKKssCOCFjYEdfZXO_3_TZGyjTbDQ3QFIw7l1ULJp8hHSEfoYTYFKk1blc9KhbGa8P3wGpd8E1YnMKoYmCV2IL-_6QCMLA2tPR4RTGwFQDWZhjXsDbTcCluxCBhcGBYeYODhruNA95OY3xcX--f57Sml3H7Oo3L9Z5-_gBbft_olgIAAA)' +title='Action Example' +caption=cap13 +comment='not sure if I need this' +%} + +Let’s look at another example: what happens if the parties pick {{ A }} and {{ D }} as the nominees? It could be a tie.  + +{% capture cap14 %}The {{ A }}{{ B }} party nominates {{ A }}. The {{ C }}{{ D }} party nominates {{ D }}. There is a tie in the general election.{% endcapture %} + +{% include sim.html +id='action_2_tie_sim' +gif='gif/action_2_tie.gif' +link='[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRMU4EMQz8S-oUsR072X0FBd1qi0OiQIoAobvihODtOB50W5xOKcbOJJMZ5zuVtG5b40xqe964zqrve040iV6inQR5pZOQtJacalopJ43a_Cjnu-VnmzMl3y1n-kNmechQiedoWpsto4UhqgBYIgO4AaqO_hw7LLHLruObTJGBOTiGDCMYu4w4QIYbuo5uiQsyRzdtCcW2wI4IWNgR19lc7_f9Mka6Ic1rhoPQFsQWJ2rBBGZJR8lHKSE6RWqN21WPHYsAteGbYLku-C4MUGFYEVwlpqH_P6oABNeGIz1eUQS3AsD4DLEN4zO9xTBIGBwYRt_goOFu44CX0xgf5-fr52ta09O4fJ3G2_mafv4AxW1RZ54CAAA)' +title='Tie Example' +caption=cap14 +comment='not sure if I need this' %} + +In a real election with millions of voters, there won't be a tie, so let's model this tie as a probability: there's a 50/50 chance between {{ A }} and {{ D }}. How do we compute the utility? We could average the two utilities of the two outcomes. But we wouldn't get the full picture. We want to know what our risks are, so we consider both possibilities. There is another variable. It's kind of like another player. We're treating chance as another player in the game. + +I'm going to use a table to show these actions and the utility of the outcome. The table below shows the utility for the {{ A }}{{ B }} party when the {{ A }}{{ B }} party nominates {{ A }}. In each column is a different set of actions that are outside the control of the {{ A }}{{ B }} party. I used a +/- sign to represent chance. {{ D }}+ means chance favors us, and {{ D }}- means we had a negative outcome. The entries in the table show the outcome and the utility of that outcome. + +**Outcomes When {{ A }}{{ B }} party nominates {{ A }}** - Columns are {{ C }}{{ D }} party and chance. + +| {{ D }}+ | {{ D }}- | {{ C }} | +| ---- | ---- | ---- | +| {{ A }} **→** 4 | {{ D }} **→** 1 | {{ C }} **→** 2 | + +We can extend this table to consider when the {{ A }}{{ B }} party nominates {{ B }}. We add rows for each action the {{ A }}{{ B }} party can take. + +**Strategy Table for {{ A }}{{ B }}** - Row is {{ A }}{{ B }} party, columns are {{ C }}{{ D }} party and chance. + +| {{ A }}{{ B }}'s Nominee | {{ D }}+ | {{ D }}- | {{ C }}+ | {{ C }}- | +| ---- | ---------- | ---------- | ---------- | --------- | +| {{ A }} | {{ A }} **→** 4 | {{ D }} **→** 1 | {{ C }} **→** 2 | {{ C }} **→** 2 | +| {{ B }} | {{ B }} **→** 3 | {{ B }} **→** 3 | {{ B }} **→** 3 | {{ C }} **→** 2 | + +### Playing the Game + +Say you’re the {{ A }}{{ B }} party. Let’s look at your strategy table. You have several outcomes that you can’t control on the columns. and the row is your choice.   + +Which candidate should you choose? If you choose {{ A }}, then you'll either lose to the more moderate {{ C }} or you’ll have a toss-up between your most favorite and your least favorite, {{ A }} and {{ D }}. If you choose {{ B }} you’ll probably come out on top with a more moderate candidate. So you probably should choose {{ B }}. If you really don’t know what the other side is going to do, you could just add up your scores for each case. + +**Utility Table for {{ A }}{{ B }} - Averaging out Chance** + +| {{ A }}{{ B }}'s Nominee | {{ C }} +/- | {{ D }} +/- | +| ------------ | ----- | ----- | +| {{ A }} | 2.5 | 2 | +| {{ B }} | 3 | 2.5 | + +**Strategy Table for {{ A }}{{ B }} - Averaging out Chance and the {{ C }}{{ D }} Party's Choice** + +| {{ A }}{{ B }}'s Nominee | {{ C }}/{{ D }} +/- | +| ------------ | ------- | +| {{ A }} | 2.25 | +| {{ B }} | 2.75 | + +The same strategy works for both sides, which means that there is a competitive pressure. Both sides should choose a more moderate candidate. And knowing what the other side is going to do affects your strategy. You’re even more convinced that your only chance to win is to pick a moderate. Both parties are concerned with picking someone electable. We talked about this earlier: electability is a consideration of whether the candidate you choose will win the general election. + +We can make the same table for the {{ C }}{{ D }} party. {{ D }} is the party center candidate and {{ C }} is the moderate. You can be lucky (+), or you could be unlucky (-). And you can even use the same values in reverse. You have {{ D }} {{ C }} {{ B }} {{ A }} in order from best to worst and I used the numbers 4 3 2 1 again. + +Side Note: They could be any numbers just as long as the candidates are in this order. For example, the numbers could be 8 6 1 0. By choosing 4 3 2 1, I actually made this a zero-sum game which means that, in a slightly wrong technical sense, no candidate is better than any other candidate. I don't think utilities can be added in this way, but if these were a divisible good like dollars, then this would be a zero-sum game . The numbers are really just here as useful tools for making comparisons for a single player. Maybe you could extend the idea if you had two outcomes being decided. + +**{{ C }}{{ D }} Party's Utility (or Happiness) for Each Outcome** + +| Outcome **→** Utility | +| --------------------- | +| {{ A }} **→** 1 | +| {{ B }} **→** 2 | +| {{ C }} **→** 3 | +| {{ D }} **→** 4 | + +**Strategy Table for {{ C }}{{ D }}** - Row is {{ C }}{{ D }} party, columns are {{ A }}{{ B }} party and chance. + +| {{ A }}{{ B }}'s Nominee | {{ A }}+ | {{ A }}- | {{ B }}+ | {{ B }}- | +| ------------ | ---------- | ---------- | ---------- | --------- | +| {{ D }} | {{ D }} **→** 4 | {{ A }} **→** 1 | {{ B }} **→** 2 | {{ B }} **→** 2 | +| {{ C }} | {{ C }} **→** 3 | {{ C }} **→** 3 | {{ C }} **→** 3 | {{ B }} **→** 2 | + +<!--Repeating, maybe take out--> + +Will the parties actually behave strategically like this? Well that would be the best-case scenario, but it may not happen. We would like voters to behave in this strategic way because, optimistically, there would be this competitive pressure between the parties. Having that competitive pressure would encourage more moderate candidates to run. Each party has to consider if the other party is going to challenge them with a moderate candidate, and **in this best-case scenario, we have each party putting out a candidate that would best represent everybody, not just one side**. + +Voters can try to find out what's best for everybody by looking at the polls. Specifically, there are head-to-head polls done each election that put one candidate from one side against one candidate from the other side. If a voter sees that the candidate they like best would lose to a candidate from the other side then rationally, if they want to have an effect on the results of the election, they need to support someone else. They need to support a candidate that can win: a candidate that is electable. <!--\<third time mentioning electable\> \<maybe take out whole paragraph\>--> + +Let's show this competitive pressure with a map. If a candidate enters the race in the right position, they can win. The map below shows those positions in grey. This win region is only on the side with the losing party because the winning party doesn't need a new candidate and won't vote for them. All the pressure is on the losing party, which is kind of common sense. If you're losing, you need to try harder. Even existing candidates will feel this pressure to move toward the middle. + +{% include sim.html +id='new_can_win_circle_sim' +gif='gif/new_can_win_circle.gif' +link='[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRQU4DMQz8SuVzDrHjOLv7CMSBC9rdQ5F6QFpRBOVQIXg7jgdUoarKwXbGmYzHn5RpmmeVxFXXNA-jJ-KJZE2sY8_MQW3rmoh77_DXK1I84w4UmnIipYkT1cjNWyVdHe9tjuR0dRwZbiLjTYRzfMddWi8FJQSxIkASW-hjF8Dq0b_rYQxQclTCqJxGPIBGMJg4TfFguGyoBlRjPCg5hHK3BExF4m0pwCGoONPMKU5vNYBgLBiW3a7ioIKyz6p8SeWSFpD1VINB_9Mr5taGJUGwjlgW7KvOTN8LPR7el-Vld3fcLUR-DRsqxNff_VYEsFaw1iF-rrDBMgJHi2EnBjOthvI-msFJgyLDIhoUNbxtsO9pv23H08P59eA677ePt_32fDrT1w_cKFitvwIAAA)' +title='Candidate Pressure' +caption="A new candidate can enter the contest if they're in the center. (when there is competitive pressure)" +comment='FPTP+primary with win map for new can, same setup as before' +%} + +(That's all the game theory you need for this article. Later, you can see more [game theory through this class on youtube](https://www.youtube.com/user/gametheoryonline). There are some quiz questions that are only on the Coursera site.) + +### Problems with this Game Model + +#### Other Voter Behavior + +Primaries will only work if everything goes according to this plan. You can’t rely on all voters to behave in the same way. Voters might consider other things than electability. Some voters don't pay attention to the polls. And you could imagine some voters don't know what is best for everybody. There is also a rational argument for choosing your favorite even if they are not electable because maybe your focus is not to affect the election that's happening now but to affect an election that is going to happen in the future or to affect decisions that are made by your party in a convention. + +#### Honesty + +Or you may feel that you can't betray your favorite. Emotionally it is hard to vote strategically because you know it's your duty to vote, and you'll be abandoning the people on your side that voted for the candidate that you like the best. They stood up for the right thing, and maybe you should do your duty to report your opinion so that we can all make a better decision together. But you are pulled in another direction by a candidate who also needs your vote, and although they don’t agree with you on as much, you choose them over your favorite because they agree with you on a lot of things, and you can’t betray them as well. You’d rather not betray anyone. + +{% include sim.html +id='honest_primary_sim' +gif='gif/honest_primary.gif' +link='[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRu27DMAz8F80axIco21_RoZvhIQUyFDDQokiHoOi_l-QlDYIg8HCkjzrdUT-llWVdTSt12-pKTNdqiqp7xSxe6bbVQjE8-DLC8k9IWVotWhaqpWdtPsr14fPZ4UyrD58z01NmfspQy-sorEXLaGGIFABLZAA3QOro1wXM-ZdbdkyZgV2GHSDDCMYuIw6Q4YFuQjfnAWlplGIlUBIGCHgYEldafb_xxaiBhKIgLPm6xEmFZGRVupV8KwViUWoq6L28WlrQgUeCYZ3xWFhfJwBid5jtl_fsAMTuA9yUN3XEtgZAZMMbGJZnPZ1GFIOEwYFh8QMOBs4OrGvg7Nth3z9Or-fPY1nKy_79ddjfT-fy-wf2gqrYtwIAAA)' +title='Honest Voters' +caption='Honesty is not the best policy. Strategy would have helped elect the best candidate.' +comment='example here with honest voters, just like the two player game example, but people voting honestly and losing for their party' +%} + +#### Asymmetric Information + +Additionally there's another concern in this strategic scenario. If one party knows the other party's choice, then they can adjust their strategy. In particular, if one party knows the other party chose a party center candidate, then suddenly a lot of candidates become electable that are not moderates but closer to their own party’s center.  + +Here's an example. You have {{ A }}{{ B }}{{ C }}{{ D }}. Now say {{ D }} is the {{ C }}{{ D }} party’s choice. Then the game table gets cut in half. Which candidate should the {{ A }}{{ B }} party put forward? Say there's a candidate between {{ A }} and {{ B }}: call them {{ E }}. Say {{ E }} has a 3.5 utility for that party; that's better than {{ B }} at 3. The {{ A }}{{ B }} party would choose {{ E }} because {{ E }} and {{ B }} are both electable and they might as well get something more. So {{ E }} is chosen, and {{ E }} wins the general election even though they are not the best candidate for everyone; {{ B }} is a better candidate for everyone; more people prefer {{ B }} over {{ E }}. That's a problem: competitive pressure can go away when a candidate is chosen by the other party. You end up with a candidate that is not good for everybody but just good for one party. + +**{{ A }}{{ B }} Strategy Table When {{ D }} is Certain** + +| {{ A }}{{ B }}'s Nominee | {{ D }}+ | {{ D }}- | +| ------------ | ----------- | ----------- | +| {{ A }} | {{ A }} **→** 4 | {{ D }} **→** 1 | +| {{ E }} | {{ E }} **→** 3.5 | {{ E }} **→** 3.5 | +| {{ B }} | {{ B }} **→** 3 | {{ B }} **→** 3 | + +{% capture cap11 %}{{ D }} is certain, so {{ E }} is chosen even though more people would prefer the moderate {{ B }}.{% endcapture %} + +{% include sim.html +id='asymmetric_info' +gif='gif/asymmetric_info.gif' +link='[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRu24DQQj8F-otlseyd_cVKdKdrnCkFJFOSRTZhRXF3x6WseXCsrYAdmAY4JcqLevatbD1rays4bUanlQLj8df9UCnbSvEt-QEhJESgNJSCxktXKil75Eq5eFFbg-klocXyPQUmZ8iXLMdD2kjFIQQxAYDSewwISCUE0e7Yeb8lZqRcM4gQSNhQCMYTIJGw4BGOqIJ0ZwFWlMoj5VwAipZqwocgjSYVi7xf_k87TuFlyWOJDArhuaixQI0UI-Zje-u3F0F6XAtGezW5vrrKcU6jgXhNuNoWGOD6IbxG0S3610bDMZvHdiUnRrG9wqDJTpu4Viit1SqIcRB4VDgOECHgo7ajrW9Hfb96_h6_n6nhV72089h_zie6e8fixjlGbgCAAA)' +title='Information Failure' +caption=cap11 +comment='remove C, add E between A and B. In reference to ABCD in a line.' +%} + +#### Lopsided Districts + +The same scenario plays out when one party is dominant: when we don't have a 50-50 split between parties. There is no reason for a party to back down from nominating somebody who they think they would be best for their party without thinking about what's best for everyone. And if nobody is considering you then you don't have any representation, because choices are made without you. And in this scenario where there is a “safe seat”, the losing side doesn't get any representation.  + +{% include sim.html +id='party_dominance_sim' +gif='gif/party_dominance.gif' +link='[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRu2oDMRD8F9UqtE-d7itcpDtcOJAicBBjnMIE_3tWOwQTjFExuzer0czeT2ll3TZfKlk71o2Eo-KoWPrftyFZHWuhOZyMjmB85HAQUtZWi5aVarGsPUa5Pp2Y7cG0-nSCWV4y4yVDLZ-jaW22jBaGSAGwRA4IA6SB8dyEka65ZcehwwEMgAwjGIeMBECGO7oF3cgL0tIozZVQEsJ5VwQ8DEkobVTzzFEHCUVBWKqS61JIzqxKj5IfpeSqp5BqKuh_efW0oB0_CYYVsQ3rM5g1xDaYNZg1wyRiWwe35EuG2N4AlJOOf-BYnls6lTDikHA48JHQ4aDjbse63k_7_nV9u50_yloO-_fltH9eb-X-C08n7YKwAgAA)' +title='Party Dominance' +caption='One party can leave out any consideration of the other voters and choose a candidate from their own party center.' +comment='The big party picks a candidate in their own center, since all their own candidates are electable.' +%} + +<!--next color background for each big section--> + +The Primary Does Not Eliminate Vote Splitting: Choose Only One Splits Votes in the Primary Too +---------------------------------------------------------------------------------------------- + +<!--Alt title = Primaries only work in the case where there are at most two electable candidates in each primary.--> + +Let's finally get back to vote splitting. I think we've saved the best for last. + +So far we have only looked at primaries in which there are at most two electable candidates, but that’s very limited, so I’m going to show you that when there are more than two electable candidates, primaries won’t work. Primaries don’t work because they don't address the major problem in the way we vote: they still ask you to choose only one, so they still split the vote. + +What happens when you do have vote splitting? How do people strategize in that situation? What kind of negative behavior do they show?  + +What happens when there are more than two electable candidates is that similar candidates hurt each other. See the election below with the additional candidates {{ E }} and {{ F }}. {{ E }} and {{ F }} are just as moderate as {{ B }}, so there's no strategic advantage for either one, and voters only care which one they are closest to. In other words, there's three electable candidates. {{ B }}&{{ F }} share the same space at the bottom, so {{ B }} might say to {{ F }}, “Please drop out, and I'll help you out later.” You don't do favors for nothing right? That puts {{ F }} into kind of a bluffing game if he stays in. A bluffing game is a game where you try to get a good outcome by threatening to hurt another candidate, and where if you follow through with the threat then you end up hurting yourself, too. + +{% capture cap19 %}{{ B }} gets squeezed out, even though {{ B }} is the best option{% endcapture %} + +{% include sim.html +id='center_squeeze_primary_sim' +gif='gif/center_squeeze_primary.gif' +link='[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRMW4EMQj8i2sKAwav9xUp0q2uuEgpIq2UKLoUpyh_D2ZyuWJ1cgEYGIbhu9SybpsvxFZPtLFKeBKeaL_9sRFrelKJe3oLZ92JCifA-G-LYuOZ0LJWKq2sTMXS9ygVOryo7ZGpdHiRWR5mxsMM1xzHk9oMBSEIcYMBJXaYIMAtbIzzMCN_JXDiUzh3kICRMIARLCYBo2EAIx3Rgmhkg9YkylMSzoRK9qoiD0IaSFuoenuz3FEAVMXCTEqNLGVrgJ47N767cncVoNNtidKOY5onndZxMJBvA4eDlAbiBgkMxO3vtgYDCawjt-Q0gwReYSCk4x4OId2SrQYRB4SDgeMIHQw6ejukeznv-_vl-frxWtbytH99nve3y7X8_AL0S0pr0AIAAA)' +title='Center Squeeze' +caption=cap19 +comment='could be titled vote-splitting. Maybe just show EBF, not ACD' +%} + +When there are more than two candidates, the voters need to coordinate to put their unified support behind a candidate. This coordination problem might involve some lying. You might say, “Well, my candidate is doing really well, and yours isn't doing very well. You should back my candidate and your candidate should probably drop out. You don't want to waste your vote.” + +You might think you can look at polls and polls will tell you who can win the nomination (a kind of electability) but there are no head-to-head polls in the primary, so you’re out of luck. Nearly all the polls I've seen are general election polls with one person from each party. The only meaningful polls I've seen have been from election reform organizations like Fair Vote and the Center for Election Science. <!--[footnotes](#footnotes)--> The only polls I have seen on the news are choose only one polls that are trying to break the news earlier about which candidate is going to win.  + +The poll numbers are important to voters so they can tell who they should be coordinating with to show their support. The voters are going to go nuts here because if they only had one candidate, they could easily show how large their group is, but because they have two candidates splitting their group’s support, the voters have less of a chance of being considered by the candidates. Now {{ B }}&{{ F }}, they have more supporters than {{ E }}, and they could win if they decide to work together. If {{ B }}&{{ F }} really feel like they are closer together than {{ E }}, then they are on the same team. Voters will also realize if perhaps {{ F }} is not getting as many votes or maybe {{ B }} is not getting as many votes by looking at the polls. Then voters will switch their vote to whichever one, {{ B }} or {{ F }}, has a little advantage. The little advantage now becomes a bigger advantage. The polls get amplified and locked in. + +The problem is those polls aren't accurate. If you put {{ B }}&{{ F }} head to head, then {{ B }} would easily win, but in a choose-only-one poll, {{ B }} can get squeezed out because {{ E }}&{{ F }} take votes from both sides. Even though {{ B }} is the center candidate, {{ B }} can lose because {{ B }} can get squeezed out. + +{% include sim.html +id='amplify_sim' +gif = "gif/amplify.gif" +link='[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRu2oDQQz8F9VbrF67d_6KFOmOKxxIEViSEOzChOTbo9XYpDDm4EaP1WgkfVOlw7axSmGXvYTFhdnTqoVX2fdCPN8sGk_6dJUOtZDl3_Pf4oGUuy_e9sjUcvdFZnmYWR9muGY7noKmK3AhiKGIIYkbIASwBUY7DVgzKsETQQkeDggaCQCNGIKeBQIa6fAWeGsW6Nwdz4VwhhU8qgDI0eDZ6Pf9PAaV4L1a_5EkaChBF50L4GIlzkA2m8zZjW-G3Ay99jfLOvOsA6W1HMM6jgXhtmbQsUaHbIdsh2zH-O4AjO8dOSzRMX6rACyx4RYNS2yeN9QQ0kDRoKDhAB0KOmq7JLwcx_g4PV8-X-lAT-P8dRxvpwv9_AFQpOx0pQIAAA)' +title='Poll Amplification' +caption='A small advantage in polls becomes a big advantage.' +comment='right now, this is not implemented' +%} + +### False Leverage + +There is a flawed idea that single-choice voting can give power to a small groups of voters through a mechanism of leverage. The idea goes that voting for only one makes your vote valuable and gives your favorite candidate leverage over the others so that your policies can get adopted. Your candidate would pull some strings to make a deal in exchange for dropping out of the election. Philosophically, the bad part about this argument is that it gives all the power to the candidate. Candidates are deciding among themselves who will be elected rather than asking the voter who is best. Practically, not all voters would be on-board to vote for their favorite if their favorite has no chance of winning. Without the voters, the candidate will have no leverage. In the worst case, a corrupt candidate could use their leverage for personal gain, so less power goes to the voter. + +**Decision for Viable Candidate** - Leverage only works if the voters follow through. + +| Action | Outcome If Voters Vote Strategically | Outcome If Voters Follow Through | +| ------ | ------------------------------------ | -------------------------------- | +| Reject | win independently | lose | +| Adopt | win | win | + +Afterword +--------- + +So, we’ve described the problems that you run into when you hold an election with primaries. And we've made some diagrams to make it easier to think about voting systems. We've talked about what dilemmas the voter Faces, what voters should rationally do, and what mistakes they could make. We talked about how we can make it easier on the voter if the voter is willing to consider all the candidates. To conclude we can solve the problem of vote-splitting, we don't need primaries, and we should move to a better way of voting that allows you to count by pairs. + +Footnotes +--------- + +### Utility + +Finally, we can think about some other ideas that we touched on. Can the numbers in the utility table be determined? There could be a balance between how much utility of voter gets from a candidate and how many points they give the candidate on their ballot. If they pretend to like the candidate more than they really do so that the candidate gets elected, then maybe there can be some risk to that. The risk might be that the candidate is the only candidate that the voter gets to choose, while other voters get to be considered in the election of multiple candidates. So there can be some trade-off between voting for that candidate with a reasonable number of points versus exaggerating your support. That means, we might be able to see what voters really think. + +<!--Link to multi-winner method page that is a future project for now--> + +### Rankings + +We said we could use utilities because it would help us put the candidates in order . But we couldn't make sense of adding utilities from different people, so we don't need to add. Grades have an order and can't be added, so maybe we could use those. But grades imply comparisons between different people, and that doesn't make sense either. Really, the most accurate notation we could use would be rankings. Rankings only have an order. That's it. No adding, no comparisons between different people. Exactly what we need. + +### Chicken Dilemma + +Even in some better voting methods, there can be a game of chicken. The game of chicken is weird. Let's consider the center squeeze example with {{ B }} {{ E }} {{ F }} again. {{ F }} voters can pull back their support from {{ B }} if they think {{ F }} can win against {{ E }}. And some of the {{ B }} supporters can pull away from {{ F }}. But if the {{ F }} supporters are overconfident, they can lose to {{ B }}. + +[The page on STAR voting](star) has a more thorough chicken dilemma explanation. Personally, I have a hard time trying to think of the chicken dilemma for pairwise voting, so I don't think it would affect voters. + +{% include sim.html +id='chicken_sim' +gif = "gif/election11_chicken_score.gif" +link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3WSwUoEMQyG36XnIk3SpO3cfAa97e5hld3TwiyLCCL67Cb9UUZEBiZNk_75kvY9lbTsdiScSfmQfaWZOsVqVN8bh0NOFDmjZ-LhgViMMTN8ZSUyJC0lp5qW9EmUctLpm5_zYHOTaPj2z69sP0_pnlLyn88j498IlVmEgo7IN82yjWwtW8-t5EaRxEiSQNun9Xzep2BwVHIDTjIYB6Xq1mlkci6cE3sZ32SaJ5hh0DBDhnUeYMhwg9fhQUXK7IMyx7SgJdASmfkCLdFoxxODXgyHoShjo1HL92DCoa3DG4UqW6dOnbotUW2WrQ03COg65qZiwkowPGFUYICrCoPWtSHWf09bMQMrMOjecDOGSZrODsSJDFoGFBvTNKA0nG1AaQKvIgaUFq8u3V-vt_X1ePHqD8_r7ZTiHTYkxGOLETZ02QsMpDuwOrA6JtZ11usg69DpAOwBeKf-UoE4tncx4i40-xeObJyn4-Wyvjy-XU8-KyB-fAE9NyDXjgMAAA)" +altlink="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VTy0oEMRD8l5wbSXenO8ne_Advyx4UvA3uIiqI6Lfb6XJxQGSYqfQjlUol81FqORyP3Cex6YmOXDux9xw5scrpRIVXjyv11REo4jEQVuI2VoOWQ6XSyqF8cS1ULGOPaVHsAfFZcb51_0R9RL3Snycq899KrLJW4KWMmdzJ50oL0tDDIYgDoIYdEHK4BcayGhBrCBUJvkgKAwRJ0AhoxHKCgEY6ooEILLrM5OUHZ1olZ6pmr4JHbfnx9fS6bYUi_hn9ZpLAQYhVdBnB1Mii1OrVlRXwPpBsA0PTfdCSp9k-5ymndZwdNtJmJg32GrZhsMMUgG2YAWCHddRgqsEOrwDOTsfZOEx1S-FxoYqDwqHAZ0KHgo65HUZ2BTTUcK79es06irhOTEJKbeWxpVEBIBwQMyBmwJ5hqXdAzwDfgKyxZN1Y3EkIm3vj5zLeyPJ8pu6Ch_ttO7_cvV8e49-4vVyez2_3W_n8Bpfywo53AwAA)" +title='Chicken Dilemma' +caption='I want my friend to win.. but I would rather win myself.' +comment='just B E F' +%} + +### Primary Variations in the US + +Let's talk a little bit about the variations on the primary. In the US, these variations serve to amplify the wins that a candidate gets in a state. One party uses a winner-take-all primary, which puts people into districts and only counts the votes for the candidate with the highest number of votes of a district. This highest number is called a plurality of votes as opposed to a majority of votes (more than 50%). The other party uses a method that dumps any votes for a candidate that got less than 15%, then rounds votes to the nearest quota, which is just a number that makes it easy to assign delegates. Then if none of the candidates gets a majority of the rounded totals, the party requires a majority of delegate votes to decide the nominee. Both parties make different states vote at different times so one state may vote based on the results from another state’s vote. All these variations amplify the wins. Vote splitting, itself, amplifies the wins. + +## Try the Sandbox + +Finally, here's a sandbox where you can use many more options to modify the election configuration and try new scenarios with more voting methods. \ No newline at end of file diff --git a/proportional.md b/proportional.md new file mode 100644 index 00000000..c06680dc --- /dev/null +++ b/proportional.md @@ -0,0 +1,257 @@ +--- +permalink: /proportional/ +layout: page-3 +title: Proportional Methods +description: An Interactive Guide to Proportional Voting Methods +byline: 'by Paretoman, June 2020' +--- + +I'm going to describe how we can motivate proportional voting from a mathematical perspective, and we'll look at simpler methods that keep the same motivation. + +[You may first like to read this page on STV to see the political motivation for proportional methods](stv). + +Then continue on to read about proportional voting methods other than STV and how they compare. + +## A Closer Candidate + +Proportional representation is similar to a well-known problem called the facility location problem. Say you’re in the grocery supply business. You want to save on gas, so you want to minimize the distance between the distribution facility and the grocery stores. So you want to minimize the sum of the distances between each store and its assigned facility. + +**Facility Location Problem** - Colors indicate facility-store assignments. Blank facilities were proposed and rejected. Northern England is pictured. + +<img src="img/facility location.png" alt="facility location" class="picture100" /> + +Let's apply this idea to voting. Above, we're selecting multiple facilities to serve a larger set of stores. Now we’re thinking of electing multiple candidates to multiple seats in a legislature for the same voter population. + +We already used the idea of minimizing distance to find a candidate that is in the middle of the voters (on the common ground page). This is what the median does. + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA4VTu04EMQz8l9QRih9xsvsVFHSnKw6JAmklEDoKhLhvx-tJruHQaYuxHWc8mU2-U0nr4ZgTORyoUuZejvmwzICWOiLmMmvUZqTXmsmMmo6o2dwqfUQ6S7bMNQOFa5C0ck6a1nQpKaea1pKTQV9zKPnP5yvdV9KF9h0X5nSjY4kO5hotUm71-P59GtGwg5EKQAFQRAZwSaSOHalPoZzYebzIFGdxPQGgYUXmNOIAGm7IwMJLtMj4K0JRFagRGCRQI3X0GKrgkQVV3Rko__PtDXTb0asjyvca5N4IDUk6hKqFQ9pCvuLACtsq7K84b4VtFbZVRUsFwLba0NJjRF1wV0oUDSwG1wwsVuM44kIMFAYFhr0NChrFlMYAAcDxhv_f5pVsWOxB7A9mr-E4HWQdZB1COoR03IJeAdDSwdU7YJf00PzmlsifT9v2dn76en_xt_G4fX6cttfzV_r5BWjH9i2-AwAA)" +title = "Median in 1D" +caption = "Recall this example of finding the median. <b>Try moving some voters. Add a candidate (+). </b> The total area of the bars below is the sum of all the distances between the voters and the candidate or median. The median minimizes this sum. A winning candidate should also minimize this sum." +comment = "" +id = "median_1d_sim" +gif = "gif/median.gif" +%} + +A median can also be found in two dimensions by using the same idea of minimizing distance. + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA4VTwUpEMQz8l56DNGmS5u1XePC27GEFD8KCInoQcb_dpLOC4Iq8wzTJdDpN8z5ab7v9gRon7FmERMeB9hHEvOWCw4i9V0ppFk4mtuIMTQ4Xp3fizWuluS9qZZIpq5QbudaiNnLUSiatmrCTcGmJZFH5kE5G23Vq2nbt3Bs1W6HD5Uzo9OvLSmSlnbl2nEXaFca2GCK2KKNf4-T-Oo350hRBCEusADhiB6Ql1sRAmKcwNUmdTErqSIIAICOKKGVGAmRkIoKKbIsyLm8zeGUH3IyBItwMu3AcWeiMDVkthezsX19R-HpPf3RF5X_K-P8gXdb0Ylh9dUonnhsXV7TP8AyGexvaZ2P11BQUA6B9NkGJdYRtmJm-kg4VR_ccj-C2rpSj1xwSDgeOvRMOJq9TpgAGAJ2fmIP5PZoTxVjCTFI5XCcgFhALGAkYCUxDGABeAloRgLJ0M3OC-4rvj6fT0-vd-_ND_iO3p7eX4-nx9b19fgGnVnGdzAMAAA)" +title = "Median in 2D" +caption = "<b>Move some voters. Add a candidate (+). </b> Just like for the 1D case, the total area or length of the lines below is the sum of all the distances between the voters and the candidate or median. The median we chose here minimizes this sum. A winning candidate should also minimize this sum." +comment = "" +id = "median_2d_sim" +gif = "gif/median_2d.gif" +%} + +We can get distances from score voting, or approval voting, or star voting. We can just use the ballot scores and try to find the candidates with the highest scores. This is the same as finding the candidates with the shortest distance to the voters. You've seen this before on the common ground page. + +{% include sim.html +link='[link](https://paretoman.github.io/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRQU4DMQz8i885xI6dbPfMD-ht1QO0i6hUdau2CCEEb8fOlAuoymHs2J7MOJ-UaZymVhNr26SJeUg81IjMI5WIhna7E_GotM0mEccYZ8-zRl5ozImURuZE5pCoektO_443t7uV4W5ldbfCub_NISlSQQpFrAAD1K6MXQCroz9nDqteFOfxS-HeIwIAjSgypykOFZcN2YDMWcQXkbtQjp2AqYCpgKmAqTjTxOl2orliHJwFdjlJKkm9rEEbfRo2w7bKb-DEU4lA-6z-pVa41oY_glxd9UvD8owBkGqQatid4TsNpq11nTb0twymawbAcMUPVLBU61ZCYAVFhYKKtTcoaJht0pmenw6H5br-OM000uN2Oc-U6PK6vD_Ml-15f7rul6NXvt-Ou_llf5x39PUDc7xiOMsCAAA)' title='Scoring Measures Distance' +caption='Basically, scoring asks, "Is the candidate close to you?"' +comment='simple,lots of candidates, one voter' +id='score_sim' +gif = "gif/score.gif"%} + +Let's introduce now a new idea that we are also going to make assignments. Each voter will be assigned to exactly one candidate out of the set of elected winners. Only the scores they gave to that candidate will matter for the optimization. You maximize the scores that each candidate gets from only their assigned voters. That means a candidate can get closer to their assigned voters. This is very similar to districts except the voting system is doing the hard part of dividing the voters into groups. + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA61WbU_bSBD-K4ulOxJlZfxuJxDQXUulSld6arjrhyRCjrMhlow39W5ouZb77Tez45cEQvlygmpfZuaZZ2YeL_1uOdZoOg0c7obOnE8Tj3tuCBvXcbnrm13ocM_xcDcccjdIYOcFAfc8vPPiGELd-ZxbLiLFgBRhmBeG3I1jNPjWyOFWYI2sfz3X4lZozhG4gzGGxeHPfsCSvGgZvmhxHYPtIhefxzxAg5ugxSMLkXEDWoiKG9ECXNwAVsgcwwJpXG55AAmXwHzkweLRQjBeQC4A48NCMF5Mp4ROQxPgO4azi-1wjcH3TKzvk50I-YA0hc53PxgQkQvh-lS9yz3u84CHPAKXgOCxBYHbbb1uC2mmnoELAoMTHEoVRIZUENPIqISAGhFSb0OiH1IjQqIfUiPCkBZqRBiTLTH5QmpE5NDiGs-IphJRO6PQ8PWBSEQQETGIhmaJiUFMsTE1MPZpoQbGxCBGdVmT679BbuLLNi3epVle5PrhD5mlOpcl3H-QZSUFm4gvbGKhEmMKTXb7i_dUfuLQQskTIp4Q8SQwRSUhLcQ9IbyESkiwBDsE8VIRw25MQxxTyEPc-t2WMIdU0JAwh_HTodWDW6RFIfX1w0bAZzbJZCWgwkwuxeUy17K6Ft80fn8za7UtM2xAb5krXeWZ5uwO3ArO5AbvVf_7bFaaX13fsDG7EYUwYW_FKt0WWvUab_K8Tyu2kUWhIQ-6L8RtXl7WMV0qylSH8tlsZimkipsaqBCaZanJ2UTZSqe3Qk1NMB3mNvgs82WqhWrZnpywa-jCA9NrwdLNppL3aaHYr-wWML_mZSmqo44tNQzzEO691KKaCG2D9-9ke1sTaPn321yIoE0yKPYfUUmTuYfM60JWsup1eZhcNRnr_uodn7YYlpe1m20as-sMvybjtPWes8F4333HdtoGPtLusSHf8L9Lv5koKCHcM1CvzNgzuS31Z3Pumez9084PEoIU0I12vTqQxryjjEwWsgI_crPNsc2Yr1gjJlvlS7FIqz7rNEiDfZMaj_aqowmwddqps1O0GRCJEdWF_zqjMQzIcqY2acmyIlVqfKzuoMbj831vJIg3v221xJWNG82kcPMnqL7PGsTmG3gh1-KcOp4q9gtKAkYAIUrli0KM2NnJ4vxsUR1IT-mWpLI367TSTYv0fhbdOtC4OCqy_uyaefNaiHYhylu97h_CIbZPqTwyUSixn7nRcD52Tll-hvlqYDgOBvsCbvQAU0HHaT638-XpE4-GAxWdZ_CAZP0BMjlWzFQwMvwGNxtRZaLU72R1l-qdB63-TubspBU5ATwvqfs-9nb5qndU6-tHo6-6rPOxt18UyPPqI_v8_urq8tPFEfv4iV2_v7w4ujg61NgbnQvc00h5Dd0_fQK4I10QDeChNhjSZ00NbMD2lXZoOj-f5-FmEydqGFuDVPE9Xee3a6E09Z8zJW3b_imgyXiCH9crPq-CLJjSD4UYH5t3Y2RGb7akCRM9IP5leif-2oAs9oowvYP5TJ7ledrp_y3Ta6Pafd7qV7EmsuPYyrF5JttnQG68PjNP3FrgAd9gJStNf4XoobZVkWeiB_9577-E0GRuQeod-VdCb6uydkJKjzPLevwPfvBbaDIMAAA)" +title = "Equal Facility Location Problem" +caption = "Similar results to STV in broadness of support." +comment = "Maybe choose a more interesting example." +id = "facility_sim" +gif = "gif/facility.gif" +%} + +That was actually a lot of computation, but the good thing is that this problem is already solved. There are techniques called the branch and bound method and the Simplex method that are used together to solve this problem. Basically, branch and bound means you can look at all the possibilities without calculating every one. You put them into groups called branches, and the best-case scenario for that branch is bound by some value. Then you can cut branches off and simplify the problem. + +If you would like to know more about branch and bound and Simplex, they are part of a more general set of algorithms called mixed integer linear programming. These tools are used in the business world in what is known as the field of operations research. They deal with logistics - physically moving around things, like groceries. They also apply to our case of electing representatives to serve voters. + +This is a good method to inspire other methods like the Single Transferable Vote, STV. Voters might be more willing to accept STV because they can see the calculations being done. Also, STV gets pretty close to the motivating idea of the facility location problem, and if you compare their visualizations, they look very similar. There are more methods like the Monroe method that are also very close to this motivating idea of optimizing assignments of voters to the candidates they scored highest. + +## Sequential Monroe Score Voting + +Here's a simple method to get a good solution to the Equal Facility Problem quickly. First, pick the winner with the highest score among a quota of voters. Then count only the remaining voters who weren't in that quota. And repeat. The unofficial name for this is Sequential Monroe Score Voting. It uses the same concept of the Equal Facility Problem but uses a simpler technique to find a feasible solution. + +Also, I'm trying out a visualization of utility, so click the "+" next to "utility chart" to see it. Basically, the idea here is that each voter's utility is best measured by the utility of the closest winning candidate to them, so I chart that with dark grey dots. + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA61WbW_bNhD-K4yALTZMKBL1aidOsLUpUGBNhzpbP9hGIMt0LECRXJFOm7XZb98dTy924jRfBjsgxbt77rm7R3S-W441mk59h7uBM-fTWHDhBrBxHZe7ntkFDheOwN1wyF0_hp3wfS4EnokoglB3PueWi0gRIIUYJoKAu1GEBs8aOdzyrZH1r_AsbgXmOQR3MEawOPzZByzxi5bhixbXMdgucvF4xH00uDFaBFmIjOvTQlTckBbg4vqwQuYIFkjjcksAJBwKgBSwCFoIRvjkAjAeLAQjInqK6WloAjzHcHaxHa4xeMLEeh7ZiZAHSFPofPfBgJBcCNej6l0uuMd9HvAQXHyCxxb4brcV3RbSTIWB832D4x9K5YeGlB_RyKgEnxoRUG8Doh9QIwKiH1AjgoAWakQQkS02-QJqROjQ4hrPkKYSUjvDwPD1gEhIECExCIdmiYhBRLERNTDyaKEGRsQgQnVZk-u_QW7yyzbJ3yVplmf64Y8yTXRWFnD-oSyqUrKJ_MImFioxotB4t794TuXHDi2UPCbiMRGPfVNUHNBC3GPCi6mEGEuwAxAvFTHsxjTEMQU8wK3XbQlzSAUNaTDD6OnQzAdRFkmel_r6YSPhRZukZSWhxrRcystlpsvqWn7T-AbOrNW2SLEFvWWmdJWlmrM7cMs5Kzd4rvrfZ7PCfHV9wsbsRubShL2Vq2Sba9VrvMnzPqnYpsxzDXnQfSFvs-KyjulSUaY6lM9mM0shVdzUQLnULE1MzibKVjq5lWpqgulhboPPMlsmWqqW7ckJu4YuPDC9lizZbKryPskV-5XdAubXrChkddSxpYZhHsK9L7WsJlLb4P072d7WBFr-_TYXImiTDIr9R1alydxD5nUhq7LqdXlYuWoy1v3VOz5tMSwrajfbNGbXGb4m47T1nrPBeN99x3baBj7S7rEh3_C_S76ZKCgh2DNQr8zY03Jb6M_muWey9087P0gIUkA32vXqQBrzjjLSMi8r8CM32zy2GbMVa8Rkq2wpF0nVZ50GabBvEuPRHnU0AbZOO3V2ijYDIjGiuvCvMxrDgCxnapMULM0TpcbH6g5qPD7f90aCePLbVpe4snGjmQRO_gTV91mD2LwDL-RanFPHE8V-QUnACCBEqWyRyxE7O1mcny2qA-kp3ZJU9madVLppkd7PolsHGhdHRdavXTNvXgvRzmVxq9f9QzjE9imVRyZzJfczNxrOxs4py84wXw0Mj4PBvoAbPcBU0HGaze1sefrEo-FARWcpXCBpf4BMjhUzFYwMv8HNRlapLPS7srpL9M6FVr8nc3bSipwAnpfUvR97u2zVO6r19aPRV13W-VjsFwXyvPrIPr-_urr8dHHEPn5i1-8vL44ujg419kZnEvc0Ul5D90-fAO5IF0QDeKgNhvRZUwMbsH2lHZrOz-d5uNnEiRrG1iBVvE_X2e1aKk3950yVtm3_FNBkPMGX6xWfV0EWTOmHXI6Pzb0xMqM3W9KEiR4Q_yK5k39tQBZ7RZjewXwmz_I87fT_lum1Ue1eb_WtWBPZcWzl2FyT7TVQbkSfmStuLfEB72BVVpp-heiitlWepbIH_773X0JoMrcg9Y78K6m3VVE7IaXHmWU9_gdWHeE1NAwAAA)" +title = "Sequential Monroe Score Voting" +caption = "Similar results to STV in broadness of support and easy to show calculations like STV" +comment = "Maybe choose a more interesting example." +id = "sequential_monroe_score_sim" +gif = "gif/sequential_monroe_score.gif" +%} + +### Clustering + +Basically, what these methods try to do is make clusters. One idea of clusters is that you can represent a bunch of data points by a much smaller number of data points. This is really familiar. It's basically what a legislature does. The members of the legislature are the representatives of the people. + +## STV's Quota + +(mostly a refresher from the STV page) + +STV uses a quota to assign voters to candidates in a similar way to the facility location problem. + +Once a candidate has been elected by a quota of voters, the voters have successfully used their ballot to get representation, so it is not counted again for a second candidate. + +**Refresher:** STV asks voters to rank the candidates in order from best to worst. During counting, your vote counts for your top candidate. One-by-one, the candidate with the least number of votes is eliminated and taken out of the running. **Check out the sketch below** that shows everyone connected to their first pick. Colored flow lines show some voters moving to their next choice after their top pick is eliminated. + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA61XbU_jRhD-K4ullkRZGb_bCQR0vaPSfSjXAu19SCLkOBti1diRd0OPXulv78zO2k5CgC8VnHZ2Z3aeZ2YeL7rvlmONJpPA4W7ozPjEdcEKIrCCgHvOEI-ciHuuD1bicE_74iF3PRcMz4u5G2GUFwdgeWhF6HX0WQg3Q7R8F6zEWK4XzWbcchE5BrxIh4Qhd-MYHb41crgVWCPrXzeyuBXqfQTh4IxhcfiLH_Akr3qGr3pcR-d2kYvPY46NSPDco3Oi4ga0EBHgpBdg4gawAq7rwQooLrc8yAinHmSEQ8-jhfJ4AYVAHh8WyuPFtEtoN9QXfEdTdrEbrnb4xMj3yU-MfMg0gYYe-sGrEQUTgk9tgElwnwc85BEUnHCYFhxieECg2JfA7UyvMwF84unUQaBzBm8TCCJdcBDTRKnEgBoVUutDKi-kRoVUXkiNCkNaqFFhTL5EI4fUqMihxdWRETUpoixRqKcKorQiShERg2iol5gYxHQ39mjxaaEGx8QgRvFZv20qld5kVS1AlHrzYb2uq8e0aPa_5GX-kH6D7c3tHxaqNab7yXbr8Zx6kDi0EIOE2CckliTQRSQhLVRAQvkSqiPBOuwQBE6VDLupDXFqIQ_R9DuTcg6pqiHlHDaf1P705mlRVOr2aS3gU7xOyz_FAirLqoW4XOSqqm_FN4Uf6dRabspM5VXZW-RS1XmmOHuAsIKzao3nsv99Oi31rzInbMzuRCH0tU9imW4KJXtNNEU-pjVbV0WhAAfD5-I-Ly_NnQ6KkMxVPp1OLYkzQsMkKoRiWaoxm1u2VOm9kBN9mTYzG2IW-SJVQrZsT07YLbThiamVYKkZt2Q_snvI-VdelqI-6thSxxCH8j5WStQ3QtkQ_RP5PhkCLf9-i4UZlAaDYv8WdaWRe8jcFLKs6l6Hw6plg2j6q7Zi2mJYXpowWzdmOxh-NeKkjZ6xwXg3fMt32l58Juu5Id_wB_XrW1BCuOOgXumxZ9WmVF_1vqfR-6ddHACCFDCMrJ65SGPeUkZWFVUNcRRm622LmC9ZIyZb5gsxT-s-6zRIg_2Y6oj2qKMJaQ3sxNkqWg-IxIjqwn-dUzsG5DmT67RkWZFKOT6WD1Dj8fluNBLEkw8bVeHKxo1mUjj5FVTfZ03G5ht4BWt-Th1PJfsBJQEjgCtS5vNCjNjZyfz8bF4fgCe4Bans4yqtVdMitYui2gAaF0dFms-umTc3QrQLUd6rVf9QHmK7T-WZiUKKXeRGw_nYOWX5GeKZxLAdDHYF3OgBpoKBk3xm54vTvYiGAxWdZ_CAZP0BMjmWTFcw0vwGd2tRZ6JUP1f1Q6q2HjTznczYSStySvCypO772LHyZe_I6OufRl-mrPOxt1sUyPPqC_v6-erq8vriiH25ZrefLy-OLo4ONfZO5QJtGik3qfunewm3pAuigXyoDYb0WVMDG7BdpR2aztvzPNxs4kQNYyuQKr6nq_x-JaSi_nMmK9u230yoEU_w43on5t0kcybVUyHGx_rdGOnRa5M0oW8PiH-ZPojf1yCLnSJ072A-Ny9w9jv9vyG9N6rt5828iobIVmArx-aZbJ-Bau31mX7iVgI3-AbLqlb0V4gealsWeSZ68B-B_msZGuQ2ibEovhZqU5cmCCk9Ty3r-T_heTuKhwwAAA)" +title = "Quotas Give Proportional Results" +caption = "Here's two groups with a 1:2 ratio (really 4:7). The winners are also in the same ratio." +comment = "Maybe choose a more interesting example." +id = "proportional_two_to_one_sim" +gif = "gif/proportional_two_to_one.gif" +%} + +### Visualization + +(refresher from STV page) + +Notice the chart that shows a visual of the process of elimination. It starts at the top and each row tracks who the **voter's** top pick is. Each column is a voter. Transparency is used to represent the excess vote that remains after a quota is filled. As candidates are eliminated, the groups of voters become visually apparent. + +Below this chart are a couple of charts that are a measure of voter power. They track the weight of the each voter's contribution to a candidate's election. When a candidate is elected in a round, the voters whose vote counted for that candidate are added to fill up the chart. The intuition is that the voter could have voted for someone else, so the candidate owes them some share of their power. + +In the first chart of the "Voter Weighting Used by the Method", the exact weights used by the voting method to select winners in each round are shown. To choose the final winner, the election is similar to a single-winner election. All that the last guy needs to win is 50% of the remaining vote weight. In the background is a dark bar with a height that corresponds to a vote at its full weight. As candidates get elected, the bar is covered. Any part of the bar that is still showing after all candidates are elected shows votes that are still not counted. + +In the second chart of the "Voter Weight Contributed to Candidates", the total weight given to each candidate is rescaled so that it is equal for each candidate and sums across candidates to the full amount of representation available. In the background is a dark bar with a height that corresponds to every voter contributing equally to the election of the candidates. The height of this bar is each voter's ideal share of representation. + +(Also, here are a few more specifics about these charts. The voters and candidates are arranged in a line by using an algorithm that solves the traveling salesman problem to keep voters together who are near each other in 2D space. Specifically, the ballots are used as coordinates or feature vectors since this 2D space isn't something you'd be able to see in an election.) + +### Visualization of Voter Power + +(refresher from STV page) + +Voter weight contributed to candidates is not exactly voter power. Power is a collective phenomenon. + +Think of single-winner voting methods. The winner is most representative of the median of the group. The median is a collective measure. If there are two sides and both are competing for the median, then both sides are represented. To see why this is the case, consider a case where the election is not competitive and the median belongs to only one side, then all the voters on that side benefit from that power, which is not very representative. The most representative election would be a competitive election where the median could be on either side. + +In STV, there are multiple "medians" (more like percentiles), one for each winner. These medians can be spread out over a larger region than for the single-winner case. This means there are more ways to be part of a group that wins and candidates have to pay attention to more voters. + +Also, consider what would happen if, after the election, a candidate shifted their position toward the center of the group that elected them. They would lose the more moderate voters that voted for them when the next election comes. + +------ + +***Draft*** - the first part above is nice. The second part below is a draft and includes voting methods that are in development. + +------ + +## Using STV's Quota with Other Ballots + +The concept of a quota extends to multiple ballot counting methods: + +1. Top-choice-counts Ranking Methods: the Single Transferable Vote (STV), also known as multi-winner Ranked Choice Voting RCV +2. Scoring methods (including Approval Voting and STAR voting): the facility location problem, Sequential Monroe Score Voting, Allocated Score, and STAR Proportional Representation. +3. Clustering with STV, then electing with pairwise methods: I made one method that uses STV to form equal clusters of voters. Then it uses Minimax within the voter clusters to elect candidates. +4. Pairwise ballot methods. (I'm working on a version of minimax) + +<!-- SNTV? SNTV kind of is proportional--> + +The power chart is more useful for scoring methods because the voter can support multiple candidates at the same time. That means the voter is able to say that their vote counted for a candidate so that candidate owes them some share of representation. Also, the votes by round has an additional data dimension for the same reason, and it's hard to visualize, so you need to click through the rounds to see how the election was counted. + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSu04DMRD8F9dbeN--_AZ0pxSA0kUKQjQIwbez9hAUCUVXjNezOx6P77P1dth3TcpxpJ2lU0otTEh0-90RmysZnXiRwlIrPh6p8ZxOreY5nkGRs8GCOMZs0Hbo1Kwd2jdvjZqvOmqsyCzo9O8rZtxltrsM96XNDHEWlHDABsD5HIAywFZYx0VBaQs1KZ3alNLRAsEmZKRkuMDBQUYS1UAFFe3LKM8MeBEqa1YVPAxpKe1Mf99sDzRAVXFhJiElJ6sG69cMZsG3hdwWepWehS01-3-cxbJliZfCJWxbm45IHRdwROG4gCMKdwCi8AQ31mmOKKIDeHUG3iUQaPhyrGUkIBFwENuChIPEbCLCVAAiTLxpXn-qBDlucpupJa40IDggOCA4IDhsGR0OwDs8P53Pl_fHj9dT_cQPL5e3U_v6AVYwU-E3AwAA)" +title = "Score Voting with Quotas" +caption = "Similar results to STV in this three-party example. Mouse over the rounds to see the progression." +comment = "Maybe choose a more interesting example." +id = "score_quota_sim" +gif = "gif/score_quota.gif" +%} + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSsWpDMQz8F88aLMmSnGz9h26PDCl0CySUUiil_fbKuqYEQvBwlk8-n8_-ar3tt02DYh5oY-kUkpMhJLr7W5GxZjI7cZHCkjM-HKjx2h2r2ZIIIx-rYRix22rQtu_URtu3H45Gzar23JZkJHS6G8nMh8zuIcO9tJkhzoISDngAcD47IA3wSMzjPCG1hZqkTi5K6miCYBEykjKcYOAgI4FqooKK9jLKKwMuQqX2qoKHIU2ljel_rHZHA1QVF2YSUjLKeNvo1wxWwbeF3BZ6lV7FKLVxf9zwsjUCL4VLjF0tGiI1XMAQhWnlY4jCDIAoLNAy6zRDFN4BXJ2Od3EE6laONY04JBwOfFcQcBDYG4gwFIAIA28a108VIOdNbiu1wJUmBCcEJwQnBOcoo9MAeIeX4-l0fn_-vLzmJ366XN7OH8dT-_4Fp_UcyToDAAA)" +title = "Approval Voting with Quotas" +caption = "Pretty much the same as score. Mouse over the rounds." +comment = "Maybe choose a more interesting example." +id = "approval_quota_sim" +gif = "gif/approval_quota.gif" +%} + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA61W2W7jNhT9FUZAGxsmFG3UYscJ2pm0mJfMIEkxD7YRyDIdC5AlQ6TTSafut88lLyVLWSYvhW1wuYfnnruI1nfLscazmR_RKF7Qmes5NPJgEnjU8xOz4wVq5sUOdbXRcz2YuYsFtVx1OlJgBoaI0TBQgIBRN2QK4Ftjh1qBNbb-80KLWkyvQzgGxggGh774gCV-05K8aXEdze26SO56uEQFboAD-ndDHECAG8AI7kIYgNujlgc8sOkBjw-Dh5tI4wGNCwNDG9J4Ea5iXCGL72ihrsqBqw2-p8_6PtpRkA9MM5e2HwUPEYCsPgbsUo_6lFFIrxU4TQ7Uwu0uvO7Cb6jVItBswUt3QahlBRFWCoMIEr3JMKUMA2CYCoYBMEwFYzhgKliEtlh7Y5iK0MHB1cgQ6xJiQkOmFfsgJESKEBWEiR4iVBDh2QhTGPk4YAojrGnUNFWExriTN5W1CEOKkTBGwhjFxCgmDrTQmOGAemLki1FWrGTZDPoQeZJu-hOVfgZVYmrhdxfInGCyEmROon4xOkVJYvXEzK31vsxkXpWDVS5knWeSkm214gUl1U7ti-H3-bzUX2l2yJTc84LrYx_5Ot0XUgwaNCIf05rsqqKQ_JtU8CV_yMsrc-boCj2Zo3Q-n1siq2quJoao4JJkqfbZnLKFTB-4mOnDuFjYgFnlq1Ry0ao9OyN3aVE8EbnhJN3t6uoxLQT5lTwA5995WfL65Kh2CdBKKj_I-1hJXt9yaQP6d7R9NAJa_cPWl2KQ2hkE-w-vK-15oJSbQNZVPTj6IdW68WjyKzuYNhiSlwZm68R0wfDVHmctekFG0z68Y5u0Bw84OzTiG_3b9Js-BSGwngFzpcueVftSftXrgfY-nBxx4BBaQcFwNjAHscydzsiqoqoBhzBbL1uP-Zo0zWSLfMWXaT0kxx7Ewn5INaLdOsoEWuN25nSC1gXCZlTdpX5HozaM0HIudmlJsiIVYnoqthDj6UUfrQSqnd_2slIjmTY9k8LOF-j6IWkYm2fgDV_LC8x4KsgvqiWgBHBEiHxZ8DE5P1tenC_rV9yjuxV22YdNWssmRbLvRbYALBdVHWkeu6be1DSiXfDyQW6Gr_Gg2udSDoQXgvc9Nz2cT50Jyc-VP0MMy9Go38BNP0BVFHCWL-x8NXmGaDRg0HkGF0g2HCklp4LoCMZa3-h-x-uMl_KPqt6msnOhmedkQc7aJkeClyEdn4_eLF8PTkx__dv0lwnrYur1g4L2vP5Mvn66vr66uTwhn2_I3aery5PLk9cSey9zruZYUmqoh5NnhJ3WhaYBPtUbRMknTQxkRPqd9lp1fl7P15ONmjBhZAOtqu7TTf6w4UJi_ikRlW3bPyXUHs_Uw_UO5l2SJRHyqeDTU31vjHXp9RR7Qp8eof4y3fK_dtAWvSB07qA-ty_8PM_0_-bpvVJ1rzdzKxohHWDbjs012V4D1c4bEn3FbbhaqDtYVLXEfyG8qG1R5BkfwMv28C2GxnNLYmaIr7nc16UBKUmHuWWpl2LzVuzgOxzeJndPOw4vGLeqPQCk_03_rKv97gb-kartLecrAe8mfkyTMKQxvNQffgBGqmUCKgwAAA)" +title = "Allocated Score Voting" +caption = "Elect the highest scoring candidate, then assign the closest voters to that winner and elect more candidates with the rest of the voters." +comment = "" +id = "allocated_score_sim" +gif = "gif/allocated_score.gif" +%} + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA61W2W7jNhT9FUZAGxsmFG3UYscJ2pm0mJfMIEkxD7YRyDIdC5AlQ6TTSafut88lLyVLWSYvhW1wuYfnnruI1nfLscazmR_RKF7Qmes5NPJgEnjU8xOz4wVq5sUOdbXRcz2YuYsFtVx1OlJgBoaI0TBQgIBRN2QK4Ftjh1qBNbb-8yKLWkyvQzgGxggGh774gCV-05K8aXEdze26SO56uEQFboAD-ndDHECAG8AI7kIYgNujlgc8sOkBjw-Dh5tI4wGNCwNDG9JAYHoV4wpZfEcLdVUOXG3wPX3W99GOgnxgmrm0_Sh4iABk9TFgl3rUp4xCeq3AaXKgFm534XUXfkOtFoFmC166C0ItK4iwUhhEkOhNhillGADDVDAMgGEqGMMBU8EitMXaG8NUhA4OrkaGWJcQExoyrdgHISFShKggTPQQoYIIz0aYwsjHAVMYYU2jpqkiNMadvKmsRRhSjIQxEsYoJkYxcaCFxgwH1BMjX4yyYiXLZtCHyJN005-o9DOoElMLv7tA5gSTlSBzEvWL0SlKEqsnZm6t92Um86ocrHIh6zyTlGyrFS8oqXZqXwy_z-el_kqzQ6bknhdcH_vI1-m-kGLQoBH5mNZkVxWF5N-kgi_5Q15emTNHV-jJHKXz-dwSWVVzNTFEBZckS7XP5pQtZPrAxUwfxsXCBswqX6WSi1bt2Rm5S4viicgNJ-luV1ePaSHIr-QBOP_Oy5LXJ0e1S4BWUvlB3sdK8vqWSxvQv6PtoxHQ6h-2vhSD1M4g2H94XWnPA6XcBLKu6sHRD6nWjUeTX9nBtMGQvDQwWyemC4av9jhr0QsymvbhHdukPXjA2aER3-jfpt_0KQiB9QyYK132rNqX8qteD7T34eSIA4fQCgqGs4E5iGXudEZWFVUNOITZetl6zNekaSZb5Cu-TOshOfYgFvZDqhHt1lEm0Bq3M6cTtC4QNqPqLvU7GrVhhJZzsUtLkhWpENNTsYUYTy_6aCVQ7fy2l5UaybTpmRR2vkDXD0nD2DwDb_haXmDGU0F-US0BJYAjQuTLgo_J-dny4nxZv-Ie3a2wyz5s0lo2KZJ9L7IFYLmo6kjz2DX1pqYR7YKXD3IzfI0H1T6XciC8ELzvuenhfOpMSH6u_BliWI5G_QZu-gGqooCzfGHnq8kzRKMBg84zuECy4UgpORVERzDW-kb3O15nvJR_VPU2lZ0LzTwnC3LWNjkSvAzp-Hz0Zvl6cGL669-mv0xYF1OvHxS05_Vn8vXT9fXVzeUJ-XxD7j5dXZ5cnryW2HuZczXHklJDPZw8I-y0LjQN8KneIEo-aWIgI9LvtNeq8_N6vp5s1IQJIxtoVXWfbvKHDRcS80-JqGzb_imh9nimHq53MO-SLImQTwWfnup7Y6xLr6fYE_r0CPWX6Zb_tYO26AWhcwf1uX3h53mm_zdP75Wqe72ZW9EI6QDbdmyuyfYaqHbekOgrbsPVQt3Boqol_gvhRW2LIs_4AF62h28xNJ5bEjNDfM3lvi4NSEk6zC1LvRSbt2IH3-HwNrl72nF4wbhV7QEg_W_6Z13tdzfwj1RtbzlfCXg38WOahCGN4aX-8AOL8n5rKgwAAA)" +title = "STAR Proportional Representation" +caption = "Elect the best candidate according to STAR voting, then assign the closest voters to that winner and elect more candidates with the rest of the voters." +comment = "" +id = "star_pr_sim" +gif = "gif/star_pr.gif" +%} + +{% include sim.html +link = "[link](http://127.0.0.1:5500/sandbox/?v=2.5&m=H4sIAAAAAAAAA61W2W7jNhT9FVpAGxsmFO2LHSdoZ9JiXjJFkmIebCOQZToWKkuGSGcmnabf3kteUpayTF4K2-ByD889dxGt75ZjTeZzP6ZxsqRz13No7MEk8Kjnp3rHC-TMSxzqKqPnejBzl0tqufJ07ANYHo8jGsUSEETUjRIJ8K2JQ63Amlj_eqFFrVCtIzgGxhgGh774gCV505K-aXEdxe26SO56uEQFboAD-ncjHECAG8AI7iIYgNujlgc8sOkBjw-Dh5tI4wGNC0OINqTxYlwluEIW31FCXZkDVxl8FOQjk4-CfGCau7T9SHiEBMjqY8Au9ahPQxoAIHBMDuTC7S687sI31HIRKLbgpbsgUiEFMVYKgwhStRliSkMMIMRUhD4OmIowxAFTEcZoS5S3MMVqO2ozchUywjREmIYoVIp9EBIhRYQKIjwbo4IYz8YeDj4OmMIYaxqbporRmHTyJrMWY0gJEiYYUoJiEhSTIGGChAnqSZAvQVmJlGWH0IfIk3bTn8r0h1ClUC787iJQKUgxWSkyp3G_GJ2ipIl8YhbW5lDloqir4brgoilyQcmuXrOSknov9_no-2JRqa_QO2RG7ljJ1LGPbJMdSsGHBo3Ih6wh-7osBfsmJHzF7ovqUp85ukJP-ihdLBYWz-uGyYkmKpkgeaZ8mlM2F9k943N1GBdLGzDrYp0Jxlu1p6fkNivLRyK2jGT7fVM_ZCUnP5N74PxaVBVrBke1K4DWQvpB3odasOaGCRvQv6LtoxbQ6h-1viSDUM4g2L9ZUyvPQ6lcB7Kpm-HRD6k3xqPOr-hg2mBIUWmYrRLTBcNXeZy36CUZz_rwjm3aHnzC2ZMRb_Tvsm_qFIQQ9gyYK1X2vD5U4otaD5X30fSIA4fQChKGs6E-iGXudEZel3UDOITZatl6LDbENJPNizVbZc2IHHsQC_shU4h26ygTaLXbudMJWhUIm1F2l_wdjcowRssZ32cVycuM89kJ30GMJ-d9tBQod345iFqOZGZ6JoOdP6DrR8QwmmfgDV-rc8x4xslPsiWgBHCE82JVsgk5O12dn62aV9yjuzV22Ydt1giTItH3IloAlovKjtSPnak31Y1ol6y6F9vRazyo9rmUJ8JKzvqeTQ8XM2dKijPpTxPDcjzuN7DpB6iKBM6LpV2sp88QRgMGXeRwgeSjsVRywomKYKL0je_2rMlZJX6rm10mOheafk6W5LRtciR4GdLx-ejNis1woPvrH9NfOqzzmdcPCtrz6jP58unq6vL6YkA-X5PbT5cXg4vBa4m9EwWTcywp1dSj6TPCTutC0wCf7A0i5RMTAxmTfqe9Vp0f1_P1ZKMmTBjZQqvK-3Rb3G8ZF5h_Snht2_YPCZXHU_lwvYN5l2RFuHgs2exE3RsTVXo1xZ5Qp8eov8p27M89tEUvCJU7qM_NCz_PM_2_eXqvVN3rTd-KWkgH2LajuSbba6DeeyOirrgtkwt5B_O6EfgvhBe1zcsiZ0N42R69xWA8tyR6hviGiUNTaZCU9LSwLPlSrN-KHXzhwdvk9nHP4AXjOqv-YmtAqb_T35v6sIetdb27YWzN4eXET2gaRTSJguXTfzeyCIMrDAAA)" +title = "STV Minimax" +caption = "Use STV to form equal clusters of voters. Then use Minimax within the voter clusters to elect candidates." +comment = "" +id = "stv_minimax_sim" +gif = "gif/stv_minimax.gif" +%} + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSMU4EMQz8S2oXsZ3Y3n0GolttcYirOAEFDULwdpwMi046nbaYOONMxpP9KrWs26ZOHjttLJVcctGERJe_HWljJVGJJyksueJ9p8LjtGs2j-NuZD4amhFbjAYta6XSylp-OAqVPmvLY0l6QqWbL5m4yyx3Ga5TmxniLCjhgBsA97MB0gC3xLzOElJbqEjq5KakjiYINiEjKcMJHRxkxFEFKqhonUZ5ZMCTUBhSKCkMaSptTP_faDcIQFUxMJOQUqeWDa0eGYyCrwu5LvSQHkWbau32umZzpOZ4KQzRlrnZEWnHAB1RdAUgit4BiKI7uJi3dURhFcCz0xCDIQbr07GmEYOEwYEtExwOHGddAApAhI439eOncpBxldtIzTFSQDAwUsBMwExAMCAY8PN0ulzePh4_38_5Ez-cXl_Oz-X7F5mNX244AwAA)" +title = "An Attempt at Pairwise Voting with Quotas" +caption = "Under active development. Mouse over the pairs." +comment = "Maybe choose a more interesting example." +id = "pair_quota_sim" +gif = "gif/pair_quota.gif" +%} + +## Proportionality + +Let's get back to the idea of proportionality. You can see that in these proportional methods, a voter group with two times as many voters gets two times as many representatives. + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA31TTY8TMQz9LzlbKHbsJNMbF05wgF1xGfUwlB6qHTpl1UWsEPx27DzQIsGikfr8Ffv5Jf2WctrNs2Ziy3uamd3S6pYqSZ4ilCsJF7d6Jhm5NhELuyHSiGtUSVO3JKwa2Txi5ictrMJu9V8WS93vKXFMbj6vjhIz4tYiUdIuU9K0Sz-4Jko2_OrlnmwOmf76PNOfzUzPZjiP3hxcCjXSSATRxIIMyLACQMVZDXAurI4-mcXR5zAl8Z4eFe_pQREA-oiixPsUB_SRBq_Dm8aBkgdpDj14JAoYlYI8GBXvNLuk__riaEUxJhQI4XdBhZSMqq_cye_Lg1GuGBrKKD-Z8mT68FlGa9XRU_9PQOtYWBvuFCsqhDKIb1jPIJRhPYNQZgAIZQ25PiYbhKoZwKOyQqQKuasN5v54U0WLCgZ1GtDAoOFsE0ABQOAGBi2eX3r7sF2Xm8N2f_RnOZyXl8v99mVZf_tvTufTp-Wruze37_33-PlhWV8th9N6uj6-3g7L9bSdU7zjhr79zyuJOLTpGQBtOrbq2Krr2LgbAIt19OvYr8d-L8yfPjb8sKzrdr19vBz9X_VuOd8dP6bvPwEoyRya_QMAAA)" +title = "Quotas Give Proportional Results" +caption = "Try all the voting systems. They each give a 1:2 ratio of voters between the groups. Also try a larger number of seats. The large group is 63% of voters, so it's a 4:7 ratio." +comment = "Maybe choose a more interesting example. altlink is [link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA31SvW6UMRB8F9dbeNf27vddxwNQQCKa0xUfcEXEkTtFAYEQPDuznpwUKQS5mP3f8di_Si27_b5X0VEPsleF1R1W72J1zVB1MW2wzFQ0sswWlIXBiiHWMxnIWdabr7BmVSZ1pNUU1vJkqfnhIEVzczTRJfuXgKFZ0V3Ul6xoZVel9LIrf9SLlDF9R5_Ji4P6QKbKi4PM8mpmfTWjda7T5NkkpD-FjWGS004gNbCcACLagVirBsQSgGEgooaBDWAMco5hjgIGc5xjQW-hxymtTsaa-uhMNJu9rTFPRg2T9tD6XydbncXc0KgCHkmadBniuO8ieEgEs7zXq0zp6HPHnjvtujSdPmf3_xPpPsn34Fvzqn2dwcEXGLzmoGCD1xwUbAwCBRvB3DI3DwrmlaCz0vl8Ttl9TPb4gMU5wsnA1wlBBsHeoNDRCBQ6yCDyW5Z3386P282n88MR33U6by6Xh_P37XT1397d333dfsC9uf1Q8tPG7P-4nU7nx9uflyO--_vt_svxc_n9Fwg5q72fAwAA)" +id = "again_proportional_two_to_one_sim" +gif = "gif/again_proportional_two_to_one.gif" +%} + +This proportionality applies even when there aren't distinct groups. In the example below, there is a difference between STV and the other methods. STV picks a set of candidates that is spaced more widely to cover a larger area of voters. The other methods tend to pick candidates more toward the center. This is probably because STV considers the first choices above others, while the other systems consider all the options at the same time. + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA41Ty24TQRD8lzm30EzPq9c3LpzgEBJxsXxYjA9WjNeJHESEyLdT02WEI4SELG2_pqurH_4RYlit192k2UbWqlGsupIkWYaW6nTRpiypmLtUkqahxSxTHErH--jaFBEsQ0v5kqlIaOqZRaw7fsErL1miqA5fzdLyZiMhDUpaKjnVKM1rFUN99ZQmmtp4msMqSihhFV5SCxKq2w0ACHaIKH_9EDFEkBGR8aLNvxoQg4zh1cvJX2q-dl-FATEKpuSlkg2X0kVqqVCQGDi6ALNUIMEjVcjJ3ai9glcBliCAoxDE0UIncDIEcbTTMlqTJ-RILmM6yQOZjHJmnIwykNZY2r9_A6AxhXXyRGRRyVIE-5EuJrgPOJGAqxgngrwS_4x-mOlSi7SKvo6C1zoDMkkeJvhhJuV_-JXmUymdZ8A5lMmdlaupnEHlNCtnUDnNWik4zdoZM2-2cpqNKI0ojZNsRGmDojrpRohGBo377Mzt3GdXikzBLXQy6ONiw83Tcp5vt8vjDhfpxtvT6XH5Nh9-2x_2x_3X-TvM27tP-O4enubDu3m7P-zPz--X7XzeL8cwTr8T1643NvycjUUKMjN2Zbw0K96cVQo2ZsQz9mejvzdY0sQOP8-Hw3K-ez7t8Ef8OB_vd1_Cz19X_VUPWwQAAA)" +title = "Differences Regarding Evenly Spaced Winners" +caption = "STV picks a broader set of candidates to match the voters, but other methods pick more broad candidates that more voters would like." +comment = "The explanation is going to be tricky" +id = "distribution_matching_sim" +gif = "gif/distribution_matching.gif" +%} + +### Apportionment-Based Multi-Winner Methods + +There are more methods that only provide proportionality to distinct groups, and don't provide the kind of distribution matching that STV does to cover an area of voters with evenly spaced candidates. They are mechanically different. They apply a method of counting votes that is used for apportionment. Apportionment means you have separate groups, like different states, and you want to find out how many representatives to give to each state. Three examples are given below: reweighted range voting, reweighted approval voting, and proportional approval voting. + +{% include sim.html link = "[link](http://localhost:5500/sandbox/?v=2.5&m=H4sIAAAAAAAAA61WbU_jRhD-K4ullkRZGb_bCYTT9Y6rTqq4E9DehyRCjrMhlhw79W7o0Sv97Z3ZWdsJBPhSEbKzO2_PzDze-IflWKPJJHC4GzozPnFdkIIIpCDgnjPEIyfinuuDlDjc07p4yF3PBcHzYu5GaOXFAUgeShFqHX0WgmeIku-ClBjJ9aLZjFsuZo4hX6RNwpC7cYwK3xo53AqskfWvG1jcCvU-AnNQxrA4_NkfaJIXNcMXNa6jY7uIxecxD1CBQC3XIw2BARx6IShuRAtgcQNYIbPrwQp5XG55EBNOPYgJh55HC8XxAjKBOD4sFMeLaZfQbqgdfEeDdrEfrlb4nvb1fdITIh8iTaClh_7QNSJjyuBTI2AW3OcBD3kEJScc5gWHaB5QUuxM4Hai14mQfOLp0EGgywxeBxBEGnQQ00ypxIAaFVLzQyovpEaFVF5IjQpDWqhRYUy6RFcTUqMihxZXW0Y0tojaHYUaOZDXiihERAiioV5iQhCTb0wNjn1aqMExIYiRftb1zR_Ax6sr_f0ev8Wf27T4lGZ5kauH36osVXlVwvlX0CJbY_JOdhuP59SBxKGF8ieEPSHsSaDrSkJaCH5C8RKqIsEq7BAITnUMu5kNcWYhD1H0O5FiDqmmIcUcxocniPYJPoRTa7ktMyyst8ilqvNMcbauFqLgrNrguez_mE5L_VHmhI3ZrSiEdvsolum2ULLXWJPlfVqzTVUUSnxXaD4Xd3l5YXy6VJTJuPLpdGrJrKoFCiZQIRTLUp2z8bKlSu-EnGhn2sxssFnki1QJ2aI9OWE3aVE8MLUSLN1s6uo-LST7md1BzL_yshT1UYd2DqaVwjwU975Sor4WygbrX0j30QBo8ffbXBhB6WRQ7N-irnTmHiI3hSyrutflYdWyyWj6q3Zs2mJYXhozWzdm1xg-OuOktZ6xwXjffEd32jo-kvTYgG_wr9Pv2gtKCPcU1Cs99qzaluqb3vd09v5pZwcJgQpoRlLPONKYd5iRVUVVgx2Z2XrbZsyXrCGTLfOFmKd1n3UcpMF-SLVFe9TBhLAm7cTZKVoPiMiI7ML_TqkVA9KcyU1asqxIpRwfyzXUeHy-b40A8eT9VlW4snHDmRROvgLr-6yJ2DwDL-San1PHU8l-QkrACMBFynxeiBE7O5mfn83rA-kp3YJY9mGV1qppkdrPoloDGhdHRprHrpk3N0S0C1HeqVX_UBxC-xTKIxOFFPuZGw7nY-eU5WeYzwSG7WCwT-CGDzAVNJzkMztfnD6xaDBQ0XkGF0jWHyCSY8l0BSONb3C7EXUmSvWpqtep2rnQzHMyYyctySnA85K652NPype9I8Ovfxp-mbLOx95-UUDPyy_s2-fLy4urd0fsyxW7-Xzx7ujd0aHG3qpcoEwj5SZ0__RJwB3qAmkgHnKDIXzW1MAGbJ9ph6bz-jwPN5swUcPYCqiK9-kqv1sJqaj_nMnKtu1XA-qMJ_hwvWHzZpA5k-qhEONjfW-M9Oi1SJzQ3gPCX6Zr8fsGaLFXhO4dzOf6WZ6nnf7fMr01qt3rzdyKBsiOYUvH5ppsr4Fq4_WZvuJWAjd4B8uqVvQrRBe1LYs8Ez140e-_FKHJ3AYxEtnXQm3r0hghpMepZeF7tnnRduhlj26Tm4eNgBeMa6QHGOlf01_raru5gl-kan0txELiizm8okbR7PE_6mW-5q0MAAA)" +title = "Apportionment-Based Methods" +caption = "STV picks a broader set of candidates to match the voters, but other methods pick more broad candidates that more voters would like." +comment = "The explanation is going to be tricky" +id = "semi_proportional_sim" +gif = "gif/semi_proportional.gif" +%} + +### Party Proportional Methods + +Additionally, there are ways to have proportionality by using a party system, but that is a mechanically-different method that I haven't added to the simulator yet, so we'll discuss it on another page to come in the near future. + +## Future Work + +I still need to work out what the strategies would be for voters and candidates. So far, in the above examples, I've been using the honest strategy for ranking and the normalization strategy for scoring. + +## Afterword + +I hope that by seeing how proportional methods work you're inspired to improve our ability to represent all members of society. Basically, proportional methods allow candidates to better serve a segment of society by being closer to them. + diff --git a/quotaApproval.html b/quotaApproval.html new file mode 100644 index 00000000..86a4fc1e --- /dev/null +++ b/quotaApproval.html @@ -0,0 +1,53 @@ +--- +permalink: /quotaApproval/ +layout: page-6 +title: Quota Methods +banner: Multiseat Methods with Quotas +description: An Interactive Guide to Quota Approval Voting and Maybe More +byline: By Paretoman, May 2019 +twuser: paretoman1 +--- + + <div class="words"> + + <p> + Quota-Approval mode + </p> + + <h2> + Quotas! + </h2> + + <p> + In Quota-Approval mode, you position voters in only one dimension and you use the second dimension to control how broad the candidates are. Up is broader. + </p> + + </div> + + <div class="sim-test"> + <div class="sim-container"> + <p class="caption-test"> + Move the candidates and get equal representation for voters. + </p> + <div id="elect_quotaApproval" class="div-election div-ballot-in-sandbox div-model" > + </div> + <script> + sandbox({quick:"elect_quotaApproval"}) + </script> + </div> + </div> + <div class="words"> + <p> + The dark bar shows where voters are unrepresented... it's bad. We want candidates to cover each voter's quota of representation, AKA the dark bar. The quota is an amount of representation that everyone could have ideally if every voter were represented equally. I used a gradient-colored bar to show that it is more important to elect a candidate that represents the underrepresented. The goal is to cover all of the bar, starting with the darker parts of the bar. + </p> + <p> + The quota approval method elects the candidate that reduces the quota the most. To do this, we use the remaining quota as a weight. A voter who is already represented has less quota left to count towards the next candidate to elect. Their vote was already counted. A candidate they liked was already seated. + </p> + <p> + The light bars that cover each candidate's bars show votes that were already counted. We don't want to count these votes twice. So we wash out the color from that part. Then we count the votes that are still unrepresented. Most votes wins. + </p> + <p> + + </p> + <p>Putting it all together, here's a sandbox for you to try out all the different systems and to make your own scenarios:</p> + </div> diff --git a/rbvote/calc.html b/rbvote/calc.html new file mode 100644 index 00000000..60376dd6 --- /dev/null +++ b/rbvote/calc.html @@ -0,0 +1,330 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> +<html><head><title>Ranked-ballot voting calculator + + + +

Ranked-ballot voting calculator

+

+This form calculates the winners of several ranked-ballot voting methods.  See below for input examples. +

+Enter ranked ballots:
+ +

+Enter candidates to ignore (optional):
+ +

+Enter tiebreaking ranking (optional):
+ +

+Reverse all rankings +

+ + +

+Or see how a specific method chooses its winner:
+
+ + + + + + + + +
+ + + + + + + + +
+

+
+

+Each line must consist of candidate names separated by > or =.  Each candidate name must be one +word and consist only of uppercase and lowercase letters (case is significant, so LeGrand is not the same as +Legrand).  One ballot per line is assumed unless the line begins with a number followed by a colon; the number specifies +how many ballots share that ranking.  If a candidate appears more than once on a line, only the first (highest) occurrence is counted.  A +# begins a comment that extends to the end of the line; the calculator ignores comments.  Ballots themselves can be +commented out by inserting a # at the beginning of the line. +

+If a list of candidates to ignore is given, those candidates will be treated as if they dropped out of the election between the collection and counting of +the ballots. +

+If a tiebreaking ranking is given, it must include all of the candidates.  If none is given and a tiebreaking ranking is needed, it will be generated +by drawing a random ballot and breaking its tied preferences randomly. +

+Checking the “Reverse all rankings” box effectively flips the preferences in each ballot and in the tiebreaker; it’s useful to test for +reverse-symmetry violations. +

+Here are some example ranked-ballot inputs to use.  Just copy one of these text blocks and paste it into the input field above.  See if you can +guess the results! +

+

+ + + + + + + + + + + + + + +
# example from method description page
 98:Abby>Cora>Erin>Dave>Brad
 64:Brad>Abby>Erin>Cora>Dave
 12:Brad>Abby>Erin>Dave>Cora
 98:Brad>Erin>Abby>Cora>Dave
 13:Brad>Erin>Abby>Dave>Cora
125:Brad>Erin>Dave>Abby>Cora
124:Cora>Abby>Erin>Dave>Brad
 76:Cora>Erin>Abby>Dave>Brad
 21:Dave>Abby>Brad>Erin>Cora
 30:Dave>Brad>Abby>Erin>Cora
 98:Dave>Brad>Erin>Cora>Abby
139:Dave>Cora>Abby>Brad>Erin
 23:Dave>Cora>Brad>Abby>Erin

+

+ + + + +
# 1980 American presidential election
45:Reagan>Anderson>Carter
20:Anderson>Carter>Reagan
35:Carter>Anderson>Reagan

+

+ + + + + + + + + + + + + + + + + +
# 1860 American presidential election
2117:Lincoln>Douglas>Bell>Breckinridge
1861:Lincoln>Bell>Douglas>Breckinridge
1119:Breckinridge>Bell>Douglas>Lincoln
 859:Douglas>Bell>Lincoln>Breckinridge
 804:Douglas>Lincoln>Bell>Breckinridge
 753:Douglas>Bell>Breckinridge>Lincoln
 687:Breckinridge>Douglas>Bell>Lincoln
 487:Douglas>Breckinridge>Bell>Lincoln
 448:Bell>Douglas>Lincoln>Breckinridge
 381:Bell>Douglas>Breckinridge>Lincoln
 256:Bell>Breckinridge>Douglas>Lincoln
 170:Bell>Lincoln>Douglas>Breckinridge
  22:Douglas>Breckinridge>Lincoln>Bell
  13:Breckinridge>Douglas>Lincoln>Bell
  11:Douglas>Lincoln>Breckinridge>Bell
   4:Bell>Breckinridge>Lincoln>Douglas

+

+ + + + + + + + + +
# 2000 American presidential election
11:Browne>Bush>Buchanan>Gore>Nader
 2:Buchanan>Bush>Browne>Nader>Gore
 8:Bush>Browne>Buchanan>Gore>Nader
16:Bush>Buchanan>Browne>Gore>Nader
12:Bush>Buchanan>Browne>Nader>Gore
17:Gore>Nader>Browne>Bush>Buchanan
 3:Nader>Browne>Gore>Bush>Buchanan
31:Nader>Gore>Browne>Bush>Buchanan

+

+ + + +
48:Gore>McCain>Bush
25:McCain>Bush>Gore
27:Bush>McCain>Gore

+

+ + + + + + + +
# 2004 American Democratic primary
33:Dean>Gephardt>Edwards>Lieberman>Clark
22:Clark>Edwards>Gephardt>Lieberman>Dean
18:Lieberman>Clark>Edwards>Gephardt>Dean
16:Gephardt>Lieberman>Edwards>Clark>Dean
 7:Edwards>Clark>Gephardt>Lieberman>Dean
 4:Edwards>Lieberman>Gephardt>Clark>Dean

+

+ + + + + + + + + + +
# 2009 mayoral election in Burlington, Vermont
1332:Montroll>Kiss>Wright
 767:Montroll>Wright>Kiss
 455:Montroll>Kiss=Wright
2043:Kiss>Montroll>Wright
 371:Kiss>Wright>Montroll
 568:Kiss>Montroll=Wright
1513:Wright>Montroll>Kiss
 495:Wright>Kiss>Montroll
1289:Wright>Kiss=Montroll

+

+ + + + + +
# Where to have a Tennessee state conference?
42:Memphis>Nashville>Chattanooga>Knoxville
26:Nashville>Chattanooga>Knoxville>Memphis
15:Chattanooga>Knoxville>Nashville>Memphis
17:Knoxville>Chattanooga>Nashville>Memphis

+

+ + + + + + + +
# Hare jumps to extremes on a left-right spectrum
18:FarLeft>Left>Center>Right>FarRight
16:Left>FarLeft>Center>Right>FarRight
17:Center>Left>Right>FarLeft>FarRight
 9:Center>Right>FarRight>Left>FarLeft
19:Right>FarRight>Center>Left>FarLeft
21:FarRight>Right>Center>Left>FarLeft

+

+ + + + + + + + + + + + +
# sometimes it's better to be eliminated early rather than late under Hare
13:Libertarian>Right>Left>Statist
15:Libertarian>Left>Right>Statist
14:Left>Libertarian>Statist>Right
12:Left>Statist>Libertarian>Right
12:Statist>Left>Right>Libertarian
10:Statist>Right>Left>Libertarian
11:Right>Statist>Libertarian>Left
13:Right>Libertarian>Statist>Left
# These preferences are consistent with the Nolan chart.
# Try adding 3 more Statist>Right>Left>Libertarian ballots.
# Or, try deleting 3 Right>Statist>Libertarian>Left ballots.

+

+ + + + + + + + + + +
# Which team should have been Miami's opponent in the 2002 Rose Bowl?
 7788:Colorado>Nebraska>Oregon
10536:Colorado>Oregon>Nebraska
 6303:Nebraska>Colorado>Oregon
 5172:Nebraska>Oregon>Colorado
15248:Oregon>Colorado>Nebraska
 3838:Oregon>Nebraska>Colorado
# Yahoo! Sports conducted this ranked-ballot poll in late 2001.
# In the event, Miami played Nebraska and won 37-14.
# Oregon beat Colorado 38-16 in the Fiesta Bowl.

+

+ + + + + +
# three yes/no issues in order of importance
600:NNN>NNY>NYN>YNN>NYY>YNY>YYN>YYY
300:NYY>NYN>NNY>YYY>NNN>YYN>YNY>YNN
302:YNY>YNN>YYY>NNY>YYN>NNN>NYY>NYN
303:YYN>YYY>YNN>NYN>YNY>NYY>NNN>NNY

+

+ + + + + +
# three large special-interest groups
300:NNN>NNY>NYN>YNN>NYY>YNY>YYN>YYY
301:NNY>NYY>YNY>YYY>NNN>NYN>YNN>YYN
303:NYN>NYY>YYN>YYY>NNN>NNY>YNN>YNY
307:YNN>YNY>YYN>YYY>NNN>NNY>NYN>NYY

+

+ + + + + + + +
# EM list political party poll
Green>Democratic>Libertarian>NaturalLaw>Reform>Republican>Constitution
Green>Democratic>NaturalLaw>Libertarian>Reform>Republican>Constitution
Green>NaturalLaw>Democratic>Libertarian>Republican>Reform>Constitution
Libertarian>Constitution>Republican>Reform>Democratic>NaturalLaw>Green
Reform>Libertarian>Republican>Green>Democratic>Constitution>NaturalLaw
Republican>Libertarian>Democratic>Constitution>Reform>Green>NaturalLaw

+

+ + + + + + +
32:Labour>Liberal>Conservative>SocialDemocrat
30:Conservative>Liberal>Labour>SocialDemocrat
23:SocialDemocrat>Liberal>Labour>Conservative
10:SocialDemocrat>Liberal>Conservative>Labour
 3:Liberal>SocialDemocrat>Labour>Conservative
 2:Liberal>SocialDemocrat>Conservative>Labour

+

+ + + + + +
# Demorep's silly Hare example
34:Hitler>Washington>Stalin
33:Stalin>Washington>Hitler
16:Washington>Hitler>Stalin
16:Washington>Stalin>Hitler

+

+ + + + + + +
# 1969 referendum on name of new city created from Fort William and Port Arthur, Ontario
10256:ThunderBay>Lakehead>TheLakehead
 5614:ThunderBay>TheLakehead>Lakehead
15302:Lakehead>TheLakehead>ThunderBay
 8377:TheLakehead>Lakehead>ThunderBay
# Thunder Bay won real referendum with a plurality

+

+ + + + +
# Which game should we play?
Uno>TrivialPursuit>Scrabble>Life>poker>Risk>Yahtzee>Monopoly>BargainHunter>Pokerkub
Scrabble>Risk>Pokerkub>Yahtzee>poker>Life>Uno>Monopoly>TrivialPursuit>BargainHunter
TrivialPursuit>poker>Life>Scrabble>BargainHunter>Yahtzee>Pokerkub>Monopoly>Risk>Uno

+

+ + + + +
# Bucklin and Woodall disagree
25:Brown>Jones>Davis>Smith
26:Davis>Smith>Brown>Jones
49:Jones>Smith>Brown>Davis

+

+ + + + + + +
# Coombs and Hare are capricious
18:A>C>E>D>B
20:B>A>C>D>E
19:C>A>B>E>D
22:D>B>E>C>A
21:E>D>A>B>C

+

+ + + +
25:B>C>A>E>D
49:D>A>C>B>E
26:E>B>A>C>D

+

+ + + + + + +
21:A>D>B>C
12:B>C>A>D
33:C>B>A>D
 6:C>D>B>A
25:D>A>B>C
 3:D>B>A>C

+

+ + + + + + + +
47:A>D>B>C
23:A>D>C>B
36:B>D>C>A
51:C>A>B>D
52:C>B>A>D
23:D>B>A>C
14:D>B>C>A

+

+ + + + + + +
# Heitzig, Schulze and Tideman disagree
16:B>A>E>D>C
27:C>B>A>E>D
17:C>E>A>D>B
31:D>A>B>C>E
 9:E>D>B>C>A

+ +

+ + + + + + +
# A is Borda-superior to Condorcet winner B
A>C>E>D>B
A>D>C>B>E
B>A>C>D>E
B>A>D>E>C
E>B>A>C>D

+

+ + + + + + +
A>C>B>F>D>E
B>C>E>F>D>A
D>B>A>F>E>C
E>A>B>C>F>D
E>D>A>B>C>F
F>C>D>A>B>E

+

+ + + + +
A>C>D>E>B
B>A>C>E>D
B>A>E>D>C
D>C>E>B>A

+

I, paretoman, am modifying this code that I saw on Rob Legrand's site, and his closing message is as follows:

+

+This ranked-ballot voting calculator was inspired in part by Rob Lanphier’s +Pairwise Methods Demonstration; Lanphier maintains the +Election Methods mailing list.  In turn, my calculator inspired Eric Gorr’s +Condorcet Matrix. +

+Please e-mail any questions, problems or suggestions to rob@approvalvoting.org. +


+

Back to Ranked-ballot voting methods

+ diff --git a/rbvote/rbvote.js b/rbvote/rbvote.js new file mode 100644 index 00000000..6af8e305 --- /dev/null +++ b/rbvote/rbvote.js @@ -0,0 +1,2778 @@ +rbvote = function(){ + +var pvote, rvote, candtonum, numtocand, rvotenum, rvotetie, numtotb, tbtonum, equalranks, schwartz, smith, i +var returnstring = false + +function setreturnstring() +{ + returnstring = true +} +function calcall() +{ + var result, str, strmeth = new Array(), tbused = false; + if (readvotes()) + { + str = "Election results\n" + + "\n" + + "\n" + + "

Election results

\n"; + for (i in candtonum) + strmeth[i] = ""; + result = calcbald(false); + if (result.tiebreak) + tbused = true; + strmeth[result.winner] += "Baldwin" + (result.tiebreak ? "*" : "") + "
"; + result = calcblac(false); + if (result.tiebreak) + tbused = true; + strmeth[result.winner] += "Black" + (result.tiebreak ? "*" : "") + "
"; + result = calcbord(false); + if (result.tiebreak) + tbused = true; + strmeth[result.winner] += "Borda" + (result.tiebreak ? "*" : "") + "
"; + if (!equalranks) + { + result = calcbuck(false); + if (result.tiebreak) + tbused = true; + strmeth[result.winner] += "Bucklin" + (result.tiebreak ? "*" : "") + "
"; + result = calccare(false); + if (result.tiebreak) + tbused = true; + strmeth[result.winner] += "Carey" + (result.tiebreak ? "*" : "") + "
"; + result = calccoom(false); + if (result.tiebreak) + tbused = true; + strmeth[result.winner] += "Coombs" + (result.tiebreak ? "*" : "") + "
"; + } + result = calccope(false); + if (result.tiebreak) + tbused = true; + strmeth[result.winner] += "Copeland" + (result.tiebreak ? "*" : "") + "
"; + result = calcdodg(false); + if (result.tiebreak) + tbused = true; + strmeth[result.winner] += "Dodgson" + (result.tiebreak ? "*" : "") + "
"; + if (!equalranks) + { + result = calchare(false); + if (result.tiebreak) + tbused = true; + strmeth[result.winner] += "Hare" + (result.tiebreak ? "*" : "") + "
"; + } +/* result = calclegr(false); + if (result.tiebreak) + tbused = true; + strmeth[result.winner] += "LeGrand" + (result.tiebreak ? "*" : "") + "
"; */ + result = calcnans(false); + if (result.tiebreak) + tbused = true; + strmeth[result.winner] += "Nanson" + (result.tiebreak ? "*" : "") + "
"; + result = calcrayn(false); + if (result.tiebreak) + tbused = true; + strmeth[result.winner] += "Raynaud" + (result.tiebreak ? "*" : "") + "
"; + result = calcschu(false); + if (result.tiebreak) + tbused = true; + strmeth[result.winner] += "Schulze" + (result.tiebreak ? "*" : "") + "
"; + result = calcsimp(false); + if (result.tiebreak) + tbused = true; + strmeth[result.winner] += "Simpson" + (result.tiebreak ? "*" : "") + "
"; + result = calcsmal(false); + if (result.tiebreak) + tbused = true; + strmeth[result.winner] += "Small" + (result.tiebreak ? "*" : "") + "
"; + result = calctide(false); + if (result.tiebreak) + tbused = true; + strmeth[result.winner] += "Tideman" + (result.tiebreak ? "*" : "") + "
"; + str += "

\n" + + "\n"; + for (i in numtocand) + if (strmeth[numtocand[i]].length > 0) + str += "\n"; + str += "
winnermethod(s)
" + numtocand[i] + "" + strmeth[numtocand[i]].replace(/
$/, "") + + "

"; + if (tbused) + str += "\n

* The ranking " + printtiebreak() + " was used as a random-ballot tiebreaker.

\n"; + str += "
\n" + + "

The ranked ballots:

\n" + + printrvote() + + "

The pairwise matrix:

\n" + + printpmatrix(pvote); + if (smith.length > 1) + { + str += "

There is no Condorcet winner.  The Smith set "; + if (smith.length < numtocand.length) + { + str += "is {"; + for (i in smith) + str += "" + numtocand[smith[i]] + "" + (i < smith.length - 1 ? ", " : ""); + str += "}."; + } + else + str += "includes all of the candidates."; + if (schwartz.length < smith.length) + if (schwartz.length > 1) + { + str += "  The Schwartz set is {"; + for (i in schwartz) + str += "" + numtocand[schwartz[i]] + "" + (i < schwartz.length - 1 ? ", " : ""); + str += "}."; + } + else + str += "  The Schwartz winner is " + numtocand[schwartz[0]] + "."; + str += "

\n"; + } + else + str += "

" + numtocand[smith[0]] + " is the Condorcet winner.

\n"; + document.write(str + ""); + document.close(); + } +} +function calcbald(onlyone) +{ + var consider = new Array(), elim, elimset, i, j, nconsider, result = new Object(), score = new Array(), str1, str2, worstscore; + + var readwritedocument = ( ! returnstring ) && onlyone // calling from rbvote + // rwd rs oo + // F T T betterballot + // T F T rbvote + // F T F betterballot + // F F F rbvote + if (readwritedocument) + { + if (!readvotes()) + return; + } + if (onlyone) + { + str1 = "Baldwin election results\n" + + "\n" + + "\n" + + "

Baldwin election results

\n"; + str2 = "

The ranked ballots:

\n" + + printrvote() + + "

The pairwise matrix:

\n" + + printpmatrix(pvote) + + "

A candidate’s Borda score can be found by subtracting its column sum from its row sum.

\n" + + "

The candidates’ Borda scores:

\n"; + } + for (i in numtocand) + { + consider[consider.length] = true; + score[score.length] = 0; + } + nconsider = numtocand.length; + result.tiebreak = false; + while (true) + { + for (i in pvote) + if (consider[i]) + { + score[i] = 0; + for (j in pvote[i]) + if (i != j && consider[j]) + score[i] += pvote[i][j] - pvote[j][i]; + } + if (onlyone) + { + str2 += "\n"; + for (i in numtocand) + if (consider[i]) + str2 += "\n"; + str2 += "
" + numtocand[i] + "" + score[i] / 2 + "
\n" + + "

"; + } + worstscore = Number.MAX_VALUE; + for (i in score) + if (consider[i]) + if (score[i] < worstscore) + worstscore = score[i]; + elimset = new Array(); + for (i in score) + if (consider[i]) + if (score[i] == worstscore) + elimset[elimset.length] = i; + elim = elimset[0]; + if (elimset.length == 1) + { + if (onlyone) + str2 += "" + numtocand[elim] + " has the single worst Borda score and so is eliminated.

\n"; + } + else + { + for (i in elimset) + if (numtotb[elimset[i]] > numtotb[elim]) + elim = elimset[i]; + result.tiebreak = true; + if (onlyone) + { + if (elimset.length == nconsider) + str2 += "All of the candidates"; + else + for (i in elimset) + str2 += "" + numtocand[elimset[i]] + "" + + (i == elimset.length - 2 ? " and " : i == elimset.length - 1 ? "" : ", "); + str2 += " have equally worst Borda scores, so a random-ballot tiebreaker must be used:

\n" + + "

     " + printtiebreak() + "

\n" + + "

" + numtocand[elim] + " is the lowest of them in the tiebreaking ranking and so is eliminated.

\n"; + } + } + consider[elim] = false; + --nconsider; + if (nconsider <= 1) + break; + if (onlyone) + str2 += "

The reduced pairwise matrix:

\n" + + printpmatrix(pvote, consider) + + "

The candidates’ new Borda scores:

\n"; + } + for (i in consider) + if (consider[i]) + result.winner = numtocand[i]; + if (onlyone) + { + str1 += "
" + result.winner + " wins the Baldwin election" + + (result.tiebreak ? " using the tiebreaking ranking " + printtiebreak() : "") + ".
\n"; + str2 += "

" + result.winner + " is the only remaining candidate and so wins the election.

\n" + + ""; + } + if (readwritedocument) + { + document.write(str1 + str2); + document.close(); + } + else if (onlyone) + { + result.str = str1 + str2 + return result + } + else + return result; +} +function calcblac(onlyone) +{ + var bestscore = -Number.MAX_VALUE, i, j, result = new Object(), score = new Array(), str1, str2, winset = new Array(); + + var readwritedocument = ( ! returnstring ) && onlyone // calling from rbvote + if (readwritedocument) + { + if (!readvotes()) + return; + } + if (onlyone) + { + str1 = "Black election results\n" + + "\n" + + "\n" + + "

Black election results

\n"; + str2 = "

The ranked ballots:

\n" + + printrvote() + + "

The pairwise matrix:

\n" + + printpmatrix(pvote); + } + for (i in pvote) + { + score[score.length] = 0; + for (j in pvote[i]) + if (i != j) + if (pvote[i][j] > pvote[j][i]) + ++score[i]; + } + for (i in score) + if (score[i] > bestscore) + bestscore = score[i]; + if (bestscore == numtocand.length - 1) + { + for (i in score) + if (score[i] == bestscore) + winset[winset.length] = i; + result.winner = numtocand[winset[0]]; + result.tiebreak = false; + if (onlyone) + str2 += "

" + result.winner + " wins all of its pairwise comparisons and so wins the election outright.

\n"; + } + else + { + if (onlyone) + str2 += "

There is no Condorcet winner, so the candidates’ Borda scores are compared.

\n" + + "

A candidate’s Borda score can be found by subtracting its column sum from its row sum.

\n" + + "

The candidates’ Borda scores:

\n" + + "\n"; + for (i in pvote) + { + score[i] = 0; + for (j in pvote[i]) + if (i != j) + score[i] += pvote[i][j] - pvote[j][i]; + } + if (onlyone) + { + for (i in numtocand) + str2 += "\n"; + str2 += "
" + numtocand[i] + "" + score[i] / 2 + "
\n" + + "

"; + } + bestscore = -Number.MAX_VALUE; + for (i in score) + if (score[i] > bestscore) + bestscore = score[i]; + for (i in score) + if (score[i] == bestscore) + winset[winset.length] = i; + if (winset.length == 1) + { + result.winner = numtocand[winset[0]]; + result.tiebreak = false; + if (onlyone) + str2 += "" + result.winner + " has the single best Borda score and so wins the election outright.

\n"; + } + else + { + result.winner = winset[0]; + for (i in winset) + if (numtotb[winset[i]] < numtotb[result.winner]) + result.winner = winset[i]; + result.winner = numtocand[result.winner]; + result.tiebreak = true; + if (onlyone) + { + if (winset.length == numtocand.length) + str2 += "All of the candidates"; + else + for (i in winset) + str2 += "" + numtocand[winset[i]] + "" + + (i == winset.length - 2 ? " and " : i == winset.length - 1 ? "" : ", "); + str2 += " have equally best Borda scores, so a random-ballot tiebreaker must be used:

\n" + + "

     " + printtiebreak() + "

\n" + + "

" + result.winner + + " is the highest of them in the tiebreaking ranking and so wins the election.

\n"; + } + } + } + if (onlyone) + { + str1 += "
" + result.winner + " wins the Black election" + + (result.tiebreak ? " using the tiebreaking ranking " + printtiebreak() : "") + ".
\n"; + str2 += ""; + } + if (readwritedocument) + { + document.write(str1 + str2); + document.close(); + } + else if (onlyone) + { + result.str = str1 + str2 + return result + } + else + return result; +} +function calcbord(onlyone) +{ + var bestscore = -Number.MAX_VALUE, i, j, result = new Object(), score = new Array(), str1, str2, winset = new Array(); + + var readwritedocument = ( ! returnstring ) && onlyone // calling from rbvote + if (readwritedocument) + { + if (!readvotes()) + return; + } + if (onlyone) + { + str1 = "Borda election results\n" + + "\n" + + "\n" + + "

Borda election results

\n"; + str2 = "

The ranked ballots:

\n" + + printrvote() + + "

The pairwise matrix:

\n" + + printpmatrix(pvote) + + "

A candidate’s Borda score can be found by subtracting its column sum from its row sum.

\n" + + "

The candidates’ Borda scores:

\n" + + "\n"; + } + for (i in pvote) + { + score[score.length] = 0; + for (j in pvote[i]) + if (i != j) + score[i] += pvote[i][j] - pvote[j][i]; + } + if (onlyone) + { + for (i in numtocand) + str2 += "\n"; + str2 += "
" + numtocand[i] + "" + score[i] / 2 + "
\n" + + "

"; + } + for (i in score) + if (score[i] > bestscore) + bestscore = score[i]; + for (i in score) + if (score[i] == bestscore) + winset[winset.length] = i; + if (winset.length == 1) + { + result.winner = numtocand[winset[0]]; + result.tiebreak = false; + if (onlyone) + str2 += "" + result.winner + " has the single best Borda score and so wins the election outright.

\n"; + } + else + { + result.winner = winset[0]; + for (i in winset) + if (numtotb[winset[i]] < numtotb[result.winner]) + result.winner = winset[i]; + result.winner = numtocand[result.winner]; + result.tiebreak = true; + if (onlyone) + { + if (winset.length == numtocand.length) + str2 += "All of the candidates"; + else + for (i in winset) + str2 += "" + numtocand[winset[i]] + "" + + (i == winset.length - 2 ? " and " : i == winset.length - 1 ? "" : ", "); + str2 += " have equally best Borda scores, so a random-ballot tiebreaker must be used:

\n" + + "

     " + printtiebreak() + "

\n" + + "

" + result.winner + " is the highest of them in the tiebreaking ranking and so wins the election.

\n"; + } + } + if (onlyone) + { + str1 += "
" + result.winner + " wins the Borda election" + + (result.tiebreak ? " using the tiebreaking ranking " + printtiebreak() : "") + ".
\n"; + str2 += ""; + } + if (readwritedocument) + { + document.write(str1 + str2); + document.close(); + } + else if (onlyone) + { + result.str = str1 + str2 + return result + } + else + return result; +} +function calcbuck(onlyone) +{ + var bestscore = 0, i, j, quota, result = new Object(), score = new Array(), str1, str2, winset = new Array(); + + var readwritedocument = ( ! returnstring ) && onlyone // calling from rbvote + if (readwritedocument) + { + if (!readvotes()) + return; + if (equalranks) + { + alert("Bucklin elections require fully-ranked ballots with no tied preferences. Please correct the ranked ballots and try again or try " + + "another method."); + return; + } + } + if (onlyone) + { + str1 = "Bucklin election results\n" + + "\n" + + "\n" + + "

Bucklin election results

\n"; + str2 = "

The ranked ballots:

\n" + + printrvote(); + } + for (i in numtocand) + score[score.length] = 0; + quota = 0; + for (i in rvote) + quota += rvotenum[i]; + quota /= 2; + if (onlyone) + str2 += "

A majority total of " + Math.floor(quota + 1) + " votes is necessary to win.

\n" + + "

The candidates’ first-rank totals:

\n" + + "\n"; + for (i in numtocand) + { + for (j in rvote) + score[rvote[j][i]] += rvotenum[j]; + for (j in score) + if (score[j] > bestscore) + bestscore = score[j]; + if (onlyone) + { + for (j in numtocand) + str2 += "\n"; + str2 += "
" + numtocand[j] + "" + score[j] + "
\n" + + "

"; + } + if (bestscore > quota) + break; + if (onlyone) + str2 += "No candidate has a majority yet, so " + (i == 0 ? "second" : i == 1 ? "third" : i == 2 ? "fourth" : i + 2 + "th") + + "-rank totals are added:

\n" + + "\n"; + } + for (i in score) + if (score[i] == bestscore) + winset[winset.length] = i; + if (winset.length == 1) + { + result.winner = numtocand[winset[0]]; + result.tiebreak = false; + if (onlyone) + str2 += "" + result.winner + " has the single best Bucklin score and so wins the election outright.

\n"; + } + else + { + result.winner = winset[0]; + for (i in winset) + if (numtotb[winset[i]] < numtotb[result.winner]) + result.winner = winset[i]; + result.winner = numtocand[result.winner]; + result.tiebreak = true; + if (onlyone) + { + if (winset.length == numtocand.length) + str2 += "All of the candidates"; + else + for (i in winset) + str2 += "" + numtocand[winset[i]] + "" + + (i == winset.length - 2 ? " and " : i == winset.length - 1 ? "" : ", "); + str2 += " have equally best Bucklin scores, so a random-ballot tiebreaker must be used:

\n" + + "

     " + printtiebreak() + "

\n" + + "

" + result.winner + " is the highest of them in the tiebreaking ranking and so wins the election.

\n"; + } + } + if (onlyone) + { + str1 += "
" + result.winner + " wins the Bucklin election" + + (result.tiebreak ? " using the tiebreaking ranking " + printtiebreak() : "") + ".
\n"; + str2 += ""; + } + if (readwritedocument) + { + document.write(str1 + str2); + document.close(); + } + else if (onlyone) + { + result.str = str1 + str2 + return result + } + else + return result; +} +function calccare(onlyone) +{ + var consider = new Array(), elimset, i, j, nconsider, quota, result = new Object(), score = new Array(), str1, str2; + + var readwritedocument = ( ! returnstring ) && onlyone // calling from rbvote + if (readwritedocument) + { + if (!readvotes()) + return; + if (equalranks) + { + alert("Carey elections require fully-ranked ballots with no tied preferences. Please correct the ranked ballots and try again or try another " + + "method."); + return; + } + } + if (onlyone) + { + str1 = "Carey election results\n" + + "\n" + + "\n" + + "

Carey election results

\n"; + str2 = "

The ranked ballots:

\n" + + printrvote() + + "

The candidates’ first-rank totals:

\n"; + } + for (i in numtocand) + { + consider[consider.length] = true; + score[score.length] = 0; + } + nconsider = numtocand.length; + result.tiebreak = false; + while (true) + { + for (i in score) + score[i] = 0; + quota = 0; + for (i in rvote) + for (j in rvote[i]) + if (consider[rvote[i][j]]) + { + score[rvote[i][j]] += rvotenum[i]; + quota += rvotenum[i]; + break; + } + quota /= nconsider; + if (onlyone) + { + str2 += "\n"; + for (i in numtocand) + if (consider[i]) + str2 += "\n"; + str2 += "
" + numtocand[i] + "" + score[i] + "
\n" + + "

The average first-rank total is " + Math.round(1000 * quota) / 1000 + ".

\n" + + "

"; + } + elimset = new Array(); + for (i in score) + if (consider[i]) + if (score[i] < quota) + elimset[elimset.length] = i; + if (elimset.length == 0) + for (i in score) + if (consider[i]) + if (score[i] <= quota) + elimset[elimset.length] = i; + if (elimset.length == nconsider) + break; + for (i in elimset) + { + consider[elimset[i]] = false; + --nconsider; + if (onlyone) + str2 += "" + numtocand[elimset[i]] + "" + + (i == elimset.length - 2 ? " and " : i == elimset.length - 1 ? "" : ", "); + } + if (onlyone) + str2 += (elimset.length == 1 ? " has a below-average first-rank total and so is" : " have below-average first-rank totals and so are") + + " eliminated.

\n"; + if (nconsider <= 1) + break; + if (onlyone) + str2 += "

The reduced ranked ballots:

\n" + + printrvote(consider) + + "

The candidates’ new first-rank totals:

\n"; + } + if (nconsider > 1) + { + result.winner = elimset[0]; + for (i in elimset) + if (numtotb[elimset[i]] < numtotb[result.winner]) + result.winner = elimset[i]; + result.winner = numtocand[result.winner]; + result.tiebreak = true; + if (onlyone) + str2 += "All of the " + (nconsider == numtocand.length ? "" : "remaining ") + + "candidates have equal first-rank totals, so a random-ballot tiebreaker must be used:

\n" + + "

     " + printtiebreak() + "

\n" + + "

" + result.winner + " is the highest of them in the tiebreaking ranking and so wins the election.

\n"; + } + else + { + for (i in consider) + if (consider[i]) + result.winner = numtocand[i]; + str2 += "

" + result.winner + " is the only remaining candidate and so wins the election outright.

\n"; + } + if (onlyone) + { + str1 += "
" + result.winner + " wins the Carey election" + + (result.tiebreak ? " using the tiebreaking ranking " + printtiebreak() : "") + ".
\n"; + str2 += ""; + } + if (readwritedocument) + { + document.write(str1 + str2); + document.close(); + } + else if (onlyone) + { + result.str = str1 + str2 + return result + } + else + return result; +} +function calccoom(onlyone) +{ + var consider = new Array(), elim, elimset, i, j, nconsider, result = new Object(), score = new Array(), str1, str2, worstscore; + + var readwritedocument = ( ! returnstring ) && onlyone // calling from rbvote + if (readwritedocument) + { + if (!readvotes()) + return; + if (equalranks) + { + alert("Coombs elections require fully-ranked ballots with no tied preferences. Please correct the ranked ballots and try again or try another " + + "method."); + return; + } + } + if (onlyone) + { + str1 = "Coombs election results\n" + + "\n" + + "\n" + + "

Coombs election results

\n"; + str2 = "

The ranked ballots:

\n" + + printrvote() + + "

The candidates’ last-rank totals:

\n"; + } + for (i in numtocand) + { + consider[consider.length] = true; + score[score.length] = 0; + } + nconsider = numtocand.length; + result.tiebreak = false; + while (true) + { + for (i in score) + score[i] = 0; + for (i in rvote) + for (j = rvote[i].length - 1; j >= 0; --j) + if (consider[rvote[i][j]]) + { + score[rvote[i][j]] += rvotenum[i]; + break; + } + if (onlyone) + { + str2 += "\n"; + for (i in numtocand) + if (consider[i]) + str2 += "\n"; + str2 += "
" + numtocand[i] + "" + score[i] + "
\n" + + "

"; + } + worstscore = 0; + for (i in score) + if (consider[i]) + if (score[i] > worstscore) + worstscore = score[i]; + elimset = new Array(); + for (i in score) + if (consider[i]) + if (score[i] == worstscore) + elimset[elimset.length] = i; + elim = elimset[0]; + if (elimset.length == 1) + { + if (onlyone) + str2 += "" + numtocand[elim] + " has the single largest last-rank total and so is eliminated.

\n"; + } + else + { + for (i in elimset) + if (numtotb[elimset[i]] > numtotb[elim]) + elim = elimset[i]; + result.tiebreak = true; + if (onlyone) + { + if (elimset.length == nconsider) + str2 += "All of the candidates"; + else + for (i in elimset) + str2 += "" + numtocand[elimset[i]] + "" + + (i == elimset.length - 2 ? " and " : i == elimset.length - 1 ? "" : ", "); + str2 += " have equally largest last-rank totals, so a random-ballot tiebreaker must be used:

\n" + + "

     " + printtiebreak() + "

\n" + + "

" + numtocand[elim] + " is the lowest of them in the tiebreaking ranking and so is eliminated.

\n"; + } + } + consider[elim] = false; + --nconsider; + if (nconsider <= 1) + break; + if (onlyone) + str2 += "

The reduced ranked ballots:

\n" + + printrvote(consider) + + "

The candidates’ new last-rank totals:

\n"; + } + for (i in consider) + if (consider[i]) + result.winner = numtocand[i]; + if (onlyone) + { + str1 += "
" + result.winner + " wins the Coombs election" + + (result.tiebreak ? " using the tiebreaking ranking " + printtiebreak() : "") + ".
\n"; + str2 += "

" + result.winner + " is the only remaining candidate and so wins the election.

\n" + + ""; + } + if (readwritedocument) + { + document.write(str1 + str2); + document.close(); + } + else if (onlyone) + { + result.str = str1 + str2 + return result + } + else + return result; +} +function calccope(onlyone) +{ + var bestscore = 0, i, j, result = new Object(), score = new Array(), str1, str2, winset = new Array(); + + var readwritedocument = ( ! returnstring ) && onlyone // calling from rbvote + if (readwritedocument) + { + if (!readvotes()) + return; + } + if (onlyone) + { + str1 = "Copeland election results\n" + + "\n" + + "\n" + + "

Copeland election results

\n"; + str2 = "

The ranked ballots:

\n" + + printrvote() + + "

The pairwise matrix:

\n" + + printpmatrix(pvote) + + "

A candidate’s Copeland score is the number of its pairwise victories, where a pairwise tie counts as half a victory.

\n" + + "

The candidates’ Copeland scores:

\n" + + "\n"; + } + for (i in pvote) + { + score[score.length] = 0; + for (j in pvote[i]) + if (i != j) + if (pvote[i][j] > pvote[j][i]) + score[i] += 2; + else if (pvote[i][j] == pvote[j][i]) + ++score[i]; + } + if (onlyone) + { + for (i in numtocand) + str2 += "\n"; + str2 += "
" + numtocand[i] + "" + + (score[i] % 2 == 1 ? (score[i] > 2 ? (score[i] - 1) / 2 : "") + "½" : score[i] / 2) + "
\n" + + "

"; + } + for (i in score) + if (score[i] > bestscore) + bestscore = score[i]; + for (i in score) + if (score[i] == bestscore) + winset[winset.length] = i; + if (winset.length == 1) + { + result.winner = numtocand[winset[0]]; + result.tiebreak = false; + if (onlyone) + str2 += "" + result.winner + " has the single best Copeland score and so wins the election outright.

\n"; + } + else + { + result.winner = winset[0]; + for (i in winset) + if (numtotb[winset[i]] < numtotb[result.winner]) + result.winner = winset[i]; + result.winner = numtocand[result.winner]; + result.tiebreak = true; + if (onlyone) + { + if (winset.length == numtocand.length) + str2 += "All of the candidates"; + else + for (i in winset) + str2 += "" + numtocand[winset[i]] + "" + + (i == winset.length - 2 ? " and " : i == winset.length - 1 ? "" : ", "); + str2 += " have equally best Copeland scores, so a random-ballot tiebreaker must be used:

\n" + + "

     " + printtiebreak() + "

\n" + + "

" + result.winner + " is the highest of them in the tiebreaking ranking and so wins the election.

\n"; + } + } + if (onlyone) + { + str1 += "
" + result.winner + " wins the Copeland election" + + (result.tiebreak ? " using the tiebreaking ranking " + printtiebreak() : "") + ".
\n"; + str2 += ""; + } + if (readwritedocument) + { + document.write(str1 + str2); + document.close(); + } + else if (onlyone) + { + result.str = str1 + str2 + return result + } + else + return result; +} +function calcdodg(onlyone) +{ + var bestscore = Number.MAX_VALUE, i, j, result = new Object(), score = new Array(), str1, str2, winset = new Array(); + + var readwritedocument = ( ! returnstring ) && onlyone // calling from rbvote + if (readwritedocument) + { + if (!readvotes()) + return; + } + if (onlyone) + { + str1 = "Dodgson election results\n" + + "\n" + + "\n" + + "

Dodgson election results

\n"; + str2 = "

The ranked ballots:

\n" + + printrvote() + + "

The pairwise matrix:

\n" + + printpmatrix(pvote) + + "

A candidate’s Dodgson score is the sum of the margins of its pairwise defeats.

\n" + + "

The candidates’ Dodgson scores:

\n" + + "\n"; + } + for (i in pvote) + { + score[score.length] = 0; + for (j in pvote[i]) + if (i != j && pvote[i][j] < pvote[j][i]) + score[i] += pvote[j][i] - pvote[i][j]; + } + if (onlyone) + { + for (i in numtocand) + str2 += "\n"; + str2 += "
" + numtocand[i] + "" + score[i] / 2 + "
\n" + + "

"; + } + for (i in score) + if (score[i] < bestscore) + bestscore = score[i]; + for (i in score) + if (score[i] == bestscore) + winset[winset.length] = i; + if (winset.length == 1) + { + result.winner = numtocand[winset[0]]; + result.tiebreak = false; + if (onlyone) + str2 += "" + result.winner + " has the single best Dodgson score and so wins the election outright.

\n"; + } + else + { + result.winner = winset[0]; + for (i in winset) + if (numtotb[winset[i]] < numtotb[result.winner]) + result.winner = winset[i]; + result.winner = numtocand[result.winner]; + result.tiebreak = true; + if (onlyone) + { + if (winset.length == numtocand.length) + str2 += "All of the candidates"; + else + for (i in winset) + str2 += "" + numtocand[winset[i]] + "" + + (i == winset.length - 2 ? " and " : i == winset.length - 1 ? "" : ", "); + str2 += " have equally best Dodgson scores, so a random-ballot tiebreaker must be used:

\n" + + "

     " + printtiebreak() + "

\n" + + "

" + result.winner + " is the highest of them in the tiebreaking ranking and so wins the election.

\n"; + } + } + if (onlyone) + { + str1 += "
" + result.winner + " wins the Dodgson election" + + (result.tiebreak ? " using the tiebreaking ranking " + printtiebreak() : "") + ".
\n"; + str2 += ""; + } + if (readwritedocument) + { + document.write(str1 + str2); + document.close(); + } + else if (onlyone) + { + result.str = str1 + str2 + return result + } + else + return result; +} +function calchare(onlyone) +{ + var consider = new Array(), elim, elimset, i, j, nconsider, result = new Object(), score = new Array(), str1, str2, worstscore; + + var readwritedocument = ( ! returnstring ) && onlyone // calling from rbvote + if (readwritedocument) + { + if (!readvotes()) + return; + if (equalranks) + { + alert("Hare elections require fully-ranked ballots with no tied preferences. Please correct the ranked ballots and try again or try another " + + "method."); + return; + } + } + if (onlyone) + { + str1 = "Hare election results\n" + + "\n" + + "\n" + + "

Hare election results

\n"; + str2 = "

The ranked ballots:

\n" + + printrvote() + + "

The candidates’ first-rank totals:

\n"; + } + for (i in numtocand) + { + consider[consider.length] = true; + score[score.length] = 0; + } + nconsider = numtocand.length; + result.tiebreak = false; + while (true) + { + for (i in score) + score[i] = 0; + for (i in rvote) + for (j in rvote[i]) + if (consider[rvote[i][j]]) + { + score[rvote[i][j]] += rvotenum[i]; + break; + } + if (onlyone) + { + str2 += "\n"; + for (i in numtocand) + if (consider[i]) + str2 += "\n"; + str2 += "
" + numtocand[i] + "" + score[i] + "
\n" + + "

"; + } + worstscore = Number.MAX_VALUE; + for (i in score) + if (consider[i]) + if (score[i] < worstscore) + worstscore = score[i]; + elimset = new Array(); + for (i in score) + if (consider[i]) + if (score[i] == worstscore) + elimset[elimset.length] = i; + if (worstscore == 0) + { + if (onlyone) + { + for (i in elimset) + str2 += "" + numtocand[elimset[i]] + "" + + (i == elimset.length - 2 ? " and " : i == elimset.length - 1 ? " " : ", "); + str2 += (elimset.length == 1 ? "has no first-rank votes and so is" : "have no first-rank votes and so are") + " eliminated.

\n"; + } + for (i in elimset) + consider[elimset[i]] = false; + nconsider -= elimset.length; + } + else + { + elim = elimset[0]; + if (elimset.length == 1) + { + if (onlyone) + str2 += "" + numtocand[elim] + " has the single smallest first-rank total and so is eliminated.

\n"; + } + else + { + for (i in elimset) + if (numtotb[elimset[i]] > numtotb[elim]) + elim = elimset[i]; + result.tiebreak = true; + if (onlyone) + { + if (elimset.length == nconsider) + str2 += "All of the candidates"; + else + for (i in elimset) + str2 += "" + numtocand[elimset[i]] + "" + + (i == elimset.length - 2 ? " and " : i == elimset.length - 1 ? "" : ", "); + str2 += " have equally smallest first-rank totals, so a random-ballot tiebreaker must be used:

\n" + + "

     " + printtiebreak() + "

\n" + + "

" + numtocand[elim] + + " is the lowest of them in the tiebreaking ranking and so is eliminated.

\n"; + } + } + consider[elim] = false; + --nconsider; + } + if (nconsider <= 1) + break; + if (onlyone) + str2 += "

The reduced ranked ballots:

\n" + + printrvote(consider) + + "

The candidates’ new first-rank totals:

\n"; + } + for (i in consider) + if (consider[i]) + result.winner = numtocand[i]; + if (onlyone) + { + str1 += "
" + result.winner + " wins the Hare election" + + (result.tiebreak ? " using the tiebreaking ranking " + printtiebreak() : "") + ".
\n"; + str2 += "

" + result.winner + " is the only remaining candidate and so wins the election.

\n" + + ""; + } + if (readwritedocument) + { + document.write(str1 + str2); + document.close(); + } + else if (onlyone) + { + result.str = str1 + str2 + return result + } + else + return result; +} +function calcheit(onlyone) +{ + var beat = new Array(), beatall, highest, i, j, k, l, loser, nclear, ncontra, nimplied, result = new Object(), seen = new Array(), str1, str2; + + var readwritedocument = ( ! returnstring ) && onlyone // calling from rbvote + if (readwritedocument) + { + if (!readvotes()) + return; + } + if (onlyone) + { + str1 = "Heitzig election results\n" + + "\n" + + "\n" + + "

Heitzig election results

\n"; + str2 = "

The ranked ballots:

\n" + + printrvote() + + "

The pairwise matrix:

\n" + + printpmatrix(pvote); + } + for (i in pvote) + { + beat[beat.length] = new Array(); + seen[seen.length] = new Array(); + for (j in pvote[i]) + beat[i][beat[i].length] = seen[i][seen[i].length] = false; + } + result.tiebreak = false; +bigloop: + while (true) + { + highest = nclear = ncontra = nimplied = 0; + for (i in numtocand) + for (j in numtocand) + if (i != j && !seen[i][j]) + if (pvote[i][j] > highest) + highest = pvote[i][j]; + for (i in numtocand) + for (j in numtocand) + if (i != j && !seen[i][j] && pvote[i][j] == highest) + { + if (beat[i][j] && !beat[j][i]) + ++nimplied; + if (!beat[i][j] && beat[j][i]) + ++ncontra; + if (!beat[i][j] && !beat[j][i]) + ++nclear; + } + if (nimplied > 0) + for (i in numtocand) + for (j in numtocand) + if (i != j && !seen[i][j] && pvote[i][j] == highest && beat[i][j]) + { + if (onlyone) + str2 += "

" + numtocand[i] + ">" + numtocand[j] + " has a strength of " + + (highest % 2 == 1 ? (highest - 1) / 2 + "½" : highest / 2) + " but is already locked.

\n"; + seen[i][j] = true; + } + if (ncontra > 0) + for (i in numtocand) + for (j in numtocand) + if (i != j && !seen[i][j] && pvote[i][j] == highest && beat[j][i]) + { + if (onlyone) + str2 += "

" + numtocand[i] + ">" + numtocand[j] + " has a strength of " + + (highest % 2 == 1 ? (highest - 1) / 2 + "½" : highest / 2) + " but " + numtocand[j] + ">" + + numtocand[i] + " is already locked.

\n"; + seen[i][j] = true; + } + if (nclear > 0) + { + if (nclear > 1) + { + result.tiebreak = true; + if (onlyone) + { + str2 += "

"; + for (i in numtocand) + for (j in numtocand) + if (i != j && !seen[i][j] && pvote[i][j] == highest) + str2 += "" + numtocand[i] + ">" + numtocand[j] + "" + + (--nclear > 1 ? ", " : nclear > 0 ? " and " : " have a strength of "); + str2 += (highest % 2 == 1 ? (highest - 1) / 2 + "½" : highest / 2) + ", so a random-ballot tiebreaker must be used:

\n" + + "

     " + printtiebreak() + "

\n"; + } + loser = new Array(); + for (i in numtocand) + loser[loser.length] = false; + for (i in numtocand) + for (k in numtocand) + if (i != k && !seen[i][k] && pvote[i][k] == highest) + { + loser[k] = true; + j = k; + } + ncontra = 0; + for (i in loser) + if (loser[i]) + { + if (numtotb[i] > numtotb[j]) + j = i; + ++ncontra; + } + loser = new Array(); + for (i in numtocand) + loser[loser.length] = false; + for (k in numtocand) + if (j != k && !seen[k][j] && pvote[k][j] == highest) + { + loser[k] = true; + i = k; + } + nimplied = 0; + for (k in loser) + if (loser[k]) + { + if (numtotb[i] > numtotb[k]) + i = k; + ++nimplied; + } + if (onlyone) + { + if (ncontra > 1) + str2 += "

Of the losers of each comparison, " + numtocand[j] + + " is the lowest in the tiebreaking ranking.

\n"; + if (nimplied > 1) + str2 += "

Of the winners of each " + (ncontra > 1 ? "defeat of " + numtocand[j] + "" : "comparison") + + ", " + numtocand[i] + " is the highest in the tiebreaking ranking.

\n"; + } + seen[i][j] = true; + if (onlyone) + str2 += "

" + numtocand[i] + ">" + numtocand[j] + + " wins the tiebreaker and so is locked.

\n"; + } + else + { +findloop: + for (i in numtocand) + for (j in numtocand) + if (i != j && !seen[i][j] && pvote[i][j] == highest) + break findloop; + if (i == j || seen[i][j] || pvote[i][j] != highest) + continue bigloop; + seen[i][j] = true; + if (onlyone) + str2 += "

" + numtocand[i] + ">" + numtocand[j] + " has a strength of " + + (highest % 2 == 1 ? (highest - 1) / 2 + "½" : highest / 2) + " and so is locked.

\n"; + } + beat[i][j] = beatall = true; + for (k in numtocand) + if (i != k && !beat[i][k]) + beatall = false; + if (beatall) + { + result.winner = numtocand[i]; + break bigloop; + } + for (i in numtocand) + for (j in numtocand) + if (i != j) + for (k in numtocand) + if (j != k && beat[i][k] && beat[j][i] && !beat[j][k]) + { + if (onlyone) + str2 += "

" + numtocand[j] + ">" + numtocand[k] + + " is implied by " + numtocand[j] + ">" + numtocand[i] + + " and " + numtocand[i] + ">" + numtocand[k] + " and so is locked.

\n"; + beat[j][k] = beatall = true; + for (l in numtocand) + if (j != l && !beat[j][l]) + beatall = false; + if (beatall) + { + result.winner = numtocand[j]; + break bigloop; + } + } + } + } + if (onlyone) + str2 += "

" + result.winner + " has locked victories over all other candidates and so wins the election.

\n"; + if (onlyone) + { + str1 += "
" + result.winner + " wins the Heitzig election" + + (result.tiebreak ? " using the tiebreaking ranking " + printtiebreak() : "") + ".
\n"; + str2 += ""; + } + if (readwritedocument) + { + document.write(str1 + str2); + document.close(); + } + else if (onlyone) + { + result.str = str1 + str2 + return result + } + else + return result; +} +/* function calclegrandschulze(onlyone) +{ + var beat = new Array(), beatlength, beatold = new Array(), beatstr = new Array(), beatstrold = new Array(), i, j, k, result = new Object(), + score = new Array(), str1, str2, winset = new Array(); + + var readwritedocument = ( ! returnstring ) && onlyone // calling from rbvote + if (readwritedocument) + { + if (!readvotes()) + return; + } + if (onlyone) + { + str1 = "LeGrand election results\n" + + "\n" + + "\n" + + "

LeGrand election results

\n"; + str2 = "

The ranked ballots:

\n" + + printrvote() + + "

The pairwise matrix:

\n" + + printpmatrix(pvote); + } + for (i in pvote) + { + beat[beat.length] = new Array(); + beatold[beatold.length] = new Array(); + beatstr[beatstr.length] = new Array(); + beatstrold[beatstrold.length] = new Array(); + for (j in pvote[i]) + { + beat[i][beat[i].length] = beatold[i][beatold[i].length] = pvote[i][j]; + beatstr[i][beatstr[i].length] = beatstrold[i][beatstrold[i].length] = numtocand[i] + ">" + numtocand[j]; + } + score[score.length] = 0; + } + for (beatlength = 1; beatlength < numtocand.length; ++beatlength) + { + if (onlyone) + { + str2 += "

Comparisons of strongest beatpaths of length at most " + beatlength + " for each pair of candidates:

\n" + + "\n"; + for (i in numtocand) + for (j in numtocand) + if (i < j) + str2 += " beat[j][i] ? " class=\"win\"" : beat[i][j] < beat[j][i] ? " class=\"loss\"" : "") + + ">" + beatstr[i][j] + " beat[j][i] ? " class=\"win\"" : beat[i][j] < beat[j][i] ? " class=\"loss\"" : "") + ">" + + (beat[i][j] % 2 == 1 ? (beat[i][j] > 2 ? (beat[i][j] - 1) / 2 : "") + "½" : beat[i][j] / 2) + " beat[i][j] ? " class=\"win\"" : beat[j][i] < beat[i][j] ? " class=\"loss\"" : "") + ">" + + (beat[j][i] % 2 == 1 ? (beat[j][i] > 2 ? (beat[j][i] - 1) / 2 : "") + "½" : beat[j][i] / 2) + " beat[i][j] ? " class=\"win\"" : beat[j][i] < beat[i][j] ? " class=\"loss\"" : "") + ">" + + beatstr[j][i] + "\n"; + str2 += "
\n" + + "

"; + } + for (i in score) + score[i] = 0; + for (i in numtocand) + for (j in numtocand) + if (i != j && beat[i][j] > beat[j][i]) + ++score[j]; + for (i in score) + if (score[i] == 0) + winset[winset.length] = i; + if (winset.length > 0) + break; + if (onlyone) + str2 += "Each candidate loses a beatpath comparison, so the beatpaths are expanded by one.

\n"; + for (i in beat) + for (j in beat[i]) + beatold[i][j] = beat[i][j]; + for (i in beatstr) + for (j in beatstr[i]) + beatstrold[i][j] = beatstr[i][j]; + for (i in numtocand) + for (j in numtocand) + if (i != j) + for (k in numtocand) + if (i != k && j != k && beat[i][j] < beatold[i][k] && beat[i][j] < pvote[k][j]) + { + beat[i][j] = beatold[i][k] < pvote[k][j] ? beatold[i][k] : pvote[k][j]; + beatstr[i][j] = beatstrold[i][k] + ">" + numtocand[j]; + } + } + if (winset.length == 1) + { + result.winner = numtocand[winset[0]]; + result.tiebreak = false; + if (onlyone) + str2 += "" + result.winner + + " is the only candidate to lose no beatpath comparisons and so wins the election outright.

\n"; + } + else + { + result.winner = winset[0]; + for (i in winset) + if (numtotb[winset[i]] < numtotb[result.winner]) + result.winner = winset[i]; + result.winner = numtocand[result.winner]; + result.tiebreak = true; + if (onlyone) + { + if (winset.length == numtocand.length) + str2 += "All of the candidates"; + else + for (i in winset) + str2 += "" + numtocand[winset[i]] + "" + + (i == winset.length - 2 ? " and " : i == winset.length - 1 ? "" : ", "); + str2 += " lose no beatpath comparisons, so a random-ballot tiebreaker must be used:

\n" + + "

     " + printtiebreak() + "

\n" + + "

" + result.winner + " is the highest of them in the tiebreaking ranking and so wins the election.

\n"; + } + } + if (onlyone) + { + str1 += "
" + result.winner + " wins the LeGrand election" + + (result.tiebreak ? " using the tiebreaking ranking " + printtiebreak() : "") + ".
\n"; + str2 += ""; + } + if (readwritedocument) + { + document.write(str1 + str2); + document.close(); + } + else if (onlyone) + { + result.str = str1 + str2 + return result + } + else + return result; +} */ +function calcnans(onlyone) +{ + var consider = new Array(), elimset, i, j, nconsider, result = new Object(), score = new Array(), str1, str2; + + var readwritedocument = ( ! returnstring ) && onlyone // calling from rbvote + if (readwritedocument) + { + if (!readvotes()) + return; + } + if (onlyone) + { + str1 = "Nanson election results\n" + + "\n" + + "\n" + + "

Nanson election results

\n"; + str2 = "

The ranked ballots:

\n" + + printrvote() + + "

The pairwise matrix:

\n" + + printpmatrix(pvote) + + "

A candidate’s Borda score can be found by subtracting its column sum from its row sum.

\n" + + "

The candidates’ Borda scores:

\n"; + } + for (i in numtocand) + { + consider[consider.length] = true; + score[score.length] = 0; + } + nconsider = numtocand.length; + result.tiebreak = false; + while (true) + { + for (i in pvote) + if (consider[i]) + { + score[i] = 0; + for (j in pvote[i]) + if (i != j && consider[j]) + score[i] += pvote[i][j] - pvote[j][i]; + } + if (onlyone) + { + str2 += "\n"; + for (i in numtocand) + if (consider[i]) + str2 += "\n"; + str2 += "
" + numtocand[i] + "" + score[i] / 2 + "
\n" + + "

"; + } + elimset = new Array(); + for (i in score) + if (consider[i]) + if (score[i] < 0) + elimset[elimset.length] = i; + if (elimset.length == 0) + for (i in score) + if (consider[i]) + if (score[i] <= 0) + elimset[elimset.length] = i; + if (elimset.length == nconsider) + break; + for (i in elimset) + { + consider[elimset[i]] = false; + --nconsider; + if (onlyone) + str2 += "" + numtocand[elimset[i]] + "" + + (i == elimset.length - 2 ? " and " : i == elimset.length - 1 ? "" : ", "); + } + if (onlyone) + str2 += (elimset.length == 1 ? " has a negative Borda score and so is" : " have negative Borda scores and so are") + " eliminated.

\n"; + if (nconsider <= 1) + break; + if (onlyone) + str2 += "

The reduced pairwise matrix:

\n" + + printpmatrix(pvote, consider) + + "

The candidates’ new Borda scores:

\n"; + } + if (nconsider > 1) + { + result.winner = elimset[0]; + for (i in elimset) + if (numtotb[elimset[i]] < numtotb[result.winner]) + result.winner = elimset[i]; + result.winner = numtocand[result.winner]; + result.tiebreak = true; + if (onlyone) + str2 += "All of the " + (nconsider == numtocand.length ? "" : "remaining ") + + "candidates have equal Borda scores, so a random-ballot tiebreaker must be used:

\n" + + "

     " + printtiebreak() + "

\n" + + "

" + result.winner + " is the highest of them in the tiebreaking ranking and so wins the election.

\n"; + } + else + { + for (i in consider) + if (consider[i]) + result.winner = numtocand[i]; + str2 += "

" + result.winner + " is the only remaining candidate and so wins the election outright.

\n"; + } + if (onlyone) + { + str1 += "
" + result.winner + " wins the Nanson election" + + (result.tiebreak ? " using the tiebreaking ranking " + printtiebreak() : "") + ".
\n"; + str2 += ""; + } + if (readwritedocument) + { + document.write(str1 + str2); + document.close(); + } + else if (onlyone) + { + result.str = str1 + str2 + return result + } + else + return result; +} +function calcrayn(onlyone) +{ + var consider = new Array(), elim, elimset, i, j, nconsider, result = new Object(), score = new Array(), str1, str2, worstscore; + + var readwritedocument = ( ! returnstring ) && onlyone // calling from rbvote + if (readwritedocument) + { + if (!readvotes()) + return; + } + if (onlyone) + { + str1 = "Raynaud election results\n" + + "\n" + + "\n" + + "

Raynaud election results

\n"; + str2 = "

The ranked ballots:

\n" + + printrvote() + + "

The pairwise matrix:

\n" + + printpmatrix(pvote) + + "

A candidate’s Simpson score is the fewest number of votes it received in any single pairwise comparison.

\n" + + "

The candidates’ Simpson scores:

\n"; + } + for (i in numtocand) + { + consider[consider.length] = true; + score[score.length] = Number.MAX_VALUE; + } + nconsider = numtocand.length; + result.tiebreak = false; + while (true) + { + for (i in pvote) + if (consider[i]) + { + score[i] = Number.MAX_VALUE; + for (j in pvote[i]) + if (i != j && consider[j] && pvote[i][j] < score[i]) + score[i] = pvote[i][j]; + } + if (onlyone) + { + str2 += "\n"; + for (i in numtocand) + if (consider[i]) + str2 += "\n"; + str2 += "
" + numtocand[i] + "" + + (score[i] % 2 == 1 ? (score[i] > 2 ? (score[i] - 1) / 2 : "") + "½" : score[i] / 2) + "
\n" + + "

"; + } + worstscore = Number.MAX_VALUE; + for (i in score) + if (consider[i]) + if (score[i] < worstscore) + worstscore = score[i]; + elimset = new Array(); + for (i in score) + if (consider[i]) + if (score[i] == worstscore) + elimset[elimset.length] = i; + elim = elimset[0]; + if (elimset.length == 1) + { + if (onlyone) + str2 += "" + numtocand[elim] + " has the single worst Simpson score and so is eliminated.

\n"; + } + else + { + for (i in elimset) + if (numtotb[elimset[i]] > numtotb[elim]) + elim = elimset[i]; + result.tiebreak = true; + if (onlyone) + { + if (elimset.length == nconsider) + str2 += "All of the candidates"; + else + for (i in elimset) + str2 += "" + numtocand[elimset[i]] + "" + + (i == elimset.length - 2 ? " and " : i == elimset.length - 1 ? "" : ", "); + str2 += " have equally worst Simpson scores, so a random-ballot tiebreaker must be used:

\n" + + "

     " + printtiebreak() + "

\n" + + "

" + numtocand[elim] + " is the lowest of them in the tiebreaking ranking and so is eliminated.

\n"; + } + } + consider[elim] = false; + --nconsider; + if (nconsider <= 1) + break; + if (onlyone) + str2 += "

The reduced pairwise matrix:

\n" + + printpmatrix(pvote, consider) + + "

The candidates’ new Simpson scores:

\n"; + } + for (i in consider) + if (consider[i]) + result.winner = numtocand[i]; + if (onlyone) + { + str1 += "
" + result.winner + " wins the Raynaud election" + + (result.tiebreak ? " using the tiebreaking ranking " + printtiebreak() : "") + ".
\n"; + str2 += "

" + result.winner + " is the only remaining candidate and so wins the election.

\n" + + ""; + } + if (readwritedocument) + { + document.write(str1 + str2); + document.close(); + } + else if (onlyone) + { + result.str = str1 + str2 + return result + } + else + return result; +} +function calcschu(onlyone) +{ + var beat = new Array(), beatstr = new Array(), i, j, k, result = new Object(), score = new Array(), str1, str2, winset = new Array(); + + var readwritedocument = ( ! returnstring ) && onlyone // calling from rbvote + if (readwritedocument) + { + if (!readvotes()) + return; + } + if (onlyone) + { + str1 = "Schulze election results\n" + + "\n" + + "\n" + + "

Schulze election results

\n"; + str2 = "

The ranked ballots:

\n" + + printrvote() + + "

The pairwise matrix:

\n" + + printpmatrix(pvote) + + "

Comparisons of strongest beatpaths for each pair of candidates:

\n"; + } + for (i in pvote) + { + beat[beat.length] = new Array(); + beatstr[beatstr.length] = new Array(); + for (j in pvote[i]) + { + beat[i][beat[i].length] = pvote[i][j]; + beatstr[i][beatstr[i].length] = numtocand[i] + ">" + numtocand[j]; + } + score[score.length] = 0; + } + for (i in numtocand) + for (j in numtocand) + if (i != j) + for (k in numtocand) + if (i != k && j != k && beat[j][k] < beat[j][i] && beat[j][k] < beat[i][k]) + { + beat[j][k] = beat[j][i] < beat[i][k] ? beat[j][i] : beat[i][k]; + beatstr[j][k] = beatstr[j][i] + beatstr[i][k].replace(/^[^>]+/, ""); + } + if (onlyone) + { + str2 += "\n"; + for (i in numtocand) + for (j in numtocand) + if (i < j) + str2 += " beat[j][i] ? " class=\"win\"" : beat[i][j] < beat[j][i] ? " class=\"loss\"" : "") + + ">" + beatstr[i][j] + " beat[j][i] ? " class=\"win\"" : beat[i][j] < beat[j][i] ? " class=\"loss\"" : "") + ">" + + (beat[i][j] % 2 == 1 ? (beat[i][j] > 2 ? (beat[i][j] - 1) / 2 : "") + "½" : beat[i][j] / 2) + " beat[i][j] ? " class=\"win\"" : beat[j][i] < beat[i][j] ? " class=\"loss\"" : "") + ">" + + (beat[j][i] % 2 == 1 ? (beat[j][i] > 2 ? (beat[j][i] - 1) / 2 : "") + "½" : beat[j][i] / 2) + " beat[i][j] ? " class=\"win\"" : beat[j][i] < beat[i][j] ? " class=\"loss\"" : "") + ">" + + beatstr[j][i] + "\n"; + str2 += "
\n" + + "

"; + } + for (i in numtocand) + for (j in numtocand) + if (i != j && beat[i][j] > beat[j][i]) + ++score[j]; + for (i in score) + if (score[i] == 0) + winset[winset.length] = i; + if (winset.length == 1) + { + result.winner = numtocand[winset[0]]; + result.tiebreak = false; + if (onlyone) + str2 += "" + result.winner + + " is the only candidate to lose no beatpath comparisons and so wins the election outright.

\n"; + } + else + { + result.winner = winset[0]; + for (i in winset) + if (numtotb[winset[i]] < numtotb[result.winner]) + result.winner = winset[i]; + result.winner = numtocand[result.winner]; + result.tiebreak = true; + if (onlyone) + { + if (winset.length == numtocand.length) + str2 += "All of the candidates"; + else + for (i in winset) + str2 += "" + numtocand[winset[i]] + "" + + (i == winset.length - 2 ? " and " : i == winset.length - 1 ? "" : ", "); + str2 += " lose no beatpath comparisons, so a random-ballot tiebreaker must be used:

\n" + + "

     " + printtiebreak() + "

\n" + + "

" + result.winner + " is the highest of them in the tiebreaking ranking and so wins the election.

\n"; + } + } + if (onlyone) + { + str1 += "
" + result.winner + " wins the Schulze election" + + (result.tiebreak ? " using the tiebreaking ranking " + printtiebreak() : "") + ".
\n"; + str2 += ""; + } + if (readwritedocument) + { + document.write(str1 + str2); + document.close(); + } + else if (onlyone) + { + result.str = str1 + str2 + return result + } + else + return result; +} +function calcsimp(onlyone) +{ + var bestscore = -Number.MAX_VALUE, i, j, result = new Object(), score = new Array(), str1, str2, winset = new Array(); + + var readwritedocument = ( ! returnstring ) && onlyone // calling from rbvote + if (readwritedocument) + { + if (!readvotes()) + return; + } + if (onlyone) + { + str1 = "Simpson election results\n" + + "\n" + + "\n" + + "

Simpson election results

\n"; + str2 = "

The ranked ballots:

\n" + + printrvote() + + "

The pairwise matrix:

\n" + + printpmatrix(pvote) + + "

A candidate’s Simpson score is the fewest number of votes it received in any single pairwise comparison.

\n" + + "

The candidates’ Simpson scores:

\n" + + "\n"; + } + for (i in pvote) + { + score[score.length] = Number.MAX_VALUE; + for (j in pvote[i]) + if (i != j && pvote[i][j] < score[i]) + score[i] = pvote[i][j]; + } + if (onlyone) + { + for (i in numtocand) + str2 += "\n"; + str2 += "
" + numtocand[i] + "" + + (score[i] % 2 == 1 ? (score[i] > 2 ? (score[i] - 1) / 2 : "") + "½" : score[i] / 2) + "
\n" + + "

"; + } + for (i in score) + if (score[i] > bestscore) + bestscore = score[i]; + for (i in score) + if (score[i] == bestscore) + winset[winset.length] = i; + if (winset.length == 1) + { + result.winner = numtocand[winset[0]]; + result.tiebreak = false; + if (onlyone) + str2 += "" + result.winner + " has the single best Simpson score and so wins the election outright.

\n"; + } + else + { + result.winner = winset[0]; + for (i in winset) + if (numtotb[winset[i]] < numtotb[result.winner]) + result.winner = winset[i]; + result.winner = numtocand[result.winner]; + result.tiebreak = true; + if (onlyone) + { + if (winset.length == numtocand.length) + str2 += "All of the candidates"; + else + for (i in winset) + str2 += "" + numtocand[winset[i]] + "" + + (i == winset.length - 2 ? " and " : i == winset.length - 1 ? "" : ", "); + str2 += " have equally best Simpson scores, so a random-ballot tiebreaker must be used:

\n" + + "

     " + printtiebreak() + "

\n" + + "

" + result.winner + " is the highest of them in the tiebreaking ranking and so wins the election.

\n"; + } + } + if (onlyone) + { + str1 += "
" + result.winner + " wins the Simpson election" + + (result.tiebreak ? " using the tiebreaking ranking " + printtiebreak() : "") + ".
\n"; + str2 += ""; + } + if (readwritedocument) + { + document.write(str1 + str2); + document.close(); + } + else if (onlyone) + { + result.str = str1 + str2 + return result + } + else + return result; +} +function calcsmal(onlyone) +{ + var bestscore, consider = new Array(), elimset, i, j, nconsider, result = new Object(), score = new Array(), str1, str2; + + var readwritedocument = ( ! returnstring ) && onlyone // calling from rbvote + if (readwritedocument) + { + if (!readvotes()) + return; + } + if (onlyone) + { + str1 = "Small election results\n" + + "\n" + + "\n" + + "

Small election results

\n"; + str2 = "

The ranked ballots:

\n" + + printrvote() + + "

The pairwise matrix:

\n" + + printpmatrix(pvote) + + "

A candidate’s Copeland score is the number of its pairwise victories, where a pairwise tie counts as half a victory.

\n" + + "

The candidates’ Copeland scores:

\n"; + } + for (i in numtocand) + { + consider[consider.length] = true; + score[score.length] = 0; + } + nconsider = numtocand.length; + result.tiebreak = false; + while (true) + { + for (i in pvote) + score[i] = 0; + for (i in pvote) + if (consider[i]) + for (j in pvote[i]) + if (i != j && consider[j]) + if (pvote[i][j] > pvote[j][i]) + score[i] += 2; + else if (pvote[i][j] == pvote[j][i]) + ++score[i]; + if (onlyone) + { + str2 += "\n"; + for (i in numtocand) + if (consider[i]) + str2 += "\n"; + str2 += "
" + numtocand[i] + "" + + (score[i] % 2 == 1 ? (score[i] > 2 ? (score[i] - 1) / 2 : "") + "½" : score[i] / 2) + "
\n" + + "

"; + } + bestscore = 0; + for (i in score) + if (score[i] > bestscore) + bestscore = score[i]; + elimset = new Array(); + for (i in score) + if (consider[i]) + if (score[i] < bestscore) + elimset[elimset.length] = i; + if (elimset.length == 0) + for (i in score) + if (consider[i]) + if (score[i] <= bestscore) + elimset[elimset.length] = i; + if (elimset.length == nconsider) + break; + for (i in elimset) + { + consider[elimset[i]] = false; + --nconsider; + if (onlyone) + str2 += "" + numtocand[elimset[i]] + "" + + (i == elimset.length - 2 ? " and " : i == elimset.length - 1 ? "" : ", "); + } + if (onlyone) + str2 += (elimset.length == 1 ? " has a non-best Copeland score and so is" : " have non-best Copeland scores and so are") + " eliminated.

\n"; + if (nconsider <= 1) + break; + if (onlyone) + str2 += "

The reduced pairwise matrix:

\n" + + printpmatrix(pvote, consider) + + "

The candidates’ new Copeland scores:

\n"; + } + if (nconsider > 1) + { + result.winner = elimset[0]; + for (i in elimset) + if (numtotb[elimset[i]] < numtotb[result.winner]) + result.winner = elimset[i]; + result.winner = numtocand[result.winner]; + result.tiebreak = true; + if (onlyone) + str2 += "All of the " + (nconsider == numtocand.length ? "" : "remaining ") + + "candidates have equal Copeland scores, so a random-ballot tiebreaker must be used:

\n" + + "

     " + printtiebreak() + "

\n" + + "

" + result.winner + " is the highest of them in the tiebreaking ranking and so wins the election.

\n"; + } + else + { + for (i in consider) + if (consider[i]) + result.winner = numtocand[i]; + str2 += "

" + result.winner + " is the only remaining candidate and so wins the election outright.

\n"; + } + if (onlyone) + { + str1 += "
" + result.winner + " wins the Small election" + + (result.tiebreak ? " using the tiebreaking ranking " + printtiebreak() : "") + ".
\n"; + str2 += ""; + } + if (readwritedocument) + { + document.write(str1 + str2); + document.close(); + } + else if (onlyone) + { + result.str = str1 + str2 + return result + } + else + return result; +} +function calctide(onlyone) +{ + var beat = new Array(), beatall, highest, i, j, k, l, loser, nclear, ncontra, nimplied, result = new Object(), seen = new Array(), str1, str2; + + var readwritedocument = ( ! returnstring ) && onlyone // calling from rbvote + if (readwritedocument) + { + if (!readvotes()) + return; + } + if (onlyone) + { + str1 = "Tideman election results\n" + + "\n" + + "\n" + + "

Tideman election results

\n"; + str2 = "

The ranked ballots:

\n" + + printrvote() + + "

The pairwise matrix:

\n" + + printpmatrix(pvote); + } + for (i in pvote) + { + beat[beat.length] = new Array(); + seen[seen.length] = new Array(); + for (j in pvote[i]) + beat[i][beat[i].length] = seen[i][seen[i].length] = false; + } + result.tiebreak = false; +bigloop: + while (true) + { + highest = nclear = ncontra = nimplied = 0; + for (i in numtocand) + for (j in numtocand) + if (i != j && !seen[i][j]) + if (pvote[i][j] > highest) + highest = pvote[i][j]; + for (i in numtocand) + for (j in numtocand) + if (i != j && !seen[i][j] && pvote[i][j] == highest) + { + if (beat[i][j] && !beat[j][i]) + ++nimplied; + if (!beat[i][j] && beat[j][i]) + ++ncontra; + if (!beat[i][j] && !beat[j][i]) + ++nclear; + } + if (nimplied > 0) + for (i in numtocand) + for (j in numtocand) + if (i != j && !seen[i][j] && pvote[i][j] == highest && beat[i][j]) + { + if (onlyone) + str2 += "

" + numtocand[i] + ">" + numtocand[j] + " has a strength of " + + (highest % 2 == 1 ? (highest - 1) / 2 + "½" : highest / 2) + " but is already locked.

\n"; + seen[i][j] = true; + } + if (ncontra > 0) + for (i in numtocand) + for (j in numtocand) + if (i != j && !seen[i][j] && pvote[i][j] == highest && beat[j][i]) + { + if (onlyone) + str2 += "

" + numtocand[i] + ">" + numtocand[j] + " has a strength of " + + (highest % 2 == 1 ? (highest - 1) / 2 + "½" : highest / 2) + " but " + numtocand[j] + ">" + + numtocand[i] + " is already locked.

\n"; + seen[i][j] = true; + } + if (nclear > 0) + { + if (nclear > 1) + { + result.tiebreak = true; + if (onlyone) + { + str2 += "

"; + for (i in numtocand) + for (j in numtocand) + if (i != j && !seen[i][j] && pvote[i][j] == highest) + str2 += "" + numtocand[i] + ">" + numtocand[j] + "" + + (--nclear > 1 ? ", " : nclear > 0 ? " and " : " have a strength of "); + str2 += (highest % 2 == 1 ? (highest - 1) / 2 + "½" : highest / 2) + ", so a random-ballot tiebreaker must be used:

\n" + + "

     " + printtiebreak() + "

\n"; + } + loser = new Array(); + for (i in numtocand) + loser[loser.length] = false; + for (i in numtocand) + for (k in numtocand) + if (i != k && !seen[i][k] && pvote[i][k] == highest) + { + loser[k] = true; + j = k; + } + ncontra = 0; + for (i in loser) + if (loser[i]) + { + if (numtotb[i] > numtotb[j]) + j = i; + ++ncontra; + } + loser = new Array(); + for (i in numtocand) + loser[loser.length] = false; + for (k in numtocand) + if (j != k && !seen[k][j] && pvote[k][j] == highest) + { + loser[k] = true; + i = k; + } + nimplied = 0; + for (k in loser) + if (loser[k]) + { + if (numtotb[i] > numtotb[k]) + i = k; + ++nimplied; + } + if (onlyone) + { + if (ncontra > 1) + str2 += "

Of the losers of each comparison, " + numtocand[j] + + " is the lowest in the tiebreaking ranking.

\n"; + if (nimplied > 1) + str2 += "

Of the winners of each " + (ncontra > 1 ? "defeat of " + numtocand[j] + "" : "comparison") + + ", " + numtocand[i] + " is the highest in the tiebreaking ranking.

\n"; + } + seen[i][j] = true; + if (onlyone) + str2 += "

" + numtocand[i] + ">" + numtocand[j] + + " wins the tiebreaker and so is locked.

\n"; + } + else + { +findloop: + for (i in numtocand) + for (j in numtocand) + if (i != j && !seen[i][j] && pvote[i][j] == highest) + break findloop; + if (i == j || seen[i][j] || pvote[i][j] != highest) + continue bigloop; + seen[i][j] = true; + if (onlyone) + str2 += "

" + numtocand[i] + ">" + numtocand[j] + " has a strength of " + + (highest % 2 == 1 ? (highest - 1) / 2 + "½" : highest / 2) + " and so is locked.

\n"; + } + beat[i][j] = beatall = true; + for (k in numtocand) + if (i != k && !beat[i][k]) + beatall = false; + if (beatall) + { + result.winner = numtocand[i]; + break bigloop; + } + for (i in numtocand) + for (j in numtocand) + if (i != j) + for (k in numtocand) + if (j != k && beat[i][k] && beat[j][i] && !beat[j][k]) + { + if (onlyone) + str2 += "

" + numtocand[j] + ">" + numtocand[k] + + " is implied by " + numtocand[j] + ">" + numtocand[i] + + " and " + numtocand[i] + ">" + numtocand[k] + " and so is locked.

\n"; + beat[j][k] = beatall = true; + for (l in numtocand) + if (j != l && !beat[j][l]) + beatall = false; + if (beatall) + { + result.winner = numtocand[j]; + break bigloop; + } + } + } + } + if (onlyone) + str2 += "

" + result.winner + " has locked victories over all other candidates and so wins the election.

\n"; + if (onlyone) + { + str1 += "
" + result.winner + " wins the Tideman election" + + (result.tiebreak ? " using the tiebreaking ranking " + printtiebreak() : "") + ".
\n"; + str2 += ""; + } + if (readwritedocument) + { + document.write(str1 + str2); + document.close(); + } + else if (onlyone) + { + result.str = str1 + str2 + return result + } + else + return result; +} +function printpmatrix(pmatrix, consider) +{ + var firstcand = true, i, j, nconsider = 0, str; + if (typeof consider != "object") + { + consider = new Array(); + for (i in numtocand) + consider[consider.length] = true; + } + for (i in consider) + if (consider[i]) + ++nconsider; + if (nconsider == 0) + return "

Error!  No candidate left!

"; + else if (nconsider == 1) + return "

Error!  Only one candidate left!

"; + str = "

\n" + + "\n" + + ""; + for (i in pmatrix) + if (consider[i]) + str += ""; + str += "\n"; + for (i in pmatrix) + if (consider[i]) + { + str += ""; + if (firstcand) + { + firstcand = false; + str += ""; + } + str += ""; + for (j in pmatrix[i]) + if (consider[j]) + str += " pmatrix[j][i] ? " class=\"win\">" : pmatrix[i][j] < pmatrix[j][i] ? " class=\"loss\">" : ">") + + (i == j ? "" : pmatrix[i][j] % 2 == 1 ? (pmatrix[i][j] > 2 ? (pmatrix[i][j] - 1) / 2 : "") + "½" : pmatrix[i][j] / 2) + + (pmatrix[i][j] > pmatrix[j][i] ? "" : "") + ""; + str += "\n"; + } + return str + "
against
" + numtocand[i] + "
for" + numtocand[i] + "

\n"; +} +function printrvote(consider) +{ + if (returnstring) return "-- skipped --" // don't put the ballots in the string + + var i, j, nconsider = 0, str; + if (typeof consider != "object") + { + consider = new Array(); + for (i in numtocand) + consider[consider.length] = true; + } + for (i in consider) + if (consider[i]) + ++nconsider; + if (nconsider == 0) + return "

Error!  No candidate left!

"; + else if (nconsider == 1) + return "

Error!  Only one candidate left!

"; + str = "

\n"; + for (i in rvote) + { + str += "\n"; + } + return str + "
" + rvotenum[i] + ":"; + for (j in rvotetie[i]) + if (consider[rvote[i][j]]) + str += numtocand[rvote[i][j]] + (rvotetie[i][j] ? "=" : ">"); + if (consider[rvote[i][rvote[i].length - 1]]) + str += numtocand[rvote[i][rvote[i].length - 1]]; + else + str = str.replace(/>$/, ""); + str += "

\n"; +} +function printtiebreak() +{ + var i, str = ""; + for (i in tbtonum) + { + if (i > 0) + str += ">"; + str += numtocand[tbtonum[i]]; + } + return str + ""; +} +function readvotes() +{ + var absent, beat = new Array(), i, ignoreinput, j, k, l, nextcand, rating = new Array(), regexp = new RegExp(), rvotecomment, rvoteinput, + rvoteline = new Array(), tiebreakinput; + rvoteinput = document.rbform.rvote.value.replace(/[\f\r\v]/g, "\n") + "\n"; + rvoteinput = rvoteinput.replace(/[\t\n ]+\n/g, "\n"); + rvoteinput = rvoteinput.replace(/^\n/, ""); + rvoteinput = rvoteinput.replace(/\n$/, ""); + if (!/\S/.test(rvoteinput)) + { + document.rbform.rvote.value = ""; + alert("No input was detected. Please enter ranked ballots and try again."); + return false; + } + rvoteline = rvoteinput.split("\n"); + rvotecomment = rvoteinput.split("\n"); + for (i = 0; i < rvoteline.length; ++i) + { + rvoteline[i] = rvoteline[i].replace(/[\t\n ]*#.*$/, ""); + if (/\S/.test(rvoteline[i])) + { + rvoteline[i] = rvoteline[i].replace(/[^\t \d:=>A-Za-z]/g, "?"); + if (!/\?/.test(rvoteline[i]) && !/^[\t ]*(\d+[\t ]*:[\t ]*)?[A-Za-z]+([\t ]*[=>][\t ]*[A-Za-z]+)*$/.test(rvoteline[i])) + rvoteline[i] = "!" + rvoteline[i]; + } + rvotecomment[i] = /#/.test(rvotecomment[i]) ? "#" + rvotecomment[i].replace(/^[^#]*#/, "") : ""; + } + rvoteinput = ""; + for (i = 0; i < rvoteline.length; ++i) + { + if (i > 0) + rvoteinput += "\n"; + rvoteinput += rvoteline[i] + (rvoteline[i].length && rvotecomment[i].length ? " " : "") + rvotecomment[i]; + } + document.rbform.rvote.value = rvoteinput; + for (i in rvoteline) + { + rvoteline[i] = rvoteline[i].replace(/[\t ]+/g, ""); + if (/\w/.test(rvoteline[i]) && !/:/.test(rvoteline[i])) + rvoteline[i] = "1:" + rvoteline[i]; + } + rvoteinput = rvoteline.join("\n"); + if (!/\S/.test(rvoteinput)) + { + alert("No input was detected. Please enter or uncomment ranked ballots and try again."); + return false; + } + if (/\?/.test(rvoteinput)) + { + alert("Illegal characters were detected and replaced with question marks. The only legal characters (except inside comments) are letters, " + + "digits, :, >, = and whitespace. Please correct the ranked ballots and try again."); + return false; + } + if (/!/.test(rvoteinput)) + { + alert("Badly-formed lines were detected and marked with exclamation points. Please correct the ranked ballots and try again; see below for " + + "examples."); + return false; + } + rvoteinput = rvoteinput.replace(/\n\n+/g, "\n"); + rvoteinput = rvoteinput.replace(/^\n+/, ""); + rvoteinput = rvoteinput.replace(/\n+$/, ""); + rvoteline = rvoteinput.split("\n"); + tiebreakinput = document.rbform.tiebreak.value.replace(/[^>A-Za-z]/g, ">"); + tiebreakinput = tiebreakinput.replace(/>>+/g, ">"); + tiebreakinput = tiebreakinput.replace(/^>/, ""); + document.rbform.tiebreak.value = tiebreakinput = tiebreakinput.replace(/>$/, ""); + ignoreinput = document.rbform.ignore.value.replace(/[^A-Za-z]/g, " "); + ignoreinput = ignoreinput.replace(/\s\s+/g, " "); + ignoreinput = ignoreinput.replace(/^\s/, ""); + document.rbform.ignore.value = ignoreinput = ignoreinput.replace(/\s$/, ""); + if (ignoreinput.length > 0) + { + ignoreinput = ignoreinput.match(/[A-Za-z]+/g); + for (i in rvoteline) + { + rvoteline[i] = rvoteline[i].replace(/:/, ": ") + " "; + rvoteline[i] = rvoteline[i].replace(/>/g, " > "); + rvoteline[i] = rvoteline[i].replace(/=/g, " = "); + for (j in ignoreinput) + { + regexp.compile(" " + ignoreinput[j] + " =", "g"); + rvoteline[i] = rvoteline[i].replace(regexp, " "); + regexp.compile("= " + ignoreinput[j] + " ", "g"); + rvoteline[i] = rvoteline[i].replace(regexp, " "); + regexp.compile(" " + ignoreinput[j] + " >", "g"); + rvoteline[i] = rvoteline[i].replace(regexp, " "); + regexp.compile("> " + ignoreinput[j] + " ", "g"); + rvoteline[i] = rvoteline[i].replace(regexp, " "); + regexp.compile(" " + ignoreinput[j] + " ", "g"); + rvoteline[i] = rvoteline[i].replace(regexp, " "); + } + rvoteline[i] = rvoteline[i].replace(/\s+/g, ""); + } + rvoteinput = rvoteline.join("\n"); + tiebreakinput = ">" + tiebreakinput + ">"; + for (i in ignoreinput) + { + regexp.compile(">" + ignoreinput[i] + ">", "g"); + tiebreakinput = tiebreakinput.replace(regexp, ">"); + } + tiebreakinput = tiebreakinput.replace(/^>/, ""); + tiebreakinput = tiebreakinput.replace(/>$/, ""); + } + candtonum = new Object(); + numtocand = (rvoteinput + " " + tiebreakinput).match(/[A-Za-z]+/g); + for (i = 0; i < numtocand.length; ++i) + candtonum[numtocand[i]] = 0; + numtocand = new Array(); + for (i in candtonum) + numtocand[numtocand.length] = i; + if (numtocand.length < 2) + { + alert("The election must include at least two candidates. Please correct the ranked ballots and try again."); + return false; + } + numtocand.sort(); + candtonum = new Object(); + for (i in numtocand) + candtonum[numtocand[i]] = i; + rvote = new Array(); + rvotenum = new Array(); + rvotetie = new Array(); + for (i = j = 0; i < rvoteline.length; ++i) + { + k = new Number(rvoteline[i].match(/\d+/)[0]); + if (k <= 0) + continue; + rvotenum[rvotenum.length] = k; + rvoteline[i] = " " + rvoteline[i].replace(/[\d:]+/g, "") + " "; + rvoteline[i] = rvoteline[i].replace(/=/g, " = "); + rvoteline[i] = rvoteline[i].replace(/>/g, " > "); + rvote[rvote.length] = new Array(); + rvotetie[rvotetie.length] = new Array(); + while (/[A-Za-z]/.test(rvoteline[i])) + { + nextcand = rvoteline[i].match(/[A-Za-z]+/)[0]; + rvote[j][rvote[j].length] = candtonum[nextcand]; + rvotetie[j][rvotetie[j].length] = (rvoteline[i] + ">").match(/[=>]/)[0] == "="; + regexp.compile(" " + nextcand + " =", "g"); + rvoteline[i] = rvoteline[i].replace(regexp, " "); + regexp.compile("= " + nextcand + " ", "g"); + rvoteline[i] = rvoteline[i].replace(regexp, " "); + regexp.compile(" " + nextcand + " >", "g"); + rvoteline[i] = rvoteline[i].replace(regexp, " "); + regexp.compile("> " + nextcand + " ", "g"); + rvoteline[i] = rvoteline[i].replace(regexp, " "); + regexp.compile(" " + nextcand + " ", "g"); + rvoteline[i] = rvoteline[i].replace(regexp, " "); + } + for (k in numtocand) + { + absent = true; + for (l in rvote[j]) + if (rvote[j][l] == k) + absent = false; + if (absent) + { + rvote[j][rvote[j].length] = k; + rvotetie[j][rvotetie[j].length] = true; + } + } + rvotetie[j].pop(); + ++j; + } + if (j < 1) + { + alert("At least one ballot must have a nonzero count. Please correct the ranked ballots and try again."); + return false; + } + numtotb = new Array(); + tbtonum = new Array(); + for (i in numtocand) + numtotb[numtotb.length] = 0; + while (/[A-Za-z]/.test(tiebreakinput)) + { + nextcand = tiebreakinput.match(/[A-Za-z]+/)[0]; + tbtonum[tbtonum.length] = candtonum[nextcand]; + regexp.compile(nextcand, "g"); + tiebreakinput = tiebreakinput.replace(regexp, ""); + } + if (tbtonum.length == 0) + { + document.rbform.tiebreak.value = ""; + i = Math.random(); + i = Math.floor(i * rvote.length); + for (j in rvote[i]) + tbtonum[tbtonum.length] = rvote[i][j]; + } + if (tbtonum.length < numtocand.length) + { + alert("If given, the tiebreaking ranking must list all of the candidates. Please either correct or delete it and try again."); + return false; + } + if (document.rbform.reverse.checked) + { + for (i in rvote) + { + for (j = 0, k = rvote[i].length - 1; j < k; ++j, --k) + { + l = rvote[i][j]; + rvote[i][j] = rvote[i][k]; + rvote[i][k] = l; + } + for (j = 0, k = rvotetie[i].length - 1; j < k; ++j, --k) + { + l = rvotetie[i][j]; + rvotetie[i][j] = rvotetie[i][k]; + rvotetie[i][k] = l; + } + } + for (i = 0, j = tbtonum.length - 1; i < j; ++i, --j) + { + k = tbtonum[i]; + tbtonum[i] = tbtonum[j]; + tbtonum[j] = k; + } + } + for (i in tbtonum) + numtotb[tbtonum[i]] = i; + equalranks = false; + for (i in rvotetie) + for (j in rvotetie[i]) + if (rvotetie[i][j]) + equalranks = true; + pvote = new Array(); + for (i in numtocand) + { + pvote[i] = new Array(); + for (j in numtocand) + pvote[i][j] = 0; + } + for (i in rvote) + { + k = 0; + for (j in rvote[i]) + { + rating[rvote[i][j]] = k; + if (j < rvotetie[i].length && !rvotetie[i][j]) + ++k; + } + for (j in numtocand) + for (k in numtocand) + if (j < k) + if (rating[j] < rating[k]) + pvote[j][k] += 2 * rvotenum[i]; + else if (rating[j] > rating[k]) + pvote[k][j] += 2 * rvotenum[i]; + else + { + pvote[j][k] += rvotenum[i]; + pvote[k][j] += rvotenum[i]; + } + } + for (i in numtocand) + { + beat[beat.length] = new Array(); + for (j in numtocand) + beat[i][beat[i].length] = i != j && pvote[i][j] > pvote[j][i]; + rating[i] = true; + } + for (i in beat) + for (j in beat[i]) + if (i != j) + for (k in numtocand) + if (i != k && j != k && beat[i][k] && beat[j][i]) + beat[j][k] = true; + for (i in beat) + for (j in beat[i]) + if (i != j && !beat[i][j] && beat[j][i]) + rating[i] = false; + schwartz = new Array(); + for (i in numtocand) + if (rating[i]) + schwartz[schwartz.length] = i; + for (i in beat) + { + for (j in beat[i]) + if (i != j && pvote[i][j] == pvote[j][i]) + beat[i][j] = true; + rating[i] = true; + } + for (i in beat) + for (j in beat[i]) + if (i != j) + for (k in numtocand) + if (i != k && j != k && beat[i][k] && beat[j][i]) + beat[j][k] = true; + for (i in beat) + for (j in beat[i]) + if (i != j && !beat[i][j] && beat[j][i]) + rating[i] = false; + smith = new Array(); + for (i in numtocand) + if (rating[i]) + smith[smith.length] = i; + return true; +} + + +function readballots(ballots,district,model) +{ + var absent, beat = new Array(), i, ignoreinput, j, k, l, nextcand, rating = new Array(), regexp = new RegExp(), rvotecomment, rvoteinput, + rvoteline = new Array(), tiebreakinput; + + // goal: pvote, rvote, candtonum, numtocand, rvotenum, rvotetie, numtotb, tbtonum, equalranks, schwartz, smith + tiebreakinput = "" + // candtonum is numbers indexed by candidate names + // numtocand is names indexed by numbers + + // convert candidate names to numbers + + numtocand = Object.keys( (district.candidatesById)) + numtoiconcand = Object.keys( (district.candidatesById)) .map(x => model.icon(x) ) + candtonum = new Object(); + for (i in numtocand) candtonum[numtocand[i]] = i; + + // convert ballot format to rvote format + rvote = new Array(); + rvotenum = new Array(); + rvotetie = new Array(); + for (i in ballots) + { + rvotenum[rvotenum.length] = 1 + rvote[rvote.length] = new Array(); + rvotetie[rvotetie.length] = new Array(); + var b = ballots[i].rank + for (j in b) { + rvote[i][rvote[i].length] = candtonum[b[j]] + rvotetie[i][rvotetie[i].length] = false + } + rvotetie[i].pop() + } + + numtotb = new Array(); + tbtonum = new Array(); + for (i in numtocand) { + numtotb[numtotb.length] = 0; + } + i = Math.random(); + i = Math.floor(i * rvote.length); + for (j in rvote[i]) { + tbtonum[tbtonum.length] = rvote[i][j]; + } + for (i in tbtonum) + numtotb[tbtonum[i]] = i; + equalranks = false; + for (i in rvotetie) + for (j in rvotetie[i]) + if (rvotetie[i][j]) + equalranks = true; + pvote = new Array(); + for (i in numtocand) + { + pvote[i] = new Array(); + for (j in numtocand) + pvote[i][j] = 0; + } + for (i in rvote) + { + k = 0; + for (j in rvote[i]) + { + rating[rvote[i][j]] = k; + if (j < rvotetie[i].length && !rvotetie[i][j]) + ++k; + } + for (j in numtocand) + for (k in numtocand) + if (j < k) + if (rating[j] < rating[k]) + pvote[j][k] += 2 * rvotenum[i]; + else if (rating[j] > rating[k]) + pvote[k][j] += 2 * rvotenum[i]; + else + { + pvote[j][k] += rvotenum[i]; + pvote[k][j] += rvotenum[i]; + } + } + for (i in numtocand) + { + beat[beat.length] = new Array(); + for (j in numtocand) + beat[i][beat[i].length] = i != j && pvote[i][j] > pvote[j][i]; + rating[i] = true; + } + for (i in beat) + for (j in beat[i]) + if (i != j) + for (k in numtocand) + if (i != k && j != k && beat[i][k] && beat[j][i]) + beat[j][k] = true; + for (i in beat) + for (j in beat[i]) + if (i != j && !beat[i][j] && beat[j][i]) + rating[i] = false; + schwartz = new Array(); + for (i in numtocand) + if (rating[i]) + schwartz[schwartz.length] = i; + for (i in beat) + { + for (j in beat[i]) + if (i != j && pvote[i][j] == pvote[j][i]) + beat[i][j] = true; + rating[i] = true; + } + for (i in beat) + for (j in beat[i]) + if (i != j) + for (k in numtocand) + if (i != k && j != k && beat[i][k] && beat[j][i]) + beat[j][k] = true; + for (i in beat) + for (j in beat[i]) + if (i != j && !beat[i][j] && beat[j][i]) + rating[i] = false; + smith = new Array(); + for (i in numtocand) + if (rating[i]) + smith[smith.length] = i; + return true; +} + + +return{ + calcall:calcall, + calcbald:calcbald, + calcblac:calcblac, + calcbord:calcbord, + calcbuck:calcbuck, + calccare:calccare, + calccoom:calccoom, + calccope:calccope, + calcdodg:calcdodg, + calchare:calchare, + // calclegr:calclegr, + calcnans:calcnans, + calcrayn:calcrayn, + calcschu:calcschu, + calcsimp:calcsimp, + calcsmal:calcsmal, + calctide:calctide, + readballots:readballots, + returnstring:returnstring, + setreturnstring:setreturnstring, + readvotes:readvotes + } +}(); \ No newline at end of file diff --git a/rbvote/style.css b/rbvote/style.css new file mode 100644 index 00000000..8538611e --- /dev/null +++ b/rbvote/style.css @@ -0,0 +1,36 @@ +a {text-decoration: none} +a:active {color: #3030a0} +a:hover {bottom: 1px; position: relative; text-decoration: underline} +a:link {color: #000080} +a:visited {color: #6060c0} +body {background-color: #c0c8d0; color: #000000} +td.against {background-color: #d0c0c8; font-family: monospace} +td.cfail1 {background-color: #ff5050} +td.cfail2 {background-color: #ff6060} +td.cfail3 {background-color: #ff7070} +td.cfail4 {background-color: #ff8080} +td.cfail5 {background-color: #ff9090} +td.cfail6 {background-color: #ffa0a0} +td.cfail7 {background-color: #ffb0b0} +td.cfail8 {background-color: #ffc0c0} +td.cfail9 {background-color: #ffd0d0} +td.cfaila {background-color: #ffe0e0} +td.cfailb {background-color: #fff0f0} +td.cpass1 {background-color: #50ff50} +td.cpass2 {background-color: #60ff60} +td.cpass3 {background-color: #70ff70} +td.cpass4 {background-color: #80ff80} +td.cpass5 {background-color: #90ff90} +td.cpass6 {background-color: #a0ffa0} +td.cpass7 {background-color: #b0ffb0} +td.cpass8 {background-color: #c0ffc0} +td.cpass9 {background-color: #d0ffd0} +td.cpassa {background-color: #e0ffe0} +td.cpassb {background-color: #f0fff0} +td.for {background-color: #c8d0c0; font-family: monospace} +td.loss {background-color: #d0c0c8} +td.win {background-color: #c8d0c0; font-weight: bold} +.cand {font-family: monospace} +.email {font-family: monospace} +.endtag {font-size: 60%; font-style: italic; margin-left: 2em} +.url {font-family: monospace} diff --git a/sandbox/embedbox.html b/sandbox/embedbox.html new file mode 100644 index 00000000..4aeab388 --- /dev/null +++ b/sandbox/embedbox.html @@ -0,0 +1,7 @@ +--- +layout: embed +title: Embed Box for Smart Voting Simulator +description: an interactive guide to alternative voting systems +twuser: ncasenmare +--- + {% include sandbox-embed.html %} \ No newline at end of file diff --git a/sandbox/favicon-original.ico b/sandbox/favicon-original.ico new file mode 100644 index 00000000..baccb22a Binary files /dev/null and b/sandbox/favicon-original.ico differ diff --git a/sandbox/favicon-original.png b/sandbox/favicon-original.png new file mode 100644 index 00000000..baccb22a Binary files /dev/null and b/sandbox/favicon-original.png differ diff --git a/sandbox/favicon.ico b/sandbox/favicon.ico new file mode 100644 index 00000000..39abfb4c Binary files /dev/null and b/sandbox/favicon.ico differ diff --git a/sandbox/favicon.png b/sandbox/favicon.png index baccb22a..39abfb4c 100644 Binary files a/sandbox/favicon.png and b/sandbox/favicon.png differ diff --git a/sandbox/index.html b/sandbox/index.html index d7e5f10d..4fed49d4 100644 --- a/sandbox/index.html +++ b/sandbox/index.html @@ -1,106 +1,9 @@ - - - +--- +layout: page-4 +title: Sandbox +title-share: Sandbox for Smart Voting Simulator +twuser: ncasenmare +--- - - To Build a Better Ballot - - - - + {% include sandbox-index.html %} - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- -

- This is the "Sandbox Mode" for To Build A Better Ballot. - If you haven't already played it, check it out here! -

-

- Make your own voting simulation model with this tool! Here's a few that others have made: -

-

-

- If you'd like your own model included here, - save it, copy the saved link, and tweet it while mentioning me, @ncasenmare! - And then!... maaaaaybe I'll see it and include it in the list. - I dunno, I'm really bad at checking my Twitter, which, - honestly, is probably for the best. -

-
-
- - \ No newline at end of file diff --git a/sandbox/original.html b/sandbox/original.html new file mode 100644 index 00000000..a1b3a68f --- /dev/null +++ b/sandbox/original.html @@ -0,0 +1,55 @@ +--- +title: Sandbox for To Build a Better Ballot +description: sandbox for an interactive guide to alternative voting methods +twuser: ncasenmare +--- + + + + + + {{ page.title }} + {% include meta-1.html %} + {% include meta-share-original.html title=page.title description=page.description twuser=page.twuser %} + + + + {% include css-sandbox-original.html %} + {% include js-original.html %} + + + + + + {% include sandbox-index-original.html %} + + \ No newline at end of file diff --git a/sandbox/sandbox.html b/sandbox/sandbox.html deleted file mode 100644 index 74853656..00000000 --- a/sandbox/sandbox.html +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - -
-
- - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/scorefamily.md b/scorefamily.md new file mode 100644 index 00000000..5473e412 --- /dev/null +++ b/scorefamily.md @@ -0,0 +1,249 @@ +--- +permalink: /scorefamily/ +layout: page-3 +title: The Approval Score STAR and 3-2-1 Family +description: An Explorable Guide to Voting Methods +byline: By Paretoman and Jameson Quinn, July 2020 +--- + +{% include letters.html %} + +**Approval, Score, STAR, and 3-2-1** are voting methods that allow voters to find common ground as a group. + +* Voters give their favorite a top score, and +* candidates on the same side might offer support to each other. + +These voting methods are all part of the same family of voting methods that allow voters to score every candidate, rather than just picking one candidate. + +**Compare that to choosing only one candidate (FPTP)**, where + +* Voters can face a dilemma of whether to vote for their favorite or someone who is more electable, and +* candidates are always pitted against each other to get your one vote. + +We will also go into detail on how the different members of this family of voting methods deal with voter strategies. + +## Common Ground + +Approval voting is the simplest way to find common ground. It simply allows people to vote for more than one candidate. I can make a diagram of this that nearly everyone will recognize as a Venn diagram. + +{% capture cap21 %}The voters vote for everyone in their circle.  Both like {{ C }}.{% endcapture %} +{% include sim.html title='Approval Voting Venn Diagram' +caption=cap21 +link='[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSu24CMRD8lZNrF16_jqMLRbpICSAaoHCiE5BcOHRAJBQl356xp0AKQlfMrmc93p29b2XUeLmMUYt1a70UMVrqmCNf46xEowZnHpF1gjOzXmsl-Zo1qAkCpgERXCacGhutvBpDSYWSRNRaffOhuAZj9M0HZnSXae4yYspzknvLqWXKjsQT2JJEAhoQD8RzAQBt0cpCB4cWOhZgCZSxniWQcQDK2JrZiFlTLjhTGpXsiRTC2XLXOfJsyEFpCff45eJImpqO4woMcxo7UN6wPodCK3Jor6G7Fvii4f8_4WNpxNfcFdv2HD7QxABt9btS0_6Sumqa3ler_eQ8bNqhmqRhj-ypTadqtk1vH0gW7Waza6vFrusS0udh99lXk8kL4vm2rR7781DN2nTs98eVUpCnqcEVpwNNDYFAUwO7C6MyQ6Cp0RCkVEZuOHI1MRQP8BurSInIyWJToOZkNe_WXMZr6rr-NL8cWsz7cDgM_Vfq1M8fSXNUexgDAAA)' +comment='' id='venn_diagram_sim' %} + +It's important that you're able to support more than one candidate because that means two candidates can share your support and could possibly work together. It also means you can always give your favorite a top score. + +Why does approval voting work? It uses a median. A median is basically just the middle. Technically it's the point that minimizes the total distance to all the other points. Imagine you have to pick a point for you and your friends to meet. You might like to choose the median because you'll use less gas to get there. Check out the 2D median example below. + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA4VTwUpEMQz8l56DNGmS5u1XePC27GEFD8KCInoQcb_dpLOC4Iq8wzTJdDpN8z5ab7v9gRon7FmERMeB9hHEvOWCw4i9V0ppFk4mtuIMTQ4Xp3fizWuluS9qZZIpq5QbudaiNnLUSiatmrCTcGmJZFH5kE5G23Vq2nbt3Bs1W6HD5Uzo9OvLSmSlnbl2nEXaFca2GCK2KKNf4-T-Oo350hRBCEusADhiB6Ql1sRAmKcwNUmdTErqSIIAICOKKGVGAmRkIoKKbIsyLm8zeGUH3IyBItwMu3AcWeiMDVkthezsX19R-HpPf3RF5X_K-P8gXdb0Ylh9dUonnhsXV7TP8AyGexvaZ2P11BQUA6B9NkGJdYRtmJm-kg4VR_ccj-C2rpSj1xwSDgeOvRMOJq9TpgAGAJ2fmIP5PZoTxVjCTFI5XCcgFhALGAkYCUxDGABeAloRgLJ0M3OC-4rvj6fT0-vd-_ND_iO3p7eX4-nx9b19fgGnVnGdzAMAAA)" +title = "Median in 2D" +caption = "Move some voters. Add a candidate (+). The total length of all the lines below is the sum of all the distances between the voters and the candidate or median. The median we chose here minimizes this sum. A winning candidate should also minimize this sum." +comment = "" +id = "median_2d_sim" %} + +How does approval voting use a median? Approval voting is asking you whether each candidate is close to you or far from you, so you’re measuring distance and writing it on your ballot. + +In approval, score, STAR, and 3-2-1, you're doing the same thing, you're writing your distance on your ballot. Higher score means shorter distance. All of these methods add up the scores to find the highest totals, and they do so in different ways. + +Score is approval voting with more levels of support. + +STAR combines the two ways of finding the middle, scoring and counting by pairs. Its name is an acronym, STAR, Score Then Automatic Runoff. First we score. Then we find the top two and send them to a runoff. The runoff uses the same scores but counts them by pairs for whoever each voter prefers. + +In 3-2-1 voting, voters rate each candidate "Good", "OK", or "Bad". To find the winner, you first narrow it down to three semifinalists, the candidates with the most "good" ratings. Then, narrow it further to two finalists, the candidates with the fewest "bad" ratings. Finally, the winner is the one preferred on more ballots. + +{% include sim.html +link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VTTW_CMAz9LzmHqU6cOO0NiV8wuFUcGHQaEqIVsE3TtP322XljBybUw4s_8vzsuJ-ucV3fC3nKtPY9pfx7Ctz8-YoPUcyXSU9pvfaO7FobPRGbGV3XeMeuc99EzrvkOvIua5IGRaHx_z6NlLuR9m6EmlqLTEG1A2xIIAYkQK46SBUQK2q9pNDWYFAidQaqOSEAQBMYltJEhQynwCqwlCVo5zbAoNrIxgCuCK4YkQGuqFy9zhmfJWeEwRrRsQ7YR88aZiO2PL52yuF6UOI-2oHrXb6lZvTNgmeBYG6rM2F-iQChikiQmiA14fkS2k6CWKm1EtrODQANZ7xBxvByqq2YwAyKDAUZgxcoENwVKJAIC-8nUCC2QW4-TafxbXPQvVpux9OgGGdhZnu2XM0fna2YIL_cDFHQc2kAqFigtkBt4Vq_YGMKBBfwFegupvtBdCuh_GlzOIyX1cc06MJfJZ1fxvfFcN6e9tNlPx7tV3g97obn_XHYua8fEHJuRmkDAAA)" +altlink='[link](https://paretoman.github.io/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRQU4DMQz8i885xI6dbPfMD-ht1QO0i6hUdau2CCEEb8fOlAuoymHs2J7MOJ-UaZymVhNr26SJeUg81IjMI5WIhna7E_GotM0mEccYZ8-zRl5ozImURuZE5pCoektO_443t7uV4W5ldbfCub_NISlSQQpFrAAD1K6MXQCroz9nDqteFOfxS-HeIwIAjSgypykOFZcN2YDMWcQXkbtQjp2AqYCpgKmAqTjTxOl2orliHJwFdjlJKkm9rEEbfRo2w7bKb-DEU4lA-6z-pVa41oY_glxd9UvD8owBkGqQatid4TsNpq11nTb0twymawbAcMUPVLBU61ZCYAVFhYKKtTcoaJht0pmenw6H5br-OM000uN2Oc-U6PK6vD_Ml-15f7rul6NXvt-Ou_llf5x39PUDc7xiOMsCAAA)' title='Approval Score STAR and 3-2-1 Ballots' +caption="Try all four voting methods in this family. They all use the Normalize strategy, which we'll get to next." +comment='one-voter ballots for approval, score, STAR, and 3-2-1, switch between them' id='score_family_ballot_sim' %} + +[See the page on finding common ground for more examples.](commonground) + +To show that these voting methods find the middle, we're going to have to talk about strategies. These strategies get very detailed, so I'll make a simple point that if there is a candidate in the middle, this family of voting methods finds it. The only really complicated situations involve factions, which we'll get to after strategy. + +{% include sim.html +link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSTUsDMRD9LzmnkslkJtneevFctLelhyq9FXYpIojob_clj4oosoc3ny9vZuc9pLCd56lFydMxzmIpimq3JoFlx2MM0kukKPzcfQ3bFEMJ2_ApKcRgw3dUIVkBII3p94dcQ-5PfGSmfzN4obNLF9GiV4TdezwzTjFSCJQiToAWKUC8qwA8IjFkECKYQZgBmUCaXFhioyGTJld6jd40GjQNxdLXISOhZFIlUJCCacYiWehMkU85s8SMVBl03ZCbkW8GCOdRU0Zf-UlZfAgulb-EEgsHNW7OKM8ozyjPOKgZgYNaZY7rMg7qiSCj0rl157rcxgg4l-CkcCrwaUClgsremglK4IIqFdR-PWG3rtfl9XTBTT0-L9dzx8PuAaCbvBHg_f6wD_3KKtvabYdRY-lxjt4Sgf-4cfRG0a3QMwJ1N_I1ym9d_l3FWXKAp9Plsrwc3tYzbv5b5McXZ2njRj0DAAA)" +title='Finding the Middle' +caption="The voting methods in this family find the middle and FPTP does not. We're using a frontrunner-based strategy and getting frontrunners from polls. We'll get to this next." +comment='example of center squeeze working , but not for FPTP' +id='score_family_election_sim' %} + +{% include sim.html +link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSPU9DMQz8L5ldFCexnXTrwlxBt6cOBXWr1AohJITgt2PnVBAg9IbzVy53fnlLOa2XZXTiMva0sNg16kLDIhhesrrfU-KY5VaJa4m8pnWm1NI6fXBOlGTm6lPeNAdnp_z781733p_67Ix_O35DsHOI6KTmZdWoF9QhhhsAUlgBroWbo98b4JcwpZJnVpywOBQAaErDiNNUB9AUQ9aRjXmg5qmYYx08GxVMtQIgqDrTwjS_GFU0wVjhmqlQ9WablBHwNSjXwEmXEkGbJ9tP2qZTdjP8GAhtsCvYn0CkQKRApMCuCAB2xdDr8yaBXc0AnpOK3SuWpjJthAUFhUKBjgkGBYazVgAVgDUZFFi8obS5XJ7OL4eTv6z7x_PTMXC3uXOoq7Jix9vtbpvirRmO9e89Uos6rPcMwJ_usN4hujdkAoDuDr4O-T3k34g_Thh4OJxO5-fd6-XoL_9L5PsngazMqUwDAAA)" +title='Finding the Middle in a Tough Situation' +caption="Again, like the above, FPTP doesn't find the middle. The voting methods in this family do find the middle. And they are strategizing based on polls." +comment='a tougher example of center squeeze working , but not for FPTP' +id='score_family_election_tougher_sim' %} + +## Strategy + +In practice, voters can adjust their self-reported distances, which is called using a strategy. Strategy makes finding the median more complicated. Strategy is the reason these different voting methods exist. + +First, let's look at an example where voters don't use strategy. The voter judges the candidates based on distance. This measures distance well and finds the median. + +See if you notice something odd about the ballot below. + +No candidates got a 5 and no candidates got a 0. This voter is giving up some of their voting power. If {{ A }} loses, then they'll wish they gave {{ A }} a bigger score. + +{% include sim.html +title = "Judge Strategy" +caption = "Give a score based on distance. Drag the voter and see that the circles stay the same size." +id = "ballot4" +comment = "Description: this should just be an unstrategic score voter, as in ncase's thing. Allow ballot picture to show 0 scores." +link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSQU4DMQz8S84RihPHzu6ZH8Bt1QO0i6hUdau2CCEEb8fOUA5FVQ4T28lkxvFnSGGcJqVIQqs4UZXfXeb0l2sxF_WckO3qahUD-bWhRCL2sIQxxcBhDN9EIYYaRopB7JAV1SDFf8sq7WZluFmh1N8iVyCmtnguIwcZxIAKkK6FTAWxob1ZDYZezEZmyUz9TM4A0GRGZDTFQJBURA2RsWRz703Mpo-8FeAqkFTAVcBVjGuyXmP5YQEBWAtcW5NjiWxldmI_x-7W3XO-bIx4cu_M_S5fUzN8s-JrIJiHnqzoYSVA7iJqAUBqxRdW2K6KWutvVdiWBIBhgWGBYandigsUUAgUCBqvUKC4q1CgBRH-T_F_epkiRbFd9UhhqSUALDWIaRDTQNhA2KCnga9BVnNZdzYYA4Q9P-12y_nx4zDbTD-sl-NsU316Xd7v59P6uD2ct8vep_1tv5lftvt5E75-ABCA83FMAwAA)" + +%} + +Most voters will want to make sure that their ballot contains at least one candidate at the top rating and one candidate at the bottom rating. The simplest way to do that is "normalization". That means that you give your favorite a 5, you give your least-favorite a 0, and you judge the rest on that scale between 0 and 5. + +{% include sim.html +link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSQU4DMQz8S84RihPHzu6ZH8Bt1QO0i6hUdau2CCEEb8fOUA5FVQ4T28lkxs5nSGGcJqVIQqs4UZXfXeb0l2sxF_WckO3qahUD-bWhRCL2sIQxxcBhDN9EIYYaRopB7JAV1SDFf8sq7WZluFmh1N8iVyCmtnguIwcZxIAKkK6FTAWxob1ZDYZezEZmyUz9TM4A0GRGZDTFQJBURA2RsWRz703Mpo-8FeAq4CrgKuAqxjVZr7H8sIAArAWurcmxRLYyO7GfY3fr7jlfNkY8uXfmfpevqRm-WTEaCOahJyt6WAmQu4haAJBaMcIK21VRa_2tCtuSADAsmIHAsNRuxQUKKAQKBI1XKFDcVSjQggjzU8xPL79IUWxXPVJYagkAwgYxDWIaCBsIG_Q08DXIai7rzj7GAGHPT7vdcn78OMz2px_Wy3G2X316Xd7v59P6uD2ct8vef_vbfjO_bPfzJnz9AEHATI9MAwAA)" +title = "Normalizing Strategy" +caption = "Stretch your vote to the max score, 5, and the min score, 0." +id = "ballot5" +comment = "Description: this should be like ncase's 'drag the voter' examples, with a normalizing score voter. This voter should have a series of circles, where the closest one intersects the closest candidate and the farthest one the farthest candidate. Thus, as you moved the voter or candidates the circles would change size." +%} + +Normalizing as above is a pretty weak strategy. To strategize even more strongly, voters could look at polls to see which candidates are the frontrunners, and use them as the endpoints for normalization. Any candidate better would get a 5, and any candidate worse would get a 0. The technical name for this is clipping. + +{% include sim.html +link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSQU4DMQz8S84RihPHzu6ZH8Bt1QO0i6hUdau2CCEEb8fOUA5FVQ6T2M5kxvFnSGGcJqVIQqs4UZXfXeb0F2sxF_WYkO3qahUD-bWhRCL2YwljioHDGL6JQgw1jBSDWJEl1SDFf8sy7WZmuJmh1N8iVyCmtngsIwYZxIAKkK6FTAWxob1ZDYaezEZmwUy9JmcAaDLjZDTFQBBUnBpOxpLNvTcxmz7yVoCrZKTAVcBVjGuyXmN5saAKrAWurcmxRLY0O7HXsbt195wvGyOe3Dtzv8vX1AzfrPgaCOahByt6WAkAqbUAILXiCytsV0Wu9bcqbEsCwLDgDwSGpXYrLlBAIVAgaLxCgeKuQoEWnPB_iv_TyxQpku2qRwpLLQGoMzWIaRDTQNhA2KCnga9BVnNZdzYYA4Q9P-12y_nx4zDbTD-sl-NsU316Xd7v59P6uD2ct8vep_1tv5lftvt5E75-APMG_FdMAwAA)" +title = "Frontrunner Strategy" +caption = "Consider polling data and stretch your vote to the max score only for the frontrunners. Push other candidates to 5 if closer, 0 if farther, and normalize in between." +id = "ballot8" +comment = "Single-voter example with 3 candidates and a voter who normalizes based on only the 2 of them." +%} + +Strongly strategic voters can have more power than normalized voters. We'll see this in the next section on playing chicken. + +Even stronger strategic voters could decide to only score the best of the frontrunners (and anyone better). These are risk-takers. + +{% include sim.html +link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSQU4DMQz8S84RihPHzu6ZH8Bt1QO0i6hUdau2CCEEb8fOUA5FVQ6T2M5kxvFnSGGcJqVIQqs4UZXfXeb0F2sxF_WYkO3qahUD-bWhRCL2YwljioHDGL6JQgw1jBSDWJEl1SDFf8sy7WZmuJmh1N8iVyCmtngsIwYZxIAKkK6FTAWxob1ZDYaezEZmwUy9JmcAaDLjZDTFQBBUnBpOxpLNvTcxmz7yVoCr5F5YwFXAVYxrsl5jebGAAKwFrq3JsUS2NDux17G7dfecLxsjntw7c7_L19QM36z4GgjmoQcrelgJkLuIWgCQWvGFFbarItf6WxW2JQFgWPAHAsNSuxUXKKAQKBA0XqFAcVehQAtO-D_F_-llihTJdtUjhaWWANQVNohpENNA2EDYoKeBr0FWc1l3NhgDhD0_7XbL-fHjMNtMP6yX42xTfXpd3u_n0_q4PZy3y96n_W2_mV-2-3kTvn4AokZDqUwDAAA)" +title = "Best Frontrunner - Optimist Strategy" +caption = "Vote for the best of the frontrunners and everybody you like better." +id = "ballot11" +%} + +A more risk-averse voter could try to avoid the worst frontrunner by voting 100% for everyone they feel is better. + +{% include sim.html +link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSQU4DMQz8S84RihPHzu6ZH8Bt1QO0i6hUdau2CCEEb8fOUA5FVQ4T28lkxs5nSGGcJqVIQqs4UZXfXeb0l2sxF_WckO3qahUD-bWhRCL2sIQxxcBhDN9EIYYaRopB7JAV1SDFf8sq7WZluFmh1N8iVyCmtnguIwcZxIAKkK6FTAWxob1ZDYZezEZmyUz9TM4A0GRGZDTFQJBURA2RsWRz703Mpo-8FeAquTMXcBVwFeOarNdYflhAANYC19bkWCJbmZ3Yz7G7dfecLxsjntw7c7_L19QM36wYDQTz0JMVPawEyF1ELQBIrRhhhe2qqLX-VoVtSQAYFsxAYFhqt-ICBRQCBYLGKxQo7ioUaEGE-Snmp5dfpCi2qx4pLLUEoN72BjENYhoIGwgb9DTwNchqLuvOPsYAYc9Pu91yfvw4zPanH9bLcbZffXpd3u_n0_q4PZy3y95_-9t-M79s9_MmfP0A1o3sPUwDAAA)" +title = "Not the Worst Frontrunner - Pessimist Strategy" +caption = "Vote at max for anybody better than the worst frontrunner." +id = "ballot12" +%} + +There is a whole range of strategies in between these two extremes. + +These strategies apply to score voting and also to approval voting. They are also the basis for strategies in STAR and 3-2-1 voting. + +{% include sim.html +link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VTwU7DMAz9F58jVCeOk_aGxB_ArdphbEVMmtZqGyCE4Nux8wChIdTDS2zn5T3HfaOOhnEsHFh5FUbO-rWK0v3EaoipeEzZVnm1CsR-rE-BWXybaOgCCQ30wR0FyjRwILUiSxaDLvz5LFP_zfT_Zozf72JXoKY2IRoRhRAWQAZoU8Omg8XQbs0GfUtGo7Ng5FYTIwA0UbAzmmSgCBbsKnbGEs2_tzGaQvZmgCtFpMCVwJWMa7Ru4_NiRRVYE3xbm0MKYmlxYq8T9-tOJX4vjHhMvpB2Vi6pBb6l4HEgWPoWzOhiZgCk5gSA1IxHzLCdC3K13ZVhWzsADCveQGFYc7PiAhUUCgWKxhcoKDhboKAk7PB-BQqKzxFdL8txfl7vbbpuN_NxIp-sgoJ60bUCk7UDcOOukFchr-KKihGpUFjBVyG0utArG5UeUu_X-_18vntdJpvzX3JOj_PLzXTaHHfLeTcf_Cd4Omynh91h2tL7J1jR5XxjAwAA)" +title = "Approval Strategy" +caption = "Approval strategy is just score strategy with two levels." +comment = "approval strategy - flip between strategies with a single ballot" +id = "approval_strategy_sim" +%} + +Even with these strategies, voters are still able to find common ground. The perception of frontrunners changes a little with each poll until the polls stabilize. + +{% include sim.html +link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSTUsDMRD9LzmnkslkJtneevFctLelhyq9FXYpIojob_clj4oosoc3ny9vZuc9pLCd56lFydMxzmIpimq3JoFlx2MM0kukKPzcfQ3bFEMJ2_ApKcRgw3dUIVkBII3p94dcQ-5PfGSmfzN4obNLF9GiV4TdezwzTjFSCJQiToAWKUC8qwA8IjFkECKYQZgBmUCaXFhioyGTJld6jd40GjQNxdLXISOhZFIlUJCCacYiWehMkU85s8SMVBl03ZCbkW8GCOdRU0Zf-UlZfAgulb-EEgsHNW7OKM8ozyjPOKgZgYNaZY7rMg7qiSCj0rl157rcxgg4l-CkcCrwaUClgsremglK4IIqFdR-PWG3rtfl9XTBTT0-L9dzx8PuAaCbvBHg_f6wD_3KKtvabYdRY-lxjt4Sgf-4cfRG0a3QMwJ1N_I1ym9d_l3FWXKAp9Plsrwc3tYzbv5b5McXZ2njRj0DAAA)" +title='Repeat: Finding the Middle' +caption="This is the same example as before. The voting methods in this family find the middle and FPTP does not. Also, the high-risk strategy F+ doesn't find the middle." +comment='same as above, example of center squeeze working , but not for FPTP' +id='score_family_election_copy_sim' %} + +## Playing Chicken + +Let's look at an example where the voter's choice of strategy gets interesting. + +In the example below, there are three groups of voters. Two are on one side and can win with candidate {{ A }} or {{ B }} if they cooperate. To make things interesting, each group has its own favorite candidate. If group {{ A }} betrays group {{ B }}, then {{ A }} can win alone, and vice versa. + +Groups {{ A }} and {{ B }} are playing a game of chicken. They can "swerve" and let their second-favorite win, or they can "drive straight" and either win (if the other side swerves) or crash (if the other side doesn't). + +This makes sense. If voters are willing to take a risk, then they are willing to accept the loss. Also, voters that don't take the risk will accept a second-best outcome because at least it wasn't the worst outcome. + +{% include sim.html +link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3WSwUoEMQyG36XnIk3Spu3cfAa97e5hld3TwiyLCCL67Cb5UUZECpOmSf98yfQ9lbTsdiScqfEh265lGuS7We1sHg45kefMkYmnBXwzZ2TYTotnSFpKTjUt6ZMo5dTCV7tnwW4m0bTjn0_ZLksZllLyn2WR-W-EShQhpyOyQ9WsM2vPOnIvuZMnMZLE0fZpPZ_3yRkMlcyAkxTGQKmaNRoJzoVzYitjh0xxgxkGDTNkuMUFhgx3eAMeVKREH5TZpwUtgZZI5Au0pHk7luj0orgMRZkbjVq-B-MObR3eKFTZOjV06rZE1ShbO_4goOuMw4YJN4LhgGkCA9zWYNB664iN39NumIEWGHSv-DOKSWqLDsSIFFoKFJ1hOlA67nagdIFXEQNK91eX7q_X2_p6vFj1h-f1dkr-DjsS_LH5CDu6HAUG0gNYA1gDExst6g2QDegMAA4HvGv2UoH4dLxc1pfHt-vJhoDaH1-UCak9ZwMAAA)" +altlink="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSTWsbMRT8K0Xnl6KnjyfJt6QQ6C24Sy6OD27inExljEkppf3tHWlo2VLKwo6eNJo383a_O-82u50GFU1tLzvVKlr8WLUkmtt-L04nR03AmxysapqcDE4enOg2XlxyG_dTvROXZ224icMCcMGwHbKTFsSvHxAqCF7-eXDS_nuCNqOFDneq2DQTa2JFrErxUnSQAklxGHty_fX1ycGGwqgC6FKNAJuagHATAWgd4BhtsBl03giBwLiBMgg1LgTKhMKqsqJK9DOHShizolakVoyTH6kV84gD4nAfjZepGNtKI_nfgxmFrouwUkhxXaSpk9Ytks22qfD70XRqczNzwlkJYZrJkUC7ORMYPRee1b-nnTkD8wSmN34Z4yQtzwQRjoxaRivWJhRaKbxbaKVEVolntFLGP-fuH5YHdP64fcT7rl9eDsDb8_nS3w4nLD8998sR-KF_eemX5-N17C23W0C8CTcKXPr53fK1Y7W9e-zXoxs_cmGPuvoShcOqnkCHlekq01UOvuZpuzJgpVZlzjpyvs_44Zn08-F06tfl2_mIWf4x_uMXJYo73bEDAAA)" +title = "Playing Chicken with Approval and Score Voting" +caption = "" +id = "election11" +%} + +If voters play chicken and crash, they might wish they had played it safe. It's impossible to have the foresight to know the election result ahead of time. All they can do is rely on polling to make their decision. It is their judgement of risk that leads to their decision. In a way, this is more information that gets fed into the voting method. + +## STAR Voting + +STAR voting was created to address scoring strategies. It uses a final runoff where stretching your scores doesn’t matter. Voters aren't playing the game of chicken anymore because STAR's extra round can resolve the contention between factions. + +Let's repeat the definition of STAR. STAR combines the two ways of finding the middle, scoring and counting by pairs. Its name is an acronym, STAR, Score Then Automatic Runoff. First we score. Then we find the top two and send them to a runoff. The runoff uses the same scores but counts them by pairs for whoever each voter prefers. + +All the usual advantages of this family of voting methods still apply: voters can give their favorite a top score, and candidates might offer support to other candidates on the same side. + +The strategies for STAR voting are almost the same as in score voting. The difference is that the ratings are changed to avoid giving the same score to two frontrunners. That is an important change that can matter in the runoff. + +In this model, the risk-takers are not taking as much risk and the risk-averse are taking a little more. I didn't go with more extreme strategies because these are good endpoints to illustrate the logic of STAR. + +{% include sim.html +link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSy04DMQz8l5wjFDtO4t0zf1Buqx6gXUSlqlu1RQgh-HbsjApCqNrDxI-dzDj-CCmM01SHSFrWcaJS7cR24sI_J4kstF7HQN5MSSPl5HEOY4pBwhi-iEMMJYwUQ7UuKzaDFP99VtGbleFmhVK_i1xCNZHZc4wcZJAACqB2LWQqSAztToehFzn1iKn3MANAw4LIaLJBRbIhUkTGwubeZ8emj3wU4MqMErgyuLJxTRT7560VPeDM8EyRo3sSJ_UucafuXPh6MNKp90j_U_7SChxLw6NAqgw9WTC9QgCILBkAkQWPV2C4NNS031RguCYArFZMv8JqLd2Gy6ugqFBQMfIGBQ3_NihoGRFeruHl2nV_Gor6O58onoclTQDqTAoxCjEKQgWhQo-CTyFLXdZdsZWDsKfH_X65PLwfZ9vm1WY5zbbP55fl7X4-b06742W3HHzPXw_b-Xl3mLfh8xuKlzI8PQMAAA)" +title = "STAR Strategy" +caption = "These strategies are based on the score strategies, except they try to give each frontrunner their own unique score." +id = "ballot9" +comment = "one-voter star election" +%} + +In STAR voting, the runoff resolves the game of chicken. There is actually very little downside to giving a 5 to {{ A }} and a 1 to {{ B }}. {{ C }} still gets a 0, so he'll still lose in the final round runoff. + +{% include sim.html +link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3WSQU4DMQxF75K1hWI7dpJeA3ZtF4DaVaUixAahcnYcf1UahNAs_jh2fp6dfJVadvs9CxO3eaQ98yDudf3NRmzzeKTCWcNOUZc18Tda1ljU2KrRsqtUWtmVb5ZCxTL22BnJHhLGIjSF6vaL5IhkpT9fZOa_Ga5pz4uMmdzJ51oWLOvCOJTr-XwowcKBxSFgYocEFLfQOF9D4jChImEci8K5QwSC5gQ2YrlBYCMd0UAEF61JziRrMvBSeKlmvcJLbTUQhbwKHZvhqHPj0ep9FCvgbSAbh6bboKVP2x7RPI9tHbcF6DZz0TBTY4gkjCkEuGYQtG4dufF72oYZeIWge8fNOCbplh1oEDm8HCg-UzpQOvZ2oHRF1JDDZfb7C-tIjvvQSKmtdfQ2KgSGAzADMANzGpanDPAM-A1gjYX1YPEiAfbyfLlcP54-307R-uPr9f1Ubj-GvhvISwMAAA)" +title = "Not Playing Chicken with Star Voting" +caption = "" +id = "election13" +comment = "chicken with star" +%} + + + +## 3-2-1 voting + +3-2-1 voting is another method that avoids the chicken dilemma by addressing scoring strategies. + +Let's repeat the definition of 3-2-1. In 3-2-1 voting, voters rate each candidate "Good", "OK", or "Bad". To find the winner, you first narrow it down to three semifinalists, the candidates with the most "good" ratings. Then, narrow it further to two finalists, the candidates with the fewest "bad" ratings. Finally, the winner is the one preferred on more ballots. + +3-2-1 voting is doing something similar to STAR by adding a second additional round of counting votes. It's kind of like there are two rounds of counting approval ballots and one round of counting preference ballots. The first round of approval just counts "good". The second round counts "good" and "okay". + +Important todo: I haven't put good strategies into 3-2-1 yet. Right now, these strategies are okay for this example, but other examples might not be correct. + +{% include sim.html +link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSy2oDMQz8F59NsWTZ1u65n5Dbsoe22ZJASEIelFLab6_kIS2lhD2M9djxjKyPkMI4TXWIpGWOE5VqJ7YTF_45SWSheY6BvJmSRsrJ4xzGFIOEMXxRDjGUMFIM1bqs2AxS_PdZRe9WhrsVSv0ucgnVRGbPMXKQQQIogNq1kKkgMbQ7HYZe5NQjpt7DDAANCyKjyQYVyYZIERkLm3ufHZs-8lGAKzNK4MrgysY1Ueyft1b0gDPDM0WO7kmc1LvEnbpz4dvBSKfeI_1P-UsrcCwNjwKpMvRkwfQKASCyZABEFjxegeHSUNN-U4HhmgCwWjH9Cqu1dBsur4KiQkHFyBsUNPzboKBlRHi5hpdrt_1pKOrvfKJ4HpY0AagzKcQoxCgIFYQKPQo-hSx1WQ_FVg7Cnp92u8Nl9X5cbJtXm9Oy2D6fN4e3x-X8ctoeL9vD3vf8ul8vr9v9sg6f3_tmOp49AwAA)" +title = "3-2-1 Strategy" +caption = "These strategies try to give frontrunners their own score with three levels of support." +id = "ballot10" +comment = "one-voter 3-2-1" +%} + +Here again, the game of chicken is not being played anymore. The Frontrunner strategy doesn't change the result from Normalize strategy. Candidates wouldn't have to go negative against their nearby rivals in order to ensure that their voters would at least be moderately strategic and wouldn't just normalize. + +{% include sim.html +link="[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3WSQWoDMQxF7-K1KJZkyXbOkV2SRQsJXQRSSjelNGevrE9gSimz-CNL_n6S_VVq2R0OLEzc5okOzIO41_U3G7HN04kKZw07RV3WxN9oWWNRY6tGy65SaWVX7qyFimXssTOSPSSMRWgK1e0XyRHJSn--yMx_M1zTnhcZM7mTz7UsWNaFcSy3y-VYgoUDi0PAxA4JKG6hcb6GxGFCRcI4FoVzhwgEzQlsxHKDwEY6ooEILlqTnEnWZOCl8FLNeoWX2mogCnkVOjbDUefGo9XHKFbA20A2Dk23QUuftj2ieR7bOm4L0G3momGmxhBJGFMIcM0gaN06cuP3tA0z8ApB946bcUzSLTvQIHJ4OVB8pnSgdOztQOmKqCGHy-yPF9aRHI-hkVJb6-htVAgMB2AGYAbmNCxPGeAZ8BvAGgvryeJFAuzl-Xq9few_387R-v71_Xwu3z-WC-vnSwMAAA)" +title = "Not Playing Chicken with 3-2-1 Voting" +caption = "" +id = "election14" +comment = "chicken with 3-2-1" +%} + +## Afterword + +The major point we've been supporting on this page has been that this family of voting methods allows voters to find common ground. + +We described some of the mechanics of how this family of voting methods works. Voters are reporting their score. Counting the highest score is equivalent to finding the median. + +We also described differences within this family of voting methods. Score and approval voting work on the principle that the voter is assessing the risk of using a strategy. STAR voting and 3-2-1 voting use additional rounds to get more information from the ballot. That encourages the voter to put more information on the ballot. The additional rounds also reduce the influence of polling on voter strategy and reduce the dilemmas voters face. + + + diff --git a/script/bootstrap b/script/bootstrap new file mode 100644 index 00000000..492e5535 --- /dev/null +++ b/script/bootstrap @@ -0,0 +1,6 @@ +#!/bin/sh + +set -e + +gem install bundler +bundle install diff --git a/script/build.bat b/script/build.bat new file mode 100644 index 00000000..b79f3fbc --- /dev/null +++ b/script/build.bat @@ -0,0 +1 @@ +bundle exec jekyll build \ No newline at end of file diff --git a/script/easy_build.bat b/script/easy_build.bat new file mode 100644 index 00000000..d78faf98 --- /dev/null +++ b/script/easy_build.bat @@ -0,0 +1 @@ +bundle exec jekyll build --config _config.yml,_config_make_easy.yml \ No newline at end of file diff --git a/script/forcepollserve.bat b/script/forcepollserve.bat new file mode 100644 index 00000000..b80da13b --- /dev/null +++ b/script/forcepollserve.bat @@ -0,0 +1 @@ +bundle exec jekyll serve --force-polling \ No newline at end of file diff --git a/script/prod.bat b/script/prod.bat new file mode 100644 index 00000000..29a45b02 --- /dev/null +++ b/script/prod.bat @@ -0,0 +1,5 @@ +REM change the jekyll environment. Just change it locally for this script. + +SETLOCAL +SET JEKYLL_ENV=production +bundle exec jekyll build \ No newline at end of file diff --git a/script/safeserve.bat b/script/safeserve.bat new file mode 100644 index 00000000..a41d6554 --- /dev/null +++ b/script/safeserve.bat @@ -0,0 +1 @@ +bundle exec jekyll serve -l \ No newline at end of file diff --git a/script/serve.bat b/script/serve.bat new file mode 100644 index 00000000..aa97cf14 --- /dev/null +++ b/script/serve.bat @@ -0,0 +1 @@ +bundle exec jekyll serve -l -I \ No newline at end of file diff --git a/script/server b/script/server new file mode 100644 index 00000000..10c59e79 --- /dev/null +++ b/script/server @@ -0,0 +1,3 @@ +#!/bin/sh + +bundle exec jekyll serve -l diff --git a/social/original-thumbnail.png b/social/original-thumbnail.png new file mode 100644 index 00000000..71fb3733 Binary files /dev/null and b/social/original-thumbnail.png differ diff --git a/social/reddit.png b/social/reddit.png new file mode 100644 index 00000000..38e1a316 Binary files /dev/null and b/social/reddit.png differ diff --git a/social/thumbnail.png b/social/thumbnail.png index 71fb3733..fab9508e 100644 Binary files a/social/thumbnail.png and b/social/thumbnail.png differ diff --git a/social/ycombinator.png b/social/ycombinator.png new file mode 100644 index 00000000..825cb3cb Binary files /dev/null and b/social/ycombinator.png differ diff --git a/splash/splash.js b/splash/splash.js index cfeb0890..97fa7577 100644 --- a/splash/splash.js +++ b/splash/splash.js @@ -1,4 +1,6 @@ -Loader.onload = function(){ +var l = new Loader() + +l.onload = function(){ // Properties var width = 1500; @@ -100,11 +102,11 @@ Loader.onload = function(){ } // RANDOMLY SCRAMBLE NEAR THE MOUSE. - //Mouse.x = Mouse.x || 0; - //Mouse.y = Mouse.y || 0; - if(Mouse.x && Mouse.y){ - var x = Math.floor(Mouse.x/SIZE); - var y = Math.floor(Mouse.y/SIZE); + //mouse.x = mouse.x || 0; + //mouse.y = mouse.y || 0; + if(mouse.x && mouse.y){ + var x = Math.floor(mouse.x/SIZE); + var y = Math.floor(mouse.y/SIZE); var neighbors = _getNeighbors(x,y,-1); for(var i=0;i 50 % | +| 2 | 33 % | \> 66 % | +| 3 | 25 % | \> 75 % | +| 4 | 20 % | \> 80 % | +| 5 | 17 % | \> 83 % | +| n | 1/(n+1) | \> n/(n+1) | + +{% include sim.html link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSu04DMRD8F9dbeN9OPgPRnVIEkYoIKGgQgm9n7eGkSCi6Yryv8ez4vlpvx23TpBwn2lg6pdTBhEQPfxmxeRJh4uzzNHqd5HSixnM6tZrneAZFzgYL4hizQduxU7N2bD8cjZqvOGqsilnQ6d9XlXG3crhb4b64mUHOghAK2AC4v6QsKAFshXVdFBS3UJPiqaQUjxYIkqCRouECRw00kogGIrBoX0J5esCroBCkijoEaTFtZez-zfZAA1gVCzMJKRl5NVjfPZgB3wZyG-hOPQNbbPb_Oou1kiVeCkvYYSUdljoWcFjhWMBhhTsAVniiNtZtDisCLMGrM2BDwNDwpVhLSIAioCAOCxKzidkUgAJgYeJNc_-pEsVx69vMY6XRAVhpQMyAmGFL73AA3uHpfL2-fTx-vl_qJ344v75cntv3LyTk-II4AwAA)" +title = "Three Winners" +caption = "Each of three groups is able to be represented." +comment = "three parties is a cognitive achievement. don't show power chart." +id = "three_sim" +gif = "gif/three.gif" +%} + +Ranked-Choice Voting is better (more representative) when it allows smaller groups to be represented. Five would be a great number. More would be even more representative but could be overwhelming. Voters don’t have to rank all the candidates. They just need to rank enough to get one candidate elected, or maybe two. + +Before we move on, let's revisit this idea of representation with a few tangent ideas that we'll only discuss briefly. Like we said before, in the final round, between the last two contenders, more than 50% of people who had a preference between the two got their preferred candidate. In that sense, more than 50% of people are represented because their votes mattered in deciding the outcome. Let's dive into a little more detail. + + + +**Tangent 1:** Which supporters does a candidate represent? Which supporters could affect the outcome? Maybe candidates don't have to pay much attention to their most ardent supporters because their vote is guaranteed. The voters that really matter to them in this model are "swing voters" because those could go either way. In that case, STV is pretty good because there are many boundaries between candidates where voters can swing either way, so more voters matter to the outcome. + +**Tangent 2:** In another sense, the only position the candidate represents is their own. In that case, STV is still pretty good because there are more positions represented and voters have a candidate with a closer position to their own. + +**Tangent 3:** I didn't model voter engagement, but there is a point to be made here, and it's where I think an improvement could be made to this model. Disengaged voters are on a candidate's side but they don't vote because it takes time and effort. One way I could model engagement is to figure that the closer a voter's position is to a candidate, the more engaged that voter will be. In that case, having more candidates will lead to greater voter engagement. That is a key feature of voting methods where voters can give their opinion on many candidates, like STV. + + + +## Counting Ballots + +The fundamental part of what makes STV work is that it counts quotas exactly once. + +Once a candidate has been elected by a quota of voters, the voters have successfully used their ballot to get representation, so it is not counted again for a second candidate. + +This is kind of like how in districts, you only vote in one district. + +This counting method is important because it is what allows small groups to come together to get representation and to not see each other as competing factions. + +### Visualization + +See the example chart below for a visual of the process of elimination. It starts at the top and each row tracks who the **voter's** top pick is. Each column is a voter. Transparency is used to represent the excess vote that remains after a quota is filled. As candidates are eliminated, the groups of voters become visually apparent. + +Also, notice that the choice of quota size makes sense because 50% of remaining votes are required to elect the final candidate. + +{% include sim.html link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSu04EMQz8l9QuYjuxs_sZiG61xSGu4gQUNAjBt-N4OGnF6bTFxK_xZLJfpZZ129TJx04bSyWXODQh0eUvI22eRJjY6zyNGifZdyo8p12jeY67kflsaEZsYzZoWSuVVtbyw1ao9IwtxmLD_y_6PSqVbr6ojLuV5W6Fa67jKXOGghCiuAEgKdQlhABugbHOAoJbqEjwRFKCRwMESdBI0HBARw004ogGIrBoTaE8beEsqOSsKuoQpMG0hdfXb7YbGsCquDCHZUqNejS0evVgBnwM5BjolXoGLdna7bpmKas5Hg-XaEsmOyztuECHFR0X6LCidwCs6I7ayG0dVlgFcHYa3sVgqPVUrCHEQGFQYEuCQ4Fj1mGhKwAWOt7UDe_ujuI4-jbzIBwgfDpdLm8fj5_v5_hbH06vL-fn8v0LhmM1_yEDAAA)" +title = "Voter Chart By Round" +caption = "See the chart at the bottom for a visualization of where votes were counted towards a candidate's victory. Mouse over the rounds to see how the chart progresses through the rounds. " +comment = "voter chart time" +id = "voter_chart_sim" +gif = "gif/voter_chart.gif" +%} + +Below this chart are a couple of charts that are a measure of voter power. They track the weight of the each voter's contribution to a candidate's election. When a candidate is elected in a round, the voters whose vote counted for that candidate are added to fill up the chart. The intuition is that the voter could have voted for someone else, so the candidate owes them some share of their power. + +In the first chart of the "Voter Weighting Used by the Method", the exact weights used by the voting method to select winners in each round are shown. To choose the final winner, the election is similar to a single-winner election. All that the last guy needs to win is 50% of the remaining vote weight. In the background is a dark bar with a height that corresponds to a vote at its full weight. As candidates get elected, the bar is covered. Any part of the bar that is still showing after all candidates are elected shows votes that are still not counted. + +In the second chart of the "Voter Weight Contributed to Candidates", the total weight given to each candidate is rescaled so that it is equal for each candidate and sums across candidates to the full amount of representation available. In the background is a dark bar with a height that corresponds to every voter contributing equally to the election of the candidates. The height of this bar is each voter's ideal share of representation. + +(Also, here are a few more specifics about these charts. The voters and candidates are arranged in a line by using an algorithm that solves the traveling salesman problem to keep voters together who are near each other in 2D space. Specifically, the ballots are used as coordinates or feature vectors since this 2D space isn't something you'd be able to see in an election.) + +{% include sim.html link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSu04EMQz8l9QuYjuxs_sZiG61xSGu4gQUNAjBt-N4OGnF6bTFxK_xZLJfpZZ129TJx04bSyWXODQh0eUvI22eRJjY6zyNGifZdyo8p12jeY67kflsaEZsYzZoWSuVVtbyw1ao9IwtxmLD_y_6PSqVbr6ojLuV5W6Fa67jKXOGghCiuAEgKdQlhABugbHOAoJbqEjwRFKCRwMESdBI0HBARw004ogGIrBoTaE8beEsqOSsKuoQpMG0hdfXb7YbGsCquDCHZUqNejS0evVgBnwM5BjolXoGLdna7bpmKas5Hg-XaEsmOyztuECHFR0X6LCidwCs6I7ayG0dVlgFcHYa3sVgqPVUrCHEQGFQYEuCQ4Fj1mGhKwAWOt7UDe_ujuI4-jbzuNIA4dPpcnn7ePx8P8ff-nB6fTk_l-9fV4vS8SEDAAA)" +title = "Weight Charts" +caption = "See the chart at the bottom for a visualization of where votes were counted towards a candidate's victory. Click through the rounds to see how the chart progresses through the rounds. " +comment = "power chart time" +id = "power_sim" +gif = "gif/power.gif" +%} + +### Voter Power + +Voter weight contributed to candidates is not exactly voter power. Power is a collective phenomenon. + +Think of single-winner voting methods. The winner is most representative of the median of the group. The median is a collective measure. If there are two sides and both are competing for the median, then both sides are represented. To see why this is the case, consider a case where the election is not competitive and the median belongs to only one side, then all the voters on that side benefit from that power, which is not very representative. The most representative election would be a competitive election where the median could be on either side. + +In STV, there are multiple "medians" (more like percentiles), one for each winner. These medians can be spread out over a larger region than for the single-winner case. This means there are more ways to be part of a group that wins, and there are more "swing voters" that could belong to one group or another. Both of these effects mean candidates have to pay attention to more voters. + +Also, consider what would happen if, after the election, a candidate shifted their position toward the center of the group that elected them. They would lose the more moderate voters that voted for them when the next election comes. + +### Quota Excess + +What happens if a candidate gets more votes than just a quota? The rule is that exactly a quota of votes is used up by the winner, which means the excess number of votes above the quota remains in the count. These votes will count in proportion to that excess amount. It's a calculation of dividing the excess by the quota to find the new weight for the vote. This bookkeeping makes sure that the power each voter gets from their vote is the same. It makes sure that all voters are as equal as possible. + +## Proportionality + +Let's get back to the idea of proportionality. You can see that in STV, a voter group with two times as many voters gets two times as many representatives. + +{% include sim.html link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA31Ty05cMQz9l6y9iJ3Ezp1dP6ALHmIzmsWlnQXqlBkhQCBEv722DxWVKNVdHL9inxznvpRaNtttr8Sj7mjL7FZXt3onqUuEqpJwc2tWkszZQizshogRa1SJdbckLI1szdjwkyOsxm7NN4tFdzsqHJPN52mWjEFsFolWNpVKL5vyi7VQGemrlwt9-LzePFPpw-eZ-Wlm-TTDNcdx0GtkFNrMiAviYMcdAG5OM8GZcHf0uSyOPoWpiHf0qHhHD4oA0Ec6SrxPc0AfMXgT3pIHWk3KHAJxJprk2daQB6Pmnbau8b--OKooxoQGGXw51KjTIPULT_IFejDKO4aGLp3fTXk3W64la3v27P8n0DVJd8OSccUOoQakH7jegFAD1xsQagwAhBqG3MzJA0JpBaCLYm2KLjpyq_FqFC0UDHRJMDAwzikGga0BILCBgcV7LGcPx_v14tvxbu_vNJ0vp9Pd8XE9_PG_3tze_Fyf3L24vCrxWg3n59_SRxwazJpwvR4Ox_vL59Pef4Pz9fbH_nt5_Q0IWhoZrgMAAA)" +title = "Quotas Give Proportional Results" +caption = "Here's two groups with a 1:2 ratio (really 4:7). The winners are also in the same ratio." +comment = "Maybe choose a more interesting example." +id = "proportional_two_to_one_sim" +gif = "gif/proportional_two_to_one.gif" +%} + +This proportionality applies even when there aren't distinct groups. Let's look again at the example we saw earlier, but now using the additional charts. + +{% include sim.html link = "[link](http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA41TzYqUQQx8lz7n0Ek6_TOPId6GOay4Jxf14EXEfXYrqQFnWQT5YNKdn-pKJfOr9Xa5XteWuW9yNeuyow4quh0njXM_HRcdmaUDQbc8dZfT87BNThbq6aI28qR-L7QwmZUeQ_Yq-IGsenF0MUtfuEy_3aRpMrIRpBRdptajG89blUwxnZnq7dKljXZprzqbtKj7BIDJuw_5C5Eu7z5ENiIA6QB5tVm_1hCD7e1N5qlM80f3QxgQyUGzC5cl6E93-o1-UtZBQ8LgXgb0dMCCjAbsKTcIXOA1ICoMcAyGODboBI7DEMcWb5u3UwXeq3dN1bQCblXrzjgZOZCuGO-_vwSYLOE7fogMjV2GYG5oewvWBk4UmOQmYCXa6H_1z6ve3yKtYW-jXgqiVDyv4AdNxv_wG7M6G4vrQR3GKWdwPkENgmoGNQiqGUFDNWMxtqvZoJqz0xBlcraTKDNq_5L0JMQkg8l5LjJYnOfiFJbTcAqLe7Fyk3NtF4P7Uej0s6Xdy3x6enn59uPjz-_P-D98ePr65flz-_0HKwv-beEDAAA)" +title = "Evenly Distributed Representation - Again - With Charts" +caption = "With five representatives, STV can spread them out to be closer to the voters. (Dots are used for voters and circles show candidate totals, including transferred votes from eliminated candidates.)" +comment = "to show how STV can prevent factions from fighting each other." +id = "even_2_sim" +gif = "gif/even_2.gif" +%} + + + +### Proportional Methods without Parties + +There are more voting methods that use a similar way of assigning voters to STV to achieve proportionality. After you're done with this page, read more about all the different kinds of proportional voting methods: + +* [Proportional Voting Methods](proportional) + +## Footnotes + +### Party Proportional Methods + +Additionally, there are ways to have proportionality by using a party system, but that is a mechanically-different method that I haven't added to the simulator yet, so we'll discuss it on another page to come in the near future. + +### Strategies + +I still need to work out what the strategies would be for voters and candidates. So far, in the above examples, I've been using the honest strategy for ranking. + +## Afterword + +The main drive for STV is to have representation of all diverse groups and to include minorities. Group bargaining in this representative group would ideally be able to offer a policy package that includes a little bit from everyone at the table. This is also a vision of a more responsive politics. We don't have to wait until the election. We can represent each sector of the population today, so that when an event occurs, action can be taken. Also, proportional representation is a further way to avoid antagonistic campaigns. It won't get rid of partisanship, but it might prevent it from being broadcast to the voters . We can change the political culture and have campaigns that are capable of bringing groups together to achieve a greater goal. + diff --git a/test-original.html b/test-original.html new file mode 100644 index 00000000..024c9753 --- /dev/null +++ b/test-original.html @@ -0,0 +1,36 @@ +--- +permalink: /test-original/ +layout: default-original +title: Original +banner: To Build a Better Ballot +description: an interactive guide to alternative voting methods +twuser: ncasenmare +--- + + +
+ + + +
+
+

+ create your own “condorcet cycle”!
+ move the voters in such a way that NOBODY wins: +

+
+
+ +
+ +
+ + +
\ No newline at end of file diff --git a/test.html b/test.html new file mode 100644 index 00000000..78860de3 --- /dev/null +++ b/test.html @@ -0,0 +1,108 @@ +--- +permalink: /test/ +layout: page-4 +--- +
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+

+ a guesstimated model of the 2016 US election?...
+ + how Clinton wins IRV, + Trump wins Score, + and Johnson wins Borda?? + +

+
+
+ +
+
+
+
+

+ Chicken Star
+

+
+
+ +
+
+
+
+

Star Strong

+

Keeps a space for the best.

+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
\ No newline at end of file diff --git a/testAttach.html b/testAttach.html new file mode 100644 index 00000000..b5abc56e --- /dev/null +++ b/testAttach.html @@ -0,0 +1,42 @@ +--- +permalink: /testAttach/ +layout: page-4 +title: Test Attach +--- + +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ \ No newline at end of file diff --git a/testBallot.html b/testBallot.html new file mode 100644 index 00000000..c6355c3d --- /dev/null +++ b/testBallot.html @@ -0,0 +1,27 @@ +--- +permalink: /testBallot/ +layout: page-4 +title: Test Ballot +--- +
+
+

Strong Strategic Voter

+

Almost like strategic approval.

+ + + +
+
+ + + +
+
\ No newline at end of file diff --git a/testLink.html b/testLink.html new file mode 100644 index 00000000..76cc35ef --- /dev/null +++ b/testLink.html @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/testPreset.html b/testPreset.html new file mode 100644 index 00000000..8b1fd3fe --- /dev/null +++ b/testPreset.html @@ -0,0 +1,17 @@ +--- +permalink: /testPreset +layout: page-4 +title: Test Preset +--- +
+
+

+

+
+
+ +
+
\ No newline at end of file diff --git a/testRuns.html b/testRuns.html new file mode 100644 index 00000000..31e4e4c0 --- /dev/null +++ b/testRuns.html @@ -0,0 +1,6 @@ +--- +permalink: /testRuns/ +layout: page-7 +title: Test Runs +--- + diff --git a/testRuns.js b/testRuns.js new file mode 100644 index 00000000..25ec4580 --- /dev/null +++ b/testRuns.js @@ -0,0 +1,151 @@ + +var e = [ + + ["main", [ + ["index","index.html"], + ["sandbox","sandbox"], + ["original","original"], + ["approval","approval"], + ["newer","newer"], + ["condorcet methods","condorcet"], + ["primaries","primaries"], + ["common ground","commonground"], + ["modify","modify"], + ["blog","blog"], + ["irv","irv"] + ]], + ["drafts", [ + ["quotaApproval","quotaApproval"], + ["bees","bees"], + ["draft common ground - old index","draft-common-ground"], + ["oncemore","oncemore"], + ["essay","essay/essay"], + ["essay-dec-5","essay/essay-dec-5"], + ]], + ["tests", [ + ["testPreset","testPreset"], + ["test (Layout)","test"], + ["testBallot","testBallot"], + ["testEmbedLink","testLink"], + ["try","try"], + ["play/examples/election1","play/examples/election1.html"], + ["play/examples/ballot1","play/examples/ballot1.html"], + ["play/examples/model1","play/examples/model1.html"], + ["play/examples/model0","play/examples/model0.html"], + ["play/examples/sandbox","play/examples/sandbox.html"], + ["play/examples/ballot1_original","play/examples/ballot1_original.html"], + ["testAttach","testAttach"], + ["sandbox/embedbox","sandbox/embedbox.html"], + ["sandbox/index","sandbox/index.html"], + ["sandbox/original","sandbox/original.html"], + ["splash/splash","splash/splash.html"], + ["oncemore (not working)","oncemore"], + ["rbvote/calc","rbvote/calc.html"], + ["testSwitcher","testSwitcher"], + ]], + ["borda viz", [ + [1,"http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA41WwXKbMBD9F511QEjGOLc2mZya9JCZXOKMRzGyrRlALhKZJpnk2_sWG7uN6lg2sMvTW2l3WbS8sYxdPDwIlXGh1CN_ECLnohy1UkKZFFwU6vGRM0HcXIIhaWAKRRU0INkFy68YZwrKBHLCLla69oazAjYsB3T-xERTsDMe_TFSYmSS8dMHODNwxAyhnD8pGoQeuh4-CgqMfcxxzOcso8ucwaURERFCF_qxyjam9da1fkROc9sr60NnlyGB643-gpZHiIwQFSGTCClOO7DsfXDNrW5MSmSg1QjuJHMaIWWEzOK8_-dRxM9CxNkQcTpEnA8RJ-QQ0drobunalV2fj37bGW9CKnu5cc6bxdb-NvXC21eTYOKabR_MjQkbV6XQa9ddDst0iey7rV4mOOIRqq4WK70Mrls8u2C6hOrQnWl1YqiNqaxuF43R7Xly2JgmYc4-2NqGl4Xf6G0CfRfWN3-p28pWOqS8AE-6rl24twnUZ-vvbLuuzffBxv9s65eTVuKLN5RK1K1WKTlt-3uEhF0qjfzDPJv6PNUHs70B_TyzctfYz_rOXNs6pBSlp3r8zKM9O0eDyWAhqOUgelLVodmIY98RxVFFVyEWNHQRBTHbz5Jnw30uiED3mD6HkOM4phYQmBY9DkoxDkxHpByR2WAqqZ8y_6tH0QPFdq_pWZPvUuypMt8vJ4e-ScoxBIm13tixNY2KHBW1U94BkTfmlezhTpFBlmTsbWWedHfkzYauKrjkaOxMDS7S-oLcUtT8dosPV4LyGJLUW7kgFR5IiMkO4Xu0GFKlKDEfgxV9DpT_3FHaP8gp-kKAF-zWkUZ5gfyrTPdp-VQ2hxyNhbenHUp2yO_7H3QVO2bUCAAA"], + [2,"http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA41WTU_jMBD9Lz7nEMdJmnLbBXFa2AMSlxZVpnFbS0ncjZ1qAcFv3zemaQFvqZsPj8dvPB-Z5PWFpexiNuN5mvA8f0hmnGcJr0apEhCKMuFl_vCQMO6xBbBFSnPBLlh2xRKWQygwFuxiJRurElYCyjKozl_YaAJ0mgQHViqswOHpE5gpMHyKqM5flAQydv2AGDnlw97mOOdzltJtzhDSqOGBhm70Y7VuVWe16eyoOY3trrR1vV66CKxV8htYFmhEoMkDTRFoytMBLAfrTHsrWxWTGWANkjuJnASaKtBMw7r_51GEz4KH1eBhOXhYDx4W5JDRWsl-abqVXp_Pftsrq1wserkxxqrFVv9VzcLqZxVhYtrt4NSNchtTx8Ab0196N30k-m4rlxGBWKQq68VKLp3pFzvjVB_RHbJXnYxMtVW1lt2iVbI7D3Yb1UbsOTjdaPe0sBu5jYC_p_XDXsqu1rV0MS_Ao2wa4-51BHSn7Z3u1o366W3s7655OmnFv3lDqUXNahVT0264R0r4SsWBf6mdas5DrVPbG8DPI2tzje_Z0Ktr3biYprTUj19x9M3OQDApLDhRDrInMT-QDT_yDi-PIliFUJDAIjmG6X6XLPXzjBOA5tg-wyDGdWzNMWBbcByEclyYjJpq1Ey9qSAaZfbPgKaHFp97Sc-aYhd8DxXZ3p3wvEnCMQUBXy_sSE2jIEYhfxdeoaJo1DPZI5wyxViRsdW1epT9ETf1rMoTkYDPWe5DTCmknIjPO6ZJ9nECfzNBAvwJDAVxa-IP0pa-MDmV4c3bEPlXn2ZU5DcKgf4PwCe7NSRRFTB-aMp9Eb40yaEiY5vtq3doUD9__Qd6XTAvuQgAAA"], + [3,"http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA41WTW_iMBD9Lz7nEMchhN52W_W07R4q9QIIucSApSRmYwdtW5Xfvm9cAm29FJMPj8dv7JnxxI9XlrKr6ZTnacLzfJ5MOc8SXg5SKSCMioQX-XyeMO6xBWEL6gt2xbIblrAcwhjtiF2tZG1VwgpAWQbV5QcTjYFOk-DCSImRUZqcv4GZAMMn8OryQ0EgYtf18JFTPGw_wz2bsZReMwaXBg0PNPSiH6t0o1qrTWsHzXlse6Ot6_TSRWCtkt_AskAjAk0eaEaBpjjvwLK3zjT3slExkQFWI7izyHGgKQPNJMz7f7Yi3AseZoOH6eBhPniYkGNEayW7pWlXen05-m2nrHKx6OXGGKsWW_1X1QurX1SEiWm2vVN3ym1MFQOvTXftl-ki0Q9buYxwxCJUWS1WculMt9gZp7qI6pCdamVkqI2qtGwXjZLtZbDbqCZizt7pWrvnhd3IbQT8Pawf9lq2la6ki_kAnmRdG_eoI6A7bR90u67VT29jf7f181kr_s0XSiVqVquYnLb9I0LCKRUH_qV2qr4MtU5t7wC_jKzMLc6zvlO3unYxRWmpHr_i6MzOQDApLDhRDqInMT-SDT_xDi9OIliFUJDAIjmayWGWLPX9jBOA-pg-QyOGcUzN0WBacByEYhgYD5py0Ey8qSAaZfZPj6KHFse9pL0m3wU_QEV2WE543iThFILAWq_sRE2DIAYhfxfeoCJv1AvZwx2wMRMlGVtdqSfZnXATz6o8EQn4nOXexZRcyon4_MLUyT52sN5UkID1BJoRcWviL9IWPjE5pWHvbYj8y089SvKeXKD_A1iT3RuSKAtoPxTlIQlfiuSYkaHMDrBjgfpsvv0DVDeg77kIAAA"], + [4,"http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA41WwXKjMAz9F599wDYQ6G23nZ623UNnekkyGRecxDOAWWwy23aab1-JhCStl8YhYFl6siUheLyTiNzM51lCU7Gkcx5zyngGEuMx5YItl5QwRPCYUZZztAhGkxSEJKWAR4QgN4TfEUpiEGYwJuRmLSurKEnBmXBQXT9hoRmgI-odYMnAkkR0-g-YHDAsj2jAiWlB5q7rIUaGGZL9Av6LBYnwsiAQ0qhhngYv-COlrlVjtWnsqJnGNnfauk4XLgBrlfwGxj2N8DSxp0k8TTodQNFbZ-pHWauQzABWQXKTyJmnyTxN7tf9P7fCvxfMrwbzy8H8ejC_IKeMNkp2hWnWenM9-7ZTVrlQdLE1xqpVq_-qamX1mwpwMXXbO_Wg3NaUIfDKdLfDNl0g-qmVRUAgFlKV5WotC2e61c441QV0h-xUIwNTrVWpZbOqlWyug91W1QFr9k5X2r2u7Fa2AfBDWj_srWxKXUoX8gC8yKoy7lkHQHfaPulmU6mfg4_93VSvk17smycUW9Ss1yE1bfpnSAneUmHgX2qnqutQ61T7APDryNLcw_us79S9rlxIU1rsx684fGdzIJgIPBhSDmSPYnwiG3bmHZaeRWAVRIEELCJgyI-rcKCAGAaGAJzD8hwGMdphaQYDLAscB0I6GmajJhs1-eAqkE6J_dND02PAgh3tgh_3EANZonCOW8AG7-TER3wUxEH4AAl3Vm_oBlunEYwZ-lhdqhfZnXH5wKCcApuTeAgGdz-cqEKqO-x6cUUDnzIIZFUKB07ioYBxcqlLhyLFWJL94IUfAtmnGRZ8jyHitwFERR4NSlgcGC8a9FibLw1zKtTYcseinpp1mH_8A0dGU8_NCAAA"], + [5,"http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA6VWS3ObMBD-LzrrgB68fGuTyalJD5nJxc54FCPHmgJykcjUySS_vbsY7DSqY01rDPpYfbvSPmB5IQmZzed5RvP8ns65ZJTJDBBLcspFeX9PCUMGlwXlTCJHMMqTYmCDjKNeno-gEAA4gKwEwNBQyqkUCBin2SDhgsoUgeQ0TXAJQWaEXxJKJIAcxpTM1qp2mpIMViccROdPMJQDO6HBATMFzKQJPf0HTgkcViY04sS4QOh818MeGYaIvC3gv1iQBC8LAluaJCyQ4AV_pDKNbp2xrZskp7ntpXG-MysfwXVafULjgUQEEhlI0kCSnd7AqnfeNjeq0TGeAa0G504y80BSBJIyjPtfUhHmgoXRYGE4WBgPFgbk4NGjVt3KtmvzeN77baed9rHs1cZap5db80vXS2eedYSKbba919fab2wVQ69tdzEs00Wyb7dqFbERB66qarlWK2-75ZP1uouoDtXpVkW62ujKqHbZaNWeJ_uNbiJs9t7Uxu-WbqO2EfS9W1_chWorUykf8wA8qLq2_s5EUJ-MuzXtY62_Djrue1vvTmqxT55QLFG7XsfEtO3vwCV4S8WRv-knXZ-nOq-310A_z6zsFbzP-k5fmdrHFKXDevzIw3c2hwaTgAbDlgPeI5SHZsOOfYdlRwhdBVmAoIsIGMrRCocWIGFgSMB7MM9hENM8mGYwgFnocQCyaSKfJMUkKQdVgf2YuJ89FD1uWLBxXvBxDTE0SwTHfQtY4IUc-hGfgNiDV0C4sn5GNVg6S2AsUMeZSj-o7sgrhw7KKbRsIofN4KLnT6RjG9zv6J-uaIL_vwmBXZwGB07JIX0ynRgoy4YUSUzI22ADP0OKP-4w3W8YIPwygZiQG4sIUwPju8djzMyHcj2kaSr4MfeHR2VM8Q_TVmqn9ViZr78BoqyTqp0JAAA"], + [6,"http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA6VWTW_bMAz9LzrrYPnbvW0telq7Q4FekiBQY6YRZluBpQRLi-a3j1TsZKuWRMPi2KLJR0okZT-_s4jdTCaiqLhI4hmfFIKLKkVBCFRl1WzGmXCQMuK5MxQpLxInJLwkH1HlvMidEPHSmcqKF6UTysErK3kROa-I3DFswm5YfMc4S1EocMzYzVI2BjjLcUYWo-r6iYEKREfcO9BSoiWL-Pk_YirE0MIDTqoF1sv2G1yjoLKw_RT_0ymL6DJluKRRIzwNXejHatVCZ5TuzKg5j-3ulLG9WtgArAF5ARZ7msTTpJ4m8zT5-QUsNsbq9lG2EJIZwhpM7iyy8DSlp6n8uv-lFX4vhF8N4ZdD-PUQfkGOGb2C7Be6W6rX69mvezBgQ9GLldYG5mv1E5q5UW8Q4KLb9cbCA9iVrkPgje5v3TR9IPppLRcBCzGYqqznS7mwup9vtYU-YHfIHjoZmGoLtZLdvAXZXQfbFbQBMTdWNcru5mYl1wHwQ1pfzK3salVLG_IAvMim0fZZBUC3yjyp7rWBr87HfO-a3VkvceEJpS2ql8uQmnabZ0wJ31Jh4G-wheY61FhYPyD8OrLW9_g-2_RwrxobsikN7cfPOHpnx0gwEXoIohzMnsT0SDbixDsiP4nIKoRCCVkkwaEaosRIASkOggB0j-FjHJLRjqEFDhgWOQ6FfDQUo6YcNZVzTYiEiRHFoE_iIXbiSJKE03oTDPxOpHzgoXgUsoPwgQiaEd7IDafMIxxL8jGqhhfZn3DEfTHPuMDJU1qEm_TySVCivsNq_vFKzvH_OCfE1vzTQYbUNSnNRjvpcteIlMq-dxHoY6P8446auqdy0PcHVoA9apKoETj-9hAMffi0KY9NGbf10OHjAzE09IfqarkDGPbfxy8UsCJXeAkAAA"], + [7,"http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA6VWTW_bMAz9LzrrYH3YsXvbWvS0docCvTRFoMZMI8y2Aksp1hbtbx_p2MlWLY2GxbFFPz5SIimbfmUZO7u7kzrjQul7fpcLLjKJglAll3l2f8-ZGCgq51LkqJECNVlFEmGyRKnQozDLUFDkqEBh56jimlxXFS8GQGRcF-RYsTMmLxhnGoUZjjk7W5nGA2cFzskkQqdPdDRDdsajAzUlavKMH_8jp0KOqDADp0_KBmYs9Ftco6DEsPc5_udzltFlznBJEyIihC70Y7VtofPWdX5CjnO7C-tDb5chgevBfEKTEaIiREdIHiHF8QUstz649tq0kBIZ0hoM7ihzFiFlhFRx3v9SirgWIs6GiNMh4nyIOCH7iB7B9EvXrezj6eg3PXgIqezl2jkPi439Cc3C2xdIMHHtZhvgCsLa1Sn0xvXnwzR9IvtmY5YJC_EYqqkXK7MMrl88uQB9wu4wPXQmMdQWamu6RQumO00Oa2gTfG6DbWx4Xvi12STQd2F98eemq21tQsoD8GCaxoVbm0B9sv7Gdo8NfB1s_PeueT5qJT55QmmLutUqJafd9hZDwrdUGvkbPEFzmuoDbK6QfppZu0t8n217uLRNSNmUnvbjRx69syU2mAwtBLUcjJ5EvW824tB3RHEQsasQCyXsIgqHavQisQVoHAQR6B7dSxzUpEfXAgd0iz0OhWJSzCaknJBqMFXUhqkjihFXcvSthiZJwmG9Ch2_Ulve9SE5CflOeEMGzQgvZIZTFhmOJdl4W8OD6Q886n2S51zg5JoWMUz6-UlUan271fzjlYzl_xgr6tb8w0EKPRRJ55OesGIohKa0vw8e6GOj_OOOivpO6aDvD8wAu3YkUSFw_O0hGOvwYVPuizJt67HC-wdiLOgP29XmGWDcf2-_AI61-7Z6CQAA"], + [8,"http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA4VSPVPDMAz9L5o9RP5I48xcJ2BkSTsE6vRyVxpIUgZ67W_nyW4IUDjquH6S9fEk-UgZlVXltcr9WlVaZxdgCsXeAbH1SrsMyDnFRbZeK2LxYZcpbJENlaRvSJGl0ihyVDb1bgiKchhqdbXgssBNpq4WbgrcIPTfH2w8bNgj__9b6KLGsT-ADwtzOq_wrdKf_Ggb6v6p2zftdtLETajoynbTLUM9HvqwbHdj6H91YAaUxJrKDIf0p2sa3LL97A3PbeJ8hmgMW5xog8PhYwSNCqDUoM84EFbjMOnOJqWLvdd5Ui6SVCTJRwcjs6bh9VD3AVzGvq33210QpoajodExljHJfiZrEP1Icx8noCdgJmATOEEFKhTekcmATS7hC4kytJvwWPeznY9vgfEyjLIgY4WnDM7KuCIDEfRXAfkqIyA9OevkRajLEn0eK7HITOfoJc-z-CZ5kYQEBIecdN8JQlIZ5HPYHx5CP7TdHgqG5sfop4qHMbzcwTiOSLxuw1vYIf3pAzncLVteAwAA"], + ["two","http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA41SyU6EQBD9lzr3oTeW5mzmpB69AAfMNIYEB23Ag5OZb_dVd4ToxES22l-9quZMkqq6dqVwrhW1loVQMmtbQYr9pREqBYyLGgKGKtJ3JMhSVQjKqOq7cfaCclRocXOjpEBEipsbkRKRTIq_H-Q45CgnxT9e5o2BlrCCj-IR6NrQVTYEvqzpTcs3rWnShy86TgffLWvwh2FcfPh2x5eI8TVVEoLXMPU9MJTdVqD2bah844HxlYXEsBrCRQANnnBqlSwdY9okC4gKAmgGIk_OIlllslwsMHx8NL-vXfCgsoShO72MnomahGx0xDIm5e9cDdDPtG8rKRcE0I_8J-AMWuaMUXLqPBz9cxf2PBePVaGXZRoqqbz11CJ-2aVvXaBTm1Rg41w22yHySNmiO11jBf9t5Q_LscVEYGToTo8Ta2jOx_rqT-uTD_MwnfhvhefXuW5bmBf_9oDsuB0uu_cffsTmLl-jnCohGwMAAA"], + ["one","http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA4VSyU7DQAz9F5_nMFsmyxn1BBy5ND0EdSJFKg1MEg5UzbfzPEMSKEJks5-XZ4-dC0mq9ntltdAmOxwEqYSlUNIxNlSRviNBlqpcUEZV25wGL8ghEFm3N1JyeKT4dcNTwJNJ8feDmBIxqkT9_19uF_2PYUI_ijunuaZZ1oR-WdOr5latrtOHLzr2O9-MU_C77jT6sJjjS8T8mioJwWPo2xYcyq4jUNs0lNtUnF9ZSJxWQZSRQaNRGLWKRg1aDWGSzyYj6AyES8Y8oSKhMiYYXhcNb1MTPLdnVPQaHQmMSUFbhwaUF_qa0RUY3OQ_cAwDesepBUcM3dE_N2GL4y3wfK1cFB5vZGWgvwPDG2PFxo5ttmAXu7KoRHOM5t-o-IFKRlwUIEMteuxZQzHe14s_T08-DF1_5t8QlpuFrQcdRv_6gOg4Dk679-_-BHT9BGXjbRzjAgAA"], + + ]], + ["etc",[ + + ["IRV and Yee","http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA4VSyU7DMBD9lzn74DWNc0Y9UY5c2h4MNSgipJAFBIh-O29sJQEqRJzEb_bnGX-QpGq79VoUfi-2WkmxkgysE6rUQMp6oR3rHKsYKAC33wtSHKuchChZNlSRviBBlioryFF1F5o-CirgqMXZQsgKFinOFiwlLEj99wsfDx_lUf__j-nirEM3go9i5nTa0UnuCHwZ6RkVM9rt8o8fOhzXMQxjF9d1M8RuUqePiPNrqiQ2bkMbX29DizTKzl1QS0NUsUC0QKFbCgcusPmURMvUQg2iChsya2wm22xWIp3BVmTlKktllnwKMDxd6p_H0EVwGbo6tPdNZLJGJUejUy5jsv9C1iD7By0dm4CegJmAnYDL4BM2cKL4jpIGtAquU3K6vj7Em9Atfj6NX-EyGGGFAy_LlHlalmeUyLCgvwsovTUMbDqzdXwNxLzYUqRjWVSnU4rjW1n-kDxLTASCQ1W6OjJCWR7_Y2zH69j19bHlWw3Nr_nPnXqLkX1yD_ohPm0Qmohxjsv4EpvU6htEb8ITsoVx4FIPdXsIiF6uy-cX6f0TkpADAAA"], + ["cycle","http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA41STW_bMAz9LzzzYJG2LPs89LT22EuSg9toQNDMaf3RYSua375HqbWzFQVq2dbj9yOlFyqo3Wx8YFeEHW9EPTtfALkgXNe7HZMzD6mUXaNm0MC1BwjKIs48lFqSb8RUUlszVdT-6I5jZPIIFf6wEFLDUvCHBUuApSr48xc-DXxcU_AXPmsAPU7DDD7OeqHzls7FlsDXkCzIL2i7zT97aH-6it00D_HqcJzi8K5OH5HlF2oLbDaGPv6673qkceUyBbcOxPkVYgSuxI6GFRt6EiYBVyjFJaVIVmoqIEjpsFXZ5rOyzlLIUs6idqo0Ps3dEI2humRVSQlUs9PKUJHyhZYxyTvQDF6BUI3iHzSmKOgtWbCY8bCPd92w-jXpYIVxVag0Go6xTLDZ53oXfzPIZwa9DC9Tn2V1qfOpnxKM6Jyi7A6Gf6TGJCMHoQIfujkZAhk77J-xn2_jMB5Ovd1haP477WVEv2M0nzdpnOLjNWLTWC3J9_gcj0m6Q_h194h03TxZrYdDv-8Qvt6O17_GnKVhdwMAAA"], + ["Small Bug and STV allowing representation","http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA4VSTVPCMBD9L3vOoZukoe3Z8SQevQCHINHpiAX7oaMM_HbfJtKqjCMh3Y_svn3J7oEyqhaLslCOV2qhOVOzTBSbKy40NLal0rn4OHfwFauVIpYkzjOFLbahivQVKbJUsVOUU_Xgt11Q5BCp1cVCzgwnmbpYOClwAuy__4gpEcMlCPy_hS9u2bcD-LBQp9OSTtmSQFg0PWpu1JbL9JEfbXbXwfdDG67rbR_asztuIsHXVGUQ8g5NeLv3DWDYjq_A04Owm1Q8AVtIXDiHKCOIBlc4NYgyBJA1hElnNjkBZyBccs6SVSSrjAlG-krdy-DbAC59W_vmcRuErOEYaHTEMibFT2QN0A8yF2eao6anmFE75x3hBB8KHyhnQMlJjUKgunoT1r5NLZC4MraeMQhGWTCyQlb6ZKU7EU4M_d1AxYURxcbb2lwGQH0t8bt4HYvKdIpZMo7FD6sUS0jAyFGTbneioai0_Tk0w11ou3rXyDjD86vv453fQ5CYNFFdH_ZzpEZagnETXsM2PvEa2XO_B5ofein1VDcbj-xpTI6fnvReMoIDAAA"], + ["An old run from 2019 July 11 with the wrong result. Has the method changed in impelementation?","http://127.0.0.1:8000/sandbox/?v=2.4&m=H4sIAAAAAAAAA4VSTW-DMAz9Lz7nEOcDQs-77rAzVBVrsw2tpRtQqWtVfvuew6CHaZoA-8Wxn7-4kqZVWbL3in1YK6Bc5QbA6BwmKyYTFLMGCk5xJpccTDKtFbHEGyvx6Wxo5RVZWpF5IEUOYKzo6XQc6sembQ71uSLYPa2G7hQVZYjX6tcDpvzPm4AbYuNJef3XC7dC3GwRSHGh__ukFT0XxdIVjRqFjkaEFRFEFCLYJ5mLTDAT4URUeKuKtm_HYx83H8057jd9c4liTJ2PnJEkM9Nkko1lXodYt4Ixspd630sZy5QQsxgxF3bQmEIGVUw8nIgMWsBKADjtwSALQ4FfQ7np5KdTNinwWagwnUBnsD_5Laj_PNVdBNvQNXX7uo9SuOVEYidmOzFbN1dqQX6l-yRnYBaHGSwhP13ecIeSKF6Q0UpVQhyErm928bnu7n5F-jdYGWWVUx5lOalYtuhkd-InGFnLNDc5IHNpBbjU8XvT7uqvGJFyG9shdnT7BuR2HywPAwAA"], + ["fixed bug with districts and yee","http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA6VVS1PbMBD-LzrrYL38yK2F4VTogRkuhMkIWyGacazUkmmBgd_eXbmxKSQTTRvH1ifp26fXqxeSkcXtbZHTorijt1wyymQOiGUF5aK6u6OEIYPLknImkSMY5VkZ2bDGUa4o_oBSAOAA8goAQ0WKUykQME7zuMIFlQqB5FRlaEKQBeHnhBJJFooSRRZr3XpDSQ62Of10gUgBOxn9dMFOCTsqo8f_wKmAw6qMJtyYAUhS6Afwh2EyyNuSvGVLAv4iYhPiE1ouxwf-SHdufehtHfx-Jd4Hud7oI7RZuZiQnJCaUH5YcT344LZXemsSvOiA1oLTB5nFhMoJVXM23iVmzgybvWez-2z2n6nDnjwY3deuW9uH017veuNNSGXXG-e8We3sL9OuvH02CSJuuxuCuTRh45oUeuv6s2imT2Rf73Sd4IiHUHWzWus6uH716ILpE96q7k2nE0PdmsbqbrU1ujtNDhuzTdA5BNva8LTyG71LoI9hffFnumtso0NK4d7rtnXhxiZQH62_tt1Da75GGf-9a58OSrEjXxSWpluvU3LZDTcQinUJuUTyN_No2tNUH8zuEuinmY27gL4y9ObCtiGlGD3W4UcedkJOFhkM2LA787PWHUgzOfVrNrduls8QmjWTMEJrFjBUUQmHrgqLHFoqgwE0cxjEuCfHRRUFeD4uFuOsHGdVFBB4gBH_Y4DqRg8Fi7uCRwVCjKTZQwEqX8jU0PkeiBG8AgJrxDxDYAIM5qisRBlvG3Ov-5lXxSOIUzjdiEQ34PD8eOEWnhmj9X96ogr-_yrEcQdlzKtUewau5TFxEjJA3qIOPJ3Lv2YVzjAZMFEQP7lyiCBcrCnF8VBHIKJ6JeOLUXOFqHyffIVm9BCifPm-tlQ17-TZO4xWHDJyPqLX37wh2iLMCAAA"], + ["fixed bug with yee and tarena","http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA7VWy27bMBD8F555EJ-SfGsT5NSkKALkEgcGa9MxAVl0RSrNA8m3d5eqpTS1YR5ayxaHy-FyuKJ39UIKMru9LTUtyzt6ywtBWcEBMcEpZ_rujhKGDC4r6EvkCEZ5USFCG8d5ZfkbVIyyGoGuwcLQkeJUCgSMU50sXFCpEEhOVZGABudlYpeUlRpRLSlTiDQHwFGJIDPCzwklEsDbnHzrfTSXrnVb8zgnYFdktjZNsJRoEA07-HiBkxJGCvrXBSMVjKiCHv8CpwYOqwua8cPQQXRj14MehlFEzW9FUoqIjYiPaD4fbvgh7bkLsXPLGPaW9DvIDdYcoU3OxYjkiNSI9GHHyz5Ev70yW5uhogVaA6IPMssRVSOqp2i8C8wUGTapZ5N8Nuln6rCSe2u6pW_X7v606l1ng4257OXG-2AXO_dom0VwzzZjit_u-mgvbdz4VQ698d1ZWqbLZF_vzDJDSICtmtVibZbRd4sHH22X8VRNZ1uTudWtXTnTLrbWtKfJcWO3GT776BoXnxZhY3YZ9GFbn8KZaVduZWLOwf1umsbHG5dBfXDh2rX3jf2c5oSvbfN0cBY78o_Co-nX65xYtv0NbMX5jFgi-Yt9sM1paoh2dwn008yVv4C80nf2wjUx5zAGPIcfeZgJOZkV0GAKb-3PpWlhNpNjvmZT6mZ6gpCsmYQWUrOApk5OOGRVMHJIqQwa8MyhEcOYHIwqTeB6MJZDrxp6dZogsPKR8KOH040KBUujgicHQgykSaEAly9kTOh8D8QAXgHBasQ-w8YELKjRWYVzglvZ76abeHUqQZxCWSQSZUDVPHDt65LE2jGo-Kd3dM3_n2txdGPpQopMz0WqPRNtOsVflljiky-s99UfvRp7GEws-RA_cuURQZjSSwDH1wQEIrlXMj1YNZ0wpfcPT-Ey8F9EdvX-aKp6HNDFBHEJj-OaD-j1F9O7ys9CCQAA"], + ["Cool Bees","http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA4VSwW6DMAz9F59zwHFCCedpx_0A9IC2dKpUUY2CJq1qv33PyWg6VdNKwc928vxi50wVtV3HVWVYZGs621jjLQBbNuydIraGXdCkq42t3HZriNM2j22-Ul-oJftEhhy1XBvy1O6GwykaqrHSmocHezbIVObhQaZBBtx__7EmYA0HCPj_Vb045zwt0MMqna49XaueIFiR3NDmhvo-f_RHb8fnOMzLFJ_3hzlOazi9RMpvqa1gtA9j_HwdRtCwu3WBS0O4LhAtYAeLA3uYkEgstCJoIZRhwGxhJOdcDvocrHMQNALTZC-knOhk6fSxDFOElnnaD-P7IapY4bRQbOISyeuLWAH7mUrHVmBXICtwGVwQghSKX6gkUFMrfUjzZUxbDC4SOVWkw3A6glRLHXvvgLkTBS4dyXmdsvl5NF4nzQ416Jp26Z1rfnlBPZUFx6MmvRwVoajO1lu9qgokUfncUV8G5MuAvNYZljkRNPez9aFkai1y3O0UcoFa6DjS5Rtmc0Z2ZQMAAA"], + ["Metal Bees","http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA41Ty44aQQz8F599GNv95BztMT8AHEbJEK2EQGFBkbJavj3lbmZmV6soYYAu293lao_9SgNttlstmVXjnrcqkbUGIIssNrgrK8tQgKQiqOI-M5boByRXlu7TwA2UyDZY259ZavJYBUfJjpIA-bZoHGX2aGislTW3nDawdYrySJmUi7ZQ5jTs90zi0iUOUNJsow3pF2IKtJHEFGlzGI8vE1PCTuVPD85kRAb-9CBSEAH337_YU7FHKgT8--d6Uevr5QY94tLpvqP7sCMIdmQLygva7fqff-j7-Wkar7fL9PR8vE6X2d1-RM6vtBmweB1O069v4wk0EpYqyFoQSStECSRgxYWlYq2NRSEWXoVSwQJqxWI9FrozdmfqTvAYltKt2mLm7UUvP2_jZYKY6-V5PP04Tq7WpG00bVxmff-q1sD-SmvJZqAzsBmEGcQZpBnkGZQZ1IXww9t4oIVcFnZZ6GXhlyWBLBnkkeINyhCm6TcubAgnv2VtfSboOuPAkRNnLozRgdPbH7PGeAuYPnSuYGwwbxS8et45wful1cUNfW9A59YchFb-EL0l-T8eP5Na7QNE0r0x-vCUD1Z1y-8FI0IPfT07giBv0qg-cw6spY-9M-LaaXHttOh5zt6Usbxv0eg5xtvViZOnOB8ODmWF2k--_QGrnOnqsAQAAA"], + ["9","http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA41TwW7bMAz9F55ZQCRF2cp56HE_kORgbM5QIGiwNMGAFc2371GO7XbDsMVx-ChKj08M-UqJNtutZmPXPW9VhXMFcOVcYKXvuUhEsrJaF0iERRPQg3FNbVNiyTlQzqxugcRZ-lhz566dy4W7oKqZpdZ7NpXIK0nYkgeyxNoy6z2mBUwpOB86lm6_Z5IQLY6snsI32pB-IqZMGylMTpvDcHwZmQp2guj3B2c6RBL_8SDSIwLuv3-xp2KPVAj49xt6UeXL-Qo9EtLptqNb2hEEB7IFdQva7aaf-NDX0-M4XK7n8fHpeBnP83J7iYJfaZNgog7P448vwzNoJC9VkLUgUlaIEkiGxYWlwtbGohCLVYVSgQG1wtgUy9OiT16ZDHgMpp-82g5YNBa9fL8O5xFiLuen4fnbcQy1Jm2jaeMym_avag3sr7SWbAY6A5tBnoHPoMygm0E_g7oQfvg37mghl4VdFnpZ-GVJIEsGuad4gzKEafyJCxvCJW5ZW5-h0dk4szOGgHuuaGlug8RiGB6OcSlobwwNSpSjetE5Ofql1SUcfe9A59YC5Fb-7NGS_B9PnCmt9hki6dYYY3j6D14NL-4Fx6GHPp8CQVA0qWvMXABr6X3qDF87zddO88hziqb0_n2LeuQYrpcgLpHidDgElBXqdPLtF9pRypCqBAAA"], + ["cool","http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA41STW_CMAz9Lz77EMdJ2nCeOO4PAIdqKxMSAq0UTRqC377nZFAmtGmUNs8fcZ79ciJHs8Uie055xQuvntumgMxSkITMPjqgGOFyqxWT2B5RYVHbJa2ydwkoIVdtFzySs-Uqzcg_EVOgmWOKNFt320PPlFDE88ODLQ0ijh8eRFpEouPf_8jJyJHs-B-vtYL-x-EIPmJd0WVJF7ck8DWkN9Tc0HJZP_aj1_2878bj0M8327Efru7yEll9j_4vxYJhw9j1Hy_dzqxwm4VMY5E0QQxCAla0HbHkMkEPxnB60BUsqO-xaI2F6kQ5xZKqs6lWW61cNqjpTof3Yzf04DIOm273tu2NskpJVF9qqdb8iayi-ommuV2BvwK9glDBGS5Qof4TJynYJCufi8oCzZUDjg3uW3ZcKzNNjnriw9fC_u8wOCzUStVioYwgRLsbLJM_lR5DYyKVvXZR2x9WNsvagBHBkZ73hkDPJI2mrzdQFYhVgTgJGidBo53THcdSoL2_CzFPkeTusB2zt4zkb8hu0X69pvMXJ38V474DAAA"], + ["nice minimalism","http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA4VS207DMAz9Fz_7IY6TNNkz4pEf2PZQQYeQpk10nZBA7Ns5TraVi4C2aY4vsY_tvJGjxXJZPKey5qWEjiUIkA_K4jrTdZF9VKCcWLJbr5nEDkl0jGXOzrMXc86Zfch2qqqqs9KC_A0xBVo4pkiLTb89DEwJUeD1_cWRDhbHP15YMizI-_sHnwIfKSD3_7Ja0IFpPIKPWFl0WtHJrQh8DekVdVe0WrWfPfSwvx366TgOt0_baRgv6rqILL5H_acqQbBm7IaX-35nUrj2Qua2SJohGiEBO8qO2ErtoAdjKD3oCjbE99i02UJTIpxiS03ZNSk3qdQDapOnw_OxHwdwmcanfve4HYyySnVUX2OpNv-ZrCL6G819uwB_AXoBoYF3qECFhldkUrBJFr7UKQtmrhyQNrjz2IXFRBtHy_jjb2b_txkclmqhWrBQWxCi3Q2WWZ9qjaGzIdWzdlHzF6mYZGVAiOBId3tDoGcjjTZfb0BrqHi-5vNA4zzQaHn641QD5M93IZbZktwnbGn25pH8Fdkt2m82BsMZvn8Ax3faIssDAAA"], + + ]], + ["bugs",[ + ["Icons not showing correctly in explanation","http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA4VSy27CMBD8lz37ED9jc6449geAQ0RdFImCGhJVKoJv74xDCBWqinE8u17vjnd8lkoWq1V0KoaNWhkdldEJSGun6khXSsr4migGZSu32SjRPKR9pTBpW1mIeRElThaVEi-L92Z_ykoCAo16GjhSY6dSTwM7ETtI_fcfMQkxOqH-_5N0ccm-G8BHk7lc13Kt1gK-RPaO6jtar8cPf_J2XOamH7q8bPd97iZ3mSLMb3D_a7FgsBmH_LVtDrTcvRd6bosOM0Qj0GvRuLbHkkoHDRjDaUBXY0F-g8WOe250-nEJoxNpLJY4WqkcsBRXTp9D02Vw6bu2Oez2mZStLoHWlCTWjvEzWYvsZ5n7NgEzATsBN4ILXKAi-RuVLNgEpk9FZQ3NrXIo68iIkjgKUWrRMI8GMq8sgStXcp5aq9ugPxTOrmbTyyk-vPjLSrRIC4ZHTXk9EqEoJfLUyxDYksrfnu0skJ8F8qzTDH1JEB-19WneCdUDZpkjI4K5I76Kbdtt96QUUFDaj2aX5fID08NdIIMDAAA"], + ["Text Ballots","http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA4VSy27CMBD8lz374HdizlWP_YGEQ0RDhURBDYkqFcG3d8ZuCBWqinE8-_Du7K7PomXVNMmqmNaqsUarShP4oExtgYxPygbqAlV6vVZieMcErbApO1mJfRIlXlZOSZDVttufeiURjlY9LFypYNHqYcFSw4LQf__hk-BjEvL_v0kXNY7DBD6GzOXaylW3Ar5E7oaqG2rb8uFPXo_PfTdOQ_-824_9MKvzFmF8i_qvWYLAZhz6z013oORvvTBLW0xcIBphPE6UHXCgMq3EgjGUFnQNDsS3OFyx-aIMRYrlqHLnbV2klC84zlZOH1M39OAyDrvu8LbvSdmZ7OhsjuVc8V_IOkQ_y9K3GdgZuBn4Ai5QgYr0X8jkwCYyfMpTNpi5Ux5pPRlxJJ6DyLko2HsBkRtHUB6TD5y1-lnUx8zZV2x6vsWHV_-SEiXSghCQU16OREjKEQXOyxKUukPpaFgGFJYBBebppjEHqO9nG9JiifoOM82RHtHeEF_FZjds9qQUWZMcuvc8i8i-HbdbGqq5rZGp5PINUZDL8aADAAA"], + ["2 Text Ballots","http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA4VSy27bMBD8lz3zwLdEH3ooihx76s3ygXHkwoBiobKMFgnib88MGVkugqKmKc4-uDu7y1fRstluk1Ux7dTWGq0aTeCDMq0FMj4pG6gLVOndTonhHRO0wqbsZCP2myjxsnFKgmwOeTj3SiIcrfq0cKWBRatPC5YWFoT-9x8-CT4mIf__N-mixnm6gI8hc7l2ctWdgC-Ru6HmhrqufviTp_Ghz_Nl6h-Ow9xPi7psEca3qP9aJAhsxqn_vc8nSv7WC7O2xcQVohHG40TZAQcq00osGENpQdfgQHyLw1Wbr8pQpViPpnTetlVK5YLjbOX865KnHlzm6ZhPP4eelJ0pjs6WWM5V_5WsQ_RXWfu2ALsAtwBfwRtUoCL9CzI5sIkMn8qUDWbulEdaT0YciecgSi4K9l5A5K0jqI_JB85afSzqY-HsGza93OLDa_-SEiXSghCQU76PREjKEQXOyxLUukPtaFgHFNYBBebJl7kEaO9nG9JqifoOM81Ij2hviK9if5z2AylF1iSn_FxmEdm38XCgoVnaGpkqf3nsePtp_NH_mb_mYRjn80e_3wFQ-nZ1ugMAAA"], + ["Test Voters as candidates warning","http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA4VSy27bMBD8lz3zwLdEH3ooihx76s3ygXHkwoBiobKMFgnib88MGVkugqKmKc7ukrvDWb6Kls12m6yKaae21mjVaAIflGktkPFJ2UBfoEvvdkoMz5igFSZtJxux30SJl41TEmRzyMO5VxKx0apPA0caRLT6NBBpEUHqf_-xJ2GPSaj__0m6uOM8XcDHkLlcO7nqTsCXyN1Qc0NdVz_8ydP40Of5MvUPx2Hup8VdpgjzW9z_WiwYFOPU_97nEy1_08Ksspi4QghhPFZcO2DBzbQSC8ZwWtA1WJDfYnE15qszVCvWpSnK27ZaqRxw7K2cf13y1IPLPB3z6efQk7IzZaOzJZdzdf9K1iH7q6y6LcAuwC3AV_AGF6hI_4JKDmwi06fSZYOeO-VR1pMRW-LZiFKLhr03kHnrCOpj8oG9Vh-D_lg4-4ail1N8eO1fVqJFWjACasr3kQhF2aLAflmCqmioioa1QWFtUGCdfJlLgva-tyGtkajvMMuM3BHtDfFV7I_TfiClyDvJKT-XXkTqNh4ODDSLrJGl8pfHjqefxh_9n_lrHoZxPn_o_Q4b5c5FugMAAA"], + + ["JSON Parse","http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA4VSy2rDQAz8F5112Le9PvfcH3ByMMEpgdShbkKhIf32zuw2cUooZW00eqw0kvYsRrq-z05TXmvvrNHGEISotnVANmR1kbZIk1mvVSzv2GgUP3UvnbgnUQnSGZUo3XbYv48qCYFOHw6uNPAYfTjwtPAg9d8fYjJibEb9_3_SRY_H-QQ-1payXhsNxePA_GslKwF5yzam8WMzTNTCrQu7NGTTAtGCDZAgHCFy6d2hFowOhSwE8jsIX32hGmPVUhVI4yHaquVywXMr8v52GuYRXI7zbphe9qNw1rYEeldyeV_jF7Ie2c-ydHwF7gr8FYQKLjCBioyfqOTBJjF9LoOy2JbXgLKBjDiywBGWWlTcvYLMvScIpaUQuSX9ObSnwjk0HHq5xSfT_tIyNdKCElFTng9EKMoVRe7LEdS-Y51oXBYUlwVF1hlOx5Kgvd9tzIsnmTvMMgdGJHdDfBWb3bzZk1JiTzINr2UXiXM7bLd0NMujbyvZlAu5BgXM5Rvrv_uNaQMAAA"], + ["parse","http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA4VSy2rDQAz8F5112Le9PvfcH3ByMMEtgdShbkKhIf32zuwmcUooZW00eqw0kvYkRrq-z05TXmvvrNHGEISotnVANmR1kbZIk1mvVSzv2GgUP3UvnbgnUQnSGZUo3cuw-xhVEgKdPhxcaeAx-nDgaeFB6r8_xGTE2Iz6__-kix4P8xF8rC1lvTYaiseB-fdKVgLylm1M4-dmmKiFWxd2acimBaIFGyBBOELk0rtDLRgdClkI5HcQvvpCNcaqpSqQxkO0VcvlgudW5OP9OMwjuBzm7TC97kbhrG0J9K7k8r7GL2Q9sp9k6fgK3BX4KwgVnGECFRm_UMmDTWL6XAZlsS2vAWUDGXFkgSMstai4ewWZe08QSkshckt6ObSnwjk0HHq5xSfT_tIyNdKCElFTnvdEKMoVRe7LEdSJxsuDWxYUlwVF1hmOh5Kgvd9tzIsnmTvMMntGJHdDfBWb7bzZkVJiTzINb2UXqa4wNcuDbyvRVF9Dg-Tm_AMkaIQ0ZQMAAA"], + ["look how short this url is: 402 characters","http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRS64DIQy7C-ss8mfoVUZzkqd394Z4KlWtKiRC4uAY8zd4PM5zKeW66FRhmrwPHiSH1kl8kcauSSyq7LpoyL4kwXRYA0xyt9RpxW6x8WAa3nv0nnVJ6WtV7yyE6WsVcvxE1k9E6klSQbrDaJJ3WVuFQJg4miBNEqGEiFessVGhZigNLb4qqiAoiqBR0ChoFDRaNFbhQAYW45Yj2xppwLTvmgGHVVZMp9C9dnMCBqfh2VLGGXnBzi8fdiLvib6YdmLviTeXf47ybEE-8XWQ76uLwfhJSA-YEDAh7l-GCQETYqLl6FkBE5IRpDkTBiRYsuWUjARBYn6uDnPP_38Cutx9c68CAAA"], + ["weird","http://127.0.0.1:8000/sandbox/?v=2.4&m=H4sIAAAAAAAAA4VSTW-DMAz9Lz7nEOcDQs-77rAzVBVrsw2tpRtQqWtVfvuew6CHaZoA-8Wxn7-4kqZVWbL3in1YK6Bc5QbA6BwmKyYTFLMGCk5xJpccTDKtFbHEGyvx6Wxo5RVZWpF5IEUOYKzo6XQc6sembQ71uSLYPa2G7hQVZYjX6tcDpvzPm4AbYuNJef3XC7dC3GwRSHGh__ukFT0XxdIVjRqFjkaEFRFEFCLYJ5mLTDAT4URUeKuKtm_HYx83H8057jd9c4liTJ2PnJEkM9Nkko1lXodYt4Ixspd630sZy5QQsxgxF3bQmEIGVUw8nIgMWsBKADjtwSALQ4FfQ7np5KdTNinwWagwnUBnsD_5Laj_PNVdBNvQNXX7uo9SuOVEYidmOzFbN1dqQX6l-yRnYBaHGSwhP13ecIeSKF6Q0UpVQhyErm928bnu7n5F-jdYGWWVUx5lOalYtuhkd-InGFnLNDc5IHNpBbjU8XvT7uqvGJFyG9shdnT7BuR2HywPAwAA"], + + ]], + ["etc",[ + ["good minimalist crowding out","https://paretoman.github.io/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA4VSy04DMQz8lVXOFoofcbK98gvctr2BoJdW4qEKIfh2JvaBQ1WhSOtZP8eTfJVadtvG5sSqB9qEK_U6QW3E0oBYOvHKEw0gC18dJHUFciVhB-gzf0S-ks8QdyPRDmQVYLpE4aqzVxvk7XCgwkHAB3GbBFYA8xnQsqtULL4tvo5UoauD3I5IpauDyLgZWW9GGKIwDEeGUicLt5Rd-dmX-9fz5fF4el7OH-93y_Lw8rQ8f3wu59PyDgjn2_HxabkcT293-1JQl5twrsK5C3saMGeDBU9W2DXcAgbwCgcRwWCByT5i6UQfFEj2kZ5_I__WKNAaC_AUkyOgErWqGU9Gik4b040ziz3Tc4amcAzplYwaOQQatBKeDqNg3ofl5Kma8R-UPwgGm0Z3s2hr_7EwD-7W813kpraGs-WNtdyypV4tt2ypV0vdW-rVesZGzG6pl9c0qbqnVq6R4pMeWHjWe473vK6Owvr9C5BeNFdNAwAA"], + ["borda visualization is weird","http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSS25EIQy7C-ssyIcAc5WnOUnVu9fEbTXq9IlFQj6OcfhovT2uS9Ul8ymXWoqpwTNz0ZgnlvDWgreWWK_QVtGp8PZC1anXPkR7PJ_S9EBGx_VAWkdtni7zgf7TNQLJmhKGeQ4vQ3Y_kYFGP-W5fgmEmBW93cV8nyHeHl1atMeQNspPjAXa34PaiUyXt4PMus3s24xCMoXRqnCZEhW2YqEkpkFDapo0IKIBi7ELZlfUgIegacEaYAyGMBYMAsZhCGOTt8XbrgbvRUePNFoJt-p1Z56EHEgXFvJ6TkuyiMjOxyvkcwkZkoI9tOjfva-KsD_0Vq6w-5S_k_kBjGIS_9ONrKfF5CegELErOHrdBkUYlHM4cxRhcCuDco7JklUTB-XMTsOlJKVMr5I8pM6_SgIk5yfXOc_8zy9crDibWQMAAA"], + ["condorcet minimal","http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRu04EMQz8l9Qu4mdu9zMQ3WqLQ1zFCShoEOLfcTyctAKdUjj22JPx5Kv1tm5bKHHvO22i4_fG3km87zs1ni0SibgkEkJi1SJMPGaHtrVTs7YqNa975Ez2_T3ZOxLp9O8kcrqLLHcRTv2cgatDaZBVWUoFQxgbAqRxIKQQtoynks1LVSX5sihctJI0kgE0Yih6DQhoZCA7IVtqQHvJ4WkNF6BSs6rAIUiTaUsP0RiAwKdYmcsy67f9Z8LHRI6JHugMv2HHJyxKhA18FyTbUkWHkQ65jsVdUcTiDv8ci_tAC-xzLB4dASyBXwgsHVPK1BkgCLwfMH50BBj_dL5e3z4eP98vbW0P59eXy3P7_gHLK88FrQIAAA"], + ["line up","http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRu04DQQz8l61drJ97d5-B6E5XBJGKCChoEOLf8XoUKSiKthg_x2PvT-tt23d2Je520C62kPAyrR7EI9JiYVrtOKhxFXMn9Yp3ci5DyH0WaNs6NWvbSs3LjmwRuntZOzLT6e5lZnmYWR9mOBfhBK4KpUFWYSkVDGFsAEjjAKQQtsQcO2GtqPTyhItWkkYSQCOGYNJoAmhkwFvgrdWg87xTh3KFVapTFVnI0eTZmerN0kASfIqVJVeaSevX2unw9RzTkVtHb8usmOz_GIuSYgNfBtm2VtB7eQ7RjuUdoh2iHTd0LO9g8aUmOZaPDsAJAz8ROGE4_imFBCgCCgLnH1Aw0Ptyulw-vp6_P89ta0-n97fza_v9A7f6kMu8AgAA"], + ["complementary colors","http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA21Su27DMAz8lYKzBpESJcufUXRzMqSppzpxkGQJiubbS_FQFEUCweDjyOOJ8hdFGqeptlDbNkwi_OfYZ55B3dkG4l7JOQXW0uNEYwyUaRwCqfvFKiQ8HKuthsTwcAwZDKF7jfQUbY4m1ecwm3o2w06fQg3Z00Ij3Te0Xw-nZT7Mx-vufHvZr8t6vmyIrADaOcNAPRcY08rZrCnrpnlWokfCPlBsgJgBjWQkjSaZAY1URAOi5g0pulDu22MHknhvSsAhKBnTxMFPLy0AwZgaOGyzycDslN3pS-jlWX4zRjp5TfbO_J82Fx-dK94RQnPzpEaPFCIV11VcVyFSsTXFdRUsOvgkxXVLhMHSiuAnAUtRvJkJKaAoUFCw8AoFFb0Va3rfLct6fbudZnvg193xc_6g7x8bbqW7xAIAAA"], + ["bestMap","http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSS25bMQy8i9ZciF9JvorjRYs4QIGgBZx4FSRnLz99LyiMBy04IsWhNKOP1tvpfJ4LsMsFziQIKOYI-wDidblAwzhCMoFwO0O4EnmOhqMx_oHJDsiBLQcYREogHIAGWFITg2gAIdAeI7idOjRppwFNE5sPJXhYfnZ4pcPD8so8rKzDCroA6CEe2b6eWn9qDQJgAAJ0aWYmaKvwBmQDugHLpu765HZs-bmBtdN_D8Id7SNwn4H7EMwp5CK4xgKkQAauKbkJC7gDIzABM7AAa_XkhdiAB_AEXiAdBOPdlDKjKx8H_7y85ByUSpcJaBVcchSPLjB7WJklV86ThCkgOR95KCdJKqnZQEVDo3azdisbOD5gGMGYaabsZK5qXYed5xyS_r6_vjbwPxoNVkeKlZ31o739er7-_HFrp_fb_frp2TLeVfEG6cnysKKEhx9E6LjEx4SStxLdTkTO8mky6rOXDLIyqT13WiJoiamcJFpianmiJaYWi87__dNS1XqF8sbKaytvTPNBoYgVl9VVrHwddZVRvSP8-PwLEfeKBygEAAA"], + ["an actual Tie (not a cycle)","http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VTy27cMAz8F515EJ-y9lc2e0jQBMipQNqegvTbOyJjN8HC8IEjUhxKM_J76-1yvW6TuNuNrmJMbAHEfZDoXGg6iXWgEcSR23rQ7LcbNV7tYhsJ7_3CMxFyMlbX-ASbAghATABe1C5kuoAMihwrSuYLmJDnCG2XTs3aZVDzxIGhQncf9g5UOt19qGynlXlaYYjDCOuS7e9D6w-t0QK8gBBDti0Tsld0B7YD30FkU4c-uRx7ftvBPOj_D-IDHSP4mMHHEM4pAhGgsZHAsSBoKjBhknZSJhVSJTVSr548kAbpIN1IJ1kn43VvSZkZyq-NP19ecg5bpcsEjgqQnA0RAgfCzKxAOSSFU0ABnyCUk2KVBI0iFI2MWm21mtmg63EuI5QzrZKdqlWt4yh4rni1-7e2R20oTgXne_v1-uP56fGtXX6__Xn-QLZshyZoMsKTa9a_M31hND59JCbnJT0ntDyb3R_eIi9po559CWIzk95z5SWHl6yuSeUlq5c7XrJ6sfj23UkvfaNXKJeiXI9yKTyvhZ-zRXFFHSXK4VFHGdU7ljMf_wBSB9MvTgQAAA"], + ["simple colors","http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VTy05cMQz9l6y9iO3YSeYzqu6GWQxikFBRqShdVAi-HcdH9wJCKAu_j-3je59LLYfj0YS8nugo3YmbhmadZIxQ2AeJyOlEhVcqz0nCEhEXCmOlqFKfK0PLoVJp5TCpWOoeNUJfXuT2iFT68iIyvo3MbyMce3CINWN5vSr1qhRaCi9lTUo80iFbRDelbYptimdRjUXT7Jt_bMrc4d8b8a7tLXjvwXsTzi4SJChJIzESJ1l0k0zSSsqkQsGpNlJDTQ6kTtpJB-mkVqnx2luSZg7mV-LD7W324QY3jsAOEZRzCxkELzHTKzUt4SRQAk9C4JLS4AwYDQEY6bAGrJkFur6jdQjldKtkpSqiGEcD58iUb6U6gsDTwHsuf-9uLtfnx3J4evx3eQkvTh58ZEmrG8IyePscliEfDf2Y1hK_fW7ePAdsHZ8slmkznVbTMqxioMQ0YQyUGJg1UGJAsfH5CgZuvEKAYcfFHAy75djxyxUHlmMUx3U6Rumo7WD1-nx___D08_-fS_T7cf7963JTXt4AGJDm_8sDAAA"], + ["borda linear","http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSy04DMQz8lVXOOaztONnsZyBu2x5KuxIVhUWiokIIvh3HU4RKVeVge_zIeJLP0Idxmkg1kvA6Tkzm1eYRU6zJnGpI0fU6Bmq1A8XkMMXcNysxt5hliCy1eZp_vczmldYrYexjSGHUGNT9bNMs_f9YbbFMH6-OZYabmXozQ7YimSGvkFhicpjDGL5X4Xl5n7ttd1y63bzd7-bu9Lh0p_3L2yoEqwJvSjBgThnGeFIya6yaqY5y7xGT38p2C5vBGE4AbYyYwRguiAZE1RukvUujKeSwsHeKIAs6YnMmin5aaUYS86Se9yXXNPV_tef6RJeaNYivIbluTH5Durw-ZaeYCl4a66TqoPYeKZZRiKJYRiGKQluFKIopOvhNClFyDwNpM-MbQdqsWNeIZIzIYJDxLAUMCnoL5HzYHA7L8f7jdba_cLd5eZp34esHuhl9mhQDAAA"], + ["bimodal QuotaScore","http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA5VTQW4bMQz8iqEzEYiUKGn9i6K9OTk47aIt4GYN2zkUQfv2jjgFcgiCotgDR6RIjobcl5TT_nDQXKTbgxxMs_Q8QXXRMV1aVKy2QFm0VSAvYiVAhWfGLDtcYyJrojozaxMv0-ND6gS4KssywcCdEXnexDxao6haFNW_BdTQJ0eCmmgJ1EACdB4k6aRurYt6Zjt1n4GS9llSTXtdJHkcGu6avPlwuSOS5c2HyHg3srwbUeipMBo3oKrUcFvap9_36cPzdjt-_Lxd1t239XS-7i7r-bJe16fb7vH7j-3L8bT7etmez9e7-5SQxpdopeFTtNGAuFbYwXfqEm4DAXhNg4ehr8GwjlU6UafAsI51ngZPSySUHPx1iqkRKBa5pTBORgWVDpjWv79ZqDGV_Qo1xG5BpSouDVoNWQQbqEiY88agMVJsmCiGjOVKlbympFVfob3CQkYT1lCi_g_H2uKVtXODqEldwuk5Tk49nMo69XDq4ZyQU1nvjI14sFPZlmk4n2bcTlZpHluKXyU1lmhk0DjbTgaduZ0TeTyeTtvt08_zihWL1Uq__gB3Br3y1wMAAA"], + ["bimodal STV","http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA-VVW25bRwzdinC_6WLIeXsZbdAfJx-ydYsKcaTCVgsURbv2HpJDBWiRFQSCMLzDN4c8_GtL2-PTk9RCnOcnepI-gmqNuCWligSVU3D1jkfIGZWTBHd2aLBSg4m724Pu6GYF3FGUYnjrJpdwV1NQMoKbQmOWZWSG3Xl3xSqUpS33WWS5zwxqmMIEd5qx3kjcLNIWNm6WuFMNk2PVMF0ebdnjESnaHYfcDNUR1jwrOGjhs0cYIyKbPYI1QzVKoDUzos1VnlIXkfNi6Y3MJWMEp7p4nFDqLmoJ7zrNEvSmFrWANatFi-zEpCO5--PyelEVTnWppzDIdx_2TPAefqUuv8ygplJzorAq1kGYUaQpoqxc1w2EXeaBl9YD87L0wJGV3UmIpaU4lyVLBab78jWW83kPxzU0RA-7aM83pdoIquJRqz0qnnBRkoKrd1xDzqkpwR36fGa5a_BuL61OZ-nRTAnemspp55a-CKnBSyFvja4mRlidd0fs49OWc-tacy46hFYE61CbGm7a8WUlLasQEneqYXKq4PVry1qP7PSGl8wMrR6GWhS0hbsWEfQIyudX44SdT7SxAU9B72RrfjDKUEbeHhNtZXtEdlu1jwZZhPvfH4Q7OIn-9wNnfJMzv8lhoCHjYJPI1KnYtWyP2z8ft58-_Hx43_cv74fb9bC_7i-3w8vxcjqfjrf9_XDZj2_7m7Juv-6Hl_1yw9f1l8Px8Hz-cj0dXw9_XPXqdH6_vZ2ff7-dr5cfPm4b7HvKXPzwnLn5gQy54EQ-2oobT7sXhIprYYtYEKHgcENS_BKGMg43JN2_hn9NU8jJMmUtOxsji-nm7HwPKcPSE5r6e_ppSZoXwSuXvW8YnZepUCU0Nw1CA-NSoQmdjB4mxRodJNL1pegHlCOdtkyC6QPWYRqBgYMAnFiomQnAAmDPhQBkuRG2XR6UJ7qPChNWbYHHQsD80qigKwdhcIAMlQmDUzNh0dRKgCVAYB1UJ2FTNqaG4cwYS2qIF2PZMZmEldET9hFhvDu6vFCvpBtK4ZMUPIE5DAilkQmIPwCvWFvIdtAApOoOBrDSzJhqmgBh3YA0UQythi4NRWgUJOneKGSrKTXd5uQYN3WN4K-bRiEhKyyQ4TTbYtUNhD_W0Va8SXUSC38l5SuZvT2VLPZk5fts2NJseEt3CPVRL9Mua7Kv6mNeHTCqj3l1wKiOPNUBo3bnDev-6oDRkh9upYkfbqVp0TPCaG6guf_mgNXdf3fA6g4zz8fX1-vtw5-_7UDYH4-Xz_tp-_tfUbHscpUKAAA"], + ["bug new voter group not showing up in 1D+B","http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSW27DMAy7i7-FQW_HvUrQkxTb2SeLAbYiaAxEDiXRNJXX4PE4z6WU60mnCtPkvfEgObR2IkGqs3axIX4-acjuEVaS4C7JKkEbk0zb2OEk6c2p3VeYL1Iw2HgIDR8PphH9zuJUuq2qnZVhuq3KHB8z62NGuI8W6QqjSd6wtgopYeNHR-0gTqBOEqG0iFesk6PCalSLskCVZtZi0gqGnAMsGqsAGp34OvC1usG4Fcl2Rzph2r1myEOQFdMpdK1dnEiD03BzKe-Myv7hfFkhbcHucHl3Z0N6h-rY0-j_Ue59hv9J4K-Vb89EYWK-syU77umrwWBMHXcMuBVwK64_AqYHWGKiBCwBt5IR4HliegmWDMy2hCQMT_Qm5jWhYKJ3bpe_fwHDP9pRCQMAAA"], + + ["bug need to use districts","http://127.0.0.1:8000/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRu04EMQz8F9cuYsdONvkMRLfa4hBXcQIKGoT4dxwP14BW0Wr8iCcz3i8qNPd9KLdx8K5SuJcVmLNsGpHYYPVV81Uqx8Eka0a8cHwrV5rOVGkWJqM5mJymMLW4V_jfiYl-2tlOO-O0IyWflqVLlS1rihpUiQEc0AChQiww3qwBI6saZFFUSQuqANCoIfMcUNBoR7YhCxaNZZRUK6GkgqlCUAVTBVMNpl3496zLDePgrPAsrFzZom1JuwK5B3oPgnivK7Cctb_U1vI3Wcd_glwbqcOxQRcATDukOnbn2J3DtPfU6Vidw3QrABhuMNzA0jytLIENFA0KGtbeoaBjtmsyPV1ut7ePx8_3K016uLy-XJ_p-weEXJwVtAIAAA"], + ["primaries in districts","http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRvWqDMQx8F88arB_L9vcUHbp9ZEihQ-GDlpIOofTdK-uGEELwIMknn0-n31LLtu9cJ7HqiXa2QTzHyqYT97YyZ-LqpxMVXt1jkDAHIDJpyrrXslUqVjam0jL36BR6ONHbA6n0cAIZT5H5FOGanzJnh1Iny2tJFQxhbAiQxo4QQtgixrcrzLyVmpVw0krQSATQCAaUoNEIoJGOaqCa-UBryuFlDSegkm9VgUOQBtMe5q6zWh0gGBVDc9gWmykGyjWc8S2VW6ogW6klg93TG-itY1kQbBNLq1k1iG0Yu0Fsg9gG9xrGbh3YyJ8axvaKAPMcO3CY5w0bCiEOCocCh_EdCjredtj1dj6Oz8vr9eu9bOXl-Pk-Hx-Xa_n7B4FISwG4AgAA"], + ["not sure if primaries are working","http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRO2pEMQy8i2sV1sey_U6RIt1jiw2kCDxICJtiCbl7ZA1hIcviQmN9RtLou9Sy7TsbE7d-okCDeI6F3MO3kNRJk5eLK0lbyCeJzgAmxL484uMvPSh6XUCFup9OVHg1GYOEM1WCT5Zfy1apWNmYSkvskSl09yK3R6TS3YvIeBiZDyNcsylzZih1snRLTsEYjA0Go7HDxCBsYaMtr_9MtwRheIWTV4JHwoBHsKEEj4YBj3T8Bn4zC7TmPLy04QyoZK0q4phIg2mPk_1_q8yRCHaFAhwaKhk18th0UFyuGFqtrY1vUG5Q0WRBSzZ73NYcGR0XxVI2cdmav4aFGqRpkKZhoQaJG6RpHSkjORuk8QoDgR2HcrB4wxljEAeFYwLHcTom6KjtkPTlfBzvl-frx2vZytPx9Xk-3i7X8vMLSPfxwhQDAAA"], + ["again primaries","http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRu2oEMQz8F9cq9LK93q9IkW7Z4gIpDhYuhEtxhPx7ZE1xHMfhQpJHHo9Gv4XLum2DSZx32sQGifXIlCUynZlbZL7vVGQ292hhm4AOGjrvraxMxcsqVGrmLTqVnk709kCYnk4gy0tkvESE81OR7DDq5HmtqUIgTBwB0qQhhBDxiPHtDCNvlbNSSVoNGo0AGsWAGjQWATTaUS2oRj4wTjkyrZEETPOtGXAIsmDawuZ5ZmsDCEbD0BK2hdnFQTmHc7mnek8NZDP1ZPBHege9dywLgn1gaZxVhdiKsSvEVoitcK9i7NqBLflTxdiNEWBeww4azGsVGwohDRQNChqM71DQ8bbDro_TcVyu77evz7KWt-Pn-3Scr7fy9w_d2_x9twIAAA"], + ["highlight the problem","http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRMWoEMQz8i2sVlizJ631FinTLFhdIEVhICJfiCPl7ZM3BwR2HC8kz0ngk_5Za1m1zI7Zlp41bJ3aemfsVEx2BtYlpJVHbdyo825aFhGexyKAhE29lrVS0rEzFMveoFHo4UduDqfRwglmeMuMpwzUfZc6KRp00YUkXDGOsCLDGjhBGWCPGsxZhJCqhF6BwykrISATICAaUkGkRICMdtwW3kQ2tph2eq-EkmmRva-BhqIXSxnQ9s9hBQ7NhbI7FNdKgFbJzQOVbKre0QXCmmhp6_4Q68I4vg20d-LqaN4Nlw_CG4Q2WDTs0DG8dJUtqGob3ioAVOn7CoeKGfwojDgmHA8f6Oxx09HYs7e10HJ_n18vXe1nLy_HzfTo-zpfy9w8CkX4AxwIAAA"], + ["interesting, lots of middle voters","http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRy2oDMQz8F591sF72er-ih96WHFLoIbCQUtJDKP33yprDEkIwy0g78ngk_ZZa1m3rg_o40SbCRxBfREHN4ESFZ-VwYq-zoiIKQstaqVhZmYpn3KJU6OlEbQ-m0tMJZnnJjJcM13yUOSuUOln-lnTBMMYGgDVugDDCFhjPThgpJDUzCT0JEABkBA1KyGgAZKQjW5CNvKA17fAcDSehkndVwcOQhtLGlGeWNpBQVDQdWyAN0iA5mzM-QjlCxchnaKlgj_LW0oJ1LAuGDW17zcxh1tG2o22HWcf0HG17R8mSLznabhXAqdmwgwaV5thQGGmQaHDQRkKHg467HeP6OO_79fZ-__osa3nbf77P--V2L3__RaU4J7UCAAA"], + ["bug for strategies with irv","http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRu2oEMQz8F9cqrKfX-xkh3bLFhVyVI0mRJoT8e7Sag4MchzEja-TxSP5pva3bxrIQ99hpEx7EZhnxUBLhjJZJ0uVIWZJj7js1xjUhdj2YMMp9MNrWTs3aatS84shaobuVtSOZTncrmeUhMx8ynL1wAleF0iCrtJQLhjE2AKxxANIIp1_OZz1hVlZ6NSFcspIykgAZMSRTRhMgIwOnBadZF7SXHT5Gw0UolFQBMKSptDFd11EcoKGpaJtzcEr5Pc0gezRofAvlFqb8piVlVhr2_wmLasIGvgy2bVbSe50clh2WHZYdlh0zdDTvA9xSbzmajw7ACAM_ERhhOP4pjQQkAg4C4x9wMHB3CEALXk6Xy8fX8_fnua3t6fT-dn5tv3-Q54nAzQIAAA"], + ["good test example of districts and parties and yee","http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRQWoDMQz8StDZB0u25d19ROmhl7K7hxRyKCxNaZNDKO3bK2kKoYTggySPPBqPvijTNM8sY2LJa5p7TzyoJVJ7EimetWJ3w7omYm8eJHGrDogBjR0oNOVElSZO1CJXa5V0c6y3G5LTzTFkuIuMdxHOMZRdmpcS0xmCuCJAEitaTQBXizbOwxig5KiEURmNWACN4GNiNMWC4rKjGlCN8aDkEMpuCZiKxNtSgENQMaaZUxxvVYBgLPgsm11mP1VQ-l8rX1O5pgVkntZgqP_pK-hrx5IguI5YVo6qGTP9LPR8-FyWt93DcbcQ2TVsaLCh_e23IcDNBtY2xOQGGzQjcLQodqJg0RbK_WsKJxWKFIvoUNTxtsO-XiK87LfteHq6vB9M7uN2_thvr6cLff8CNBlSnccCAAA"], + ["press poll2. not working","http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VRu04EMQz8l9Qu4ley2c9AdNEWh7iKE1DQIMS_43juGk6naDW2x54dJz-lln3OIdTGQVO4Uq8rMCfeJCK2QeKr5qtUj4MKrxn2SvGtXMteqVjZeaPimbRoEbo70dyDqXR3gtkeMuMhw-GfAzg7lDpZliVdMJyxAWCNGyCMsAXGbz1gZFVCL4rCKSshIwGQEUMxZDQAMtKRbchGDmhNOxyG1soKNZVsVUUPTGmoTabrWc0NNHR1XJUktCxoS-kV8C2QWxDCU1dgOWv_pa2lD-t4L1i2kUWvmTkDsLjDqsOq4_4ci3sHt-W_HIu3CsDCDa_QcH3N8UZhpEGiwUHD1Xc46JjtAlAAHLycLpePr-fvz3PZy9Pp_e38Wn7_AN_rDtfEAgAA"], + ["nice example of lp solving","http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSwUpEMQz8l56LNGnSpO839PbYg8reFlbEi4h-u2mGxyKyPMoknXQ6yetXaWXbd5vV5qnuzHQLYkUU1ApOtdCqJG011sp72VotUrbyw1Rq0cxHVAVpAa3--4Lxu8y8y1C4pADKil5t7XHeR7BBAoAJGoBwQRIYdy6YqcIts_C8cQADIMOCkpDpAZBhQ-bIZh7oLb3QGgQl0TnP9g4ehnoo7VTzW6UDJBQ7Oo5R1x6kpOQK6Aj4CEJ05xVInpS_sjLyajH8EhgVtKstM4VJRbuKdhUmFVNTqKihxPMmRbujAShLBmY_oDIUvyWMDExswMGYCQYHhrOGMVkHwIHBgR2vx0D6bT5V1j5acgg6BB1mHGZc0qgrAH4ceu6AZevB4sFB5-X5crl-PH2-neMtP75e38_l-xcWvw90GQMAAA"], + ["proportional and distributed","http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3WSzU4DMQyE3yXnCMVJ_LN9DbitegDUWyUQ4oIQPDsTD6hSUbWHSWxn8tnZz9LKYd91qzLsWHfpo0r4WllDTNdqQ8wDqz4CsXY81iLrmGFrcyUUJdZXYpRDq2WWQ_nuUmrR3BvKkXRIq_8-ZOJmZruZEbALRLJiVF-xnvcJMWRSCCFGAYVMKO5UyJYuHWYIgvnQIZ1Cmz5ZApsBoU137oK7LQ-MliyyBiGZGD3PjsE8gQacdqm_3yo2puk52LPUjq4w3zJpu5qecln2yxL2e0-rOdNjXl8xLUGm84GIPdm8ttwpkZXN60gjJbJyhsrm1VkSLNn4yC2DJulpfAnjCE35SAAxWhgJjGedBM6zzqH5oJDASeB__5IzGVezcrYUNAwaBmGCMDETNJRCnqBfBGVh3Tl-P_o8PZ7PL-8PH68n_Nn3zy9vp_L1A26_XvU9AwAA"], + ["The colors. The colors.","http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA4VTwUpEMQz8l56DNGma5u1XePC27GEFD8KCInoQcb_dJKOouCDvMM3LdDJv2vfWetvt99KFmPVAexskMmPBwqQ9FzaJt3U4UOPksgiJjui4x6YtKR4US7IrrcTFxDM5Q4PDyek9ZCxXGvs8VzOmbvNzhuV4zo3sNX9R9YSNhFNLwhIrp5PRdp2atl0790ZtVmlhL3oroNOfJzoenXbm3HEWaRcYWzEigaKMfokT-3MaM8axoIQlVgAcsQHCEmtgGEiIKUxNelUSOhIgAMiIogqZEQAZWagc1VaU0WFjcL0dcDMGmnAzQmYf2eWTVEMTcmOrvJiEIuSm_Zt76UkKX074R0Yq_1PG_4O0HOpv-2oVny7cAaShyHTibCbCmMh0jgp6KigTgEznAsVr0txwkXq9NKgYIjWcjM36sozKIGFwYNi74GBxTVkCGAAcx8LlWF_3daHpX-eQ7_A5DjGHmMOIw4jjivgEwItDyx2Qlq5WXOte9e3xdHp4vnl9vIsf5_r08nQ83T-_tvcPBpctWgYEAAA"], + ["One art please.","http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA4VTzWqcMQx8F59Fsf5sffsUPeS27CGFHAILLSU9hNJ99kgaQlq6EHwYyxpL8431_R5znM5n0YN8XegsTDsSt5OsWQehxBGXCw0uJouQmGYmgpiP3HA4cZPDaBduJvbiqCWHizMn8VEd2PJe1M4lj7yOltOy2tRFrv4smzonvEi4aklqY-NSouM0adg4jdscNLzDlfIytxMm_bcyE5kZN64bN5Fxh3E0Q8SbovMeJ-9XN2a0Y0EISWwAKOIFSElsiSmgILswDZkdSdaRBAGgjBiiLKMJKCMbUSA6mqITMpT7VKFGFUmo0SxzTu9qFXUhiXJ6tF9MQmnysPnBvbeKwvcd_ssjk88p-nkja4X2r3xbbZ9tzADcMHjqeBuHGQ5PXdtoN1AcAE99gxLdyQ8M0uzDhSoLli68zPL-srJqocSCgoW7Gwo2d5ctAAXgOTaGY7_P60Yy3t-hzvA5gWKBYgEhASGBEQkHQEugVgSgJH3ZOdaz42-P1-v3l4fXH0_543y9_vr5eH1-eR1_3gCthqVSBAQAAA"], + ["Sort Order could use some work to avoid u turns","http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA41TzW6UMQx8l5wtFNv5cfYxELfVHoroiQo4cEGIPnvHnkW0Qkjok3YS_0wm4-zP1tvlet0hK25yNesSsxYqGo6VznNfHRcdWaUDSbdcdZfTcxEmJxv1dFEbuVK_N9o0WVU-h8Qu-oGqOnF0McvYdFl-u0nTVGRjUtLssrQODRxv1bLEdGWpt0uXNtqlPetq0mbtFwiQ3IAuf33IBDLo6Oh4tlW_1pAD9vam8lSl-evwqzQo8kBNyS5bcBmNjBvj1KeDQHUQWgB5OoAQoxN4KgwBF0QNjAoAjwHIY4NB8DiAPLa5C-5ONXivu2tapJVwKnJnnoocTFfM8t9fEiy28Bw_ZBYTlyEYEq4dgjeCIBpMcuyYfxv9j_-51ftZlDXsbdbLQbSK5xb64Mn4H31jlStj8y3Qh3EqODmfSQ8m3Zz0YNLNOQl0c27moi476ebqBLIsOrnIslKilehFikUFi_PcVLA5z20EJ3AKm-9i_362m8l4bXTGeaXoBBIGxQQfSIzSFJNAPUG-oKxIWe_g7aGwjw9PT1-_f_jx7RF_ovcPXz4_fmq_XgAvOJArFgQAAA"], + ["Minimax tie","http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA3VSu04EMQz8l9QWih3bSe4zEN3qikNcxQkoaBCCb8fxsNIJdNpi4tdkPJvPUsth26aQzyNtwpV6XQc14iFxYp0ktnK2UvV4pMJrRsSJ1VeLKE1bh6okba6WVg6VipZD-fZCxTL0mItaD6j074vKuFmZNyscG3AAZ0ejvnKS9zFUsAIggh0QKlgD404LmMkiQRZJCTIJEABoRNESNC0ANNIRDUQzB1pNLbx84Cw0CGpgahDUgmlj-v1Ws2McnA07M0lspVHWuvevgHdHViDXQbtu0-TSv1ep5zLa8Z8gX2GC1YwM0g0mWANAusFLgwnWURt5l8EErwBOTocBDgPc8LNCiIPCocBnQoeCjtkugAaAgg4FfX9THcWxe7ZyWGeAbIBsQMiAkKEpchgAWga4xgAsSXc9niB4Hk-Xy-v7w8fbOR73_enl-fxUvn4AXOcWQkgDAAA"], + ["not sure of strategy","http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA41TS2odQQy8S6-Faalbn3nHMNkNb-EQr2ISL7Ixxj57JBUPgoNDmEXpW11S97yOOS7nyTOIJa50snpaR1luxFPLOjLmq6zFtLtsZUgr6WkYOidxVKfM7NxSMXGS5WWFkjSbrTxgVpnEzZpOvquKlQ67Xmlwy9pZu6T8NS6Txh6X8b4HDW3XsihznpBz0Pz4ZS4y91e8M8enGc6dcEJpCDKvkPSBDBm8AVDBBkgZvBPzyBxj8NEskmQZlSSTBAGARzZKkmclgEccXsA7umHNVsu1Ce7EAtNaAChayXQy_eMrAkMLzlnYA5PQok1KRk5BR949cTZI3kDeZvbt1lAG3wy5GanilDJ2k-7_0bGtp9-Oq8W8G1vT2Z5iVsWsilkVW1MFYGvqyEWfr9iaTQB3peEODbs37bnzVQ8DhUGBHQ0OBY5eF8ACYNuO-_fbM3Qk48-FVhwjBV5V4CEERgqIiQ1PAdAT4AvIipJ15_luIezrw9PTz19fXp4f85-4f_jx_fHbePsNjmSW5M8DAAA"], + ["districts bug","http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA61VbW_aSBD-K4ulu4CwjN9fICS6a3NSv6SnNKd-ABQZswTrHBt5l7S5Hv3tN7OzNpjQ5ssJ0MzuzD7zzMsu3wzbGM9miWuGycKcuY5tRjYqfmA6sQua4yemG-BegFv2YmEaDp5xAtuEH649Y2ybhm-Mje9OaJhGoNYheIExAmGbrz5gicFifA8d46w1UVbXs8-bHSDugHAUvGdGuOeqwA7xcXwSxAaIKQF0HB8kBA9AJArFBTDYdAHMBeGSIBjXJxeA8UAQjBvRKqZVog54tuLiYEUcZfCIkOeRnQh5gDRzTP1B55DMhOklhGK6kJUPZl_BouI0itsoADzzUPHVWf8U2idoP6IOEV2fkg5stQqIakBJB5R0QFQDql1ASQcRucQqVkBJhzYJR2GGlHBIKGFAzQEiIUGExCBMlIiIQURnI5eER4IYRMQgaoYpImN8UqOIUooJMCbAmMjERCb2FdE4IEF8YsKLYxJIy4pg9ggnwYK7OItY8HJXFKhjzWNUCDAJ1NmEAJdpUVTy_mXL4TbcpeXffAUXIqtW_GaVy6q-518l3pO5sd6Vmcyrsr_KhazzTJrsCdwKk1Vb3BeDb_N5qb5S77Ape-AFV8fe83W6K6ToN97k-ZzWbFsVhYQ46L7kj3l5o88cQlEkfdScz-eGyKqao6KBCi5ZlqqYzSlLyPSRi5k6TIuFBT6rfJVKLlq2oxG7hzK8MLnhLN1u6-o5LQT7lT0C5pe8LHndO7ClimEcwn2uJK8_cWmB9-9ke68JtPwHbSxEkCoYJPsPrysVuY_MdSLrqu4f4rBq3UTU9ZVHPm0yLC-1m6UKc-wMXxVx1nov2HDadT-yTdqDe9L2DfmG_1P6VZ2CFIKOgWql2p5Vu1J-Vuu-ij6YHPwgIIwCupHW1wepzUeTkVVFVYMfuVlq2UbM16wZJkvkK75M6wE7zCA19l2qPNqtA02A1WHhaZp0HPQw4nTh72BUhiFZLsU2LVlWpEJML8QT5Hhx1fVGgrjz205WKNm0mZkUdv6EqR-wBrG5Az-ItbyiiqeC_YIjAS2AI0Lky4KP2eVoeXW5rM-Ep3ArmrJ3m7SWTYlkN4psHahdJk6kvnZNv009iFbBy0e5GZzDIbanVPaMF4J3IzcznE_tCcsvMZ4GhuVw2B3gZh6gK-g4yxdWvpqceDQcKOk8gwckGwyRyYVgKoOx4jd82PI646X8o6qfUnn0oOl7smCjdsgJ4HVKh_vR0fJ1v6fn699mvnRaV1O3mxSM5-1H9vnD7e3N3XWPfbxj9x9urnvXvXOFfZA5R51aamroweQE8Gh0YWgAD2eDIX3W5MCGrDtp57rz836eLzZxooKxDYwqvqeb_HHDhaT6m0xUlmX9FFBFHOHlesPnTZAlE_Kl4NML9W6MVeuVSjOhTg-Jf5k-8b-2MBadJFTtoD-fXsU5rfT_FumtVh0_b_pV1ESOHNtxbJ7J9hmotu6AqSduw3GBb7Coakn_QvRQW6LIM963TXfwI4QmcguiNfKvudzVpXZCSvu5Yez_A8WS20K7CwAA"], + ["LP Test kinda works sometimes","http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA61W227bRhD9lTWB1hJE0LxfJMtGm7hAXpwidpEHSTAoaiURoEmBu3bjpva3d3ZmlxJlJ34p7GBvZ8-cmTlc57vlWuPZLPPtOFvYM99z7cRVkzCyvdSHmRdmth-pvUhtuYuFbXnqjhe5tpd4ah1YY9e2QmtsvfiuZVsRrmNAwWECg2u_-oGTFE6sl9izcMd68SLrCJEhwg9cA_HTVxgPMvBg8DBOYCdqz0cFHgnzQhpIlhfTALq8EEZQEcGQIQvIH8OmD2Q-DD4NROOHBAGaAAai8RNapbTK8ELgohZPlcbDg8DHu0FA5yQoAKaZZ-sfBY7pmDiDjFhsH7IK4ThEWjXxzMQ3EyCeBWoS4t3wmDqMUUCYUKtIbkhJRy6uIpIaUdIRSY1IakS1iyjpKKGzFGNFlHTs0uAhZ0wdiKl0cUTNASExUcSkIM5wSEhBQncTKlYS0EAKElKQGFcldJge1SihlFIiTIkwJTEpiUlDFApOwoH0pMSXpjQoWU4CBiSezEMjzq2WFzKvNxUXc8tSBlUNiNTENCAj9ixCoozYl3lVNfL2acfhG7kpmpbDV1I0K361KmXT3vJvUn08c2v9UBeybOrBqhSyLQtps3uAVTZrdmpfDL_P5zX-Sr3DpuyOVxyvfeTr_KGSYmDQhHzMW7ZrqkpCHAVf8k1ZX-k7-1AUSV-15_O5JZRUNdFEFZesyDGmueUImW-4mOFlWiwcwKzKVS6hTEbt2Rm7hSo8MbnlLN_t2uYxrwT7lW2A8--yrnl7sldLBVNxiPexkby94dIB9O909lEL6PQPu1iKQWIwSPYf3jYYeaCU60TWTTvYx2HN2kTU9ZUHmC4ZVtYa5mBhDsHwixFnHXrBRtM-_OBs0l18ptmzEW_03-ff8BakEPUOqFbY9qJ5qOVXXA8w-nCyx0FAsIKC0WygL1KbD5xRNFXTAo5gDi67iOWaGTM5olzxZd4O2d6D1NgPOSK6rb1MoNVh4Zma9ADajMpd6t_-EA9GdHIudnnNiioXYnoq7iHH04s-WglUO789yEaNbGo8k8POn-D6ITOM5hv4QazlBVU8F-wXZQloAVwRolxWfMzOz5YX58v2jfAUbkUu-7DNW2lKJPtRZAegdtnKkfqzM_22tRGditcbuR2-xUNqj6U8M14J3o9sPFxO3Qkrz1U8TQzL0ahvYOMH6IoCzsqFU64mRwijgZIuC3hAiuFIKTkVDDMYo77R3Y63Ba_lH017n8uDB01_Jwt21pmcCF6ntP8-erNyPTjR_vrX-EundTH1-0mBPa8_s6-frq-vvlyesM9f2O2nq8uTy5O3CnsnS67m1FJbUw8nR4QH1gXTAJ_yBlPymcmBjVjfaW915-f9fLvYpIkKxrZgVfWebsvNlgtJ9beZaBzH-SkhRjxTH9c7mHdJlkzIp4pPT_HdGGPrcUqewNsj0l_n9_yvHdiilwTWDvpz8yrOcaX_t0jvterwedOvohZyAOzsaJ7J7hlodv6Q4RO35Wqh3mDRtJL-CtFD7YiqLPjAtf3hjxhM5I5EzwjfcvnQ1hqkJD3D_0ie_wOOHna90AsAAA"], + ["Test People","http://127.0.0.1:4000/ballot/sandbox/?v=2.5&m=H4sIAAAAAAAAA61V227bOBD9FUZANzYsKLpf7DjBbpsF-pIWbYo-2EYgy3QsQJEMkU6b7abfvjMcUrYSt3lZ2AaHnJkzZy6kf1iuNZ7NMt-Os4U98z3XTlwUwsj2Uh8kL8xsP8KzCI_cxcK2PPTxIteGH-4Da-zaVmiNrZ-uZVuR2sZgBLoEFtd-8QFNChrrZ-xZR7WZ0vqBe1ztAW8PFk_BB3aCZ74K7BEdL6SF2HgxLUDHC2GF4BEsmULxAQwOfQDzYfFpIRg_JBOACWAhGD-hXUq7TDkEruLiYUE8pQiIUBCQnggFgDTzbP1B45jUhBlkhGL7kFUI6lDBouAZwTcCAM8CFELlGz6HDmNFPkyoQUQ3pKQjV-0iohpR0lFAOqIaUe0iSjpKyCRVsSJKOnZp8ZRfTAnHgapnHKlUMImYIGJiEGdqSYhBQr6JTwsxSIhBQgwSM0wJKVNTIzyjdFICSwksJSIp9TANFck0ooW4pISVprQgJSeBuSOcDIuN8BkWu95VFcpY7xQFAswi5ZsRYJYcVt8-7MMyr6pG3jxuOdyRj9WuzatSPsJVKZoVv1qVsmlv-HeJF2hurXd1IcumHqxKIduykDa7B7PKZs0Wz8Xwx3xeq6_UJ2zKbnnFlds7vs53lRQDY02WD3nLtk1VSYiD5kt-V9ZX2mcfiiJpV3s-n1uiaFqOggaquGRFrmIaL0fI_I6LmXKmzcIBm1W5yiUXHduzM3YDlXhkcsNZvt22zUNeCfYHuwPMb2Vd8_Zkz5aKhnEI96GRvP3MpQPWf5HunSbQ8R92sRBBqmCQ7D-8bVTkATLXiaybdrCPw5q1iajrKw9sumRYWWszRxXm0Bi-KuKss16w0bRvfqCbdI5PJD0Z8ob_ff5deUEKUU9BtVJtL5pdLb-q_UBFH072dhAQRgHNSBpoR2rzwWQUTdW0YEdmjtp2Ecs1M8PkiHLFl3k7ZPsZpMa-zZVFd7SnCbA6LDxak56BHkacLvztlUoxIs252OY1K6pciOmpuIccTy_61kgQT_7cyQZXNjUzk8PJR5j6ITOI5g78ItbygiqeC_YGRwJaAC5ClMuKj9n52fLifNkeCU_hVjRlbzd5K02JZD-K7AyoXTZOpL52pt-2HkSn4vWd3AyP4RDb51SeGK8E70c2M1xO3QkrzzGeBobtaNQfYDMP0BU0nJULp1xNnlkYDpR0WcADUgxHyORUMJXBWPEb3W55W_Ba_t2097k8eND0PVmws27ICeBlSvv70ZPK9eBEz9e_Zr50WhdTv58UjOf1B_b1_fX11afLE_bhE7t5f3V5cnlyrLC3suQoU0ttDT2cPAM8GF0YGsDD2WBIn5kc2Ij1J-1Yd37fz-PFJk5UMLaBUcX3dFPebbiQVH-bicZxnN8CqohneLlesXkVZMmEfKz49FS9G2PVeiXSTCjvEfGv83v-ZQtj0UtC1Q768_lFnOeV_t8ivdaqw-dNv4qayIFhN47mmeyegWbrD5l64jYcN_gGi6aV9C9ED7UjqrLgA9f2h79CMJE7EC2Rfcvlrq21EVJ6mlvW038hZkQW1AsAAA"], + ["Running VSE","http://localhost:5500/sandbox/?v=2.5&m=H4sIAAAAAAAAA61WbW_aSBD-KxtLdwFhOX5_gZDqrs2d-iWtkpz6AVBkzAKWjI28S665Xvrbb3Zm10BCmy8nQDO7M_vMMy9e_M1yreFk4qWeHScze-L5ge37qKUZaKAErm97Yaa24sjO4tnMtjw8FIZgSNU6sIaubYXW0PruWrYV4TIGJ7AlIFz71QcsKVis70FsnbRmZHXd02YPiHsgPIQP7ETt-RjYIzpeSILYeDEJoOMpCcEjEBmi-AAWggAwH4RPgmD8kFwAJgBBMH5Cq5RWGR4IXOTiqYJ4aAiIUBCQnQgFgDTxbP1RzjGZCTPICMX2IasQzCHCKsUzim8UAJ4ESgnxbPgSOoyRfJhQg4huSElHLq4iohpR0hFRjSjpiGoXUdJRQrYUY0WUdOyS8PBATAnHVLo4ouYAkZggYmIQZygS6mFCZxOfRECCipUQg8QMU0LG1NRI7VE6KaWTElhKRFIikoZIMo1IEJeUsNKUhKLkJDB3hJN5OH9Tq-WFzOtVxcXUstRcquLHSgmMQuhZhEAZlTxLDlthHzYlS9WDMrWWu7qQZVP3FqWQbVlIm22aBa9s1mzVvuh_m05r_Eq9w8bsgVccj33gy3xXSdEz3uT5mLds21SV5F-lcp_zVVlf6zP7UBRJH7Wn06kliqblStFAFZesyDGmOeUIma-4mOBhWswc8FmUi1xCgQzbiwt2n1fVE5NrzvLttm0e80qwX9kKMP8u65q3Z3u2c3BtpIpDuI-N5O0dlw54_062D5pAx7_fxVIIEoNBsv_wtsHIPcVcJ7Js2t4-DmuWJqKurzzw6ZJhZa3dHCzMoTN8MeKk856xwfjY_cA26g4-k_ZsyBv-m_wrnoIUoiMD1QrbXjS7Wn7BdQ-j90d7PwgIo6DcSOvpg9Tmg8komqppwY_cHFx2EcslM8PkiHLB53nbZ_sZpMa-z9Gj29rTBFgdFi6n0ZGDHkY1Xeq3N6JhQJZLsc1rVlS5EONzsYEcz6-OvRVBtfPbTjZKsrGZmRx2PsPU95lBNM_AD2LNr6jiuWC_qJGAFsARIcp5xYfs8mJ-dTlvT4SncAuasvfrvJWmRPI4iuwcqF22mkj92Jl-23oQnYrXK7nun8Ihti-pPDNeCX4c2cxwOXZHrLxU8TQwLAeD4wE28wBdUY6TcuaUi9ELD8OBki4LuECK_kAxORcMMxgiv8HDlrcFr-UfTbvJ5cGFpp-TGbvohpwAXqe0fz6OtHLZO9Pz9a-ZL53W1dg_TgrG8-YT-_Lx5ub69t0Z-3TL7j9evzt7d3aqsA-y5Eqnltoauj96AXgwujA0gKdmgyn6zOTABux40k515-f9PF1s4kQFY2sYVXWfrsvVmgtJ9beZaBzH-SkgRrxQD9cbPm-CzJmQTxUfn-O9McTWo0ozgacHxL_ON_yvLYzFURJYO-jP3as4Lyv9v0V6q1WH15u-FTWRA8duHM012V0DzdbvM7zi1lwt1B0smlbSvxBd1I6oyoL3XNvv_wjBRO5AtEb-LZe7ttZOitIzvIuod2B6V_Fceoej2-T-acvhBeNztWvzqpRP4Ij_qH-2zW57C_9KzeaO84WA95MsjmfP_wHuegM6BAwAAA"], + // ["",""], + // ["",""], + ]], + // ["",""], + // ["",""], + // ["",""], + // ["",""], + +] + // [,""], + // [,""], +var c = "

    " +for (var [d,a] of e) { + c += "
  • " + c += d + c += "
      " + for (var [i,b] of a) { + c += "
    • " + // embed = '' + var matchString = "http://127.0.0.1:8000/" + var matchString = /http.*sandbox/ + var f = b.replace(matchString,"sandbox") + c += ' ' + i + ', ' + c += "
    • " + } + c += "
  • " +} +c += + "

" + + +document.querySelector("body").innerHTML += c +console.log("done") \ No newline at end of file diff --git a/testSwitcher.html b/testSwitcher.html new file mode 100644 index 00000000..d11d9059 --- /dev/null +++ b/testSwitcher.html @@ -0,0 +1,11 @@ +--- +permalink: /testSwitcher/ +layout: page-4 +title: Test Switcher +--- +
+ +
\ No newline at end of file diff --git a/try.html b/try.html new file mode 100644 index 00000000..fb51b421 --- /dev/null +++ b/try.html @@ -0,0 +1,70 @@ +--- +permalink: /try/ +layout: page-6 +title: To Build a Better Ballot - QAPR +description: an interactive guide to alternative voting methods +byline: By Paretoman +twuser: paretoman1 +--- + +
+ +

+ RRV +

+ +

+ RRV! +

+ +

+ RRV is similar to quota-approval except it uses a different quota. It is a quota that is derived from separate groups receiving mutually-exclusive seats. It is mathematically best in that case. +

+ +

+ RAV is just approval-style RRV +

+ +
+ +
+
+

+ Move the candidates and get equal representation for voters. +

+
+
+ +
+
+
+

+ The dark bar shows where voters are unrepresented... it's bad. We want candidates to cover each voter's quota of representation, AKA the dark bar. The quota is an amount of representation that everyone could have ideally if every voter were represented equally. I used a gradient-colored bar to show that it is more important to elect a candidate that represents the underrepresented. The goal is to cover all of the bar, starting with the darker parts of the bar. +

+

+ The quota approval method elects the candidate that reduces the quota the most. To do this, we use the remaining quota as a weight. A voter who is already represented has less quota left to count towards the next candidate to elect. Their vote was already counted. A candidate they liked was already seated. +

+

+ The light bars that cover each candidate's bars show votes that were already counted. We don't want to count these votes twice. So we wash out the color from that part. Then we count the votes that are still unrepresented. Most votes wins. +

+

+

+ +
+
+

UNSTRATEGIC BALLOT

+

Judge, don't choose.

+ +
+
+ +
+
+
+

+

Putting it all together, here's a sandbox for you to try out all the different systems and to make your own scenarios:

+