Skip to content

Commit

Permalink
Add build step.
Browse files Browse the repository at this point in the history
  • Loading branch information
sampottinger committed Aug 8, 2024
1 parent d18145c commit b8da233
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 46 deletions.
48 changes: 48 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Build

on: push

jobs:
checks:
name: Checks
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
- name: Install python deps
run: pip install -r requirements.txt
- name: Install optional build tools
run: pip install pycodestyle pyflakes nose2
- name: Check flakes
run: pyflakes *.py
- name: Check style
run: pycodestyle *.py
- name: Check types
run: mypy *.py
- name: Run unit tests
run: nose2
pipeline:
name: Pipeline
runs-on: ubuntu-latest
needs: [checks]
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
- name: Install python deps
run: pip install -r requirements.txt
- name: Run visualization
run: python draw_berkeley_bart.py berkeley_trips.csv berkeley_trips.png
- name: Upload result
uses: actions/upload-artifact@v3
with:
name: berkeley_trips
path: berkeley_trips.png
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__pycache__
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ To get this repository ready, let's execute a few tools that can help us check o

- **pyflakes**: This is a check for clear issues like undefined variables. Execute with `pyflakes *.py`.
- **nose2**: This is a unit test runner. Go ahead and give this a shot with `nose2` and notice that it looks like the test for the line length is failing. See if you can fix it!
- **pycodestyle**: Execute checks for code style issues with `pycodestyle *.py`. Can you fix any of the ifnal issues?
- **pycodestyle**: Execute checks for code style issues with `pycodestyle *.py`. Can you fix any of the final issues? This is controlled by `setup.cfg`.

We will next execute these from within CI. Note that we just did a little [test driven development](https://www.youtube.com/watch?v=B1j6k2j2eJg) where the test is written before the code.

Expand Down
92 changes: 47 additions & 45 deletions draw_berkeley_bart.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,22 @@
USAGE_STR = 'USAGE: python draw_berkeley_bart.py input_loc output_loc'

DEFAULT_DATA_LOCATION = 'berkeley_trips.csv'
BG_COLOR = '#ECECEC'
BG_COLOR = '#EAEAEA'
FG_COLOR = '#333333'
TICK_COLOR = '#FFFFFF'
TITLE = 'Bart trips from Downtown Berkeley to other stations in March 2024.'
WIDTH = 600
HEIGHT = 600
LINE_MIN_LEN = 70
LINE_MAX_LEN = 210


class Station:
"""Data record describing a single station."""

def __init__(self, name, code, count):
"""Create a new station record.
Args:
name: The full name of the station.
code: The short code name of the station.
Expand All @@ -37,26 +39,26 @@ def __init__(self, name, code, count):
self._name = name
self._code = code
self._count = count

def get_name(self):
"""Get the human-readable name of the station.
Returns:
Name of the station like "Downtown Berkeley" as provided by BART.
"""
return self._name

def get_code(self):
"""Get the short code name of the station.
Returns:
Two character codename for the station like BK.
"""
return self._code

def get_count(self):
"""Get the number of trips that went from Berkeley to this station.
Returns:
Number of trips to this station in the target month. This is the
number of "tag outs" for journies which started at Downtown Bereley
Expand All @@ -67,35 +69,35 @@ def get_count(self):

class DataFacade:
"""Object which simplifies access to ridership data."""

def __init__(self, sketch):
"""Create a new data facade.
Args:
sketch: The sketch through which ridership data will be accessed.
"""
self._sketch = sketch

def get_stations(self, loc):
"""Get a list of stations from the underlying data.
Args:
loc: The location from which to parse the data.
Returns:
List of stations from the underlying dataset sorted by total number
of trips in ascending order.
"""
raw_data = self._sketch.get_data_layer().get_csv('berkeley_trips.csv')
parsed_data = map(lambda x: self._parse_data_point(x), raw_data)
return sorted(parsed_data, key=lambda x: x.get_count())

def _parse_data_point(self, target):
"""Parse an input raw CSV row as a Station record.
Args:
target: Dictionary representing a single row from the input dataset.
Returns:
Station object representing the row after parsing.
"""
Expand All @@ -107,29 +109,29 @@ def _parse_data_point(self, target):

class StationVizPresenter:
"""Presenter which runs the visualization."""

def __init__(self, sketch):
"""Create a presenter to run a bart visualization.
Args:
sketch: The sketch to run.
"""
self._sketch = sketch

def draw(self, records):
"""Draw the visualization.
Args:
records: The stations to draw.
"""
self._sketch.clear(BG_COLOR)

max_value = max(map(lambda x: x.get_count(), records))

self._draw_title()
self._draw_axis(max_value)
self._draw_data(max_value, records)

def _draw_title(self):
"""Draw the title at the bottom of the visual."""
self._sketch.clear_stroke()
Expand All @@ -140,42 +142,42 @@ def _draw_title(self):

def _draw_axis(self, max_value):
"""Draw the axis and other non-data chart elements.
Args:
max_value: The maximum number of trips to a single station expected.
"""
# We will change the coordinate system such that 300, 300 is 0, 0 and
# push saves the original coordinate system state.
self._sketch.push_transform()

# Move to the center of the visualization
self._sketch.translate(WIDTH / 2, HEIGHT / 2)

# Setup some drawing preferences
self._sketch.set_text_align('center', 'center')
self._sketch.set_ellipse_mode('radius')

# Draw the Bereley text at the center
self._sketch.clear_stroke()
self._sketch.set_fill(FG_COLOR)
self._sketch.set_text_font('PublicSans-Regular.otf', 20)
self._sketch.draw_text(0, 0, 'Berkeley')

# Draw ticks
self._sketch.set_text_font('PublicSans-Regular.otf', 10)
for value in range(0, max_value + 5000, 5000):
x = self._get_line_length(max_value, value)

# Draw a light reference line as a circle
self._sketch.clear_fill()
self._sketch.set_stroke(TICK_COLOR)
self._sketch.draw_ellipse(0, 0, x, x)

# Draw number of trips as text
self._sketch.clear_stroke()
self._sketch.set_fill(FG_COLOR)
self._sketch.draw_text(x, 0, f'{value:,}')

# Put the coordinate system back (restore the coordinate system state
# we saved earlier with push_transform). This undoes the translate.
self._sketch.pop_transform()
Expand All @@ -184,44 +186,44 @@ def _draw_data(self, max_value, records):
# We will change the coordinate system such that 300, 300 is 0, 0 and
# rotate. Push saves the original coordinate system state.
self._sketch.push_transform()

# Move to the center of the visualization
self._sketch.translate(WIDTH / 2, HEIGHT / 2)

# Set some drawing preferences
self._sketch.set_angle_mode('degrees')

# Determine how much we have to space out stations
num_lanes = len(records) + 1

# Draw each station
for record in records:
# Figure out how far from the center this station will be drawn.
length = self._get_line_length(max_value, record.get_count())

# Rotate a little for each station
self._sketch.rotate(360 / num_lanes)

# Draw a line from the center according to (length proportional to)
# the number of trips to that station from Downtown Berkeley.
self._sketch.clear_fill()
self._sketch.set_stroke(FG_COLOR)
self._sketch.draw_line(70, 0, length, 0)

# Draw the name of the station.
self._sketch.clear_stroke()
self._sketch.set_fill(FG_COLOR)
self._sketch.set_text_font('PublicSans-Regular.otf', 10)
self._sketch.set_text_align('left', 'center')
self._sketch.draw_text(length + 2, 0, record.get_name())

# Put the coordinate system back (restore the coordinate system state
# we saved earlier with push_transform). This undoes the translate and
# rotate.
self._sketch.pop_transform()

def _get_line_length(self, max_value, count):
return 140 / max_value * count + 70
return (LINE_MAX_LEN - LINE_MIN_LEN) / max_value * count + LINE_MIN_LEN


def main():
Expand All @@ -237,16 +239,16 @@ def main():
else:
print(USAGE_STR)
sys.exit(1)

# Create a sketch and initalize the data facade (model) and presenter.
sketch = sketchingpy.Sketch2D(WIDTH, HEIGHT)
data_facade = DataFacade(sketch)
presenter = StationVizPresenter(sketch)

# Get the data and draw
data = data_facade.get_stations(data_loc)
sketch.on_step(lambda x: presenter.draw(data))

# If running interactive, show the visualization. Otherwise write to disk
# at location specified.
if interactive:
Expand Down
3 changes: 3 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[pycodestyle]
max-line-length = 100
ignore = E125,E128,E502,E731,E722,E402
40 changes: 40 additions & 0 deletions test_draw_berkeley_bart.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Unit tests for the BART Berkeley trips visualization.
License: BSD
"""

import unittest

import sketchingpy

import draw_berkeley_bart


class BerkeleyBartTests(unittest.TestCase):

def setUp(self):
self._sketch = sketchingpy.Sketch2D(500, 500)

def test_parse_data_point(self):
data_facade = draw_berkeley_bart.DataFacade(self._sketch)
result = data_facade._parse_data_point({
'name': 'test',
'code': 'te',
'count': '1,234'
})
self.assertEqual(result.get_name(), 'test')
self.assertEqual(result.get_code(), 'te')
self.assertEqual(result.get_count(), 1234)

def test_get_line_length_zero(self):
presenter = draw_berkeley_bart.StationVizPresenter(self._sketch)
self.assertEqual(presenter._get_line_length(100, 0), draw_berkeley_bart.LINE_MIN_LEN)

def test_get_line_length_max(self):
presenter = draw_berkeley_bart.StationVizPresenter(self._sketch)
self.assertEqual(presenter._get_line_length(100, 100), draw_berkeley_bart.LINE_MAX_LEN)

def test_get_line_length_half(self):
presenter = draw_berkeley_bart.StationVizPresenter(self._sketch)
halfway = (draw_berkeley_bart.LINE_MAX_LEN + draw_berkeley_bart.LINE_MIN_LEN) / 2
self.assertEqual(presenter._get_line_length(100, 50), halfway)

0 comments on commit b8da233

Please sign in to comment.