Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .github/workflows/screenshots.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Update chart screenshots

on:
workflow_dispatch:
inputs:
corporate_travel:
description: Include corporate-travel (requires the external source to respond)
type: boolean
default: true

permissions:
contents: write
pull-requests: write

concurrency:
group: chart-screenshots
cancel-in-progress: false

jobs:
render:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: '3.12'
cache: pip
- run: python -m pip install -r requirements.txt
- name: Render charts
env:
INCLUDE_CORPORATE_TRAVEL: ${{ inputs.corporate_travel }}
run: |
if [ "$INCLUDE_CORPORATE_TRAVEL" = "true" ]; then
python render.py
else
python render.py --charts pay food richest-by-age store-sales
fi
- uses: peter-evans/create-pull-request@v8
with:
branch: automated/chart-screenshots
commit-message: Update chart screenshots
title: Update chart screenshots
body: |
Regenerated with render.py on Python 3.12.
Corporate travel included: ${{ inputs.corporate_travel }}.
Please review the image changes before merging.
add-paths: |
*/screenshot.png
pay/zoomed.png
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,30 @@ Company Data

<img src="store-sales/screenshot.png" width="49%"></img>
<img src="corporate-travel/screenshot.png" width="49%"></img>

## Regenerate screenshots

Use Python 3.12, install `requirements.txt`, then run `python render.py`.
This saves PNGs without opening a window, including the pay detail view.
The scripts still support interactive use when run individually.

`corporate-travel` fetches 100 company pages. Requests have timeouts and HTTP
or missing-data failures stop the run before any existing screenshots are
replaced. The source can deny automated requests (HTTP 403); resolve source
access before retrying. To explicitly render only local datasets, use:

```
python render.py --charts pay food richest-by-age store-sales
```

Use `--output-dir /path/to/previews` to inspect outputs elsewhere first.
The charts retain their original historical datasets; regeneration does not
make them current. The pay detail view uses x=85..753 and y=63..248 to approximate
the existing screenshot's visible region.

In GitHub Actions, manually run **Update chart screenshots**. It renders all
charts by default; uncheck `corporate_travel` to leave that image unchanged.
Successful runs create or update a PR on `automated/chart-screenshots` when
images differ. Enable **Allow GitHub Actions to create and approve pull
requests** in the repository's Actions settings. This workflow opens PRs;
it does not approve or merge them.
9 changes: 7 additions & 2 deletions corporate-travel/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,14 @@
for line in s.splitlines():
if 'McKinsey' in line:
line += ' '
response = SESSION.get("https://www.businesstravelnews.com/Corporate-Travel-100/2018/" + re.sub('\s*&\s*', '-', line).replace(' ', '-').replace('.', '').replace('Intel', 'Intel1')).text
response = SESSION.get("https://www.businesstravelnews.com/Corporate-Travel-100/2018/" + re.sub(r'\s*&\s*', '-', line).replace(' ', '-').replace('.', '').replace('Intel', 'Intel1'), timeout=(10, 30))
response.raise_for_status()
response = response.text
volumeStrings = [line for line in response.splitlines() if "Volume" in line]
volumePerCompany[line] = float(re.search("\$(.*?) million", volumeStrings[0]).group(1))
match = re.search(r'\$(.*?) million', volumeStrings[0]) if volumeStrings else None
if match is None:
raise ValueError(f'No air volume found for {line}')
volumePerCompany[line] = float(match.group(1))

plt.figure(figsize=(8, 15))
plt.barh(*zip(*OrderedDict(sorted(volumePerCompany.items(), key=itemgetter(1))).items()))
Expand Down
46 changes: 46 additions & 0 deletions render.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Render charts without a display; publish files only after every chart succeeds."""
import argparse
from pathlib import Path
import runpy
import shutil
import tempfile

import matplotlib

matplotlib.use('Agg')
import matplotlib.pyplot as plt

ROOT = Path(__file__).resolve().parent
CHARTS = ('pay', 'food', 'richest-by-age', 'store-sales', 'corporate-travel')


def render(charts, output):
with tempfile.TemporaryDirectory() as directory:
staged = Path(directory)
for chart in charts:
target = staged / chart
target.mkdir(exist_ok=True)
try:
runpy.run_path(str(ROOT / chart / 'main.py'), run_name='__main__')
plt.savefig(target / 'screenshot.png')
if chart == 'pay':
# Reproduce the detail view in the existing pay/zoomed.png.
plt.gcf().set_size_inches(20.48, 10.24)
plt.xlim(85, 753)
plt.ylim(63, 248)
plt.savefig(target / 'zoomed.png')
finally:
plt.close('all')
for source in sorted(staged.glob('*/*.png')):
destination = output / source.relative_to(staged)
destination.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(source, destination)
print(destination)


if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--charts', nargs='+', choices=CHARTS, default=CHARTS)
parser.add_argument('--output-dir', type=Path, default=ROOT)
args = parser.parse_args()
render(args.charts, args.output_dir)
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
requests==2.22.0
matplotlib==3.1.1
requests==2.34.2
matplotlib==3.11.1
36 changes: 36 additions & 0 deletions test_render.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import tempfile
from pathlib import Path
from unittest.mock import patch

import matplotlib.pyplot as plt

from render import render


def test_failed_chart_preserves_existing_outputs():
with tempfile.TemporaryDirectory() as directory:
output = Path(directory)
previous = output / 'pay' / 'screenshot.png'
previous.parent.mkdir()
previous.write_bytes(b'previous screenshot')

def run_chart(path, **kwargs):
if 'food' in path:
raise ValueError('source unavailable')
plt.plot([1, 2], [3, 4])

with patch('render.runpy.run_path', side_effect=run_chart):
try:
render(('pay', 'food'), output)
except ValueError as error:
assert str(error) == 'source unavailable'
else:
raise AssertionError('Failed chart must fail the rendering run')
assert previous.read_bytes() == b'previous screenshot'
assert not (output / 'pay' / 'zoomed.png').exists()
assert not (output / 'food').exists()


if __name__ == '__main__':
test_failed_chart_preserves_existing_outputs()
print('Failed chart preserves existing outputs: passed')