Money and California Propositions (2020)

Ten years ago, I made some plots for how much money was contributed to and spent by the various proposition campaigns in California.

I decided to update these for this election, and here's the result:

Money Raised for CA Propositions November 2020 Election. Visualization by Paul Ivanov

Just in case you didn't get the full picture, here is the same data plotted on a common scale:

Money Raised for CA Propositions November 2020 Election shown with a common scale. Visualization by Paul Ivanov Money Raised for CA Propositions November 2020 Election shown with a common scale. Visualization by Paul Ivanov

So, whereas 10 years ago, we had a total of ~$58 million on the election, the overwhelming amount of in support, this time, we had ~$662 million, an 11 fold increase!

The Cal-Access Campaign Finance Activity: Propositions & Ballot Measures source I used last time was still there, but there are way more propositions this time (12 vs 5), and the money details are broken out by committee, with some propositions have a dozen committees. Another wrinkle is that website has protected by some fancy scraping protection. I could browse it just fine in Firefox, even with Javascript turned off, but couldn't download it using wget, curl, or python, even after setting up all of the same headers, not just the User-Agent one. I would just get something like this:

<META NAME="robots" CONTENT="noindex,nofollow">

or this

<html style="height:100%"><head><META NAME="ROBOTS" CONTENT="NOINDEX,
NOFOLLOW"><meta name="format-detection" content="telephone=no"><meta
name="viewport" content="initial-scale=1.0"><meta http-equiv="X-UA-Compatible"
content="IE=edge,chrome=1"></head><body style="margin:0px;height:100%"><iframe
frameborder=0 width="100%" height="100%" marginheight="0px"
marginwidth="0px">Request unsuccessful. Incapsula incident ID:

Luckily, while I was working on the scraper, I took a break and found another source that already has the tabulations.

The only catch is that this source was last updated two weeks ago, so it's not the freshest data. Also, last time I had data for both contributions and for money spent, but this summed page only has contribution totals, not spending totals (the spending figures are still there)

But I figured it's good enough to get the big picture.

Also, here's the python code used to generate these plots (largely reused from last time, so don't expect it to be pretty).

# Create contributions  bar charts of committees supporting and opposing
# various propositions on the California Ballot for November 2020
# created by Paul Ivanov (

# figure(0) - Contributions by Proposition (as subplots)
# figure(2) - Contributions on a common scale

import numpy as np
from matplotlib import pyplot as plt
import locale

# fun times! without this next line, I got a
# ValueError: Currency formatting is not possible using the 'C' locale.
locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')

elec = "Nov2020"
election =  "November 2020 Election"

# This part was done by hand by collecting data from CalAccess:
# proposition: (yes, no) 
cont = {'14': (12810328, 250), 
        '15': (56320926, 60905901), 
        '16': (19926905, 1172614),
        '17': (1363887, 0), 
        '18': (835064, 0), 
        '19': (37794775, 45050), 
        '20': (4829677, 20471086), 
        '21': (40184953, 59379159), 
        '22': (188937777, 15896808), 
        '23': (6917438, 104405156), 
        '24': (5907002, 48368),
        '25': (13446871, 10181122)

def currency(x, pos):
    """The two args are the value and tick position"""
    if x==0:
        return "$0"
    if x < 1e3:
        return '$%f' % (x)
    elif x< 1e6:
        return '$%1.0fK' % (x*1e-3)
    return '$%1.0fM' % (x*1e-6)

from matplotlib.ticker import FuncFormatter
formatter = FuncFormatter(currency)

yes,no = range(2)
c = [(6.,.5,0),'blue']  # color for yes/no stance
c = ['red','blue']  # color for yes/no stance
c = [(1.,.5,0),'blue']  # color for yes/no stance
a = [.9,.9]             # alpha for yes/no stance
a = [.6,.5]             # alpha for yes/no stance
t = ['Yes','No ']       # text  for yes/no stance

raised,spent = range(2)
title = ["Contributed to", "Spent on" ] # reuse code by injecting title specifics
field = ['Contributions', 'Expenditures']

footer ="""
Total %s 1/1/2020-10/14/2020  (1/1/2020-10/16/2020 for Prop 15)
Data from
cc-by Paul Ivanov (
""" # will inject field[col] in all plots

color = np.array((.9,.9,.54))*.9 # spine/ticklabel color
color = np.array((.52,.32,.12)) # spine/ticklabel color
color = np.array((.82,.42,.12)) # spine/ticklabel color
color = "gray"
color = np.array((85.,107,47)) / 255 # darkolivegreen

plt.rcParams['savefig.dpi'] = 200

def fixup_subplot(ax,color):
    """ Tufte-fy the axis labels - use different color than data"""
    spines = list(ax.spines.values())
    # liberate the data! hide right and top spines
    [ax.spines[s].set_visible(False) for s in ["top", "right"]]
    ax.yaxis.tick_left() # don't tick on the right

    # there's gotta be a better way to set all of these colors, but I don't
    # know that way, I only know the hard way
    [s.set_color(color) for s in spines]
    [s.set_color(color) for s in ax.yaxis.get_ticklines()]
    [s.set_visible(False) for s in ax.xaxis.get_ticklines()]
    [(s.set_color(color),s.set_size(8)) for s in ax.xaxis.get_ticklabels()]
    [(s.set_color(color),s.set_size(8)) for s in ax.yaxis.get_ticklabels()]

adjust_dict = {'bottom': 0.052, 'hspace': 0.646815834767644,
 'left': 0.13732508948909858, 'right': 0.92971038073543777,
 'top': 0.94082616179001742, 'wspace': 0.084337349397590383}

# subplots for each proposition (Fig 0 and Fig 1)
col = 0 
f = plt.figure(col); f.clf(); f.dpi=100;
for i,p in enumerate(cont):
    ax = plt.subplot(len(cont),1, i+1)
    #p = i+14    #prop number
    for stance in [yes,no]:, cont[p][stance], color=c[stance], linewidth=0,
                align='center', width=.1, alpha=a[stance])
        lbl = locale.currency(round(cont[p][stance]), symbol=True, grouping=True)
        lbl = lbl[:-3] # drop the cents, since we've rounded
        ax.text(stance, cont[p][stance], lbl , ha='center', size=8)

    ax.xaxis.set_ticklabels(["Yes on %s"%p, "No on %s"%p])

    # put a big (but faded) "Proposition X" in the center of this subplot
    common=dict(alpha=.1, color='k', ha='center', va='center', transform = ax.transAxes)
    ax.text(0.5, .9,"Proposition", size=8, weight=600, **common)
    ax.text(0.5, .50,"%s"%p, size=50, weight=300, **common)

    ax.yaxis.set_major_formatter(formatter) # plugin our currency labeler
    ax.yaxis.get_major_locator()._nbins=5 # put fewer tickmarks/labels


f.subplots_adjust( **adjust_dict)

# Figure title, subtitle
extra_args = dict(family='serif', ha='center', va='top', transform=f.transFigure)
f.text(.5,.99,"Money %s CA Propositions"%title[col], size=12, **extra_args)
f.text(.5,.975,election, size=9, **extra_args)

extra_args.update(va='bottom', size=6,ma='center')
f.text(.5,0.0,footer%field[col], **extra_args)

f.set_figheight(20.); f.set_figwidth(5); f.canvas.draw()
f.savefig('CA-Props-%s-%s-Subplots.png'%(elec, field[col]))

# all props on one figure (Fig 2 and Fig 3)
f = plt.figure(col+2); f.clf()
adjust_dict.update(left= 0.045,right=.98, top=.91, bottom=.12)
f.subplots_adjust( **adjust_dict)

extra_args = dict(family='serif', ha='center', va='top', transform=f.transFigure)
f.text(.5,.99,"Money %s CA Propositions"%title[col], size=12, **extra_args)
f.text(.5,.96, election , size=9, **extra_args)

footer = footer.replace("/\n", "/") #.replace("\nc", "c")
#footer = footer.replace("\n", "") + "\n"
extra_args.update(ha='center', va='bottom', size=6,ma='center')
#f.text(adjust_dict['left'],0.0,footer%field[col], **extra_args)
f.text(.5,0.0,footer%field[col], **extra_args)

ax = plt.subplot(111)
for stance in [yes,no]:
    total = sum([x[stance] for x in cont.values()])
    lbl = locale.currency(round(total),True,True)
    lbl = lbl[:-3] # drop the cents, since we've rounded
    lbl = t[stance]+" Total "+ lbl.rjust(12),[cont[p][stance] for p in cont], width=.1, color=c[stance],
            alpha=a[stance],align='center',linewidth=0, label=lbl)
    for i,p in enumerate(cont):
        lbl = locale.currency(round(cont[p][stance]), symbol=True, grouping=True)
        lbl = lbl[:-3] # drop the cents, since we've rounded
        #ha = 'center' if i != 2 else "right" # tweek by hand to make numbers show up
        ax.text(abscissa[i], cont[p][stance], lbl , ha="center",

ax.xaxis.set_ticklabels(["Prop %s"%p for p in cont])

# plt.legend(prop=dict(family='monospace',size=9)) # this makes legend tied
# to the subplot, tie it to the figure, instead
handles, labels = ax.get_legend_handles_labels()
l = plt.figlegend(handles, labels,loc='upper right',prop=dict(family='monospace',size=9))
ax.yaxis.set_major_formatter(formatter) # plugin our currency labeler
f.savefig('CA-Props-%s-%s.png'%(elec, field[col]))