This is a template repo for building an interactive data app with Evidence using SF 311 data. This project is part of a workshop at Small Data SF 2024.
Please read this full readme before starting.
We recommend using VS Code and installing the Evidence extension.
You can get started by clicking the "Use this template" button at the top right of the page and creating a new repository. Make sure you check the box to include all branches.
You will see some banners in Github prompting you to open a PR for the other branches - ignore these.
Once you have the repo cloned locally, check out the pages/index.md
file for instructions on what to build next.
This template repo includes a DuckDB dataset containing 100k SF 311 records, as well as all required connection setup in Evidence. You don't need to do anything to connect the data source to the Evidence project.
We have hosted a geoJSON file for mapping SF neighborhoods. You will need the URL for this file when creating an AreaMap: https://evd-geojson.b-cdn.net/sf_hoods.geojson
We've broken up the proejct into 6 sections:
- Build a basic summary page
- Build a simple categories page
- Add interactivity to your summary page
- Build a neighborhood explorer page
- Create neighborhood templated page
- Dynamic content generation - volume spikes
At the end of the project, you'll have a fast, fully interactive data app that looks great on desktop, laptop, or mobile devices.
You should get started on the main
branch of this repo.
We have created progress branches - one branch per section - which include the completed content for that section.
At any time if you get stuck and need to jump ahead to catch up, you can check out one of those branches. Branches are named section-1
through section-6
.
You will need to either commit or discard your changes when switching branches, and then can switch like so:
git checkout section-1
- Docs: https://docs.evidence.dev
- Finished Project Repo: https://github.com/hughess/sf311
- Deployed Finished Project: https://sf311.evidence.app/
If you are using this template in Codespaces, click the Start Evidence
button in the bottom status bar. This will install dependencies and open a preview of your project in your browser - you should get a popup prompting you to open in browser.
Or you can use the following commands to get started:
npm install
npm run sources
npm run dev -- --host 0.0.0.0
See the CLI docs for more command information.
Note: Codespaces is much faster on the Desktop app. After the Codespace has booted, select the hamburger menu → Open in VS Code Desktop.
-
Add your first query
- Name it
description
- Calculate
min_date
,max_date
, and case count from thesf311.cases
table
- Name it
-
Add a Details component to explain the dataset
- Use the
Value
component to display the min date, max date, and record count in theDetails
component - Add a nested
Details
component inside to display this explanation of 311 cases:311 is the city's request service for non-emergencies. Citizens can report local issues to 311 and monitor the status of the request. Each case represents something that was reported through this service.
- Use the
-
Add summary BigValues
- Add a query called
summary
to gettotal_cases
andopen_cases
(wherestatus='Open'
) - Add a
BigValue
for each of those metrics
- Add a query called
-
Add a trend line chart
- Add a query called
trend
which includes date and case count - Use the
opened
column for the date. Since this is a timestamp, you will need to usedate_trunc()
- Add a query called
-
Add a DimensionGrid
- Create a query called
cases
which pulls these columns:responsible_agency
category
request_type
source
- Add a
DimensionGrid
component which uses that data
- Create a query called
-
Add a CalendarHeatmap using your
trend
query
- Add a new page called
categories.md
- Copy and paste these instructions onto that page
- Add a query that pulls the
category
,request_type
, and case count - Add a
DataTable
component that includes conditional formatting for the case count column (see conditional formatting) - Add search to the table
- Change your
DimensionGrid
to become an input by using thename
prop - call itdimensions
- Hook this up to the
LineChart
by adding awhere
clause to your trend query. See docs here: DimensionGrid as an Input - Add the input to the chart title so you get the context when the chart updates. Inputs can be referenced in curly braces like:
{inputs.my_input}
- Add a folder called
neighborhoods
in yourpages
directory. Then add a file calledindex.md
in that folder. - Copy and paste these instructions onto that page.
- Add a page title.
- Build a neighborhood map and make it an interactive input:
- Start with a SQL query that gets neighborhood and case count - call the query
neighborhoods
- Add an
AreaMap
component pulling from that data - Make the map an input using the
name
prop - give it a name ofmap_input
- Start with a SQL query that gets neighborhood and case count - call the query
- Set up a page title that changes to show the selected neighborhood name:
- When not selected, the input will default to true
- You will need to use an if block (If-Else Documentation)
- Title should say "All Neighborhoods" when no neighborhood is selected
- Set up a query to get the case trend for the selected neighborhood:
- Name the query
filtered_trend
- Get the week and case count for that specific neighborhood using a
where
clause - Example: Use Map as Input
- Name the query
- Add a
LineChart
to display the trend data fromfiltered_trend
- Put your
AreaMap
andLineChart
into a Grid with 2 columns:- Add a header above each component (e.g.,
### Neighborhood Selector
above theAreaMap
) - Use the
Group
component to avoid creating new grid cells (see Group Documentation)
- Add a header above each component (e.g.,
- Add a category breakdown:
- Create a query called
category_breakdown
which pulls the category and case count, filtered for the selected neighborhood - Add a
DataTable
to display the results and use conditional formatting for the case count column
- Create a query called
- Create links Evidence will use to auto-generate templated pages
- In
pages/neighborhoods/index.md
, add aDataTable
at the bottom of the page pulling from theneighborhoods
query - Use the
link
prop inDataTable
to set the link to theneighborhood
column
- In
- Create a new file called
[neighborhood].md
inside your neighborhoods folder - Copy and paste these directions onto that page
- Add a title for the neighborhood using the "page param" (Templated Pages Documentation)
- Test by clicking a row in your
DataTable
to navigate to a neighborhood page - Add a summary
BigValue
:- Add a query called
summary
which pulls the case count and close rate (cases where status=closed / total cases
) - Add a
BigValue
component to display the count and close rate (use the comparison for the close rate - Comparisons Documentation)
- Add a query called
- Get the last 100 cases in that neighborhood:
- Call this query
last_100
- Filter for the neighborhood using the page param
- Call this query
- Add a
PointMap
to display the last 100 cases using the latitude and longitude columns:- Try using the URL column in the tooltip to create a link to the SF city site (see Clickable Link and Tooltip Documentation)
- Pull the top 10 categories for the neighborhood:
- Create a query called
top_categories
- Display the results in a horizontal
BarChart
(useswapXY=true
)
- Create a query called
- Create a 2-column Grid and put your
PointMap
andBarChart
inside it:- Adjust the
BarChart
height usingchartAreaHeight
to align with thePointMap
- Adjust the
- Create a dynamic link in your neighborhood explorer by adding this to the bottom of your page title:
[See neighborhood deep dive →](./{inputs.map_input.neighborhood})
- Copy and paste these directions into your
[neighborhood].md
file - Add a query to detect volume spikes in cases over time:
- Call the query
spike_detection
- Use this SQL:
- Call the query
WITH case_counts AS (
SELECT category,
date_trunc('week', opened) AS date,
COUNT(1) AS cases
FROM sf311.cases
WHERE neighborhood = '${params.neighborhood}'
GROUP BY ALL
),
category_stats AS (
SELECT category,
date,
cases,
AVG(cases) OVER (PARTITION BY category ORDER BY date ROWS BETWEEN 30 PRECEDING AND CURRENT ROW) AS rolling_avg,
STDDEV(cases) OVER (PARTITION BY category ORDER BY date ROWS BETWEEN 30 PRECEDING AND CURRENT ROW) AS rolling_stddev
FROM case_counts
)
SELECT category,
date,
cases,
rolling_avg,
rolling_stddev,
(cases - rolling_avg) / rolling_stddev AS stddev,
CASE WHEN cases > (rolling_avg + 2 * rolling_stddev) THEN 'Spike'
ELSE 'Normal'
END AS status
FROM category_stats;
-
Add another query called all_spikes to filter for spikes using the status column (status = 'Spike')
-
Add a section with a title for Volume Spikes in this neighborhood:
- Include a Details component with this definition:
A volume spike is defined as a weekly case count exceeding 2x the standard deviation of the previous 30 weeks.
- Include a Details component with this definition:
-
Pull the distinct list of categories with spikes in a query called categories_with_spikes:
- Sum the cases from the all_spikes query
- Use query chaining to accomplish this
-
Add an if block to display different content depending on whether spikes are present:
- Use categories_with_spikes.length in the if block to check if there are results
- Display text confirming whether there are spikes (e.g., "There are no spikes above 2 standard deviations from the 30-week rolling average")
-
Include a loop of line charts to display spikes:
- Inside the if block, add a Grid with 3 columns
- Inside the Grid, add a loop using an #each block (Loop Documentation)
- Add a query that filters the spike_detection query to show results only for the current category in the loop
- Use a line chart to display those results