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.0See 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.casestable
- Name it
-
Add a Details component to explain the dataset
- Use the
Valuecomponent to display the min date, max date, and record count in theDetailscomponent - Add a nested
Detailscomponent 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
summaryto gettotal_casesandopen_cases(wherestatus='Open') - Add a
BigValuefor each of those metrics
- Add a query called
-
Add a trend line chart
- Add a query called
trendwhich includes date and case count - Use the
openedcolumn 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
caseswhich pulls these columns:responsible_agencycategoryrequest_typesource
- Add a
DimensionGridcomponent which uses that data
- Create a query called
-
Add a CalendarHeatmap using your
trendquery
- 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
DataTablecomponent that includes conditional formatting for the case count column (see conditional formatting) - Add search to the table
- Change your
DimensionGridto become an input by using thenameprop - call itdimensions - Hook this up to the
LineChartby adding awhereclause 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
neighborhoodsin yourpagesdirectory. Then add a file calledindex.mdin 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
AreaMapcomponent pulling from that data - Make the map an input using the
nameprop - 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
whereclause - Example: Use Map as Input
- Name the query
- Add a
LineChartto display the trend data fromfiltered_trend - Put your
AreaMapandLineChartinto a Grid with 2 columns:- Add a header above each component (e.g.,
### Neighborhood Selectorabove theAreaMap) - Use the
Groupcomponent 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_breakdownwhich pulls the category and case count, filtered for the selected neighborhood - Add a
DataTableto 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 aDataTableat the bottom of the page pulling from theneighborhoodsquery - Use the
linkprop inDataTableto set the link to theneighborhoodcolumn
- In
- Create a new file called
[neighborhood].mdinside 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
DataTableto navigate to a neighborhood page - Add a summary
BigValue:- Add a query called
summarywhich pulls the case count and close rate (cases where status=closed / total cases) - Add a
BigValuecomponent 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
PointMapto 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
PointMapandBarChartinside it:- Adjust the
BarChartheight usingchartAreaHeightto 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].mdfile - 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