\
17 | ');
18 |
19 | new OpenDisclosure.Search({
20 | el : "#search"
21 | });
22 |
23 | if (this.collection.length > 0) {
24 | this.$('.data').html(this.collection.map(this.renderContribution));
25 | } else {
26 | this.$('.data').empty();
27 | }
28 |
29 | },
30 |
31 | renderContribution: function(c) {
32 | var contribution = new OpenDisclosure.Contribution(c.attributes);
33 |
34 | contribution.friendlyAmount = OpenDisclosure.friendlyMoney(contribution.attributes.amount);
35 |
36 | contribution.friendlyDate = moment(contribution.attributes.date).format("MMM-DD-YY");
37 |
38 | contribution.calculatedLink = contribution.recipientLinkPath();
39 |
40 | return this.template({ contribution: contribution });
41 | }
42 | });
43 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # This file was generated by the `rspec --init` command. Conventionally, all
2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3 | # The generated `.rspec` file contains `--require spec_helper` which will cause this
4 | # file to always be loaded, without a need to explicitly require it in any files.
5 | #
6 | # Given that it is always loaded, you are encouraged to keep this file as
7 | # light-weight as possible. Requiring heavyweight dependencies from this file
8 | # will add to the boot time of your test suite on EVERY test run, even for an
9 | # individual file that may not need all of that loaded. Instead, make a
10 | # separate helper file that requires this one and then use it only in the specs
11 | # that actually need it.
12 | #
13 | # The `.rspec` file also contains a few flags that are not defaults but that
14 | # users commonly want.
15 | #
16 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
17 | require 'rack/test'
18 | require 'factory_girl'
19 | require_relative '../backend/environment.rb'
20 |
21 | ENV['RACK_ENV'] = 'test'
22 |
23 | FactoryGirl.find_definitions
24 |
25 | RSpec.configure do |config|
26 | config.include FactoryGirl::Syntax::Methods
27 | config.include Rack::Test::Methods
28 |
29 | config.before(:suite) do
30 | ActiveRecord::Migration.verbose = false
31 |
32 | require_relative '../backend/schema.rb'
33 |
34 | FactoryGirl.create(:import)
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/backend/environment.rb:
--------------------------------------------------------------------------------
1 | $LOAD_PATH << '.'
2 |
3 | require 'active_record'
4 | require 'dotenv'
5 | require 'pg'
6 |
7 | Dotenv.load
8 |
9 | ENV['DATABASE_URL'] ||= "postgres://localhost/postgres"
10 |
11 | pg_stopped = `ps | grep postgres | grep -v grep`.empty?
12 | pg_stopped_message = <<-INFO
13 | PostgreSQL appears to not be running... If you installed it with homebrew you
14 | can start it with:
15 |
16 | postgres -D /usr/local/var/postgres
17 | INFO
18 |
19 | begin
20 | ActiveRecord::Base.establish_connection ENV['DATABASE_URL']
21 | ActiveRecord::Base.connection.verify!
22 | rescue PG::ConnectionBad => ex
23 | raise ex unless pg_stopped
24 | puts pg_stopped_message
25 |
26 | exit 1
27 | rescue ActiveRecord::AdapterNotSpecified
28 | url = "postgres://#{ENV['USER']}:[your password]@localhost/postgres"
29 | default_url = "postgres://#{ENV['USER']}@localhost/postgres"
30 |
31 | begin
32 | conn = ActiveRecord::Base.establish_connection default_url
33 | conn.connection
34 | url = default_url
35 | rescue PG::ConnectionBad => ex
36 | raise ex unless pg_stopped
37 | puts pg_stopped_message
38 |
39 | exit 1
40 | end
41 |
42 | puts "You need to run this command:"
43 | puts "echo DATABASE_URL=\"#{url}\" > .env"
44 |
45 | exit 1
46 | end
47 |
48 | Dir[File.dirname(__FILE__) + '/models/*.rb'].each { |f| require f }
49 | Dir[File.dirname(__FILE__) + '/fetchers/*.rb'].each { |f| require f }
50 | Dir[File.dirname(__FILE__) + '/downloaders/*.rb'].each { |f| require f }
51 |
--------------------------------------------------------------------------------
/netfile-etl/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014, Dave Guarino
2 |
3 | All rights reserved.
4 |
5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
6 |
7 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
8 |
9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
10 |
11 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
12 |
13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14 |
--------------------------------------------------------------------------------
/backend/models/contribution.rb:
--------------------------------------------------------------------------------
1 | class Contribution < ActiveRecord::Base
2 | self.inheritance_column = :_disabled
3 |
4 | belongs_to :contributor, class_name: 'Party', counter_cache: :contributions_count
5 | belongs_to :recipient, class_name: 'Party', counter_cache: :received_contributions_count
6 |
7 | before_save :set_self_contribution
8 |
9 | after_create :increment_oakland_contribution_count
10 | after_create :increment_small_contribution_ammount
11 | after_create :increment_self_contributions_total
12 |
13 | enum type: [:contribution, :loan, :inkind, :independent]
14 |
15 | def increment_oakland_contribution_count
16 | return unless contributor.from_oakland?
17 | recipient.received_contributions_from_oakland += amount
18 | recipient.save
19 | end
20 |
21 | def set_self_contribution
22 | self.self_contribution = contributor.name == recipient.short_name
23 | true
24 | end
25 |
26 | def increment_self_contributions_total
27 | # This is a bit tricky because the name of the person doesn't match the name
28 | # of the campaign, so there will be two Party instances with different IDs.
29 | # So I suppose we're stuck just string-comparing the names.
30 | return unless self_contribution
31 | recipient.self_contributions_total += amount
32 | recipient.save
33 | end
34 |
35 | def increment_small_contribution_ammount
36 | if amount.to_f < 100 && amount.to_f > -100
37 | recipient.small_contributions += amount.to_f
38 | recipient.save
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/spec/backend/fetchers/contribution_spec.rb:
--------------------------------------------------------------------------------
1 | describe DataFetcher::Contribution do
2 | before do
3 | ::Contribution.delete_all
4 | ::Party.delete_all
5 | end
6 |
7 | describe '.parse_row' do
8 | subject { described_class.new([row]).run! }
9 |
10 | context 'with a valid committe-to-committee contribution' do
11 | let(:row) { JSON.load(File.read('spec/samples/socrata_contribution_from_committee.json')) }
12 |
13 | it 'creates a Contribution record' do
14 | expect { subject }.to change { Contribution.count }.by(1)
15 | end
16 | end
17 |
18 | context 'when given a valid personal contribution from Socrata' do
19 | let(:row) { JSON.load(File.read('spec/samples/socrata_contribution_valid.json')) }
20 |
21 | it 'creates a party for the recipient' do
22 | subject
23 | expect(Party.where(committee_id: row['filer_id'])).to be_present
24 | end
25 |
26 | it 'creates a Contribution record with the right values' do
27 | subject
28 | expect(Contribution.first.amount).to equal(row['tran_amt1'].to_i)
29 | expect(Contribution.first.contributor).to be_present
30 | end
31 |
32 | context 'when the recipient exists already' do
33 | it "doesn't create a Party for the recipient" do
34 | ::Party::Committee.create(name: 'foo', committee_id: row['filer_id'])
35 |
36 | expect { subject }.not_to change { Party.where(committee_id: row['filer_id']).count }
37 | end
38 | end
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/backend/fetchers/payment.rb:
--------------------------------------------------------------------------------
1 | require 'backend/fetchers/base'
2 |
3 | class DataFetcher::Payment < DataFetcher::Base
4 | def parse_row(row)
5 | payer = get_filer(row)
6 |
7 | recipient =
8 | case row['entity_cd']
9 | when 'COM', 'SCC'
10 | # entity being paid is a Committee and Cmte_ID will be set. Same thing as
11 | # Filer_ID but some names disagree
12 | Party::Committee.where(committee_id: row['cmte_id'])
13 | .first_or_create(name: row['payee_naml'])
14 |
15 | when 'IND'
16 | # entity being paid is an Individual
17 | full_name = row.values_at('payee_naml', 'payee_namf', 'payee_nams')
18 | .join(' ')
19 | .strip
20 | Party::Individual.where(name: full_name,
21 | city: row['payee_city'],
22 | state: row['payee_state'],
23 | zip: row['payee_zip4'])
24 | .first_or_create
25 | when 'OTH'
26 | # payee is "Other"
27 | Party::Other.where(name: row['payee_naml'])
28 | .first_or_create(city: row['payee_city'],
29 | state: row['payee_state'],
30 | zip: row['payee_zip4'])
31 | end
32 |
33 | ::Payment.create(payer: payer,
34 | recipient: recipient,
35 | amount: row['amount'],
36 | code: row['expn_code'],
37 | date: row['expn_date'])
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/assets/js/views/_categoryView.js:
--------------------------------------------------------------------------------
1 | OpenDisclosure.CategoryView = Backbone.View.extend({
2 | initialize: function(options) {
3 | this.options = options;
4 | this.render();
5 | },
6 |
7 | render: function() {
8 | var attributes = this.options.attributes;
9 | this.$el.empty();
10 | this.$el.append('
4 | Any “person” may contribute up to $700* to each candidate per election cycle. “Broad-based political committees” may contribute up to $1400* to each candidate per election cycle ( Oakland Campaign Reform Act (OCRA) 3.12.050).
5 |
6 |
7 | *Candidates may accept this higher contribution limit ONLY IF they accept the OCRA expenditure ceilings by submitting the OCRA Form 301. If the candidate does not accept the expenditure ceilings then the limit for persons is $100 and the limit for broad-based political committees is $250.
8 |
9 |
10 | “Person” means an individual, proprietorship, firm, partnership, joint venture, syndicate, business, trust, company, corporation, association, committee, and any other organization or group of persons acting in concert (OCRA 3.12.040).
11 |
12 |
13 | ”Broad-based political committee” means a committee of persons which has been in existence for more than six months, receives contributions from one hundred (100) or more persons, and acting in concert makes contributions to five or more candidates (OCRA 3.12.040).
14 |
15 |
16 | For more information on Oakland’s campaign finance rules, please review the Oakland Campaign Reform Act (OCRA) and the Public Ethics Commission’s Guide to OCRA.
17 |
Percentage of total contributions that are small contributions*: {{summary.pctSmallContributions}}
51 |
Personal funds loaned and contributed to campaign: {{friendlyMoney self_contributions_total}}
52 |
% of the total amount raised is personal funds: {{summary.pctPersonalContributions}}
53 |
Declared candidacy: {{declared}}
54 |
Data last updated: {{lastUpdatedDate}}
55 |
* Candidates do not need to itemize contributions less than $100 by contributor, but do need to include all contributions in their total reported amount. FAQ
56 | {{else}}
57 |
The candidate had not reported any campaign contributions by the last filing deadline. Candidates are not required to report if they have raised less than $1,000.
Backbone-d3 currently supports simple pie charts with transitions.
74 | Changing values in a collection causes the pie chart to update to the
75 | new ratios. Adding a value adds a new segment to the pie chart, and
76 | removing a value removes that segment.
Warning! You are using an unsupported browser. ' +
88 | 'Please upgrade to Chrome, Firefox, or Internet Explorer version 9 or higher to view ' +
89 | 'charts on this site.
6 | Open Disclosure is a group of volunteer civic hackers passionate about open data, transparency and shining light on the funding that fuels Oakland’s electoral campaigns.
7 |
8 |
9 |
10 | Beginning in 2013 the Oakland Public Ethics Commission partnered with OpenOakland to form the Open Disclosure team for the purpose of ensuring government integrity and transparency in campaign activities by opening up campaign data. Disclosure of campaign finance empowers citizens and strengthens democracy because it keeps government accountable by revealing influential connections, the flow of money in politics, and potential issues with how campaign funds are raised or spent. Open Disclosure’s goal is to introduce a high standard of clarity and transparency to the disclosure of Oakland’s local campaign finance so that the public can understand how local campaigns are financed.
11 |
12 |
13 |
14 | Open Disclosure is a project of OpenOakland, a Code for America citizen brigade that works to improve the lives of Oaklanders by advancing civic innovation and open government through community partnerships, engaged volunteers, and civic technology. OpenDisclosure.io is our team’s site established to easily visualize and simplify campaign finance data for Oakland elections. We want to make this local campaign contribution and spending data easier to find, see, search and understand. We update this data as the local mayoral campaigns submit their latest campaign finance filings to NetFile, Oakland’s campaign finance disclosure database, according to filing deadlines set forth by the California Fair Political Practices Commission (FPPC). We hope Open Disclosure sheds some light on the dynamic financial foundation that fuels the campaigns of Oakland candidates. Please contact our team if you have any questions: oaklandopendisclosure@gmail.com.
15 |
16 |
17 |
18 | All development on OpenDisclosure happens publicly on Github. We invite any questions or suggestions for improvement–just create an issue on Github!
19 |
The numbers in this graph are calculated from a different data set than the contributions table above. For more details, please check the FAQ.
");
139 | }
140 | })
141 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # THIS VERSION IS DEPRECATED. SEE: https://github.com/caciviclab/odca-jekyll
2 |
3 | [](https://waffle.io/openoakland/opendisclosure)
4 | opendisclosure
5 | ==============
6 |
7 | ## Overview
8 |
9 | The goal of the project is to produce useful visualizations and statistics for Oakland's campaign finance data, starting with the November 2014 mayoral race.
10 |
11 | Meeting notes can be found in [this Google Doc](https://docs.google.com/document/d/11xji54-RiszyFBQnSBOI5Ylmzn2vC9glwAoU6A8CM_0/edit?pli=1#).
12 |
13 | ## To install the backend in a Vagrant virtual box, follow the instructions here:
14 | [Instructions for installing backend in Vagrant](/README_installation_in_vagrant.md)
15 |
16 | ## Running Locally
17 |
18 | To start, you'll need ruby installed.
19 |
20 | brew install rbenv
21 | brew install ruby-build
22 | rbenv install 2.1.2
23 |
24 | Then install bundler and foreman:
25 |
26 | gem install bundler
27 | gem install foreman
28 |
29 | Install postgres:
30 |
31 | ```bash
32 | brew install postgres
33 |
34 | # choose one:
35 | # A) to start postgres on startup:
36 | ln -sfv /usr/local/opt/postgresql/*.plist ~/Library/LaunchAgents
37 | launchctl load ~/Library/LaunchAgents/homebrew.mxcl.postgresql.plist
38 |
39 | # B) or, to run postgres in a terminal (you will have to leave it running)
40 | postgres -D /usr/local/var/postgres
41 |
42 | ARCHFLAGS="-arch x86_64" gem install pg
43 | ```
44 |
45 | Now you can install the other dependencies with:
46 |
47 | bundle install
48 |
49 | Create your postgresql user: (may be unnecessary, depending on how postgres is
50 | installed):
51 |
52 | sudo -upostgres createuser $USER -P
53 | # enter a password you're not terribly worried to share
54 | echo DATABASE_URL="postgres://$USER:[your password]@localhost/postgres" > .env
55 |
56 | You should be all set. Run the app like this:
57 |
58 | foreman start
59 |
60 | Then, to get a local copy of all the data:
61 |
62 | bundle exec ruby backend/load_data.rb
63 |
64 | ## Data Source
65 |
66 | The raw, original, separated-by-year data can be found on Oakland's "NetFile"
67 | site here: http://ssl.netfile.com/pub2/Default.aspx?aid=COAK
68 |
69 | We process that data in a nightly ETL process. Every day (or so) [this
70 | dataset][1] is updated with the latest version of the data. **There is a [data
71 | dictionary of what all the columns mean here][2].**
72 |
73 | ## Name mapping
74 |
75 | When we aggregate to find top contributors by company and employee, we use a mapping table to correct for spelling errors and different ways of representing the same entity. This is stored in backend/map.csv and gets loaded into the maps table during the data load process.
76 |
77 | Since there is no easy way to calculate when two entities are the same updating the maps table requires human intervention. Here are the steps to update the data:
78 |
79 | 1. load the most recent data (see above).
80 | 2. In your favorite Postgres interface run this query and export it:
81 | SELECT DISTINCT * FROM (
82 | SELECT 0, name, name FROM parties c, contributions
83 | WHERE contributor_id = c.id AND c.type <> 'Party::Individual'
84 | AND NOT name =ANY (SELECT Emp2 FROM maps)
85 | UNION ALL SELECT 0, employer, employer FROM parties c, contributions
86 | WHERE contributor_id = c.id AND c.type ='Party::Individual'
87 | AND NOT employer =ANY (SELECT Emp2 FROM maps)
88 | ) s
89 | 4. load map.csv and this new data into your favorite column oriented data processing tool
90 | e.g. Excel
91 | 5. sort on the Emp1 column
92 | 6. Search for rows that have 0 in the first column and see if they are equivalent
93 | to any near by entity. If they are, copy the value of Emp1 from that row
94 | to this one. If the entity is a union but "Union" in the type column.
95 | In some cases an equivalent entity might not sort near by, e.g:
96 | San Fransisco Bay Area Rapid Transit District : BART
97 | City of Oakland : Oakland, city of
98 | California Senate : State of CA Senate
99 | 7. Renumber the first column so all are unique. In Excel or equivalent you can
100 | set the first row to 1 and the second row to =A1+1 and copy that forumla to
101 | all the other rows.
102 |
103 | ## Deploying
104 |
105 | In order to deploy to production ([opendisclosure.io]) you will need a couple things:
106 |
107 | 1. A public-private SSH keypair (use the `ssh-keygen` command to make one)
108 | 2. A [Heroku](https://heroku.com) account. Make sure to associate it with your
109 | public key (`~/.ssh/id_rsa.pub`)
110 | 3. Permission for your Heroku account to deploy. You can get this from the
111 | current OpenDisclosure maintainers.
112 |
113 | Then, you can deploy via git:
114 |
115 | # first time setup:
116 | git remote add heroku git@heroku.com:opendisclosure.git
117 |
118 | # to deploy:
119 | git checkout master
120 | # highly recommended: run `git log` so you know what will be deployed. When
121 | # ready to deploy, run:
122 | git push heroku master
123 |
124 | Make sure to push changes back to this repository as well, so that heroku and
125 | this repository's master branch stay in-sync!
126 |
127 | [1]: https://data.oaklandnet.com/dataset/Campaign-Finance-FPPC-Form-460-Schedule-A-Monetary/3xq4-ermg
128 | [2]: https://data.sfgov.org/Ethics/Campaign-Finance-Data-Key/wygs-cc76
129 |
--------------------------------------------------------------------------------
/assets/js/vendor/topojson.v1.min.js:
--------------------------------------------------------------------------------
1 | !function(){function e(e,n){function t(n){var t=e.arcs[n],r=t[0],o=[0,0];return t.forEach(function(e){o[0]+=e[0],o[1]+=e[1]}),[r,o]}var r={},o={};n.forEach(function(e){var n,a,i=t(e),u=i[0],c=i[1];if(n=o[u])if(delete o[n.end],n.push(e),n.end=c,a=r[c]){delete r[a.start];var f=a===n?n:n.concat(a);r[f.start=n.start]=o[f.end=a.end]=f}else if(a=o[c]){delete r[a.start],delete o[a.end];var f=n.concat(a.map(function(e){return~e}).reverse());r[f.start=n.start]=o[f.end=a.start]=f}else r[n.start]=o[n.end]=n;else if(n=r[c])if(delete r[n.start],n.unshift(e),n.start=u,a=o[u]){delete o[a.end];var s=a===n?n:a.concat(n);r[s.start=a.start]=o[s.end=n.end]=s}else if(a=r[u]){delete r[a.start],delete o[a.end];var s=a.map(function(e){return~e}).reverse().concat(n);r[s.start=a.end]=o[s.end=n.end]=s}else r[n.start]=o[n.end]=n;else if(n=r[u])if(delete r[n.start],n.unshift(~e),n.start=c,a=o[c]){delete o[a.end];var s=a===n?n:a.concat(n);r[s.start=a.start]=o[s.end=n.end]=s}else if(a=r[c]){delete r[a.start],delete o[a.end];var s=a.map(function(e){return~e}).reverse().concat(n);r[s.start=a.end]=o[s.end=n.end]=s}else r[n.start]=o[n.end]=n;else if(n=o[c])if(delete o[n.end],n.push(~e),n.end=u,a=o[u]){delete r[a.start];var f=a===n?n:n.concat(a);r[f.start=n.start]=o[f.end=a.end]=f}else if(a=r[u]){delete r[a.start],delete o[a.end];var f=n.concat(a.map(function(e){return~e}).reverse());r[f.start=n.start]=o[f.end=a.start]=f}else r[n.start]=o[n.end]=n;else n=[e],r[n.start=u]=o[n.end=c]=n});var a=[];for(var i in o)a.push(o[i]);return a}function n(n,t,r){function a(e){0>e&&(e=~e),(l[e]||(l[e]=[])).push(s)}function i(e){e.forEach(a)}function u(e){e.forEach(i)}function c(e){"GeometryCollection"===e.type?e.geometries.forEach(c):e.type in d&&(s=e,d[e.type](e.arcs))}var f=[];if(arguments.length>1){var s,l=[],d={LineString:i,MultiLineString:u,Polygon:u,MultiPolygon:function(e){e.forEach(u)}};c(t),l.forEach(arguments.length<3?function(e,n){f.push(n)}:function(e,n){r(e[0],e[e.length-1])&&f.push(n)})}else for(var p=0,v=n.arcs.length;v>p;++p)f.push(p);return o(n,{type:"MultiLineString",arcs:e(n,f)})}function t(e,n){return"GeometryCollection"===n.type?{type:"FeatureCollection",features:n.geometries.map(function(n){return r(e,n)})}:r(e,n)}function r(e,n){var t={type:"Feature",id:n.id,properties:n.properties||{},geometry:o(e,n)};return null==n.id&&delete t.id,t}function o(e,n){function t(e,n){n.length&&n.pop();for(var t,r=s[0>e?~e:e],o=0,i=r.length;i>o;++o)n.push(t=r[o].slice()),f(t,o);0>e&&a(n,i)}function r(e){return e=e.slice(),f(e,0),e}function o(e){for(var n=[],r=0,o=e.length;o>r;++r)t(e[r],n);return n.length<2&&n.push(n[0].slice()),n}function i(e){for(var n=o(e);n.length<4;)n.push(n[0].slice());return n}function u(e){return e.map(i)}function c(e){var n=e.type;return"GeometryCollection"===n?{type:n,geometries:e.geometries.map(c)}:n in l?{type:n,coordinates:l[n](e)}:null}var f=d(e.transform),s=e.arcs,l={Point:function(e){return r(e.coordinates)},MultiPoint:function(e){return e.coordinates.map(r)},LineString:function(e){return o(e.arcs)},MultiLineString:function(e){return e.arcs.map(o)},Polygon:function(e){return u(e.arcs)},MultiPolygon:function(e){return e.arcs.map(u)}};return c(n)}function a(e,n){for(var t,r=e.length,o=r-n;o<--r;)t=e[o],e[o++]=e[r],e[r]=t}function i(e,n){for(var t=0,r=e.length;r>t;){var o=t+r>>>1;e[o]e&&(e=~e);var t=o[e];t?t.push(n):o[e]=[n]})}function t(e,t){e.forEach(function(e){n(e,t)})}function r(e,n){"GeometryCollection"===e.type?e.geometries.forEach(function(e){r(e,n)}):e.type in u&&u[e.type](e.arcs,n)}var o={},a=e.map(function(){return[]}),u={LineString:n,MultiLineString:t,Polygon:t,MultiPolygon:function(e,n){e.forEach(function(e){t(e,n)})}};e.forEach(r);for(var c in o)for(var f=o[c],s=f.length,l=0;s>l;++l)for(var d=l+1;s>d;++d){var p,v=f[l],h=f[d];(p=a[v])[c=i(p,h)]!==h&&p.splice(c,0,h),(p=a[h])[c=i(p,v)]!==v&&p.splice(c,0,v)}return a}function c(e,n){function t(e){i.remove(e),e[1][2]=n(e),i.push(e)}var r,o=d(e.transform),a=p(e.transform),i=l(s),u=0;for(n||(n=f),e.arcs.forEach(function(e){var t=[];e.forEach(o);for(var a=1,u=e.length-1;u>a;++a)r=e.slice(a-1,a+2),r[1][2]=n(r),t.push(r),i.push(r);e[0][2]=e[u][2]=1/0;for(var a=0,u=t.length;u>a;++a)r=t[a],r.previous=t[a-1],r.next=t[a+1]});r=i.pop();){var c=r.previous,v=r.next;r[1][2]0;){var r=(n+1>>1)-1,a=o[r];if(e(t,a)>=0)break;o[a.index=n]=a,o[t.index=n=r]=t}}function t(n){for(var t=o[n];;){var r=n+1<<1,a=r-1,i=n,u=o[i];if(ae;++e){var r=arguments[e];n(r.index=o.push(r)-1)}return o.length},r.pop=function(){var e=o[0],n=o.pop();return o.length&&(o[n.index=0]=n,t(0)),e},r.remove=function(r){var a=r.index,i=o.pop();return a!==o.length&&(o[i.index=a]=i,(e(i,r)<0?n:t)(a)),a},r}function d(e){if(!e)return v;var n,t,r=e.scale[0],o=e.scale[1],a=e.translate[0],i=e.translate[1];return function(e,u){u||(n=t=0),e[0]=(n+=e[0])*r+a,e[1]=(t+=e[1])*o+i}}function p(e){if(!e)return v;var n,t,r=e.scale[0],o=e.scale[1],a=e.translate[0],i=e.translate[1];return function(e,u){u||(n=t=0);var c=0|(e[0]-a)/r,f=0|(e[1]-i)/o;e[0]=c-n,e[1]=f-t,n=c,t=f}}function v(){}var h={version:"1.5.1",mesh:n,feature:t,neighbors:u,presimplify:c};"function"==typeof define&&define.amd?define(h):"object"==typeof module&&module.exports?module.exports=h:this.topojson=h}();
--------------------------------------------------------------------------------
/Vagrantfile:
--------------------------------------------------------------------------------
1 | # -*- mode: ruby -*-
2 | # vi: set ft=ruby :
3 |
4 | # Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
5 | VAGRANTFILE_API_VERSION = "2"
6 |
7 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
8 | # All Vagrant configuration is done here. The most common configuration
9 | # options are documented and commented below. For a complete reference,
10 | # please see the online documentation at vagrantup.com.
11 |
12 | # Every Vagrant virtual environment requires a box to build off of.
13 | config.vm.box = "https://cloud-images.ubuntu.com/vagrant/trusty/current/trusty-server-cloudimg-i386-vagrant-disk1.box"
14 |
15 | # Disable automatic box update checking. If you disable this, then
16 | # boxes will only be checked for updates when the user runs
17 | # `vagrant box outdated`. This is not recommended.
18 | # config.vm.box_check_update = false
19 |
20 | # Create a forwarded port mapping which allows access to a specific port
21 | # within the machine from a port on the host machine. In the example below,
22 | # accessing "localhost:8080" will access port 80 on the guest machine.
23 | # config.vm.network "forwarded_port", guest: 80, host: 8080
24 |
25 | # Create a private network, which allows host-only access to the machine
26 | # using a specific IP.
27 | # config.vm.network "private_network", ip: "192.168.33.10"
28 |
29 | # Create a public network, which generally matched to bridged network.
30 | # Bridged networks make the machine appear as another physical device on
31 | # your network.
32 | # config.vm.network "public_network"
33 |
34 | # If true, then any SSH connections made will enable agent forwarding.
35 | # Default value: false
36 | # config.ssh.forward_agent = true
37 |
38 | # Share an additional folder to the guest VM. The first argument is
39 | # the path on the host to the actual folder. The second argument is
40 | # the path on the guest to mount the folder. And the optional third
41 | # argument is a set of non-required options.
42 | # config.vm.synced_folder "../data", "/vagrant_data"
43 |
44 | # Provider-specific configuration so you can fine-tune various
45 | # backing providers for Vagrant. These expose provider-specific options.
46 | # Example for VirtualBox:
47 | #
48 | # config.vm.provider "virtualbox" do |vb|
49 | # # Don't boot with headless mode
50 | # vb.gui = true
51 | #
52 | # # Use VBoxManage to customize the VM. For example to change memory:
53 | # vb.customize ["modifyvm", :id, "--memory", "1024"]
54 | # end
55 | #
56 | # View the documentation for the provider you're using for more
57 | # information on available options.
58 |
59 | # Enable provisioning with CFEngine. CFEngine Community packages are
60 | # automatically installed. For example, configure the host as a
61 | # policy server and optionally a policy file to run:
62 | #
63 | # config.vm.provision "cfengine" do |cf|
64 | # cf.am_policy_hub = true
65 | # # cf.run_file = "motd.cf"
66 | # end
67 | #
68 | # You can also configure and bootstrap a client to an existing
69 | # policy server:
70 | #
71 | # config.vm.provision "cfengine" do |cf|
72 | # cf.policy_server_address = "10.0.2.15"
73 | # end
74 |
75 | # Enable provisioning with Puppet stand alone. Puppet manifests
76 | # are contained in a directory path relative to this Vagrantfile.
77 | # You will need to create the manifests directory and a manifest in
78 | # the file default.pp in the manifests_path directory.
79 | #
80 | # config.vm.provision "puppet" do |puppet|
81 | # puppet.manifests_path = "manifests"
82 | # puppet.manifest_file = "site.pp"
83 | # end
84 |
85 | # Enable provisioning with chef solo, specifying a cookbooks path, roles
86 | # path, and data_bags path (all relative to this Vagrantfile), and adding
87 | # some recipes and/or roles.
88 | #
89 | # config.vm.provision "chef_solo" do |chef|
90 | # chef.cookbooks_path = "../my-recipes/cookbooks"
91 | # chef.roles_path = "../my-recipes/roles"
92 | # chef.data_bags_path = "../my-recipes/data_bags"
93 | # chef.add_recipe "mysql"
94 | # chef.add_role "web"
95 | #
96 | # # You may also specify custom JSON attributes:
97 | # chef.json = { mysql_password: "foo" }
98 | # end
99 |
100 | # Enable provisioning with chef server, specifying the chef server URL,
101 | # and the path to the validation key (relative to this Vagrantfile).
102 | #
103 | # The Opscode Platform uses HTTPS. Substitute your organization for
104 | # ORGNAME in the URL and validation key.
105 | #
106 | # If you have your own Chef Server, use the appropriate URL, which may be
107 | # HTTP instead of HTTPS depending on your configuration. Also change the
108 | # validation key to validation.pem.
109 | #
110 | # config.vm.provision "chef_client" do |chef|
111 | # chef.chef_server_url = "https://api.opscode.com/organizations/ORGNAME"
112 | # chef.validation_key_path = "ORGNAME-validator.pem"
113 | # end
114 | #
115 | # If you're using the Opscode platform, your validator client is
116 | # ORGNAME-validator, replacing ORGNAME with your organization name.
117 | #
118 | # If you have your own Chef Server, the default validation client name is
119 | # chef-validator, unless you changed the configuration.
120 | #
121 | # chef.validation_client_name = "ORGNAME-validator"
122 |
123 | config.vm.network :forwarded_port, host: 5000, guest: 5000
124 | config.vm.provider :virtualbox do |virtualbox|
125 | # allocate 1024 mb RAM
126 | virtualbox.customize ["modifyvm", :id, "--memory", "2024"]
127 | end
128 |
129 | end
130 |
--------------------------------------------------------------------------------
/backend/2014_Lobbyist_Directory.csv:
--------------------------------------------------------------------------------
1 | Lobbyist ID,Lobbyist,Lobbyist_Firm,Lobbyist_Phone ,Lobbyist_Email,Lobbyist_Business_Address,Lobbyist_City,Lobbyist_State,Lobbyist_Zip,Client 1,Client 2,Client 3,Client 4,Client 5,Client 6,Client 7
2 | 1,"Aroner, Dion",AJE Partners,510-849-4811,diona@ajepartners.com,"1803 6th St, #B",Berkeley,CA,94710,Safeway,Waste Management of Alameda County,Redflex Traffic Systems (California) Inc.,,,,
3 | 2,"Chen, Serena",,510-982-3191,serena.chen@lung.org,424 Pendleton Way,Oakland,CA,94621,American Lung Association in California,,,,,,
4 | 3,"Clemens, Alex",,415-364-0000,clemens@barcoast.com,38 Mason St ,San Francisco,CA,94102,"Car2Go N.A., LLC",,,,,,
5 | 4,"Coleman, Michon",,510-752-2004,michon.a.coleman@kp.org,"4501 Broadway, 2nd Floor",Oakland,CA,94611,"Kaiser Foundation Health Plan, Inc. ",,,,,,
6 | 5,"Crowley, Michael",,510-638-4900,rhorning@oaklandathletics.com,7000 Coliseum Way,Oakland,CA,94621,Athletics Investment Group LLC,,,,,,
7 | 6,"Custar, Kristin",The Jordan Company L.P.,212-572-0800,info@olsonhagel.com,"399 Park Avenue, 30th Floor",New York,NY,10022,,,,,,,
8 | 7,"Dada, Suhail",,949-720-4518,suhail.dada@pimco.com,"840 Newport Center Drive, Suite 100",Newport Beach,CA,92660,Pacific Investment Management Company LLC,,,,,,
9 | 8,"De Luca, Niccolo",,510-835-9050,noeluca@townsenopa.com,"300 Frank Ogawa Plaza, Suite 204",Oakland,CA,94612,Oakland Museum,Oakland Zoo,,,,,
10 | 9,"Edwards, Merlin",Edwards Consulting,510-635-4669,mek011@pacbell.net,P.O. Box 1072,El Cerrito,CA,94530,Mass Mutual,Urban Core LLC,,,,,
11 | 10,"Gonzalez, Javier",,408-416-6344,jgonzalez@calrest.org,"621 Capitol Mall, Suite 2000 ",Sacramento,CA,95814,California Restaurant Association ,,,,,,
12 | 11,"Gray, Kevin",,949-720-4871,kevin.gray@pimco.com,"840 Newport Center Drive, Suite 100",Newport Beach,CA,92660,Pacific Investment Management Company LLC,,,,,,
13 | 12,"Guarino, Tom",,510-437-2552,tgg3@pge.com,"1330 Broadway, Suite 1535",Oakland,CA,94612,Pacific Gas and Electric Company,,,,,,
14 | 13,"Horning, Ryan",,510-638-4900,rhorning@oaklandathletics.com,7000 Coliseum Way,Oakland,CA,94621,Athletics Investment Group LLC,,,,,,
15 | 14,"Jewel, Elisabeth",AJE Partners,510-849-4811,elisabeth@ajepartners.com,"1803 6th St, #B",Berkeley,CA,94710,AT&T,Safeway,Redflex Traffic Systems (California) Inc.,,,,
16 | 15,"Kingsley, Daniel",SKS Investments,415-421-8200,dkingsley@sksinvestments.com,"601 California St, Suite 1310",San Francisco,CA,94108,SKS Broadway LLC,,,,,,
17 | 16,"Kraetesch, Neil",,510-638-4900,nkraetsch@athletics.com,700 Coliseum Way,Oakland,CA,94621,Athletics Investment Group LLC,,,,,,
18 | 17,"Linney, Douglas",The Next Generation,510-444-4710 x309,dklinney@nextgeneration.org,"1814 Franklin St, Suite 510",Oakland,CA,94612,American Promotional Events West,,,,,,
19 | 18,"Madrid, Michael",Grass Roots Lab,916-704-4569,madrid@grassrootslab.com,"1029 J Street, Suite 100",Sacramento,CA,95814,ecoATM,,,,,,
20 | 19,"McClure, Mark",California Capital & Investment Group,510-463-6338,mmcclure@californiagroup.com,"300 Frank Ogawa Plaza, Suite 340",Oakland,CA,94612,Foster Media,California Capital & Investment Group,Oakland Global Rail Enterprise,,,,
21 | 20,"McConnell, Gregory",The McConnell Group,510-834-0400,gmc@themcconnellgroup.com,"350 Frank Ogawa Plaza, #703",Oakland,CA,94612,Jobs and Housing Coalition,LAZ Parking,Feld Entertainment,PG&E,Xerox,,
22 | 21,"McPoyle, Nancy",,415-389-6800,nancy.mcpoyle@oracle.com,500 Oracle Parkway,Redwood Shores,CA,94070,"Oracle America, Inc.",,,,,,
23 | 22,"Mersten, David",,858-766-7650,dmersten@ecoatm.com,10515 Vista Sorrento Parkway,San Diego,CA,92121,ecoATM,,,,,,
24 | 23,"Moffatt, John",Nielsen Merksamer,916-446-6752,jmoffatt@nmgovlaw.com,"1415 L St., Suite 1200",Sacramento,CA,95814,ecoATM,,,,,,
25 | 24,"Muehlethaler, Jeff",,213-739-3720,jeff.muehlethaler@pimco.com,1633 Broadway,New York,NY,10019,Pacific Investment Management Company LLC,,,,,,
26 | 25,"Neal, Kathy",,510-430-1252,kathy@kneal.com,6114 La Salle Ave #641 ,Oakland,CA,94611,"Kneal Resource System, Inc ",Dailey-Wells,,,,,
27 | 26,"Revell, Dennis",Revell Communications,916-443-3816,DCR@revellcommunications.com,"1 Capitol Mall, Suite 210",Sacramento,CA,95814,"American Promotional Events, Inc.",,,,,,
28 | 27,"Reynolds, Jessica",Reynolds Strategies,510-450-0295,jessica@reynolds-strategies.com,645 Mariposa Ave,Oakland,CA,94610,Oakland Association of Realtors,,,,,,
29 | 28,"Romano, Mark",,949-720-6010,mark.romano@pimco.com,"840 Newport Center Drive, Suite 100",Newport Beach,CA,92660,Pacific Investment Management Company LLC,,,,,,
30 | 29,"Spees, Richard",,510-227-5600,rlspees@msn.com,2431 Mariner Sq Dr.,Alamaeda,CA,94501,St. John's Church,,,,,,
31 | 30,"Stein, Paul",SKS Investments,415-421-8200,pstein@sksinvestments.com,"601 California St, Suite 1310",San Francisco,CA,94108,SKS Broadway LLC,,,,,,
32 | 31,"Tucker, David",,510-613-2142,dtucker2@wm.com,172 98th Ave.,Oakland,CA,94603,Waste Management of Alameda County,,,,,,
33 | 32,"Turner, Ronnie",Turner Development Resource Group,510-395-2766,rtdevelops@comcast.net,"4100 Redwood Rd, #170",Oakland,CA,94619,"UrbanCore Development, LLC",,,,,,
34 | 33,"Urbina, Geoff",,206-684-6259,geoff.urbina@key.com,"1301 5th Avenue, 25th Floor",Seattle,WA,98101,KeyBanc Capital markets Inc.,,,,,,
35 | 34,"Villegas, Peter",,213-621-8406,peter.villegas@jpmchase.com,"300 S. Grand Ave., Suite 400",Los Angeles,CA,90071,JPMorgan Chase & Co.,,,,,,
36 | 35,"Wallace, Juan Carlos", SKS Investments,415-421-8200,jcwallace@sksinvestments.com,"601 California St, Suite 1310",San Francisco,CA,94108,SKS Broadway LLC,,,,,,
37 | 36,"Welch, Sean",Nielsen Merksamer,415-389-6800,swelch@nmgovlaw.com,"2350 Kerner Blvd., Suite 250",San Rafael,CA,94901,ecoATM,,,,,,
38 | 37,"Wolff, Lewis",,510-638-4900,rhorning@oaklandathletics.com,7000 Coliseum Way,Oakland,CA,94621,Athletics Investment Group LLC,,,,,,
39 | 38,"Wolmark, Steven",SKS Investments,415-421-8200,swolmark@sksinvestments.com,"601 California St, Suite 1310",San Francisco,CA,94108,SKS Broadway LLC,,,,,,
40 |
--------------------------------------------------------------------------------
/neo4j/README.md:
--------------------------------------------------------------------------------
1 | Disclosure Data in Neo4j
2 | ========================
3 |
4 | ## Warning!
5 |
6 | The Neo4j part of the project, while cool and promising, is not currently part
7 | of the project nor something the OpenDisclosure team is currently developing.
8 | This folder is here for historical purposes and in case anyone happens upon
9 | this project who is passionate about graph databases and wants to play around
10 | with this stuff.
11 |
12 | ## Overview
13 |
14 | Importing in the [Neo4j Graph Database](http://neo4j.org) allows for analysis and querying of this connected data. Neo4j can then be used to answer questions of interest, and provide data for visulatizations.
15 |
16 | ## Importing data
17 |
18 | The raw data from Oakland's "NetFile" site (http://ssl.netfile.com/pub2/Default.aspx?aid=COAK) needs the A-Contributions and E-Expenditures sheets saved to CSV. The [import.cyp](import.cyp) will then load these into Neo4j (it assumes they are saved in /tmp).
19 |
20 | ```
21 | curl -b /dev/null -L -o - "https://docs.google.com/spreadsheet/ccc?key=0AgAZooSCCSN0dGUwVXdHQzlXamwxSEVtcTVzMHNZN1E&output=csv&gid=0" > /tmp/A-Contributions.csv
22 | curl -b /dev/null -L -o - "https://docs.google.com/spreadsheet/ccc?key=0AgAZooSCCSN0dGUwVXdHQzlXamwxSEVtcTVzMHNZN1E&output=csv&gid=8" > /tmp/E-Expenditure.csv
23 |
24 | wget http://dist.neo4j.org/neo4j-community-2.1.0-M01-unix.tar.gz
25 | tar xzf neo4j-community-2.1.0-M01-unix.tar.gz
26 | cd neo4j-community-2.1.0-M01
27 | ./bin/neo4j start
28 | ./bin/neo4j-shell -file import.cyp
29 | ```
30 |
31 | ## Visualizing
32 |
33 | Using the Neo4j browser (http://localhost:7474), data can be visualized by running queries in the [Cypher query language](http://cypherlang.org).
34 |
35 | ```
36 | // Candidates and contributors who provided over $700
37 | MATCH (f:Candidate) // Find all the candidates
38 | OPTIONAL MATCH (f)<-[n:CONTRIBUTED]-(c) WHERE n.amount > 700 // Find the contributors giving > $700
39 | OPTIONAL MATCH (c)-[:LOCATION|EMPLOYER|WORKS_AS]->(l) // Match additional data abount contributors for visualization
40 | RETURN f, c, l
41 | ```
42 |
43 | 
44 |
45 | ## Querying data
46 |
47 | Using the [Cypher query language](http://cypherlang.org), the following questions can be answered:
48 |
49 | [1. Who are the top 5-10 contributors to each campaign? (people or company)](https://github.com/openoakland/opendisclosure/issues/3)
50 |
51 | ```
52 | MATCH (f:Candidate)
53 | OPTIONAL MATCH (f)<-[n:CONTRIBUTED]-(c)
54 | WITH f, c, sum(n.amount) AS amount ORDER BY amount DESC
55 | RETURN f.name AS candidate, collect({name: c.name, amount: amount})[0..10] AS contributors
56 | ```
57 | | candidate | contributors
58 | |-----------------------------|-----------------------------------------
59 | |Patrick McCullough Mayor 2014| [{name:"Patrick McCullough",amount:100}]
60 | |Re-Elect Mayor Quan 2014 | [{name:"Sprinkler Fitters & Apprentices Local 483 PAC, id#1298012",amount:1400},{name:"IBEW PAC Educational Fund",amount:1400},{name:"IUPAT-Political Action Together-Political Committee",amount:1400},{name:"Electrical Workers Local 595 PAC",amount:1300},{name:"Steven Douglas",amount:700},{name:"ARCALA Land Company",amount:700},{name:"Conway Jones, Jr.",amount:700},{name:"Annie Tsai",amount:700},{name:"Ronald Herron",amount:700},{name:"Lailan Huen",amount:700}]"
61 | |Parker for Oakland Mayor 2014| [{name:"Scott Taylor",amount:700},{name:"Larry Williams",amount:700},{name:"Terrence McGrath",amount:700},{name:"Joseph Whitehouse",amount:700},{name:"Pamela Lathan",amount:700},{name:"Rob Davenport",amount:700},{name:"Ed Page",amount:700},{name:"Nneka Rimmer",amount:700},{name:"John Lewis",amount:700},{name:"Mobile Connectory LLC",amount:700}]
62 | Libby Schaaf for Oakland Mayor 2014|[{name:"Sal Fahey",amount:1000},{name:"John Protopappas",amount:700},{name:"Joan Story",amount:700},{name:"Richard Schaaf",amount:700},{name:"Antioch Street Limited",amount:700},{name:"Lang Scoble",amount:700},{name:"Paul Weinstein",amount:700},{name:"Bradley Brownlow",amount:700},{name:"Jerrold Kram",amount:700},{name:"Julian Beasley",amount:700}]
63 | |Joe Tuman for Mayor 2014 | [{name:"James and Darcy Diamantine",amount:1400},{name:"Tod & Jen Vedock",amount:1400},{name:"John and Alanna Dittoe",amount:1400},{name:"Bill/Warrine Young/Coffey",amount:1400},{name:"Mr and Mrs EM Edward Downer",amount:1400},{name:"Mark & Susan Stutzman",amount:1400},{name:"Leonard & Silvia Silvani",amount:1400},{name:"Bradford & Barbara Dickason",amount:1400},{name:"Robert & Ann Spears",amount:1400},{name:"Patricia & Tony Theophilos",amount:1400}]
64 |
65 | [2. Which industries support each candidate? (top 5 industries, aggregate amount given from this industry to each candidate, percentage that this contribution makes in the committee’s entire fundraising efforts for this reporting period)](https://github.com/openoakland/opendisclosure/issues/4)
66 |
67 | _The import data does not yet contain industry information (although there is Occupation?)_
68 |
69 | [3. Bar graph showing how much campaign committee has raised so far versus how much that committee has spent in expenditures on the campaign.](https://github.com/openoakland/opendisclosure/issues/5)
70 |
71 | ```
72 | MATCH (f:Candidate)
73 | OPTIONAL MATCH (f)<-[n:CONTRIBUTED]-()
74 | WITH f, sum(n.amount) AS received
75 | OPTIONAL MATCH (f)-[n:PAYED]->()
76 | WITH f, received, sum(n.amount) AS spent
77 | RETURN f.name AS candidate, spent, received, received - spent AS balance ORDER BY balance DESC
78 | ```
79 | candidate|spent|received|balance
80 | ---------|-----|--------|-------
81 | Parker for Oakland Mayor 2014|33783|166884|133101
82 | Joe Tuman for Mayor 2014|19119|140100|120981
83 | Libby Schaaf for Oakland Mayor 2014|4293|117795|113502
84 | Re-Elect Mayor Quan 2014|39215|121522|82307
85 | Patrick McCullough Mayor 2014|0|100|100
86 |
87 | [4. What percentage of campaign contributions to each mayoral candidate are made from Oakland residents vs. others?](https://github.com/openoakland/opendisclosure/issues/6)
88 |
89 | ```
90 | MATCH (f:Candidate)
91 | OPTIONAL MATCH (f)<-[n:CONTRIBUTED]-(c)
92 | WHERE (c)-[:LOCATION]->({name:'OAKLAND', state:'CA'})
93 | WITH f, sum(n.amount) AS oakContributions
94 | OPTIONAL MATCH (f)<-[n:CONTRIBUTED]-(c)
95 | WITH f, oakContributions, sum(n.amount) as total
96 | RETURN f.name AS candidate, round((toFloat(oakContributions) / total) * 100) AS percentage ORDER BY percentage DESC
97 | ```
98 |
99 | candidate|percentage
100 | ---------|----------
101 | Patrick McCullough Mayor 2014|100
102 | Joe Tuman for Mayor 2014|67
103 | Libby Schaaf for Oakland Mayor 2014|53
104 | Re-Elect Mayor Quan 2014|51
105 | Parker for Oakland Mayor 2014|35
106 |
107 | [5. Evaluate any overlap between corporations and industries that employ and register a lobbyist with the City of Oakland and campaign contribution and expenditure data.](https://github.com/openoakland/opendisclosure/issues/7)
108 |
109 | _The import data does not yet contain industry information_
110 |
--------------------------------------------------------------------------------
/assets/js/vendor/GoogleChart.js:
--------------------------------------------------------------------------------
1 | // Generated by CoffeeScript 1.7.1
2 | (function() {
3 | var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
4 | __hasProp = {}.hasOwnProperty,
5 | __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
6 |
7 | Backbone.GoogleChart = (function(_super) {
8 | __extends(GoogleChart, _super);
9 |
10 | function GoogleChart() {
11 | this._listers = __bind(this._listers, this);
12 | this._removeGoogleListener = __bind(this._removeGoogleListener, this);
13 | this._addGoogleListener = __bind(this._addGoogleListener, this);
14 | this.unbind = __bind(this.unbind, this);
15 | this.bind = __bind(this.bind, this);
16 | this.render = __bind(this.render, this);
17 | this.id = __bind(this.id, this);
18 | this.onGoogleLoad = __bind(this.onGoogleLoad, this);
19 | return GoogleChart.__super__.constructor.apply(this, arguments);
20 | }
21 |
22 |
23 | /*
24 | * Initialize a new GoogleChart object
25 | *
26 | * Example:
27 | * lineChart = new Backbone.GoogleChart({
28 | * chartType: 'ColumnChart',
29 | * dataTable: [['Germany', 'USA', 'Brazil', 'Canada', 'France', 'RU'],
30 | * [700, 300, 400, 500, 600, 800]],
31 | * options: {'title': 'Countries'},
32 | * });
33 | *
34 | * $('body').append( lineChart.render().el );
35 | *
36 | * For the complete list of options please checkout
37 | * https://developers.google.com/chart/interactive/docs/reference#chartwrapperobject
38 | *
39 | */
40 |
41 | GoogleChart.prototype.initialize = function(options) {
42 | var chartOptions;
43 | chartOptions = _.extend({}, options);
44 | _(['el', 'id', 'attributes', 'className', 'tagName']).map(function(k) {
45 | return delete chartOptions[k];
46 | });
47 | google.load('visualization', '1', {
48 | packages: ['corechart'],
49 | callback: (function(_this) {
50 | return function() {
51 | return _this.onGoogleLoad("loaded");
52 | };
53 | })(this)
54 | });
55 | return this.onGoogleLoad((function(_this) {
56 | return function() {
57 | var formatter, _i, _ref, _results;
58 | _this.google = google.visualization;
59 | if (chartOptions.dataTable instanceof Array) {
60 | chartOptions.dataTable = _this.google.arrayToDataTable(chartOptions.dataTable);
61 | }
62 | if (typeof chartOptions.beforeDraw === "function") {
63 | chartOptions.beforeDraw(_this, chartOptions);
64 | }
65 | if (formatter = chartOptions.formatter) {
66 | _((function() {
67 | _results = [];
68 | for (var _i = 0, _ref = chartOptions.dataTable.getNumberOfRows() - 1; 0 <= _ref ? _i <= _ref : _i >= _ref; 0 <= _ref ? _i++ : _i--){ _results.push(_i); }
69 | return _results;
70 | }).apply(this)).map(function(index) {
71 | return _(formatter.columns).map(function(column) {
72 | return chartOptions.dataTable.setFormattedValue(index, column, formatter.callback(chartOptions.dataTable.getValue(index, column)));
73 | });
74 | });
75 | }
76 | return _this.wrapper = new _this.google.ChartWrapper(chartOptions);
77 | };
78 | })(this));
79 | };
80 |
81 |
82 | /*
83 | * Execute a callback once google visualization fully loaded
84 | */
85 |
86 | GoogleChart.prototype.onGoogleLoad = function(callback) {
87 | if (callback === "loaded") {
88 | this.googleLoaded = true;
89 | return _(this.onGoogleLoadItems).map(function(fn) {
90 | return fn();
91 | });
92 | } else {
93 | if (this.googleLoaded) {
94 | return callback();
95 | } else {
96 | return (this.onGoogleLoadItems || (this.onGoogleLoadItems = [])).push(callback);
97 | }
98 | }
99 | };
100 |
101 |
102 | /*
103 | * Execute a callback once a given element ID appears in DOM ( mini livequery ).
104 | *
105 | * We need it because GoogleChart object only draw itself on DOM elements
106 | * so we first need to wait for our element to be added to the DOM before
107 | * we call GoogleChart.draw().
108 | *
109 | * Usage:
110 | * Backbone.GoogleChart.watch("#myid", function() { console.log("I'm in") });
111 | * $("body").append(""); // 'I"m in' should be printed to console
112 | *
113 | */
114 |
115 | GoogleChart.watch = function(id, fn) {
116 | var func, timeout;
117 | (GoogleChart._list || (GoogleChart._list = {}))[id] = fn;
118 | if (GoogleChart._watching) {
119 | return;
120 | }
121 | GoogleChart._watching = true;
122 | timeout = 10;
123 | return (func = function() {
124 | _(GoogleChart._list).map(function(fn, id) {
125 | if ($(id)[0]) {
126 | return fn() & delete GoogleChart._list[id];
127 | }
128 | });
129 | if (_(GoogleChart._list).isEmpty()) {
130 | return GoogleChart._watching = false;
131 | } else {
132 | return setTimeout(func, timeout += 10);
133 | }
134 | })();
135 | };
136 |
137 |
138 | /*
139 | * Returns the wrapping element id
140 | * if no id was specified on initialization a random one will be returned
141 | */
142 |
143 | GoogleChart.prototype.id = function() {
144 | var _ref;
145 | return ((_ref = this.el) != null ? _ref.id : void 0) || this.randomID();
146 | };
147 |
148 |
149 | /*
150 | * "Instruct" the current graph instance to draw itself once its visiable on DOM
151 | * return the current instance
152 | */
153 |
154 | GoogleChart.prototype.render = function() {
155 | this.onGoogleLoad((function(_this) {
156 | return function() {
157 | return _this.constructor.watch("#" + _this.el.id, function() {
158 | return _this.wrapper.draw(_this.el.id);
159 | });
160 | };
161 | })(this));
162 | return this;
163 | };
164 |
165 |
166 | /*
167 | * Register for ChartWrapper events
168 | * For the complete event list please look at the events section under
169 | * https://developers.google.com/chart/interactive/docs/reference#chartwrapperobject
170 | *
171 | * graph = new Backbone.GoogleChart({chartOptions: options});
172 | * graph.on("select",function(graph) { console.log("Someone click on me!") })
173 | * graph.on("error",function(graph) { console.log("Oops") })
174 | * graph.on("ready",function(graph) { console.log("I'm ready!") })
175 | *
176 | */
177 |
178 | GoogleChart.prototype.bind = function(event, callback) {
179 | var _base;
180 | (_base = this._listers())[event] || (_base[event] = this._addGoogleListener(event));
181 | return GoogleChart.__super__.bind.call(this, event, callback);
182 | };
183 |
184 |
185 | /*
186 | * alias of @bind
187 | */
188 |
189 | GoogleChart.prototype.on = GoogleChart.prototype.bind;
190 |
191 |
192 | /*
193 | * Unbind events, please look at Backbone.js docs for the API
194 | */
195 |
196 | GoogleChart.prototype.unbind = function(event, callback, context) {
197 | if (event) {
198 | this._removeGoogleListener(event);
199 | } else if (callback) {
200 | _(_(this._listers()).pairs()).map((function(_this) {
201 | return function(pair) {
202 | if (pair[1] === callback) {
203 | return _this._removeGoogleListener(pair[0]);
204 | }
205 | };
206 | })(this));
207 | } else {
208 | _(_(this._listers()).values()).map(this._removeGoogleListener);
209 | }
210 | return GoogleChart.__super__.unbind.call(this, event, callback, context);
211 | };
212 |
213 |
214 | /*
215 | * alias of @unbind
216 | */
217 |
218 | GoogleChart.prototype.off = GoogleChart.prototype.unbind;
219 |
220 | GoogleChart.prototype._addGoogleListener = function(event) {
221 | return this.onGoogleLoad((function(_this) {
222 | return function() {
223 | return _this.google.events.addListener(_this.wrapper, event, function() {
224 | return _this.trigger(event, _this.wrapper.getChart());
225 | });
226 | };
227 | })(this));
228 | };
229 |
230 | GoogleChart.prototype._removeGoogleListener = function(event) {
231 | return this.onGoogleLoad((function(_this) {
232 | return function() {
233 | _this.google.events.removeListener(_this._listers()[event]);
234 | return delete _this._listers()[event];
235 | };
236 | })(this));
237 | };
238 |
239 | GoogleChart.prototype._listers = function() {
240 | return this._listersObj || (this._listersObj = {});
241 | };
242 |
243 |
244 | /*
245 | * Generate a random ID, gc_XXX
246 | */
247 |
248 | GoogleChart.prototype.randomID = function() {
249 | return _.uniqueId("gc_");
250 | };
251 |
252 | return GoogleChart;
253 |
254 | })(Backbone.View);
255 |
256 | }).call(this);
257 |
--------------------------------------------------------------------------------