LILY UPDATES

Refresh Election Map Output¶

In [ ]:
%load_ext autoreload
      %autoreload 2
      
In [ ]:
import geopandas as gpd
      import datetime as dt
      import json
      from pathlib import Path
      
      import bokeh
      from bokeh.embed import components
      from bokeh.plotting import figure
      from bokeh.models import (
          ResetTool,
          PanTool,
          WheelZoomTool,
          GeoJSONDataSource, 
          Slider,
          CustomJS,
          ZoomInTool,
          ZoomOutTool,
          RadioButtonGroup,
          LinearColorMapper, 
      )
      from bokeh.transform import transform, linear_cmap
      
      from jinja2 import FileSystemLoader, Environment
      
      import colorcet as cc
      
In [ ]:
BOKEH_VERSION = bokeh.__version__
      print(BOKEH_VERSION)
      
3.2.1
      

read in geojson source data¶

In [ ]:
# Source: MIT Election Data and Science Lab, 2018, "County Presidential Election Returns 2000-2020", https://doi.org/10.7910/DVN/VOQCHQ, Harvard Dataverse, V11; countypres_2000-2020.tab [fileName], UNF:6:HaZ8GWG8D2abLleXN3uEig== [fileUNF]
      
      p = Path('assets/mit_election_data_county_pres_mov_swing_2000_2020geo.json')
      
      with p.open('+r') as f:
          src = json.load(f)
          
      gdf = gpd.GeoDataFrame.from_features(src['features'])
      gdf.head()
      
Out[ ]:
geometry county_fips geo_id state county name lsad censusarea partisan_mov_norm_2000 partisan_mov_norm_2004 partisan_mov_norm_2008 partisan_mov_norm_2012 partisan_mov_norm_2016 partisan_mov_norm_2020 swing_norm_2000 swing_norm_2004 swing_norm_2008 swing_norm_2012 swing_norm_2016 swing_norm_2020
0 POLYGON ((-86.49677 32.34444, -86.71790 32.402... 01001 0500000US01001 01 001 Autauga County 594.436 0.704876 0.759897 0.739203 0.730152 0.744985 0.722092 None 0.555022 0.479306 0.490949 0.514832 0.477108
1 POLYGON ((-87.59893 30.99745, -87.59411 30.976... 01003 0500000US01003 01 003 Baldwin County 1589.784 0.737916 0.769561 0.757238 0.778844 0.785801 0.768812 None 0.531645 0.487676 0.521606 0.506956 0.483011
2 POLYGON ((-85.05603 32.06305, -85.05021 32.024... 01005 0500000US01005 01 005 Barbour County 884.876 0.495575 0.549504 0.507266 0.484273 0.527844 0.538315 None 0.553929 0.457762 0.477007 0.543571 0.510471
3 POLYGON ((-87.42120 32.87451, -87.42013 32.902... 01007 0500000US01007 01 007 Bibb County 622.582 0.610055 0.722566 0.729234 0.733373 0.775768 0.788640 None 0.612511 0.506668 0.504139 0.542395 0.512872
4 POLYGON ((-86.57780 33.76532, -86.75914 33.840... 01009 0500000US01009 01 009 Blount County 644.776 0.713932 0.812686 0.847530 0.870470 0.904545 0.900011 None 0.598754 0.534844 0.522940 0.534075 0.495466
In [ ]:
spath = Path('assets/lower48_states_geo.json')
      
      with spath.open('+r') as f:
          state_src = json.load(f)
          
      sgdf = gpd.GeoDataFrame.from_features(state_src['features'])
      
      sgdf.head()
      
Out[ ]:
geometry state
0 MULTIPOLYGON (((-88.12015 30.24615, -88.13708 ... 01
1 POLYGON ((-111.57953 31.49410, -112.24610 31.7... 04
2 POLYGON ((-93.48951 33.01844, -93.49052 33.018... 05
3 MULTIPOLYGON (((-121.86227 36.93155, -121.8801... 06
4 POLYGON ((-106.67563 36.99312, -106.86980 36.9... 08

Create Visualization¶

In [ ]:
# map gdf columns to Margin of Victory (aka MoV), Swing, or Other
      mov_cols = [x for x in gdf.columns if 'mov_' in x]
      swing_cols = [x for x in gdf.columns if 'swing_' in x]
      other_cols = [x for x in gdf.columns if x not in mov_cols + swing_cols]
      
      # create color mappers
      # use 0 and 1 as low and high so that 0.5 is the midpoint
      # if value for MoV or swing is < 0.5, the outcome was in favor of Democrats
      # if > 0.5, the outcome was in favor of Republicans
      color_mapper = LinearColorMapper(
          palette = cc.b_diverging_bwr_20_95_c54,
          low = 0,
          high = 1
      )
      
      # create Bokeh DataSources
      counties_gsrc = GeoJSONDataSource(geojson = gdf.to_json())
      states_gsrc = GeoJSONDataSource(geojson = sgdf.to_json())
      
      # create Bokeh figure within which the patches will be drawin
      p = figure(
          width_policy="max",
          height_policy="max", 
          aspect_ratio = 1.65,
          background_fill_color=None,
          tools = [
              ResetTool(), 
              PanTool(), 
              WheelZoomTool(dimensions = "both"),
              ZoomInTool(),
              ZoomOutTool()
          ],
          x_axis_location = None,
          y_axis_location = None,
          sizing_mode="scale_both",
          output_backend='webgl' # use WebGL for GPU rendering
      )
      
      # map county shapes
      # initial color value: MoV for 2000
      counties = p.patches(
          "xs", 
          "ys",
          fill_color=transform("partisan_mov_norm_2000", color_mapper),
          line_color="darkslategrey", 
          line_width=0.5,
          source=counties_gsrc
      )
      
      # map state outlines
      states = p.patches(
          "xs",
          "ys",
          fill_color=None,
          line_color="black",
          line_width=1,
          source=states_gsrc
      )
      
      # remove gridlines and Bokeh border
      p.grid.grid_line_color = None
      p.outline_line_color = None
      p.border_fill_color = None
      
      # add widgets
      slider = Slider(
          start = 2000, end = 2020, step = 4, title="Year", value = 2000,
          orientation='horizontal',
          sizing_mode = "scale_width", width_policy="max", 
          css_classes=["bokeh-widget", "slider"]
      )
      
      LABELS = ['Margin of Victory', 'Swing']
      radio_button_group = RadioButtonGroup(
          labels=LABELS, active = 0,
          css_classes = ['bokeh-widget', 'radio-button-group']
      )
      
      # custom JS slider callback
      # takes as input the counties patches and radio button group
      # the radio button group `active` value determine whether to use
      # logic for MoV or swing
      slider_callback = CustomJS(
          args=dict(
              counties=counties, 
              rbg=radio_button_group
          ), 
          code="""
          console.log('enter slider callback');
          
          if (rbg.active == 0) {
              counties.glyph.fill_color.field = 'partisan_mov_norm_' + this.value.toString();
          } else if (rbg.active == 1) {
              if (this.value < 2004) {
                  this.value = 2004;
              }
              counties.glyph.fill_color.field = 'swing_norm_' + this.value.toString();
          }
          
          counties.glyph.change.emit();
          """
      )
      
      # register slider callback
      slider.js_on_change('value', slider_callback)
      
      # likewise for the radio group, take as input the counties patches and slider
      # if active dataset changes to MoV, set the glyph fill color field to MoV and vice versa
      # if active dataset changes to Swing and slider value == 2000, update slider value to 2004
      rb_callback = CustomJS(
          args=dict(
              counties=counties, 
              slider=slider
          ), 
          code="""
          console.log('enter radio button group callback');
          if (this.active == 0) {
              counties.glyph.fill_color.field = "partisan_mov_norm_" + slider.value.toString();
          }
          else if (this.active == 1) {
              if (slider.value < 2004) {
                  slider.value = 2004;
              }
              slider.change.emit();
              counties.glyph.fill_color.field = 'swing_norm_' + slider.value.toString();
          }
      
          counties.glyph.change.emit();
      """)
      
      # register radio button group callback
      radio_button_group.js_on_change('active', rb_callback)
      
In [ ]:
# create components from figure & widgets
      script, div = components(
          {'map': p, 'slider': slider, 'radio_button_group': radio_button_group}
          )
      
In [ ]:
# load Jinja templates
      env = Environment(
          loader=FileSystemLoader('templates/')
      )
      
      bokeh_template = env.get_template('bokeh-template.html')
      
In [ ]:
# set template vars
      vars = {
          'bokeh_version': BOKEH_VERSION,
          'title': 'Presidential Election Results by County',
          'bokeh_script': script,
          'main_title': 'Presidential Election Results by County, 2000–2020',
          'main_viz_div': div['map'],
          'main_caption': "Credit: MIT Election Data + Science Lab",
          'widget_divs': [div['slider'], div['radio_button_group']]
      }
      
In [ ]:
# render template with vars
      rendered = bokeh_template.render(**vars)
      
In [ ]:
# save rendered template
      html_path = Path(f'election_map_{dt.date.today().strftime("%Y%m%d")}.html')
      html_path.parent.mkdir(exist_ok=True, parents=True)
      
      with html_path.open('+w') as f:
          f.write(rendered)