Advanced Charts: An Election Results Dashboard

You can see this app running online at: Election Results Dashboard App Online

What this app does:

  • Displays a subset of of various Australian election results using various charts arranged as a dashboard

This app aims to demonstrate:

  • The advanced usage of the charts widget

Setup Instructions

Use the app name 'tropofy_election_results_dashboard' to quickstart as in Running and Debugging Tropofy Apps

Full Source

"""
Author:      www.tropofy.com

Copyright 2015 Tropofy Pty Ltd, all rights reserved.

This source file is part of Tropofy and governed by the Tropofy terms of service
available at: http://www.tropofy.com/terms_of_service.html

This source file is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
"""

import pkg_resources
from sqlalchemy.types import Integer, Text, Float
from sqlalchemy.schema import Column

from tropofy.app import AppWithDataSets, Step, StepGroup
from tropofy.widgets import SimpleGrid, KMLMap, Chart
from tropofy.database.tropofy_orm import DataSetMixin


class EnrollmentAndTurnOut(DataSetMixin):
    state_name = Column(Text, nullable=False)
    enrollment = Column(Integer, nullable=False)
    turn_out = Column(Integer, nullable=False)
    turn_out_percentage = Column(Float, nullable=False)
    turn_out_swing = Column(Float, nullable=False)

    def __init__(self, state_name, enrollment, turn_out, turn_out_percentage, turn_out_swing):
        self.state_name = state_name
        self.enrollment = enrollment
        self.turn_out = turn_out
        self.turn_out_percentage = turn_out_percentage
        self.turn_out_swing = turn_out_swing


class ElectedByState(DataSetMixin):
    state_name = Column(Text, nullable=False)
    party_name = Column(Text, nullable=False)
    num_elected = Column(Integer, nullable=False)
    total_votes = Column(Integer, nullable=False)

    def __init__(self, state_name, party_name, num_elected, total_votes):
        self.state_name = state_name
        self.party_name = party_name
        self.num_elected = num_elected
        self.total_votes = total_votes


class StaticKMLFileName(DataSetMixin):
    file_name = Column(Text, nullable=False)

    def __init__(self, file_name):
        self.file_name = file_name


class EnrollmentColumnChart(Chart):
    def get_chart_type(self, app_session):
        return Chart.COLUMNCHART

    def get_table_schema(self, app_session):
        return {
            "state": ("string", "State"),
            "enrollment": ("number", "Enrollment")
        }

    def get_table_data(self, app_session):
        data = []
        for row in app_session.data_set.query(EnrollmentAndTurnOut).all():
            data.append({'state': row.state_name, 'enrollment': row.enrollment})
        return data

    def get_column_ordering(self, app_session):
        return ["state", "enrollment"]

    def get_order_by_column(self, app_session):
        return "state"

    def get_chart_options(self, app_session):
        return {
            'title': 'Number Enrolled',
            'legend': {'position': 'none'},
        }


class TurnOutColumnChart(Chart):
    def get_chart_type(self, app_session):
        return Chart.COLUMNCHART

    def get_table_schema(self, app_session):
        return {
            "state": ("string", "State"),
            "turn_out": ("number", "Turn Out")
        }

    def get_table_data(self, app_session):
        data = []
        for row in app_session.data_set.query(EnrollmentAndTurnOut).all():
            data.append({'state': row.state_name, 'turn_out': row.turn_out_percentage})
        return data

    def get_column_ordering(self, app_session):
        return ["state", "turn_out"]

    def get_order_by_column(self, app_session):
        return "state"

    def get_chart_options(self, app_session):
        return {
            'title': 'Turn Out %',
            'legend': {'position': 'none'},
        }


class TurnOutSwingColumnChart(Chart):
    def get_chart_type(self, app_session):
        return Chart.COLUMNCHART

    def get_table_schema(self, app_session):
        return {
            "state": ("string", "State"),
            "turn_out_swing": ("number", "Turn Out")
        }

    def get_table_data(self, app_session):
        data = []
        for row in app_session.data_set.query(EnrollmentAndTurnOut).all():
            data.append({'state': row.state_name, 'turn_out_swing': row.turn_out_swing})
        return data

    def get_column_ordering(self, app_session):
        return ["state", "turn_out_swing"]

    def get_order_by_column(self, app_session):
        return "state"

    def get_chart_options(self, app_session):
        return {
            'title': 'Turn Out Swing %',
            'legend': {'position': 'none'},
        }


class TotalVotesStackedColumnChart(Chart):
    def get_chart_type(self, app_session):
        return Chart.COLUMNCHART

    def get_parties(self):
        return list(set([v for _, v in get_map_party_to_major_party().items()]))

    def get_states(self, data_set):
        return list(set([row.state_name for row in data_set.query(ElectedByState).all()]))

    def get_table_schema(self, app_session):
        d = {"state": ("string", "State")}
        for party in self.get_parties():
            d[party] = ("number", party)
        return d

    def get_table_data(self, app_session):
        data = []
        for state in self.get_states(app_session.data_set):
            entry = {'state': state}
            for party in self.get_parties():
                entry[party] = 0
                for row in app_session.data_set.query(ElectedByState).filter(ElectedByState.state_name == state).all():
                    if map_to_major_party(row.party_name) == party:
                        entry[party] = entry[party] + row.total_votes
            data.append(entry)
        return data

    def get_column_ordering(self, app_session):
        return ["state"] + self.get_parties()

    def get_order_by_column(self, app_session):
        return "state"

    def get_chart_options(self, app_session):
        return {
            'isStacked': 'true',
            'title': 'First Preference Totals for Parties with Elected Members',
            'hAxis': {
                'title': 'State',
                'titleTextStyle': {'color': 'black', 'italic': 'false'}
            },
            'vAxis': {
                'title': 'Votes',
                'titleTextStyle': {'color': 'black', 'italic': 'false'}
            },
            'series': [{'color': '#FFB82C'}, {'color': '#006FB9'}, {'color': '#CB0F32'}]
        }


class ElectedPieChart(Chart):
    def get_chart_type(self, app_session):
        return Chart.PIECHART

    def get_table_schema(self, app_session):
        return {
            "party_name": ("string", "Party"),
            "num_elected": ("number", "Members Elected")
        }

    # def get_parties(self):
    #     return list(set([row.party_name for row in data_set.query(ElectedByState).all()]))

    def get_table_data(self, app_session):
        elected_per_party = {}
        for row in app_session.data_set.query(ElectedByState).all():
            elected_per_party[map_to_major_party(row.party_name)] = elected_per_party[map_to_major_party(row.party_name)] + row.num_elected if elected_per_party.get(map_to_major_party(row.party_name)) else row.num_elected
        data = [{'party_name':k, 'num_elected':v} for k, v in elected_per_party.items()]
        return data

    def get_column_ordering(self, app_session):
        return ["party_name", "num_elected"]

    def get_order_by_column(self, app_session):
        return "party_name"

    def get_chart_options(self, app_session):
        return {'title': 'Number Elected',
                'pieSliceText': 'value',
                'pieHole': '0.3',
                'slices': [{'color': '#CB0F32'}, {'color': '#006FB9'}, {'color': '#FFB82C'}]}


class CustomKMLMap(KMLMap):

    def get_kml(self, app_session):
        file_name = app_session.data_set.query(StaticKMLFileName).one().file_name
        f = open(pkg_resources.resource_filename('te_election_results_dashboard', file_name), 'r')
        kml = f.read()
        return kml


class ElectionSummaryApp(AppWithDataSets):

    def get_name(self):
        return 'Election Results Dashboard'

    def get_examples(self):
        return {
            "Demo Data for 2010": load_2010_data,
            "Data for 2007": load_2007_data,
            "Data for 2004": load_2004_data
        }

    def get_static_content_path(self, app_session):
        return pkg_resources.resource_filename('te_election_results_dashboard', 'static')

    def get_gui(self):
        step_group1 = StepGroup(name='Election Data')
        step_group1.add_step(Step(
            name='Dashboard',
             widgets=[
                {"widget": TotalVotesStackedColumnChart(), "cols": 6},
                {"widget": ElectedPieChart(), "cols": 6},
                {"widget": CustomKMLMap(), "cols": 12},
                {"widget": EnrollmentColumnChart(), "cols": 4},
                {"widget": TurnOutColumnChart(), "cols": 4},
                {"widget": TurnOutSwingColumnChart(), "cols": 4},
            ],
            help_text="KML provided by Ben Raue, http://www.tallyroom.com.au/maps"
        ))
        step_group2 = StepGroup(name='Raw Data')
        step_group2.add_step(Step(
            name='Raw Data',
            widgets=[SimpleGrid(EnrollmentAndTurnOut), SimpleGrid(ElectedByState)]
        ))
        return [step_group1, step_group2]

    def get_icon_url(self):
        return "/{}/static/{}/election_results_dashboard.png".format(
            self.url_name,
            self.get_app_version(),
        )


def map_to_major_party(party):
    return get_map_party_to_major_party()[party]


def get_map_party_to_major_party():
    return {
        'Australian Labor Party': 'ALP',
        'Independent': 'Other',
        'Liberal': 'LNP',
        'The Nationals': 'LNP',
        'Country Liberals': 'Other',
        'CLP - The Territory Party': 'Other',
        'Liberal National Party of Queensland': 'LNP',
        'The Greens': 'Other'
    }


def load_data(data_set, elected_by_state, turn_out_info, file_name):
    data_set.add_all([ElectedByState(row[0], row[1], row[2], row[3]) for row in elected_by_state])
    data_set.add_all([EnrollmentAndTurnOut(row[0], row[1], row[2], row[3], row[4]) for row in turn_out_info])
    data_set.add(StaticKMLFileName(file_name))


def load_2010_data(app_session):
    elected_by_state = [
            ['ACT', 'Australian Labor Party', 2, 100700],
            ['NSW', 'Australian Labor Party', 26, 1494490],
            ['NSW', 'Independent', 2, 172921],
            ['NSW', 'Liberal', 16, 1470146],
            ['NSW', 'The Nationals', 4, 317867],
            ['NT', 'Australian Labor Party', 1, 35589],
            ['NT', 'Country Liberals', 1, 38335],
            ['QLD', 'Australian Labor Party', 8, 800712],
            ['QLD', 'Independent', 1, 83310],
            ['QLD', 'Liberal National Party of Queensland', 21, 1130525],
            ['SA', 'Australian Labor Party', 6, 399279],
            ['SA', 'Liberal', 5, 394003],
            ['TAS', 'Australian Labor Party', 4, 143796],
            ['TAS', 'Independent', 1, 15627],
            ['VIC', 'Australian Labor Party', 22, 1361416],
            ['VIC', 'The Greens', 1, 402482],
            ['VIC', 'Liberal', 12, 1159301],
            ['VIC', 'The Nationals', 2, 101419],
            ['WA', 'Australian Labor Party', 3, 375381],
            ['WA', 'Liberal', 11, 566145],
            ['WA', 'The Nationals', 1, 43101]
        ]
    turn_out_info = [
            ['NSW', 4329115, 4099501, 94.7, -0.04],
            ['VIC', 3309800, 3139881, 94.87, -0.27],
            ['QLD', 2475611, 2320717, 93.74, -0.98],
            ['WA', 1248732, 1158687, 92.79, -1.78],
            ['SA', 1051923, 997102, 94.79, -0.75],
            ['TAS', 342809, 327892, 95.65, -0.49],
            ['ACT', 227541, 216057, 94.95, -0.02],
            ['NT', 112930, 95146, 84.25, -1.86]
        ]
    load_data(app_session.data_set, elected_by_state, turn_out_info, "AUS-VIC-HOR-2013.kml")


def load_2007_data(app_session):
    elected_by_state = [
            ['ACT', 'Australian Labor Party', 2, 114244],
            ['NSW', 'Australian Labor Party', 28, 1791171],
            ['NSW', 'Independent', 1, 134424],
            ['NSW', 'Liberal', 15, 1324311],
            ['NSW', 'The Nationals', 5, 321182],
            ['NT', 'Australian Labor Party', 2, 46794],
            ['QLD', 'Australian Labor Party', 15, 1020665],
            ['QLD', 'Independent', 1, 71068],
            ['QLD', 'Liberal', 10, 818438],
            ['QLD', 'The Nationals', 3, 239504],
            ['SA', 'Australian Labor Party', 6, 426639],
            ['SA', 'Liberal', 5, 412621],
            ['TAS', 'Australian Labor Party', 5, 139077],
            ['VIC', 'Australian Labor Party', 21, 1416252],
            ['VIC', 'Liberal', 14, 1206992],
            ['VIC', 'The Nationals', 2, 95859],
            ['WA', 'Australian Labor Party', 4, 433342],
            ['WA', 'Liberal', 11, 545365]
        ]
    turn_out_info = [
            ['NSW', 4496208, 4271005, 94.99, 0.29],
            ['VIC', 3441822, 3275620, 95.17, 0.3],
            ['QLD', 2612504, 2466561, 94.41, 0.67],
            ['WA', 1313201, 1224689, 93.26, 0.47],
            ['SA', 1076220, 1026982, 95.42, 0.63],
            ['TAS', 349753, 334938, 95.76, 0.11],
            ['ACT', 238786, 228870, 95.85, 0.9],
            ['NT', 118045, 102149, 86.53, 2.28],
        ]
    load_data(app_session.data_set, elected_by_state, turn_out_info, "AUS-VIC-HOR-2004-2010.kml")


def load_2004_data(app_session):
    elected_by_state = [
            ['ACT', 'Australian Labor Party', 2, 104836],
            ['NSW', 'Australian Labor Party', 21, 1412418],
            ['NSW', 'Independent', 2, 151436],
            ['NSW', 'Liberal', 21, 1391511],
            ['NSW', 'The Nationals', 6, 353670],
            ['NT', 'Australian Labor Party', 1, 40246],
            ['NT', 'CLP - The Territory Party', 1, 39855],
            ['QLD', 'Australian Labor Party', 6, 765507],
            ['QLD', 'Independent', 1, 69589],
            ['QLD', 'Liberal', 17, 867289],
            ['QLD', 'The Nationals', 4, 214522],
            ['SA', 'Australian Labor Party', 3, 346071],
            ['SA', 'Liberal', 8, 446372],
            ['TAS', 'Australian Labor Party', 3, 140918],
            ['TAS', 'Liberal', 2, 132724],
            ['VIC', 'Australian Labor Party', 19, 1217921],
            ['VIC', 'Liberal', 16, 1302038],
            ['VIC', 'The Nationals', 2, 105577],
            ['WA', 'Australian Labor Party', 5, 381200],
            ['WA', 'Liberal', 10, 528016]
        ]
    turn_out_info = [
            ['NSW', 4329115, 4099501, 94.7, -0.04],
            ['VIC', 3309800, 3139881, 94.87, -0.27],
            ['QLD', 2475611, 2320717, 93.74, -0.98],
            ['WA', 1248732, 1158687, 92.79, -1.78],
            ['SA', 1051923, 997102, 94.79, -0.75],
            ['TAS', 342809, 327892, 95.65, -0.49],
            ['ACT', 227541, 216057, 94.95, -0.02],
            ['NT', 112930, 95146, 84.25, -1.86],
        ]
    load_data(app_session.data_set, elected_by_state, turn_out_info, "AUS-VIC-HOR-2004-2010.kml")