Data Visualization with Rails & JavaScript
Data Visualization with Rails & JavaScript
Barrett Clark
Foreword
Preface
Acknowledgments
About the Author
Part I ActiveRecord and D3
Chapter 1 D3 and Rails
Your Toolbox—A Three-Ring Circus
Database
Application Server
Graphing Library
Maryland Residential Sales App
Evaluating Data
Data Fields
Simple Pie Chart
Summary
Chapter 2 Transforming Data with ActiveRecord and D3
Pie Chart Revisited
Legible Labels
Mouseover Effects
You Can Function
Bar Chart
New Views, New Routes
Bar Chart Controller Actions
Bar Chart JavaScript
Scatter Plot
Scatter Plot?
Scatter Plot Controller Actions
Scatter Plot Views and Routes
Scatter Plot JavaScript
Scatter Plot Revisited
Box Plot
Quartiles
Boxes, Whiskers, Circles, What?!
Box Plot Data and Views
Box Plot JavaScript
Summary
Chapter 3 Working with Time Series Data
Historic Daily Weather Data
Weather Rails App
Weather Readings Model
Weather Readings Import
Weather Stations Model
Weather Stations Import
Simple Line Graph
Weather Controller
Fetch the Data
The View Files
Draw the Line Graph
Tweak 1: Simple Multiline Graph
Tweak 2: Add Circle to Highlight the Maximum Temperature
Tweak 3: Add Circle to Highlight the Minimum Temperature
Tweak 4: Add Text to Display the Temperature Change
Tweak 5: Add a Line Between the Focus Circles
Summary
Chapter 4 Working with Large Datasets
Git and Large Files
The Cloud
Hotlinking
Benchmarking
Benchmark and Compare
Benchmark All the Things
Querying “Big Data”
Using Scopes in the Model
Adding Indices
When Benchmarks and Statistics Lie
Summary
Part II Using SQL in Rails
Chapter 5 Window Functions, Subqueries, and Common Table
Expression
Why Use SQL?
Database Portability Is a Lie
Tripping Over ActiveRecord
User-Defined Functions
Why?
Heresy!
How?
How to Use SQL in Rails
Scatter Plot with Mortgage Payment
Window Functions
Window Functions Greatest Hits
Lead and Lag
Partitions
First Value and Last Value
Row Number
Using Subqueries
Common Table Expression
CTE and the Heatmap
The Query
The Controller and View
The JavaScript
Summary
Chapter 6 The Chord Diagram
The Matrix Is the Truth
Flight Departures Data
Departures App
Airports
Carriers
Departures
Transforming the Data
Fetching the Data
Generating the Matrix
Finalizing the Matrix
Create the Views
Departures Controller and Routes
Departures View
Departures Style
Draw the Chord Diagram
Disjointed City Pairs
Using the Lead Window Function to Find Empty Leg Flights
Optimizing Slow Queries with the Materialized View
Draw the Disjointed City Pairs Chord Diagram
Summary
Chapter 7 Time Series Aggregates in Postgres
Finding Flight Segments
Creating a Series of Time
Turning Data into Time Series Data
Graphing the Timeline
Basic Timeline
Fancy Timeline
Summary
Chapter 8 Using a Separate Reporting Database
Transactional versus Reporting Databases
Worker Processes
Postgres Schemas
Working with Multiple Schemas in Rails
Defining the Schema Connection
Creating a New Schema
Creating Objects in the Reporting Schema
Materialized View in the Reporting Schema
Tables in the Reporting Schema
Summary
Part III Geospatial Rails
Chapter 9 Working with Geospatial Data in Rails
GIS Primer
It’s (Longitude, Latitude) Not (Latitude, Longitude)
Decimal Degrees
Degrees, Minutes, Seconds (DMS)
Datum
Map Projection
Spatial Reference System Identifier (SRID)
Three Feature Types
PostGIS
Postgres Contrib Modules
Installing PostGIS
PostGIS Functions
ActiveRecord and PostGIS
ActiveRecord PostGIS Adapter
Rails PostGIS Configuration
PostGIS Hosting Considerations
Using Geospatial Data in Rails
Creating Geospatial Table Fields
Latitude and Longitude
Simple GIS Calculation
Working with Shapefiles
Shapefile Import Schema
Importing from a Shapefile
Shapefile ETL
Update Missing lonlat Data
Summary
Chapter 10 Making Maps with Leaflet and Rails
Leaflet
Map Tiles
Map Layers
Incorporating Leaflet into Rails to Visualize Weather Stations
Using a Separate Rails Layout for the Map
Map Controller
Map Index
Map Data GeoJSON View
Mapping the Weather Stations
Visualizing Airports
Markers
Marker Cluster
Drawing Flight Paths
Visualizing Zip Codes
Updating the Maryland Residential Sales App for PostGIS
Zip Code Geographies
Importing the Zip Code Shapefile
Mapping Zip Codes
Choropleth
Summary
Chapter 11 Querying Geospatial Data
Finding Items within a Bounding Box
What Is a Bounding Box?
Writing a Bounding Box Query
Writing a Bounding Box Query Using SQL and PostGIS
Writing a Bounding Box Query Using ActiveRecord
Finding Items within the Bounding Box
Finding Items Near a Point
Writing the Query
Using ActiveRecord
Calculating Distance
Summary
Afterword
Appendix A Ruby and Rails Setup
Install Ruby
Create the App
More Gems
Config Files
Finalize the Setup
Appendix B Brief Postgres Overview
Installing Postgres
From Source
Package Manager
[Link]
SQL Tools
Command Line
GUI Tool
Bulk Importing Data
COPY SQL Statement
\copy PSQL Command
pg_restore
The Query Plan
Appendix C SQL Join Overview
Join Example Database Setup
Inner Join
Left Outer Join
Right Outer Join
Full Outer Join
Cross Join
Self Join
Index
Foreword
I love data.
I have spent several years working with a lot of different types of data.
Sometimes you control the data collection, and sometimes you have to hunt
down the data you need. Sometimes the data is clean and orderly, and
sometimes it requires a lot of work to clean it up.
What makes data interesting to me is that each project is different. They
each ask something different of you to bring their stories to life. As I
worked through these visualizations I was reminded just how many
different skills and techniques come into play. Everything is aimed at a
singular goal, though—to cut through the clutter and let the data say what it
has to say.
That is what this book is about—giving data a voice.
Audience
This book focuses on looking at data from the perspective of a web
developer. More specifically, I’ll speak from the perspective of a developer
writing Ruby on Rails apps.
This book will make use of the following languages and tools:
• Ruby on Rails (Rails 4.2.6)
• jQuery
• [Link]
• Leafl[Link]
• PostgreSQL
• PostGIS
Do not worry if you are not too comfortable with something on that list
or even anything on the list. I will guide you through the process so that by
the end of the book you feel comfortable with all of them.
Organization
I wrote with the intent of each chapter building on the previous chapter. You
can see in “Structure and Content” how the sections and chapters are
broken up. My goal for readers who want to read the book linearly from
cover to cover is that by the end you feel like you have a solid foundation
for working with data, including geospatial data.
You could also approach this book from the perspective of wanting to see
how to do something. In that case you could look to the Index to find what
you are looking for. You could also look at the “Supplementary Materials”
to see the commits for the three applications that are built through the
course of the book. Feel free to look through the source code and play with
it ([Link]
Supplementary Materials
Throughout the course of this book we will build three Rails applications.
The source code is available so that you verify that you are following along
correctly. The applications are broken up as follows:
Maryland Residential Sales
The first app is residential_sales. It looks at recent real estate data
from the state of Maryland. The repository is available on GitHub at
[Link]
Chapter 1: D3 and Rails
• Initial setup
• Import residential sales
• Draw the pie chart
Chapter 2: Transforming Data with ActiveRecord and D3
• Legible labels and mouseover effects
• You can function
• Bar chart
• Scatter plot
• Scatter plot revisited
• Box plot
Chapter 5: Window Functions, Subqueries, and Common Table
Expression
• Scatter Plot with mortgage pmt
• row_number() window function in console (not in app)
Chapter 10: Making Maps with Leaflet and Rails
• Import zip code shapefile and map zip codes
• Choropleth
Chapter 11: Querying Geospatial Data
• Bounding box in console (not in app)
• Items near a point in console (not in app)
• Calculating distance in console (not in app)
Flight Departures
The third app is departures. It looks at historic flight departure data.
The repository is available on GitHub at
[Link]
Chapter 6: The Chord Diagram
• Initial setup
• Import airports and carriers
• Import flight departures
• Add foreign keys to departures
• Chord diagram
• Disjointed city pair chord diagram
Chapter 7: Time-Series Aggregates in Postgres
• Timeline
• Fancy timeline
Chapter 8: Using a Separate Reporting Database
• Create reporting schema
• Scenic gem and materialized view
• Bulk insert into table in reporting schema
Chapter 9: Working with Geospatial Data in Rails
• Add PostGIS to departures app
• Shapefile import and upsert airports
Chapter 10: Making Maps with Leaflet and Rails
• Map California airports
• Airport marker clusters
• Flight path from CEC to BLH
Conventions
Code in this book appears in a monospaced font. Code lines that are
too wide for the page use the code continuation character ( ) at the
beginning of the continuation of the line.
The cover of this book has my name on it, but there are so many people
who helped directly and indirectly. This is a collection of most of the things
I’ve learned to do with Ruby and data over the years. There have been a
handful of people who were particularly instrumental in my becoming the
programmer I am today.
First and foremost, I appreciate all the love and support that my wife
Allison has given me. I am often distracted by whatever problem I am
trying to solve. Thank you for putting up with me, and for being so patient
as I worked through this book and also tolerating the travel and
conferences.
Many years ago I was a QA analyst. Two women I worked with
suggested I become a programmer. I thought that was too hard and that I
couldn’t possibly do that. Thank you Paula Reidy and Cynthia Belknap for
the initial encouragement.
I did eventually start writing more scripts, and then I started making
websites. One thing led to another, and I was introduced to Ruby. Thank
you Pete Sharum for showing me the Dave Thomas book (Agile Web
Development with Rails) that changed my life. We’ve been coworkers twice
and friends for a long time. Thank you for being a sounding board while I
worked through this book and for helping review it.
I’ve been lucky to have some great managers who gave me space to learn
and entrusted their businesses to my code. I am especially grateful to Curtis
Summers for taking a flyer on me when I didn’t know GIS and teaching me
this wonderful world. Thank you to Mark McSpadden for being so
understanding as I wrote this book.
This book was born out of a talk that I gave at RailsConf 2015 in Atlanta.
Debra Williams Cauley was in the audience and approached me afterward.
Thank you for being there and asking me to undertake this project. I made
several new friends at that RailsConf who have enriched my life. It began
when Nadia Odunayo replied to a tweet asking if anyone wanted to run.
Thank you for becoming my friend and having such great feedback on my
talks and on this book.
Speaking of feedback, there are several people who have helped make
sure that my thoughts made sense and my words were coherent. Thank you
Mary Katherine McKenzie for bringing your energy and perspective to the
project. Thank you Chris Zahn for your statistics knowledge and editing
prowess. Thank you Joe Merante for double-checking my code. Thanks
also to Tiffany Peon for your feedback and for asking great clarifying
questions.
When I got into the GIS section I reached out to Emma Grasmeder and
Julian Simioni to make sure the foundational GIS concepts were sound.
Thank you for not only checking the concepts but also helping make the
chapters flow better.
As the deadline drew near I reached out to a few friends to help read
select chapters. Thank you Jessica Suttles, Charles Maresh, and Coraline
Ada Ehmke for taking chapters at the last minute and providing good
feedback. I also had the support of friends throughout the project. Thank
you David Czarnecki for talking me through the proposal process and
helping me get my bearings when I started writing.
I’ve met so many wonderful people through the Ruby community. There
are so many generous people who are willing to listen and help. I wish I
could thank you all personally. I love this community.
Thank you.
About the Author
Your Rails app generates a lot of data and probably also contains a lot of
data. I want to be able to identify and analyze that data, and be able to
quickly see what it says—and I want to show you to how to do that too.
Before we jump into all of that, let’s first take a step back and look at the
various moving parts in a Rails app. These are the tools that you have in
your toolbox to wrestle data into meaningful information. There are three
key aspects to focus on, so maybe it’s more of a three-ring circus, at least at
times.
Database
The default database for development in Rails on your local machine is
SQLite, but that’s not a database that you would use in production. I prefer
to use PostgreSQL in production, as well as in development on my machine.
Luckily, you can specify what database you want to use when you create a
Rails app.
Why PostgreSQL?
PostgreSQL, or Postgres, is a robust open source relational database. It has
flexible data types, including JSON, DATERANGE, and ARRAY (to name a
few) in addition to the more standard CHARACTER VARYING (VARCHAR),
INTEGER, etc., that enable you to store data easily and with flexibility
Postgres has advanced features, such as window functions, transactions,
PL/pgSQL (SQL Procedural Language), and inheritance (yes, like you have
in OO programming, but with table definitions). These help you ask
interesting and sophisticated questions of the data.
Being open source, Postgres has a user community that adds to, debugs,
and generally improves the database. For that reason, Postgres is easily
expandable using extensions that the community creates, such as PostGIS
for geospatial data, HSTORE for key-value pairs, and DBLINK or
postgres_fdw for connecting to other databases. We talk more
specifically about extensions and PostGIS in Part III, “Geospatial Rails.”
Postgres is easy to install. It’s the default database that Heroku uses, and
Amazon offers Postgres in RDS.
I could go on even more about what makes Postgres so great. It’s a
fantastic database, and I really enjoy using it. In fact, if you put “postgres is
amazing” into the search engine of your choice you’ll find lots of tweets and
blog posts from other people who are also really excited about Postgres
talking about some little nugget that they either just discovered or continue
to find valuable in their work.
Database Alternatives
This book will focus on Postgres, but there are other databases of course. A
lot of people use MySQL. Larger companies may use Oracle or SQL Server.
I’ve used Rails with MySQL, SQL Server, Sybase, and, of course, Postgres.
There are also non-relational databases, nicknamed noSQL such as
MongoDB, Cassandra, and Redis to name a few.
Application Server
There are lots of ways to write web apps. I like Rails as a technology and for
its community.
Why Rails?
Well, I will give you that there is a fair amount of subjectivity here. I have
used Ruby and Ruby on Rails since 2007, so it is something that I feel very
comfortable with.
Rails is a framework that gives a programmer a lot of helpers and
conveniences. Once you understand the conventions you can get an app up
and running quickly. It’s also easy to maintain the database with
ActiveRecord migrations.
Ruby is an enjoyable language. It was created with developer happiness in
mind. I find the Ruby community to be pretty incredible on the whole.
With Ruby and Rails you can write expressive code that reveals the
developer’s intentions. There is not a lot of boilerplate, and it is not a
compiled language. The language gets out of the developer’s way, which
enables them to solve problems more easily.
Graphing Library
I love what Mike Bostock has done and continues to do with D3.
Why D3?
D3 is an incredibly powerful JavaScript library for creating Scalable Vector
Graphics (SVG). That’s fancy jargon that means you can draw shapes, and
they can scale without distortion. D3 enables you to draw any data
visualization you can imagine. You’re not locked into a handful of stock
chart types.
The documentation is very good. There are also hundreds of examples on
the D3 website and many more in blogs and on Stack Overflow. That makes
it easy to find inspiration and also to learn how to make your own
visualizations.
Details on how I set up a Rails app can be found in Appendix A, “Ruby and
Rails Setup.” Details on getting Postgres set up on your computer (or host
server) can be found in Appendix B, “Brief Postgres Overview.”
All of the data in this book is freely available from [Link]. This dataset
can be found at: [Link]
sales-pfa-2012-zipcode-00dc0 or on the Maryland Open Data Portal at
[Link] Download the CSV file. You can
also download it directly from the command line using cURL:
Click here to view code image
Code Checkpoint
To see the code at this stage, go to
[Link]
Evaluating Data
Getting clean data is a rare thing. Look at the file to see the following:
• In what format is the data?
• If you downloaded a CSV file, is the data actually comma-delimited?
• If the file is JSON I will generally try to prettify the file. This makes it
easier to look at the data, and will also tell you if the JSON is valid.
The jq command-line tool is great for this.
• What are the fields and data types?
• Do any of the fields have more than one piece of data in them?
• If you have start and end dates, think about taking advantage of the
DATE-RANGE datatype. You can index DATERANGE and
TSRANGE fields with an index that is optimized for that data, and
there are also special search operators that make it easy to find the
right records based on your date or time needs.
• If you have geographic data (latitude and longitude) think about
whether you will need to do geo queries. If so, plan to use PostGIS.
This may have a bearing on your hosting options.
• Do any of the fields contain data that needs to be cleaned?
As a rule I typically avoid modifying data significantly. I want my data to
mirror the original source as closely as possible. However, a field may have
more than one piece of information in it, or sometimes the formatting won’t
work, so little tweaks are needed to clean things up. A zip code that begins
with a zero and is stored or exported as a number will drop the leading zero,
for example. Money may have a dollar sign that we don’t want to store in the
data. Those are cases where you aren’t changing the meaning of the data.
You’re not creating something new.
Don’t create new data. Let the data stand on its own. If you need to add to
it, and sometimes you may have multiple sources to tie together, try to let
each source have its own voice (database table).
Data Fields
Sometimes you get a data dictionary that defines the fields in the dataset. We
don’t have one in the Maryland Residential Sales data, so we need to make
one. Table 1.1 lists out the headings from the CSV file and also assigns a
datatype to the data. The Ruby Float datatype is represented as Double
Precision in Postgres. The Ruby String datatype is represented as
Character Varying in Postgres.
Table 1.1 Maryland Residential Sales Data Dictionary
Looking at the data dictionary and the data, I see a few things that need to be
tidied up. The field names are inconsistent. I also prefer my database field
names to be all lowercase.
It’s idiomatic to use lowercased, snake-cased field names. Snake case
means that field names with multiple words are separated with an
underscore, like geo_code. This enables us to distinguish between
keywords, which are in all caps, and field names. You can see an idiomatic
example in the following raw SQL query:
Click here to view code image
SELECT field, another_field FROM some_table;
We also have some data that needs to be cleaned up a little. We don’t want
the dollar signs, so we need to strip those out. The data in the Zipcode
field looks good. Always remember to check those. Zip codes will
sometimes be treated as numeric data. When that happens you lose the
leading zeros from Eastern zip codes.
The last field looks like a composite field. There are four different pieces
of data in that field. We already have a zip code field, so we don’t need that
again. We also know that these are all Maryland zip codes. So we just need
to grab the latitude and the longitude and store them in their own (separate)
fields.
The Migration
Now that we know what we want to do with the data we can generate the
migration and write the import process. You can find the steps to create the
Rails app in Appendix A, “Ruby and Rails Setup.”
Click here to view code image
rails generate model sales_figure \
year:integer geo_code:string jurisdiction:string \
zipcode:string total_sales:integer median_value:float \
mean_value:float sales_inside_pfa:integer \
median_value_in_pfa:float mean_value_in_pfa:float \
sales_outside_pfa:integer median_value_out_pfa:float \
mean_value_out_pfa:float latitude:float longitude:float
The backslash at the end of each line in that command is how we tell the
Unix command line that a command continues on the next line.
I usually include the --pretend switch at the end whenever I initially
run a generator so that I can see what it thinks it needs to create and also
whether there will be any errors. If you’re new to Rails, take a look at the
files that are created.
When you are ready to create the table you can run the database
migrations with bundle exec rake db:migrate.
require 'csv'
namespace :db do
namespace :seed do
desc "Import Maryland Residential Sales CSV"
task :import_maryland_residential_sales => :environment do
def float(string)
return nil if [Link]?
Float([Link](/\$/, ''))
end
Don’t be scared by the regular expression or the match. Here’s how that
works.
Click here to view code image
>> zipcoded = "Maryland 21502 (39.64, -78.77)"
=> "Maryland 21502 (39.64, -78.77)"
>> latlng = [Link](/.*(\d{2}\.\d*), (-\d{2}\.\d*).*/)
=> #<MatchData "Maryland 21502 (39.64, -78.77)" 1:"39.64"
2:"-78.77">
>> latlng[1]
=> "39.64"
In the regex we create two buffers with one for the latitude and one for the
longitude. The match just looks for that pattern in the string. If it finds the
pattern, it returns the match and exposes the buffers (1, 2, 3... n). You access
those buffers by their buffer number. The latitude is in the first buffer, so it’s
latlng[1]. The full string parsed by the regex is available in
latlng[0].
Now run the task from the command line:
Click here to view code image
bundle exec rake db:seed:import_maryland_residential_sales
Logging
Visibility is a good thing. Look at any logs automatically generated. I like to
make sure there are no errors first and foremost. I also like to see what is
executed. For example, I like to see what SQL is generated by ActiveRecord
and how long it takes to execute. For any web request you can see how long
the total response took, and how long each component of the request took.
The database and view generation times are both broken out and the total
request time is also logged.
You can also log your own output. In a Rails app you can log to the Rails
log file using the [Link] command. Using puts will print to
STDOUT rather than to a log file. This is beneficial in local development, but
you won’t be able to see that when you deploy to Heroku and run the task
there. Learn more at
[Link]
$ rails console
>> pp [Link]; nil
SalesFigure Load (0.6ms) SELECT
"maryland_residential_sales_figures".* FROM
"maryland_residential_sales_figures" ORDER BY
"maryland_residential_sales_figures"."id" ASC LIMIT 1
#<SalesFigure:0x007fd2c47b0a68
id: 1,
year: 2012,
geo_code: nil,
jurisdiction: "Allegany",
zipcode: "21502",
total_sales: 175,
median_value: 98242.0,
mean_value: 111950.0,
sales_inside_pfa: 172,
median_value_in_pfa: 97250.0,
mean_value_in_pfa: nil,
sales_outside_pfa: 3,
median_value_out_pfa: 999.0,
mean_value_out_pfa: 999.0,
latitude: 39.6476079090005,
longitude: -78.7730260849996,
created_at: Fri, 11 Sep 2015 17:25:02 UTC +00:00,
updated_at: Fri, 11 Sep 2015 17:25:02 UTC +00:00>
=> nil
I also like to run a count on the table to make sure it lines up with what I
expected to be imported. You can use wc -l on the command line to get the
number of lines in a file. Subtract one if there is a header row in the file.
Code Checkpoint
To see the code at this stage, go to
[Link]
That will give you the controller, route, and view files that you need to serve
an index page. Delete the placeholder text in
app/views/residential/[Link]. This file can be
completely empty. We are going to generate the content with JavaScript!
And speaking of the JavaScript, we need to add another route for the
script to request the data we need for the pie chart. Manually add another
route, so that config/[Link] looks like this:
Click here to view code image
[Link] do
get 'residential/index'
get 'residential/data', :defaults => { :format => 'json' }
root :to => 'residential#index'
end
I deleted all the example routes from my version, but it doesn’t hurt anything
to leave them in. I also like to define the root route to be whatever makes
the most sense. In this case it is the residential index action.
The last view-related thing we need to do is add some CSS to style the pie
chart. Place this code in
app/assets/stylesheets/[Link]:
.arc text {
font: 10px sans-serif;
text-anchor: middle;
}
.arc path {
stroke: #fff;
}
def data
totals = [Link](:jurisdiction).sum(:total_sales)
render :json => { :totals => totals }
end
end
The data action asks the database for the sum of the total_sales
column, and it wants that sum grouped by jurisdiction. In other words,
we ask the database for the total sales by county. The SQL generated by that
ActiveRecord grouping and calculation will look something like this:
Click here to view code image
SELECT SUM("maryland_residential_sales_figures"."total_sales") AS
sum_total_sales,
jurisdiction AS jurisdiction
FROM "maryland_residential_sales_figures"
GROUP BY "maryland_residential_sales_figures"."jurisdiction"
Pie Chart JavaScript
Now all we have to do is write a little bit of D3-flavored JavaScript. Mike
Bostock, the creator of D3, has created hundreds of examples to draw
inspiration from. I grabbed the example in Listing 1.3 from
[Link]
You’ll note that this is written in JavaScript rather than CoffeeScript. You
can rename any file that Rails creates with a .coffee extension to have a
.js extension. I put this code in
app/assets/javascripts/[Link].
$(function() {
// From: [Link]
// Set the dimensions
var width = 960,
height = 500,
radius = [Link](width, height) / 2;
Ship It
If everything goes according to plan, when you run the server and go to
[Link] 3000 you will be able to marvel at your amazing pie chart,
which you can also see in Figure 1.1.
Figure 1.1 First Pie Chart
OK, so maybe it’s not “amazing” but it’s a starting point.
Code Checkpoint
To see the code at this stage, go to
[Link]
Summary
We covered a lot of ground in this first chapter. Good, clean data is
fundamentally important. Taking the time to understand your data and work
around the limitations that it brings with it will save you an immense amount
of frustration later.
This chapter was focused on giving you a taste of the three key
components of a Rails data visualization app: the database, the Rails app,
and D3. Refer to Appendix A for more information on setting up your Rails
environment, and Appendix B for more information on setting up Postgres.
We created our first Rails app and loaded a data file. We also created our
first visualization—a pie chart that shows the total sales by county for home
sales in Maryland.
The next chapter will dig even deeper into more visualizations.
Chapter 2. Transforming Data with ActiveRecord
and D3
There are so many good examples of D3 charts ranging from very simple to
very intricate. My typical workflow is to find an existing example that does
generally what I am looking for and use that as my foundation or inspiration.
That’s what we did in the previous chapter.
Once I have the data lined up and the graph in place I can start tweaking
it. That’s exactly what we are going to do with the simple pie chart we made
in the previous chapter.
Legible Labels
I would prefer to have all the labels visible in or near the pie slices. I want to
avoid having a legend with 24 items in it for this pie chart. That would be a
big legend that would steal focus from the chart itself.
We can move the labels outside the pie chart fairly easily, and we can even
highlight a slice (and its label) when you hover over the slice with your
mouse. That’s pretty helpful. If you wanted to go even further you could add
a tooltip that appears and gives even more information, but we are going to
hold off on that for now.
In the section of the JavaScript where we add the labels (inside the
$.getJSON block toward the bottom) we need to create another arc outside
the existing arc that we’ve drawn for the pie chart. Attach the label to the
new arc rather than the pie chart’s arc. We do that by replacing the existing
label creation code with the following:
Click here to view code image
// put the labels outside the pie (in a new arc/circle)
var pos = [Link]().innerRadius(radius +
20).outerRadius(radius + 20);
[Link]("text")
.attr("transform", function(d) {
return "translate(" + [Link](d) + ")";
})
.attr("dy", ".35em")
.style("text-anchor", "middle")
.text(function(d) { return [Link]; });
Mouseover Effects
One last thing that we can do with the slices and labels to help them stand
out is to add some mouseover effects. Adding an effect to the pie slice is as
easy as adding a little CSS to
app/assets/stylesheets/[Link].
[Link] {
&:hover {
opacity: .55;
}
}
With the CSS in place you can see that the opacity of the pie pieces changes
as you move the mouse around the pie chart.
To make the label stand out I want to make the text a little larger. To do
that we add some mouseover and mouseout event handlers to the
section of the function where we generate the pie slices. The bold part is the
new code.
Click here to view code image
// make each pie piece
var g = [Link](".arc")
.data(pie([Link](totals)))
.enter().append("g")
.attr("class", "arc")
.on("mouseover", function(d) {
[Link](this).select("text").style("font-weight", "bold")
[Link](this).select("text").style("font-size", "1.25em")
})
.on("mouseout", function(d) {
[Link](this).select("text").style("font-weight", "normal")
[Link](this).select("text").style("font-size", "1em")
})
;
With those two tweaks you will now see the color fade a little for each slice
as you hover, and the label will also stand out a little more.
As you hover around you may see the labels do not return to their original
size. We can tell the page what 1em means by setting the font size for the
body. Simply add “body,” before .arc text at the beginning of
app/stylesheets/[Link] to also apply the style to the
body.
Code Checkpoint
To see the code at this stage, go to
[Link]
You Can Function
There is one last change that we need to make before we can move on from
this pie chart example. The code to generate the chart is sitting in the open
on the global scope. When the main document is ready, all the JavaScript on
the global scope will be called. That’s not what we want. We want the view
to decide when it is ready to ask for a chart and which chart it wants to ask
for.
We need to wrap the JavaScript in a function of its own, and we need to
update the view to ask for that function. The function does not need to take
any parameters, so let’s just call it makePie, and then we have the view ask
for makePie() when the page has loaded. You can see the final
[Link] in the next listing.
The final version of the pie chart can be seen in Figure 2.1.
Click here to view code image
<!-- A pie chart will magically appear here -->
<script>
$(document).on('ready page:load', function(event) {
// apply non-idempotent transformations to the body
makePie();
});
</script>
Figure 2.1 Final Pie Chart
The final JavaScript to render the pie chart is shown in Listing 2.1.
function makePie() {
// From: [Link]
// Start by defining some basic variables
var width = 600,
height = width,
radius = width / 2.5,
totals = {},
// D3 provides a handful of color pallets
color = [Link].category20b();
// This is the circle that the pie will fill in
var arc = [Link]()
.outerRadius(radius - 10)
.innerRadius(0);
The G Element
You may be wondering what that G element that we added to
the page along with the SVG element is all about.
The <g> element is just an SVG element that is used to
group shapes together. You can transform the whole group as a
single shape. In the case of the pie chart before, we added the
individual pie slices to that G element. As we build more
complex charts the transformations will apply more broadly,
such as moving all shapes to allow room for a wider axis label.
Code Checkpoint
To see the code at this stage, go to
[Link]
Bar Chart
The pie chart can show proportions relative to each other. A bar chart can do
this as well. This time let’s look at individual zip codes within one of the
counties.
<script>
$(document).on('ready page:load', function(event) {
// apply non-idempotent transformations to the body
makeBar();
});
</script>
Next, you’ll need to add these routes for the view and the data.
Click here to view code image
get 'residential/bar_chart'
get 'residential/bar_data', :defaults => { :format => 'json' }
The final view-related piece that we need to add is some style to make the
bar chart look nice. Add this to
app/assets/stylesheets/[Link]:
Click here to view code image
// Bar Chart
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.bar {
fill: steelblue;
&:hover {
opacity: .85;
}
}
.[Link] path {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
Bar Chart Controller Actions
We need to write the ActiveRecord finder call to get the data for our bar
chart, which you can see in the following. At first I had the data sorted just
by zip code, which gives you a jagged bar chart. I think it’s probably easier
to see the bars in order of median value (the Y-axis). Feel free to play with
the query and see what works for you.
Click here to view code image
function makeBar() {
// From: [Link]
var margin = {top: 20, right: 20, bottom: 50, left: 50},
width = 960 - [Link] - [Link],
height = 500 - [Link] - [Link];
$.getJSON('/residential/bar_data', function(data) {
data = data.bar_data;
[Link]([Link](xValue));
[Link]([0, [Link](data, yValue)]);
[Link]("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.selectAll("text")
.attr("x", 8)
.attr("y", -5)
.style("text-anchor", "start")
.attr("transform", "rotate(90)");
[Link]("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Median Value");
[Link](".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.style("fill", "blue")
.attr("x", xMap)
.attr("width", [Link])
.attr("y", yMap)
.attr("height", function(d) { return height - yMap(d);
});
});
}
Code Checkpoint
To see the code at this stage, go to
[Link]
Scatter Plot
Pie and bar charts are pretty standard fare. They’re like the glazed and
chocolate donuts in the donut store. You have to have them, and they see a
lot of action. They aren’t always quite what you’re looking for, though.
Sometimes you want a donut with sprinkles. Enter the scatter plot.
The scatter plot uses Cartesian coordinates to display values for two
variables. If you don’t recognize “Cartesian coordinates” by name, it refers
to x, y coordinate pairs.
Scatter Plot?
A scatter plot helps you see the relationship, if any, between two continuous
variables. Unlike other charts where the X-axis is treated as the independent
variable (the variable that has an effect on the other variable) and the Y-axis
shows the dependent variable, the scatter plot simply shows correlation. If
there is also causation you would follow the dependent/independent axis
convention and probably add a line of best fit such as a regression line. In a
simple scatter plot, the X and Y axes have no particular meaning—either one
could be used for either variable.
.tooltip {
position: absolute;
width: 200px;
height: 28px;
pointer-events: none;
}
Scatter Plot JavaScript
The code to generate our scatter plot can be seen in Listing 2.3. We did not
need to stray too far from the example. Our version is a little simpler
because we don’t need to transform our data. One key difference is that our
legend has 24 entries and is therefore too long to have in the top right corner.
We break the legend into multiple columns with the help of a function nested
within the transform attribute for the legend. I know I said that a legend with
24 items was too big for a chart, but the scatter plot is a lot bigger than the
pie chart. The legend doesn’t steal the focus in this case.
function makeScatter() {
// From [Link]
var margin = {top: 20, right: 20, bottom: 100, left: 150},
width = 960,
height = 500 - [Link] - [Link];
/*
* value accessor - returns the value to encode for a given
data object.
* scale - maps value to a visual display encoding, such as a
pixel position.
* map function - maps from data value to display value
* axis - sets up axis
*/
// setup x
// data -> value
var xValue = function(d) { return d.total_sales;},
// value -> display
xScale = [Link]().range([0, width]),
// data -> display
xMap = function(d) { return xScale(xValue(d));},
xAxis = [Link]().scale(xScale).orient("bottom");
// setup y
// data -> value
var yValue = function(d) { return d.median_value;},
// value -> display
yScale = [Link]().range([height, 0]),
// data -> display
yMap = function(d) { return yScale(yValue(d));},
yAxis = [Link]().scale(yScale).orient("left");
// setup fill color
var cValue = function(d) { return [Link];},
color = [Link].category20b();
$.getJSON('/residential/scatter_data', function(data) {
data = data.scatter_data;
// x-axis
[Link]("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.append("text")
.attr("class", "label")
.attr("x", width)
.attr("y", -6)
.style("text-anchor", "end")
.text("Total Sales");
// y-axis
[Link]("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("class", "label")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Median Value");
// draw dots
[Link](".dot")
.data(data)
.enter().append("circle")
.attr("class", "dot")
.attr("r", 3.5)
.attr("cx", xMap)
.attr("cy", yMap)
.style("fill", function(d) { return color(cValue(d));})
.on("mouseover", function(d) {
[Link]()
.duration(200)
.style("opacity", .9);
[Link]([Link] + "<br/> (" + xValue(d)
+ ", $" + yValue(d) + ")")
.style("left", ([Link] + 5) + "px")
.style("top", ([Link] - 28) + "px");
})
.on("mouseout", function(d) {
[Link]()
.duration(500)
.style("opacity", 0);
});
// draw legend
var legend = [Link](".legend")
.data([Link]())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) {
numCols = 8;
xOff = (i % numCols) * 120 + 50;
yOff = [Link](i / numCols) * 20
return "translate(" + xOff + "," + yOff + ")"
});
You can see the scatter plot with a tooltip visible in Figure 2.3.
Code Checkpoint
To see the code at this stage, go to
[Link]
Next, we’ll add the mouse events to the color squares (rectangles) in the
legend. When you hover over one, all the dots will be hidden, and then just
the dots that correspond to that jurisdiction will reappear. When you mouse
out all the dots will reappear.
Click here to view code image
.style("fill", color)
.on("mouseover", function(d, i) {
name = [Link](/\W+/g, "")
$('.dot').hide();
$('.' + name).show();
})
.on("mouseout", function(d, i) {
$('.dot').show(1);
});
Great! Now we have a scatter plot that we can use to start making sense of
the data for the various jurisdictions. You can see the scatter plots with just
Prince George’s jurisdiction visible in Figure 2.4.
Box Plot
Continuing with the donut shop analogy, donuts are great but sometimes you
want a cinnamon roll. We saw a fair amount of variance in the median
prices. A box plot is a way to look at how tight or varied your data is. You
can also see the outliers more clearly. As we saw with the scatter plot, there
is a lot of variance in the data. It also looks like maybe we have some junk
data.
According to Wikipedia, “The box plot (a.k.a. box and whisker diagram)
is a standardized way of displaying the distribution of data based on the five
number summary: minimum, first quartile, median, third quartile, and
maximum.”
That’s a lot of fancy statistics jargon, but it’s not so scary.
Quartiles
Quartiles simply divide a set of numbers into quarters. Think of the set of
scores as being sorted in order (e.g., 1, 1, 1, 2, 2, 3, 3, 3, 3, 4, 5, 5). One
fourth of the scores will fall into the first quarter of that ordered set, one
fourth will fall into the second quarter, and so forth (see Figure 2.1 for an
example). The median sets the second quartile (Q2) because it is the middle
score. The first quartile (Q1) is the midpoint between the lowest number and
the median, and the third quartile (Q3) is the midpoint between the median
and the highest number. Those 3 points divide the set or ordered numbers
into 4 pieces. If you divided the data into 5 pieces they would be quintiles,
and so on. Just as with the median, if the number of scores in a quartile is
even it could be that you will have two scores of different values at the
midpoint. In that case you add them together and take the average to find the
quartile. For a clear explanation of quartiles go to
[Link]
Boxes, Whiskers, Circles, What?!
When you look at a box plot, also known as a box-and-whisker diagram, you
see a rectangle with a line in the middle of it. That middle line is the median,
and the rectangle shows the space from Q1 to Q2 and Q2 to Q3. The
whiskers (lines) show the variability outside the lower and upper quartiles.
Anything outside that is an outlier and shown as a dot.
The view will call makeBoxplot(). We also need a new route for the box
plot view:
get 'residential/boxplot'
.box line,
.box rect,
.box circle {
fill: steelblue;
stroke: #000;
stroke-width: 1px;
}
.box .center {
stroke-dasharray: 3,3;
}
.box .outlier {
fill: none;
stroke: #000;
}
Box Plot JavaScript
The first thing I always do when I need to make a chart is find prior art to see
some examples. It looks like we need to include some additional code to tell
D3 how to make a box plot. There is a file [Link] that the examples all
have, and is pretty similar. You can grab the file from
[Link]
endor/assets/javascripts/[Link] and save it to
vendor/assets/javascripts. Then, in [Link], require the file
(//= require box). Alternatively, you can include the script in the
application layout where the others are listed.
We also need to calculate interquartile range, which is a fancy way of
saying “the space in the middle” or Q3 - Q1 (again, an excellent explanation
can be found at [Link] Here is the
iqr function.
Click here to view code image
// Returns a function to compute the interquartile range.
function iqr(k) {
return function(d, i) {
var q1 = [Link][0],
q3 = [Link][2],
iqr = (q3 - q1) * k,
i = -1,
j = [Link];
while (d[++i] < q1 - iqr);
while (d[--j] > q3 + iqr);
return [i, j];
};
}
And with that we are now ready to draw some boxes, whiskers, and circles.
Listing 2.4 handles that for us.
We haven’t had to transform the data that we’ve pulled from the database
yet. To calculate the quartiles, we need the data in a particular format,
though. My data is different from the examples, but I like the way they draw
the chart. So I just rearrange my data into the format that they want. I like
using map-reduce to iterate over and transform data.
$.getJSON('/residential/scatter_data', function(d) {
// Create arrays of median values for each jurisdiction
data = d.scatter_data.reduce(function(accum, obj) {
indices = [Link](function(arr) { return arr[0]; });
idx = [Link]([Link]);
value = +obj.median_value;
if (idx > -1) {
accum[idx][1].push(value);
} else {
[Link]([[Link], [value]]);
}
if (value > max) { max = value; }
if (value < min) { min = value; }
return accum;
}, []);
// the x-axis
var x = [Link]()
.domain( [Link](function(d) { return d[0] } ) )
.rangeRoundBands([0 , width], 0.7, 0.3);
// add a title
[Link]("text")
.attr("x", (width / 2))
.attr("y", 0 + ([Link] / 2))
.attr("text-anchor", "middle")
.style("font-size", "18px")
//.style("text-decoration", "underline")
.text("Median Home Sale Value By Jurisdiction");
// draw y axis
[Link]("g")
.attr("class", "y axis")
.call(yAxis);
// draw x axis
[Link]("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (height + [Link])
+ ")")
.call(xAxis)
.selectAll("text")
.attr("x", -5)
.attr("y", 5)
.style("text-anchor", "end")
.attr("transform", "rotate(-45)");
});
}
Aside from transforming the data we didn’t have to diverge too far from the
examples. The [Link] code will sort the arrays of values, so we don’t need
to worry about that.
Now we can clearly see in Figure 2.5 that there is a value that hugs the X
axis. This is probably some sort of default or placeholder data that we don’t
really want or need. We can add a filter to the data easily, like the bolded line
in the following listing.
Figure 2.6 shows the updated box plot with some fairly different lower
quartiles. Go back and see how that change affected the scatter plot too.
Figure 2.6 Updated Box Plot wthout Default Values
Code Checkpoint
To see the code at this stage, go to
[Link]
Summary
In this chapter we revisited the pie chart from Chapter 1, “D3 and Rails,” to
make it more legible and add interactivity. We also looked at three new types
of charts: bar, scatter, and box. We learned some fun statistics terms, and we
were able to look at our data and see how the various zip codes and
jurisdictions compare to each other. The data transformation that we did to
line up the data and then calculate the inter-quartile ranges can also be done
in the database. We will get to that a little later in this book.
Chapter 3. Working with Time Series Data
In the last chapter, we looked at four different types of graphs: pie, bar,
scatter plot, and box plot. In this chapter we slow down a little and focus on
building up a multi-line graph using historic weather data. The data is
available from the Global Historical Climatology Network (GHCN) via
NOAA ([Link]
data/land-based-datasetsglobal-historical-climatology-network-ghcn).
The GHCN provides a rich set of data. There are weather stations around
the world and readings going back as far as 1763. I chose a relatively small
data file from 1836 for this chapter. In that year there were five stations
reporting temperature.
Let’s see what that data looks like!
Code Checkpoint
To see the code at this stage, go to
[Link]
require 'csv'
namespace :db do
namespace :seed do
desc "Import NOAA weather CSV"
task :import_noaa_weather => :environment do
That weird row with the dollar signs takes advantage of a strange holdover
from Ruby’s Perl heritage. The dollar dot variable ($.) tells you the last line
number read from a file.
We want to run the rake task in the context of the Rails app’s Gemfile.
Bundler can help us with that. Execute the rake task by running
Click here to view code image
bundle exec rake db:seed:import_noaa_weather
After you run the rake task, remember to take a look at the data to make sure
it makes sense.
Weather Stations Model
We have an identifier for the weather station in each of the readings. Let’s go
ahead and import the data to tell us what and where the weather stations are.
The migration for the weather stations model follows.
Click here to view code image
rails g model weather_station station_id:index \
latitude:float longitude:float elevation:float state name \
gsn_flag hcn_flag wmo_id
You may notice that we took advantage of the ability to define an index in
the migration. The station ID is a foreign key that we will refer to from the
weather readings. It’s generally a good idea to put an index on any field that
would commonly be used in a JOIN clause, such as foreign keys. We will
talk more about foreign keys in Chapter 6, “The Chord Diagram.”
We aren’t using the dollar dot variable here. Instead we are using
Enumerable#each_with_index to get the line number as we read
lines from the file.
Code Checkpoint
To see the code at this stage, go to
[Link]
I pass in the switch to tell the generator to not create any helper files. The
bulk of what we are going to do is JavaScript.
With that in place we are ready to fetch the data in the controller, which you
can see in Listing 3.3. You’ll note that this find is a little more complex. We
are doing a join to pull in the weather station data. The benefit to doing a
join is that we pull in the related data in the initial query rather than having
to ask for it later, one record at a time. See Appendix C, “SQL Join
Overview,” for more on SQL joins.
In order for Rails to know how to execute the join we need to tell the models
how they are related. A WeatherReading has one WeatherStation.
That association can be expressed by adding this line to the
WeatherReading model:
Click here to view code image
The SQL that is generated when we run the code looks like this:
Click here to view code image
SELECT [Link], [Link], wr.reading_type, wr.reading_value,
wr.source_flag,
[Link], [Link], [Link], [Link]
FROM weather_readings wr
JOIN weather_stations ws on station = ws.station_id
WHERE reading_type = 'TMAX'
ORDER BY reading_type, reading_date
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.line {
fill: none;
stroke: steelblue;
stroke-width: 1.5px;
}
function makeLineChart() {
// based on [Link]
var margin = {top: 20, right: 80, bottom: 30, left: 50},
width = 960 - [Link] - [Link],
height = 500 - [Link] - [Link];
// create the SVG element for the line(s) and feed the data
to it
var station = [Link](".station")
.data(readings)
.enter().append("g")
.attr("class", "station");
// append a label at the end of the line for the reading type
[Link]("text")
.datum(function(d) { return {reading_type:
d.reading_type, value: [Link][[Link] - 1]}; })
.attr("transform", function(d) { return "translate(" +
x([Link].reading_date) + "," + y([Link].reading_value) + ")";
})
.attr("x", 3)
.attr("dy", ".35em")
.text(function(d) { return d.reading_type; });
});
}
Code Checkpoint
To see the code at this stage, go to
[Link]
If you refresh the page at this point you’ll see two lines, and each line will
be labeled at the end with what it represents. That’s fine, but we don’t really
want the category10 color palette. We can represent the maximum and
minimum temperature lines with colors that are more meaningful. Red is
typically “hotter” and blue is typically “colder,” so let’s use that visual cue to
our advantage.
Change the line in makeLineChart() where we define the color
variable to the following:
Click here to view code image
color = [Link]()
.domain(["TMAX", "TMIN"])
.range(["red", "blue"]);
Code Checkpoint
To see the code at this stage, go to
[Link]
function mousemove() {
var x0 = [Link]([Link](this)[0]),
iMax = bisectDate(readings[0].values, x0, 1),
d0Max = readings[0].values[iMax - 1],
d1Max = readings[0].values[iMax],
dMax = x0 - d0Max.reading_date > d1Max.reading_date - x0 ?
d1Max: d0Max;
[Link]("circle.y")
.attr("transform",
"translate(" + x(dMax.reading_date) + "," +
y(dMax.reading_value) + ")");
}
Refresh the page, and you should be able to move the mouse around the
graph and see a circle track along the red line (see Figure 3.3).
Code Checkpoint
To see the code at this stage, go to
[Link]
Then we add that circle to the minimum temperature line like we did for the
maximum temperature before. Put this with its focusMax counterpart:
[Link]("circle")
.attr("class", "y")
.style("fill", "none")
.style("stroke", "black")
.attr("r", 4);
Add mouseover and mouseout events for focusMin that mirror the
focusMax events. For example, here is one:
Click here to view code image
[Link]("display", null);
Finally, we can place the circle on the line where it needs to be for the
mouseover:
Click here to view code image
[Link]("circle.y")
.attr("transform",
"translate(" + x(dMin.reading_date) + "," +
y(dMin.reading_value) + ")");
Figure 3.4 Focus Circles on Both Lines
Code Checkpoint
To see the code at this stage, go to
[Link]
Finally, we fill in the text for the label. Put this at the end of the
mousemove() function:
Click here to view code image
[Link]("text.y1")
.text(delta + '°')
.attr("transform",
"translate(" + x(dMax.reading_date) + "," +
y(dMax.reading_value) + ")");
[Link]("text.y2")
.text(delta + '°')
.attr("transform",
"translate(" + x(dMax.reading_date) + "," +
y(dMax.reading_value) + ")");
Code Checkpoint
To see the code at this stage, go to
[Link]
Refresh the page, and, as in Figure 3.6, you should see the finished version
with the focal points, temperature change label, and a dotted line between
the points.
Code Checkpoint
To see the code at this stage, go to
[Link]
Summary
In this chapter we looked at a new data set—historic weather readings from
the Global Historical Climatology Network. We created a new Rails app for
our weather visualizations. We began with a simple line graph and iterated.
The initial version was the simplest version of a line graph possible. We took
that foundation and made several small updates. I hope that through the
series of updates you were able to come to an understanding of how D3
handles data and visual elements.
Chapter 4. Working with Large Datasets
This chapter focuses more on the data than on its presentation. In the real
world, data can grow, and it can grow quickly. Being able to work with
large data sets and large files that contain the raw data can be a challenge. In
this chapter we discuss version control, storage, performance, and
benchmarking with large data sets in mind.
If you commit a file that is too large you will need to remove it from Git
and your repo’s Git history. Look at BFG ([Link]
cleaner/#usage) and GitHub’s Help
([Link]
history/) for more on how to remove a file from a repo’s history.
The Cloud
Another option for file storage is the cloud. You can put your files in
Amazon’s S3, Google’s gcloud, or a CDN (content delivery network). Some
key benefits to hosting files in the cloud are that they don’t clutter up your
git repo, and the file(s) are not propagated across all of your application
server hosts. You can fetch the file via HTTP or the vendor’s SDK to load
the database.
Hotlinking
You may not need to pull the file down from the remote server at all. All of
the data files used in this book are available from [Link], and we could
just read the file directly from the various providers. The downsides to this
method are that you maintain open connections from the app server to the
remote file and database hosts, and you slurp the entire file into memory.
You could break the process up by pulling the file down using wget or
curl, and then deleting it after the data has been loaded. I would
recommend that for larger files. Hotlinking is an option geared more toward
smaller files, or when you are constrained on the file system. Listing 4.1
shows an example of a rake task that pulls the 1940 weather data, which is
about 14 million records and 500 MB uncompressed, and processes the file
in memory.
Listing 4.1 Rake Task to Fetch and Load a Remote Compressed File
Click here to view code image
CONN = ActiveRecord::[Link]
bulk_insert = lambda { |rows|
sql = <<-SQL.strip_heredoc
INSERT INTO weather_readings (station, reading_date,
reading_type,
reading_value, measurement_flag, quality_flag, source_flag,
observation_time, created_at, updated_at)
VALUES #{[Link](',')}
SQL
[Link](sql)
}
uri =
URI("[Link]
[Link]")
gzip = Net::[Link](uri)
data = ActiveSupport::[Link](gzip)
rows = []
n = 0
[Link](data) do |row|
station = [Link](row[0])
date_parts = row[1].match(/(\d{4})(\d{2})(\d{2})/)
reading_date = [Link]("#{date_parts[1]}-#
{date_parts[2]}- #{date_parts[3]}")
reading_type = [Link](row[2])
reading_value = Integer(row[3])
measurement_flag = [Link](row[4])
quality_flag = [Link](row[5])
source_flag = [Link](row[6])
observation_time = row[7].nil? ? 'NULL' : row[7]
fields = "(#{station}, #{reading_date}, #{reading_type}, "
fields += "#{reading_value}, #{measurement_flag}, "
fields += "#{quality_flag}, #{source_flag}, "
fields += "#{observation_time}, NOW(), NOW())"
rows << fields
n += 1
if [Link] % 10000 == 0
bulk_insert.call(rows)
rows = []
puts "...#{n} rows added"
end
end
bulk_insert.call(rows)
puts "...#{n} rows added"
end
Code Checkpoint
To see the code at this stage, go to
[Link]
Benchmarking
ActiveRecord provides a helper function,
ActiveRecord::[Link]#quote, that will quote strings
for you. It is also smart enough to fill in NULL values where applicable
(instead of an empty string). That’s great, and I was happy to find it.
We have a lot of data to read though, and since we are going to call that
method a lot we should look at how performant it is. Fortunately, Ruby
gives us the ability to run benchmarks in the standard library, so we can
answer these sorts of questions. In fact, I have this block in my .irbrc
file in my home directory so that I can do this easily:
require 'benchmark'
def benchmark(n, &block)
[Link] do |x|
[Link] do
[Link] { [Link] }
end
end
end
Of course you can also break that into multiple lines and evaluate your real
code in the block.
Run bundle install and then go into the Rails console. With this new
tool available, we can do A/B testing like this:
[Link]!
end
Here are the results of the comparison:
Click here to view code image
Calculating -------------------------------------
ActiveRecord::[Link]#quote
8.425k i/100ms
safe_string method 14.442k i/100ms
-------------------------------------------------
ActiveRecord::[Link]#quote
330.841k (± 9.3%) i/s - 1.643M
safe_string method 1.394M (±15.3%) i/s - 6.744M
Comparison:
safe_string method: 1394023.4 i/s
ActiveRecord::[Link]#quote: 330841.5 i/s - 4.21x
slower
The results are in, and it doesn’t look good for ActiveRecord’s helper
method. The quote method is great, but it does a lot that we don’t need.
That slows it down, and when we have a large file to parse we want to do it
as quickly as possible.
[Link]
str = "17630104"
date = [Link](str)
result = [Link]
printer = RubyProf::[Link](result)
[Link](STDOUT)
[Link] 'Date#parse' do
date = [Link](str).to_s(:db)
end
[Link]!
end
Comparison:
Date Parse Regex: 171006.9 i/s
Date#parse: 29571.6 i/s - 5.78x slower
Regular expressions may look like sorcery at times, but as you can see
sometimes the additional complexity can be worth it. There are also online
tools like [Link] that can help craft your regular expression by
testing it in real time.
Code Checkpoint
To see the code at this stage, go to
[Link]
There is a lot going on with those six scopes. Refer back to Listing 3.3 to
see the original finder method that was in the controller. Here is the new,
skinnier, controller.
Click here to view code image
class WeatherController < ApplicationController
def index; end
def data
year = params[:year] || 1836
readings =
[Link].for_station('MILAN').for_year(year).sorted
render :json => { :readings => readings }
end
end
You can see that these scopes have greatly simplified the controller. If you
look at the for_station scope in the model, you can also see that we
were able to nest some of the scopes as we defined them.
Another benefit to scopes is that the model now has some domain
knowledge. It knows some questions that it will need to be able to answer.
We can also ask those questions more easily. We can write tests to make
sure that we get the results that we expect, and we can easily run the query
in the Rails console.
When we run that query we see that it takes a while to run.
Code Checkpoint
To see the code at this stage, go to
[Link]
Adding Indices
Not all of the fields that we use to filter the query are indexed. Specifically,
in the weather_readings table, the reading_date and
reading_type fields are used to filter the query and neither have an
index. The same goes for the name field in the weather_stations
table.
You can EXPLAIN ANALYZE the query to see how the query optimizer
executes the query. When you do that, you’ll see that we do a table scan on
both tables. That is not good. See Appendix B, “Brief Postgres Overview,”
for more information on EXPLAIN ANALYZE and query planning.
Individual Indices
Postgres has a very good query engine. It can use multiple indices when
executing a query. Adding an index is also a very easy thing to do, and we
can use an ActiveRecord migration to do it. Create the migration using the
Rails generator (rails g migration
add_index_to_weather_readings):
Click here to view code image
class AddIndexToWeatherReadings < ActiveRecord::Migration
def change
add_index :weather_readings, :reading_date
add_index :weather_readings, :reading_type
end
end
Using this version of the migration, only a single index will be created. It
will still take some time to index all the data.
Code Checkpoint
To see the code at this stage, go to
[Link]
Summary
I covered a lot of different types of information related to working with
large data sets in this chapter. It is by no means rigorous or complete.
Instead it is intended to shed light on capabilities that you have at your
fingertips.
Explore your codebase. Benchmark and profile things that you think
might be inefficient. Look at your server logs to see query execution times,
and profile those queries.
You don’t have to solve all the problems or refactor all the code, but you
can take a cut at the worst offender—the least efficient code. It could also
save you from kicking off a job that locks up the database, pegs the CPU, or
fills up the disk.
Or so I hear.
Part II: Using SQL in Rails
The first section was dedicated to using ActiveRecord for all data access.
That works really well for the bulk of what you’ll need to do. As your apps
grow and the questions you’ll need to ask of the data get more complex you
may begin to find ActiveRecord starts to get in the way or can’t easily do
what you need it to do.
In Chapter 5, “Window Functions, Subqueries, and Common Table
Expression,” we set the foundation for writing raw SQL and discuss user-
defined functions, window functions, subqueries, and Common Table
Expression. Don’t worry if you don’t know what those are. You will. We
conclude that chapter by building a heatmap to visualize temperatures.
In Chapter 6, “The Chord Diagram,” we create a new Rails app for flight
departure data and build one of my favorite visualizations—the chord
diagram. We continue with the flight departures app in Chapter 7, “Time
Series Aggregates in Postgres,” where we take a look at utilization for a
single airplane—when was it in use and when was it at rest? We build a
timeline to see that.
Finally, in Chapter 8, “Using a Separate Reporting Database,” we learn
how to isolate reporting activity away from regular application activity
using a separate database schema to minimize the effect heavy reporting
queries have on our users.
Let’s get started writing some SQL!
Chapter 5. Window Functions, Subqueries, and
Common Table Expression
This is where I start getting really excited! I love Rails, and I love Postgres.
In this chapter we teach Postgres how to do new tricks by creating our own
functions. We also talk about window functions, which I think are REALLY
cool. Before we jump into that, let’s talk about mixing the use of raw SQL
with an ORM (ActiveRecord in our case). To do this we will bounce
between our two applications a little before settling in at the end of the
chapter to create a visualization called the heatmap.
That seems simple enough, right? We are going to join articles and
categories together. If the relations are specified in the models, ActiveRecord
knows the foreign key to join on. Here is the SQL that is generated for that
statement.
Click here to view code image
You have to think a little harder on that one, don’t you? Article joins to
comments, which joins to guests. Again, the relations are defined in the
models, so ActiveRecord knows the proper foreign keys. I also must confess
that I prefer the older hashrocket style for writing hash key/value. The
kissing colons look strange to me. I digress. Here is that SQL.
Click here to view code image
SELECT articles.* FROM articles
INNER JOIN comments ON comments.article_id = [Link]
INNER JOIN guests ON guests.comment_id = [Link]
So far we are doing pretty well. We’ve had to think a little harder with that
second example, but not too hard. What about this example?
Click here to view code image
Hmmmm. I don’t really know what the expected behavior of all that is
without looking it up. The documentation says this is a nested join, so we are
building a chain of joins. That query is starting to get pretty large and
complicated. Here is what the generated SQL actually looks like.
Click here to view code image
SELECT categories.* FROM categories
INNER JOIN articles ON articles.category_id = [Link]
INNER JOIN comments ON comments.article_id = [Link]
INNER JOIN guests ON guests.comment_id = [Link]
INNER JOIN tags ON tags.article_id = [Link]
I urge you to be thoughtful about your database queries. When you start
generating complex SQL statements with ActiveRecord consider that it may
be worthwhile to simplify and just write the SQL queries. I don’t know
about you, but I had to stop and think on that second example. I had to go to
the documentation on the third example. Yuck.
Future you and other future developers will thank you for being clear and
not making them work harder than necessary to follow the intention of the
code.
You can see more information on joins in Appendix C, “SQL Join
Overview.”
User-Defined Functions
Right. So now that we’ve covered some of the rationale behind why it’s OK
to break away from the comfort of ActiveRecord to write raw SQL, let’s go
ahead and kick over another sacred cow.
You can teach Postgres new tricks. You do this by creating your own
functions. What’s a function, you ask? In other databases it may be called a
Stored Procedure. These user-defined functions simply “execute an arbitrary
list of SQL statements” according to the Postgres documentation.
Why?
You would generally do this when you need to calculate a value with a
complex formula. You can also execute a basic query in a function, but that’s
not actually the most performant way to tackle that. Refer to Appendix B,
“Brief Postgres Overview,” for information on Views and Materialized
Views to read more about that.
Heresy!
Yes, I know suggesting that you break business logic out of your application
and stuff it into the database is heretical. I am not suggesting that you put
ALL of your business logic in the database. I am saying this is a tool that
you have at your disposal, and sometimes you may find it useful.
How?
Here is an example of a very simple, and admittedly contrived, example
from the Postgres documentation. The $$ is another way to quote strings in
Postgres.
Click here to view code image
CREATE FUNCTION one() RETURNS integer AS $$
SELECT 1 AS result;
$$ LANGUAGE SQL;
SELECT one();
one
-----
1
All this function does is return a single one. It is the loneliest function, isn’t
it? We would never need this, but maybe we would want to calculate a
number. The weird dollar signs are quotes.
In the residential sales data we have average home sales amounts. I
wonder what the mortgage payments would look like for those
neighborhoods. The formula to calculate that is
I used CREATE OR REPLACE this time. You’d get an error from Postgres
if you tried to run the migration and the function already existed. This is also
how you could update the function. Note that the function name is defined
with the parameter types. So that function is referred to as pmt(double
precision, integer).
Getting the mortgage payment with the rest of the data is as easy as
including it in the query. Actually, let’s see what the 5 most expensive zip
codes were. We can jump into the Postgres psql console by running rails
dbconsole. See Appendix B, “Brief Postgres Overview,” for more on
SQL editors.
Click here to view code image
SELECT id, year, jurisdiction, zipcode, median_value, pmt(4.25,
median_value::int)
FROM sales_figures
WHERE median_value > 999
ORDER BY 3, 6
LIMIT 5;
The default migration assumes you want to create a new table. That’s not
what we need in this case. This is not a reversible migration, so we need to
specify how to handle both the creation (up) as well as the teardown (down)
of the function. Here is the updated migration:
Click here to view code image
class CreatePmtFunction < ActiveRecord::Migration
def up
# Create the pmt function
sql = <<-SQL.strip_heredoc
CREATE OR REPLACE FUNCTION pmt(
interest double precision,
principal integer)
RETURNS numeric AS $$
SELECT ROUND(
CAST(
(interest/100/12 * principal)
/ (1 - ((1 + (interest/100/12)) ^ -360))
AS numeric), 0)
$$ LANGUAGE SQL;
SQL
execute(sql)
end
def down
# Drop the pmt function
execute("DROP FUNCTION pmt(double precision, integer);")
end
end
Now we can run that query in the console and also in the controller. If you
run it in the console, you’ll see that you get back a PG::Result object.
You can do all the things you’d want to with that. It’s just an array of hashes.
Click here to view code image
>> data = SalesFigure.mortgage_payment_data
(12.6ms) SELECT id, year, jurisdiction, zipcode, total_sales,
median_value, pmt(4.25, median_value::int)
FROM sales_figures
WHERE median_value > 999
ORDER BY 3, 6
Because we’ve sidestepped ActiveRecord to run our query, we don’t get any
of the benefits that it brings to the table. All the values in the result set are
strings because we don’t get any of the type coercion that ActiveRecord
normally does on our model’s fields.
We can now simplify the scatter_data controller method.
Click here to view code image
def scatter_data
data = SalesFigure.mortgage_payment_data
render :json => { :scatter_data => data }
end
And also
Click here to view code image
var yValue = function(d) { return +[Link];}
Code Checkpoint
To see the code at this stage, go to
[Link]
Window Functions
Window functions are not unique to Postgres, but they’re one of the things
that excite me the most in Postgres. In fact, this is what I led off with in the
RailsConf talk that launched this book
([Link] The Postgres
documentation defines window functions as follows:
A window function performs a calculation across a set of table rows
that are somehow related to the current row. This is comparable to
the type of calculation that can be done with an aggregate function.
But unlike regular aggregate functions, use of a window function
does not cause rows to become grouped into a single output row —
the rows retain their separate identities. Behind the scenes, the
window function is able to access more than just the current row of
the query result.
The Postgres documentation is usually pretty good. That definition describes
exactly what a window function is—some calculation performed across a set
of records and placed in the current record. In other words, you’re folding
data from other rows into the current row.
Partitions
There was a little more going on in that query that I hadn’t mentioned yet.
Let’s take another look at the Postgres documentation to see what’s
happening.
A window function call always contains an OVER clause directly
following the window function’s name and argument(s). This is
what syntactically distinguishes it from a regular function or
aggregate function. The OVER clause determines exactly how the
rows of the query are split up for processing by the window
function. The PARTITION BY list within OVER specifies dividing
the rows into groups, or partitions, that share the same values of the
PARTITION BY expression(s). For each row, the window function
is computed across the rows that fall into the same partition as the
current row.
So really, the function is LEAD(field) OVER(). You can omit the
partition, and Postgres will use the full table as the partition.
Row Number
The final window function in my Top 5 list tells you the number of the
current row within its partition. For this example, we are back in the
residential sales app. Type this query into the SQL editor of your choice.
Click here to view code image
SELECT id, jurisdiction, zipcode, median_value,
row_number() OVER(PARTITION BY jurisdiction ORDER BY
median_value)
FROM sales_figures
WHERE median_value > 999
LIMIT 10;
The row_number() looks like it could also be a rank order value, and it
sort of is. There is a subtle difference, though, but we need a slightly more
sophisticated query to highlight it.
Using Subqueries
You can nest a query inside another query. Why would you want to do this,
you ask? You can filter a query by selecting from it. For example, I want to
see all the records where the row_number() and rank() window
functions differ. rank() is generally the same as row_number() except
that where values are equal the rank is equal—like two people being in first
place.
The query looks like this:
Click here to view code image
SELECT * FROM (
SELECT id, jurisdiction, zipcode, median_value,
rank() OVER(PARTITION BY jurisdiction ORDER BY median_value),
row_number() OVER(PARTITION BY jurisdiction ORDER BY
median_value)
FROM sales_figures
WHERE median_value > 999
) AS subq
WHERE rank <> row_number;
We’ve wrapped the main query in another query that limits the results to just
the records where the rank and row_number differ. From there you could
run a query to look specifically at one of those jurisdiction if you wanted to
see the specifics.
The subquery is named, and you could refer to the fields from the
subquery with that namespace if you needed to specify them. I called the
subquery subq, but you could name it whatever or however was most
meaningful in your situation.
Note
If you are unfamiliar with the <> operator, that is the ANSI
SQL standard for “not equal.” Postgres supports both <> and
!= for not equal. I actually use both, but the greater than and
less than keys are closer to each other and easier for me to type.
The real benefit with CTE is the legibility as you compose more
complicated queries. Let’s look and see which zip codes are the most
expensive in each jurisdiction.
Click here to view code image
WITH subq AS (
SELECT id, jurisdiction, zipcode, median_value,
row_number() OVER(PARTITION BY jurisdiction ORDER BY
median_value DESC)
FROM sales_figures
WHERE median_value > 999
), most_expensive_zipcodes AS (
SELECT * FROM subq WHERE row_number = 1
)
SELECT id, jurisdiction, zipcode, median_value
FROM most_expensive_zipcodes
ORDER BY median_value;
id | jurisdiction | zipcode | median_value
-----+-----------------+---------+--------------
3 | Allegany | 21530 | 113500
425 | Wicomico | 21830 | 172450
110 | Caroline | 21629 | 180450
385 | Somerset | 21821 | 192500
413 | Washington | 21756 | 236822
[snip]
27 | Anne Arundel | 21035 | 612500
80 | Baltimore | 21210 | 655100
243 | Howard | 20777 | 797500
398 | Talbot | 21662 | 890000
277 | Montgomery | 20818 | 933500
(24 rows)
The Query
Since we are making a heatmap of daily data, we are essentially making a
colorful calendar. The first thing that we need to make a calendar is the daily
data. Let’s go back to the weather app. Here is a CTE query that creates
daily max temperature readings. I’ve added this in a class method in the
WeatherReading model.
Click here to view code image
def [Link](station)
sql = <<-SQL.strip_heredoc
WITH days AS (
SELECT dt
FROM generate_series(
'18360101'::timestamp, '18361231'::timestamp, '1 day'
) AS dt
), temperature_readings AS (
SELECT id, reading_date, reading_value
FROM weather_readings
WHERE reading_type = 'TMAX'
AND station = '#{station}'
ORDER BY 2
)
SELECT tr.*
FROM temperature_readings tr
RIGHT JOIN days ON reading_date >= [Link] AND reading_date
<= [Link];
SQL
[Link](sql)
end
This query builds up two separate queries and then uses a RIGHT JOIN to
merge them together. The first query (days) uses the Postgres
generate_series function to generate 365 days, one for each day in
1836. I do this because you can never assume that your data is any good.
There could be missing days, and I want every day represented in the data.
The second query (temperature_readings) pulls out all the
maximum temperature values for the given station and sorts them by
reading_date. The RIGHT JOIN then takes all of the days that we
generated and includes any temperature reading records that meet the join
criteria. In this case it happens to be all of the readings, which is nice.
Next, we need to create the views and routes. We can copy the index view
file from Chapter 3, “Working with Time Series Data,” to
app/views/weather/[Link] and change it like we’ve
been doing. The JavaScript function that we are going to call is
makeHeatMap(), so update [Link] to call that function.
The routes that we need are:
Click here to view code image
get 'weather/heatmap'
get 'weather/heatmap_data', :defaults => { :format => 'json' }
.month {
fill: none;
stroke: #000;
stroke-width: 2px;
}
The JavaScript
The last thing we need to do is write the JavaScript to generate the chart. I
found an example that looked at financial data for several years
([Link] It’s not exactly what we need, but it’s
a good start.
We are looking at temperatures, and the reading_value that we get
out of the database needs to be converted. This is the second chart where we
need to do that, so I’ve extracted that calculation out of
makeLineChart() and created a separate function for it.
Click here to view code image
function fahrenheit(celcius) {
return +celcius * 0.1 * 9/5 + 32;
}
function makeHeatMap() {
var width = 960,
height = 136,
cellSize = 17, // cell size
// blue, green, yellow, orange, red
colors = ['#0000FF', '#00FF00', '#FFFF00', '#FFA500',
'#FF0000'],
format = [Link]("%Y-%m-%d"),
decimal = [Link](".1f");
// mouseover title
[Link]("title")
.text(function(d) { return d; });
I didn’t actually need to change too much from the original example. The
main differences are the color scale and how I process the data.
When you go to [Link] it will take a
moment for the colors to fill in. The finished graph looks like what you
might expect. The coldest days were in January, and the hottest days were
late June through August.
Figure 5.2 TMAX Heatmap
Code Checkpoint
To see the code at this stage, go to
[Link]
Summary
In this chapter I started making the case for when it’s OK to venture away
from the conveniences that ActiveRecord provides. Don’t get me wrong, I
love ActiveRecord. It just gets in the way sometimes. Complex joins, for
example, are probably easier to write (and also read) in raw SQL.
There are also more complex queries and deeper database functionality for
which you need to use raw SQL to fully utilize them, such as user-defined
functions and window functions. We created a function to calculate
mortgage payment values and updated the scatter plot to use our custom
function.
We will spend a little more time with window functions soon, but I
wanted to introduce them to you and also use them to show you how
subqueries and Common Table Expression (CTE) work.
Finally, we used CTE as the basis for a heatmap.
Chapter 6. The Chord Diagram
Each category (group) gets a row in the matrix. Each column index
corresponds to a category in that index’s row. Each row shows the
relationship of an entity to each entity in the list. Cell [0][3] gives us the
relationship of the first entity to the fourth entity. If this data represented
flight departures, then there are 2868 flights that departed from Airport 0 and
arrived at Airport 3.
I will talk more about generating the matrix a little later in this chapter.
First let’s have a look at the data and set up a new Rails app.
Departures App
Now that you have the data files it’s time to start working on the app. Create
a new Rails app that uses Postgres as the database:
Click here to view code image
rails new departures --skip-bundle -d postgresql
You can look in Appendix A, “Ruby and Rails Setup,” for more information
on how I generally create and configure a new Rails app. Create a new
folder, db/data_files, move the two CSV files in there, and don’t
forget to run rake db:create.
Code Checkpoint
To see the code at this stage, go to
[Link]
Airports
Before we dive into the departures, we need to load some of the related data.
As you surely know, a flight begins and ends at an airport. We will use the
file provided with the data challenge to load our airports.
That will give you an Airport model and also the migration to create the
airports table. We specify that we want the length of the iata field to
be no more than 4 characters, and we also want to index that field.
require 'csv'
CSV::Converters[:blank_to_nil] = lambda do |field|
field && [Link]? ? nil : field
end
namespace :db do
namespace :seed do
desc "Import airport data"
task :import_airports => :environment do
if [Link] == 0
filename = [Link]('db', 'data_files',
'[Link]')
fixed_quotes = [Link](filename).gsub(/\\"/,'""')
[Link](fixed_quotes, :headers => true,
:header_converters => :symbol, :converters => [:blank_to_nil]) do
|row|
[Link](row.to_hash)
end
end
end
end
end
The really cool thing here is that the CSV library can understand how to do
simple transformations as it reads the data. It can automatically convert
fields to integer (any field that Integer() would accept), float (any field
Float() accepts), date (Date::parse()), datetime
(DateTime::parse()), and any combination of these. You can also
create your own in addition to what the standard library offers, which is what
we do with :blank_to_nil.
We also needed to clean the data a little before we could feed it into CSV.
The CSV library expects quotes that are within strings to be escaped
differently than the way they were escaped in the data. CSV will consider a
double sequence of the quote character to be an escaped quote.
Carriers
The [Link] file is the list of all the airlines. There are a lot of
airlines, but the CSV file is not too large. This airline file also works nicely
with the departure data.
Code Checkpoint
To see the code at this stage, go to
[Link]
Departures
The last file you need to grab, if you haven’t already, is the 1999 departures
data ([Link] The DataExpo
site gives a nice database schema for a SQLite database. Even though we are
using Postgres, we can still use that for guidance. Unfortunately, the data is
not clean. The Airline IATA code (UniqueCarrier) is sometimes too long for
the field. Rather than modify the data, we will make the field long enough to
support the data.
Generate the Model
There are a lot of fields, and there is a lot of data. We are going to need to do
things a little differently here. We will create the model and migration using
this generator:
Click here to view code image
I like this a lot better. Note that this code is not automatically loaded, so you
have to require the file before including it. I put this at the top of the rake file
under the CSV require statement:
require 'db_sanitize'
include DBSanitize
Now we can sanitize (clean) our data as we read it. I tested bulk insert versus
inserting one record at a time with this data. I was surprised to see that the
bulk insert did not save any time. Here is the rake task to import the
departure data:
Click here to view code image
CSV::Converters[:na_to_nil] = lambda do |field|
field && field == "NA" ? nil : field
end
desc "Import flight departures data"
task :import_departures => :environment do
if [Link] == 0
filename = [Link]('db', 'data_files', '[Link]')
timestamp = [Link].to_s(:db)
[Link](
filename,
:headers => true,
:header_converters => :symbol,
:converters => [:na_to_nil]
) do |row|
puts "#{$.} #{[Link]}" if $. % 10000 == 0
data = {
:year => integer(row[:year]),
:month => integer(row[:month]),
:day_of_month => integer(row[:dayofmonth]),
:day_of_week => integer(row[:dayofweek]),
:dep_time => integer(row[:deptime]),
:crs_dep_time => integer(row[:crsdeptime]),
:arr_time => integer(row[:arrtime]),
:crs_arr_time => integer(row[:crsarrtime]),
:unique_carrier => string(row[:uniquecarrier]),
:flight_num => integer(row[:flightnum]),
:tail_num => string(row[:tailnum]),
:actual_elapsed_time => integer(row[:actualelapsedtime]),
:crs_elapsed_time => integer(row[:crselapsedtime]),
:air_time => integer(row[:airtime]),
:arr_delay => integer(row[:arrdelay]),
:dep_delay => integer(row[:depdelay]),
:origin => string(row[:origin]),
:dest => string(row[:dest]),
:distance => integer(row[:distance]),
:taxi_in => integer(row[:taxiin]),
:taxi_out => integer(row[:taxiout]),
:cancelled => boolean(row[:cancelled]),
:cancellation_code => string(row[:cancellationcode]),
:diverted => boolean(row[:diverted]),
:carrier_delay => integer(row[:carrierdelay]),
:weather_delay => integer(row[:weatherdelay]),
:nas_delay => integer(row[:nasdelay]),
:security_delay => integer(row[:securitydelay]),
:late_aircraft_delay => integer(row[:lateaircraftdelay]),
:created_at => string(timestamp),
:updated_at => string(timestamp)
}
sql = "INSERT INTO departures (#{[Link](',')})"
sql += " VALUES (#{[Link](',')})"
ActiveRecord::[Link](sql)
end
end
end
Run the migration and rake task (bundle exec rake db:migrate
db:seed:import_departures). The departures data took about a half
hour to load on my computer. Maybe (hopefully) yours is faster than mine.
Alternately I created a dump file using pg_dump that you can load a little
quicker:
Click here to view code image
I feel inclined to point out, as a matter of perspective, that these immense file
loads are not something that you would do in production very often. In the
“real world” you’d be accumulating these data gradually over time. In
essence we are playing catch-up with those production apps.
Code Checkpoint
To see the code at this stage, go to
[Link]
Foreign Keys
Now that we have the data loaded there is one more thing I want to do with
the departures table. We can use the Rails generator to create a
migration (rails g migration
AddForeignKeysToDepartures). Here are contents of the change
method:
Click here to view code image
add_foreign_key :departures, :carriers, :column =>
:unique_carrier, :primary_key => :code
add_foreign_key :departures, :airports, :column => :origin,
:primary_key => :iata
add_foreign_key :departures, :airports, :column => :dest,
:primary_key => :iata
Why did we do this when we add the relationships in the models with
belongs_to and has_many, you ask? Doing those things is definitely a
good idea. However, neither of them are absolute safeguards. To truly
enforce referential integrity, you have to actually enforce referential
integrity. We could have done this before loading the data, but checking
every record on insert makes data loads take a lot longer. Removing foreign
keys and indexes for a large data load is a common strategy. Just remember
to add them back!
Note
Referential integrity is the concept in a relational database
where relationships among data (tables) should be enforced. It
protects you from leaving orphaned records where you deleted
the record that defines a foreign key’s value.
The other thing to note is that the records for those foreign keys must already
exist. That is why we loaded the airlines and carriers first.
Code Checkpoint
To see the code at this stage, go to
[Link]
This query just gives us the counts. It does not generate a matrix for us. We
need to take those counts and turn them into a matrix.
module DepartureMatrix
def airports_matrix!(counts:)
h_matrix = counts.each_with_object({}) do |record, hash|
hash[record["origin"]] ||= [Link](0)
hash[record["origin"]][record["dest"]] =
Integer(record["count"])
end
airports = h_matrix.[Link]
total = Float(h_matrix.values.flat_map(&:values).sum)
matrix = [Link]([Link]) do |row, column|
origin = airports[row]
dest = airports[column]
h_matrix.fetch(origin, {}).fetch(dest, 0) / total
end
[airports, matrix]
end
end
Next we need to calculate the grand total of all the things. This is how we
will know what percent each individual count represents. We just asked the
hash for its keys, and now we are asking for its values.
Click here to view code image
total = Float(h_matrix.values.flat_map(&:values).sum)
The example builds a matrix with two rows and four columns.
Matrix#build takes up to two parameters for the row and column
counts. If you omit the second parameter, the column count will be set to the
row count. I rely on that behavior in my code to generate a square matrix.
You can run this from the console to see what your matrix looks like. It
should be a large array of arrays.
def departure_matrix
airports, matrix = Departure.departure_matrix
render :json => {
:airports => airports,
:matrix => matrix
}
end
Departures View
The view looks a lot like all the other views in this book. I pass the data
route into the makeChordChart() function so we can use that it for
multiple chord diagrams. This code goes in
app/views/departures/[Link]:
Click here to view code image
<div id="chart"></div>
<script>
$(document).on('ready page:load', function(event) {
// apply non-idempotent transformations to the body
makeChordChart('/departures/departure_matrix.json');
});
</script>
Departures Style
The stylesheet looks very similar to the other stylesheets. Put this code in
app/assets/stylesheets/[Link]:
Click here to view code image
@import url([Link]
family=PT+Serif|PT+Serif:b|PT+Serif: i|PT+Sans|PT+Sans:b);
body {
background: #fcfcfa;
color: #333;
font-family: "PT Serif", serif;
margin: 1em auto 4em auto;
position: relative;
width: 960px;
}
svg {
font: 10px sans-serif;
}
#circle circle {
fill: none;
pointer-events: all;
}
.group path {
fill-opacity: .5;
}
[Link] {
stroke: #000;
stroke-width: .25px;
}
#circle:hover [Link] {
display: none;
}
function makeChordChart(route) {
var width = 720,
height = 720,
outerRadius = [Link](width, height) / 2 - 30,
innerRadius = outerRadius - 24
formatPercent = [Link](".1%"),
color = [Link].category20();
[Link]("circle")
.attr("r", outerRadius);
[Link]("text")
.attr('class', 'chart_title')
.attr("x", 0)
.attr("y", -340)
.attr("text-anchor", "middle")
.style("font-size", "16px")
.text("American Airlines City Pairs (1999)");
[Link]("textPath")
.attr("xlink:href", function(d, i) { return "#group" + i;
})
.text(function(d, i) { return airports[i]; });
The title is hard-coded for American Airlines. If you do explore what the
city pairs for other airlines look like consider a small refactor to allow for
more generic JavaScript.
The chord diagram for American Airlines flights in 1999 can be seen in
Figure 6.2. It may take a few seconds to pull the data and load in the
browser.
Figure 6.2 American Airlines Flights – 1999
Code Checkpoint
To see the code at this stage, go to
[Link]
Disjointed City Pairs
Airlines are very good at resource optimization. An airplane goes from
airport to airport, picking up and delivering passengers at each stop. I
wondered, though, how often does an airplane fly an “empty leg” route? By
that I mean, how often is the destination airport different from the next
origination airport for a given airplane? We have the data that we need to
answer that question!
def down
execute("DROP MATERIALIZED VIEW aa_departures;")
end
end
Now for the Departure model. You’ve seen the full query, and you saw
how we broke it up to create the materialized view. We put the pieces back
together in Departure#disjointed_matrix.
Click here to view code image
def self.disjointed_matrix
sql = <<-SQL.strip_heredoc
SELECT dest, next_origin, count(*)
FROM aa_departures
WHERE dest <> next_origin
AND next_origin IS NOT NULL
GROUP BY 1, 2
ORDER BY 1, 2
SQL
counts = [Link](sql)
airports_matrix!(:counts => counts, :field1 => "dest", :field2
=> "next_origin")
end
You may have noticed that we now have three parameters in the
DepartureMatrix#airports_matrix! call. We need to update that
method. The modified code is below with the updated lines in bold.
Click here to view code image
The field names in the queries are different, but the matrix creation routine is
the same. We can simply pass in the data’s hash key attributes, and
everything still works.
You’ll need to restart the Rails app if you still had it running because
you’ve added new files. Go to [Link] and
you should see a chord chart that looks like Figure 6.3.
Figure 6.3 Disjointed City Pair Chord Chart
Code Checkpoint
To see the code at this stage, go to
[Link]
Summary
In this chapter we created a new Rails app for flight departures. We loaded
three new data files using both Ruby’s CSV library and a custom import rake
task. We learned about formatting options with Ruby’s CSV library and also
the Matrix class from Ruby’s standard library. Finally, we drew two chord
diagrams to look at the relationships between airports.
Chapter 7. Time Series Aggregates in Postgres
For the most part, every bit of your data has a timestamp. Each record (in a
Rails app) probably has a timestamp for when it was created and when it was
last updated. You may want to look at or analyze your data in uniform
chunks of time. Maybe you want to look at hourly sales or daily rainfall, for
example.
This chapter will walk you through how to split your data into uniform
blocks of time, including time segments where there is no data. Armed with
that information we will also draw a timeline using flight departure data
from the previous chapter.
We will use this technique to answer the following question:
What does an airplane’s utilization look like?
Looking at the results you see that Southwest Airlines (WN) owns the top 10
(in this data from 1999):
Click here to view code image
tail_num | unique_carrier | count
----------+----------------+-------
N509 | WN | 3313
N513 | WN | 3309
N502 | WN | 3287
N514 | WN | 3272
N103 | WN | 3271
N501 | WN | 3264
N105 | WN | 3259
N82 | WN | 3256
N63 | WN | 3256
N510 | WN | 3248
(10 rows)
You can see that you get a series of values with one per row. We can use the
generate_series function to generate hourly timestamps like this:
Click here to view code image
SELECT *
FROM generate_series(
'1999-01-01 00:00'::timestamp,
'1999-12-31 23:00'::timestamp,
'1 hour') AS hourly;
hourly
---------------------
1999-01-01 00:00:00
1999-01-01 01:00:00
1999-01-01 02:00:00
[...and so on...]
We need to recast all the integer fields to text because LPAD is a string
function. Put it all together, and it looks like this:
Click here to view code image
WITH hours AS (
SELECT *
FROM generate_series(
'1999-01-01 00:00'::timestamp,
'1999-12-31 23:00'::timestamp,
'1 hour') AS hourly
), flights AS (
SELECT id, year, month, day_of_month, dep_time, arr_time,
(year::text || LPAD(month::text, 2, '0') ||
LPAD(day_of_month::text, 2, '0') || ' ' || LPAD(dep_time::text,
4, '0'))::timestamp AS departure_time,
(year::text || LPAD(month::text, 2, '0') ||
LPAD(day_of_month::text, 2, '0') || ' ' || LPAD(arr_time::text,
4, '0'))::timestamp AS arrival_time,
flight_num, actual_elapsed_time, origin, dest, distance,
tail_num
FROM departures
WHERE tail_num = 'N509'
ORDER BY year, month, day_of_month, dep_time
)
SELECT ROW_NUMBER() OVER (ORDER BY hourly) AS row_id, hourly, id,
departure_time, arrival_time, dep_time, arr_time, flight_num,
actual_elapsed_time, origin, dest, distance, COALESCE(tail_num,
'NA')
FROM flights
RIGHT JOIN hours ON tsrange([Link], [Link] + '1
hour') @> departure_time
WHERE tsrange('1999-01-01 00:00'::timestamp, '1999-01-01
23:59'::timestamp) @> hourly;
I love the tsrange and its operators and have started using the timestamp
range for start and end timestamps.
If you need to set a default value where there were no records, you can use
the COALESCE function like I did with the tail number.
The results of the query look like this:
Click here to view code image
row_id
| hourly | id | departure_time | arriva
l_time | dep_time | arr_time | flight_num |
actual_elapsed_time | origin | dest | distance | coalesce
--------+---------------------+---------+---------------------+--
-------------------+----------+----------+------------+----------
-----------+--------+------+----------+----------
1 | 1999-01-01 00:00:00 | ¤ | ¤ |
¤ | ¤ | ¤ | ¤
| ¤ | ¤ | ¤ | ¤ | NA
2 | 1999-01-01 01:00:00 | ¤ | ¤ |
¤ | ¤ | ¤ | ¤
| ¤ | ¤ | ¤ | ¤ | NA
3 | 1999-01-01 02:00:00 | ¤ | ¤ |
¤ | ¤ | ¤ | ¤
| ¤ | ¤ | ¤ | ¤ | NA
4 | 1999-01-01 03:00:00 | ¤ | ¤ |
¤ | ¤ | ¤ | ¤
| ¤ | ¤ | ¤ | ¤ | NA
5 | 1999-01-01 04:00:00 | ¤ | ¤ |
¤ | ¤ | ¤ | ¤
| ¤ | ¤ | ¤ | ¤ | NA
6 | 1999-01-01 05:00:00 | ¤ | ¤ |
¤ | ¤ | ¤ | ¤
| ¤ | ¤ | ¤ | ¤ | NA
7 | 1999-01-01 06:00:00 | ¤ | ¤ |
¤ | ¤ | ¤ | ¤
| ¤ | ¤ | ¤ | ¤ | NA
8 | 1999-01-01 07:00:00 | ¤ | ¤ |
¤ | ¤ | ¤ | ¤
| ¤ | ¤ | ¤ | ¤ | NA
9 | 1999-01-01 08:00:00 | 2042055 | 1999-01-01 08:20:00 |
1999-01-01 09:28:00 | 820 | 928 | 818
| 68 | SJC | SNA | 342 | N509
10 | 1999-01-01 09:00:00 | 2134606 | 1999-01-01 09:55:00 |
1999-01-01 11:10:00 | 955 | 1110 | 754
| 75 | SNA | OAK | 371 | N509
11 | 1999-01-01 10:00:00 | ¤ | ¤ |
¤ | ¤ | ¤ | ¤
| ¤ | ¤ | ¤ | ¤ | NA
12 | 1999-01-01 11:00:00 | 2041051 | 1999-01-01 11:45:00 |
1999-01-01 12:59:00 | 1145 | 1259 | 841
| 74 | OAK | SNA | 371 | N509
13 | 1999-01-01 12:00:00 | ¤ | ¤ |
¤ | ¤ | ¤ | ¤
| ¤ | ¤ | ¤ | ¤ | NA
14 | 1999-01-01 13:00:00 | 2027659 | 1999-01-01 13:15:00 |
1999-01-01 14:25:00 | 1315 | 1425 | 724
| 70 | SNA | SJC | 342 | N509
15 | 1999-01-01 14:00:00 | 2042206 | 1999-01-01 14:50:00 |
1999-01-01 16:00:00 | 1450 | 1600 | 868
| 70 | SJC | SNA | 342 | N509
16 | 1999-01-01 15:00:00 | ¤ | ¤ |
¤ | ¤ | ¤ | ¤
| ¤ | ¤ | ¤ | ¤ | NA
17 | 1999-01-01 16:00:00 | 2134637 | 1999-01-01 16:20:00 |
1999-01-01 17:31:00 | 1620 | 1731 | 780
| 71 | SNA | OAK | 371 | N509
18 | 1999-01-01 17:00:00 | ¤ | ¤ |
¤ | ¤ | ¤ | ¤
| ¤ | ¤ | ¤ | ¤ | NA
19 | 1999-01-01 18:00:00 | 2041020 | 1999-01-01 18:00:00 |
1999-01-01 19:15:00 | 1800 | 1915 | 836
| 75 | OAK | SNA | 371 | N509
20 | 1999-01-01 19:00:00 | 2027721 | 1999-01-01 19:35:00 |
1999-01-01 20:45:00 | 1935 | 2045 | 795
| 70 | SNA | SJC | 342 | N509
21 | 1999-01-01 20:00:00 | ¤ | ¤ |
¤ | ¤ | ¤ | ¤
| ¤ | ¤ | ¤ | ¤ | NA
22 | 1999-01-01 21:00:00 | 2069352 | 1999-01-01 21:30:00 |
1999-01-01 22:33:00 | 2130 | 2233 | 1091
| 63 | SJC | LAX | 308 | N509
23 | 1999-01-01 22:00:00 | ¤ | ¤ |
¤ | ¤ | ¤ | ¤
| ¤ | ¤ | ¤ | ¤ | NA
24 | 1999-01-01 23:00:00 | ¤ | ¤ |
¤ | ¤ | ¤ | ¤
| ¤ | ¤ | ¤ | ¤ | NA
(24 rows)
Basic Timeline
You know the drill by now. We need to add routes, a view, and controller
actions. Put these methods in DepartureController:
Click here to view code image
def timeline; end
def timeline_data
data = [Link]
render :json => { :data => data }
end
We also need to execute the query. Take the query from the “Turning Data
into Time Series Data” section above and put it in the Departure model:
Click here to view code image
def [Link]
sql = <<-SQL.strip_heredoc
WITH hours AS (
SELECT *
FROM generate_series(
'1999-01-01 00:00'::timestamp,
'1999-12-31 23:00'::timestamp,
'1 hour') AS hourly
), flights AS (
SELECT id, year, month, day_of_month, dep_time, arr_time,
(year::text || LPAD(month::text, 2, '0') ||
LPAD(day_of_month::text, 2, '0') || ' ' || LPAD(dep_time::text,
4, '0'))::timestamp AS departure_time,
(year::text || LPAD(month::text, 2, '0') ||
LPAD(day_of_month::text, 2, '0') || ' ' || LPAD(arr_time::text,
4, '0'))::timestamp AS arrival_time,
flight_num, actual_elapsed_time, origin, dest, distance,
tail_num
FROM departures
WHERE tail_num = 'N509'
ORDER BY year, month, day_of_month, dep_time
)
SELECT ROW_NUMBER() OVER (ORDER BY hourly) AS row_id, hourly,
id,
departure_time, arrival_time, dep_time, arr_time,
flight_num,
actual_elapsed_time, origin, dest, distance, tail_num
FROM flights
RIGHT JOIN hours ON tsrange([Link], [Link] + '1
hour') @> departure_time
WHERE tsrange('1999-01-01 00:00'::timestamp, '1999-01-01
23:59'::timestamp) @> hourly
SQL
counts = [Link](sql)
end
The trick to generating a timeline with D3 is that it’s just an X-axis with
timestamps as the ticks (see Listing 7.1).
[Link]("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.selectAll(".tick text")
.style("text-anchor", "start")
.attr("x", 6)
.attr("y", 6);
[Link]('/departures/timeline_data?date=1999-01-01',
function(error, data) {
var data = [Link];
// reset the X axis to the correct day's hours
var startDate = new Date(data[0].hourly);
var endDate = new Date(data[0].hourly);
[Link]([Link]() + 1);
[Link]([startDate, endDate]);
[Link](".[Link]")
.call(xAxis)
.selectAll(".tick text")
.style("text-anchor", "start")
.attr("x", 6)
.attr("y", 6);
We look at the utilization for a single day. In this example I have the date
hard-coded for January 1, 1999. The code to generate the timeline is fairly
simple because we are able to utilize D3’s X-axis that already understands
how to handle dates.
The timeline is simple, which can be a good thing. It’s perhaps a little too
simple, though. We can give the chart a bit more meaningful context by
showing how long each flight is, and where each flight originated and
landed.
Figure 7.1 presents the timeline.
Code Checkpoint
To see the code at this stage, go to
[Link]
Fancy Timeline
Taking the simple X-axis timeline that we just created, we can define a few
more elements to give it more context. We define a function for drawing a
curved line from the departure time to the arrival time. We also add some
text to the timeline in the form of a chart title, plus a mouseover tooltip.
Listing 7.2 is the revised, full code with new sections noted in bold and
with a double asterisk in the comments.
[Link]("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.selectAll(".tick text")
.style("text-anchor", "start")
.attr("x", 6)
.attr("y", 6);
[Link]('/departures/timeline_data?date=1999-01-01',
function(error, data) {
var data = [Link];
var departures = [Link](function(d) { return [Link]
!== null });
// reset the X axis to the correct day's hours
var startDate = new Date(data[0].hourly);
var endDate = new Date(data[0].hourly);
[Link]([Link]() + 1);
[Link]([startDate, endDate]);
[Link](".[Link]")
.call(xAxis)
.selectAll(".tick text")
.style("text-anchor", "start")
.attr("x", 6)
.attr("y", 6);
Code Checkpoint
To see the code at this stage, go to
[Link]
Summary
In this chapter we dug a little deeper into the departures data to look at
specific airplanes by tail number and how they are utilized. We used to
generate_series to create a series of timestamps that we could join
against the departures data to turn the individual departures for an airplane
into time series data. We then created a simple timeline depicting each
departure for Southwest Airlines tail number N509 on January 1, 1999. We
then took that simple timeline and added curved lines to depict the departure
and arrival times for each flight.
Chapter 8. Using a Separate Reporting Database
Fortunately, there are a couple of ways that you can help isolate your
reporting activities.
Worker Processes
Taking a step back from databases and reporting, one tactic is to take
reports out of the request-response cycle. Put a job on a queue that can be
picked up by a separate process from your main application. Rails has made
this fairly easy to implement with ActiveJob
([Link]
Postgres Schemas
Background processes are fantastic. They won’t be the answer to all of your
problems, though (and they can create some of their own).
Rather than run a big query that transforms complex data on the fly to
generate your metrics, you can transform the data in the background and put
it in a new reporting database. Even better, Postgres schemas offer separate
connections in the same database. Here is how the Postgres documentation
explains schemas:
A database contains one or more named schemas, which in turn
contain tables. Schemas also contain other kinds of named objects,
including data types, functions, and operators. The same object
name can be used in different schemas without conflict; for
example, both schema1 and myschema can contain tables named
mytable. Unlike databases, schemas are not rigidly separated: a
user can access objects in any of the schemas in the database he is
connected to, if he has privileges to do so.
In other words, a database host has databases. A database has schemas. A
schema has tables (and other objects). Access can be controlled to the
databases as well as the schemas.
As long as the user has access to both schemas you are good to go now.
def down
sql = "DROP SCHEMA IF EXISTS reporting;"
execute(sql)
end
end
This will create the schema if it doesn’t already exist. Your database user
will have super user permissions in the new schema. The migration grants
the postgres user access, which ensures that pg_dump and pg_restore
will work. Those are the Postgres utilities to export and import data from
the database. They’re how you create a backup, and are also used when you
run migrations to get the new schema file.
Speaking of the schema file, you need to change its format from Ruby to
SQL. This enables us to have a more complicated database setup than what
the Ruby schema file can communicate. Again, that’s a simple single-line
addition. This time the change is made in config/[Link].
Click here to view code image
config.active_record.schema_format = :sql
With that in place you should be able to run the migration to get your new
schema. If all goes well, you’ll also get a new file called
db/[Link] with all the SQL required to create all of your
database objects. You can delete db/[Link] because it won’t be used
anymore.
Go ahead and run bundle exec rake db:migrate if you haven’t
already to create the new schema. If you run the migration and receive an
error that role postgres does not exist, do the following.
1. Run rails dbconsole.
2. See what roles are specified in your database by running SELECT *
FROM pg_roles.
3. Change the migration to one of the roles available to your user, and
rerun it.
Code Checkpoint
To see the code at this stage, go to
[Link]
We can tell Scenic to make the view a materialized view by passing in the -
-materialized flag. The materialized view will be called
june_departures.
mv db/views/june_departures_v01.sql →
db/views/reporting.june_departures_v01.sql
The materialized view will be created (materialized) when you run the
migration.
Model for a Materialized View in the Reporting Schema
The final thing is optional but a good idea. We can remind ActiveRecord
that the table is in a different schema by providing the fully qualified name
in the model. See the bold line in the following:
Click here to view code image
class JuneDeparture < ActiveRecord::Base
# NOTE: if you do not specify this, the schema is still in the
# search path, so ActiveRecord will still find the table.
# Name collisions between schema would be a problem.
self.table_name = "reporting.june_departures"
def [Link]
[Link].refresh_materialized_view(table_name)
end
end
Code Checkpoint
To see the code at this stage, go to
[Link]
When you run this migration you will get a new table in the reporting
schema. Take a look at the [Link] file to see how the table and
all of its related objects are documented by Postgres.
Ruby’s Lambda
Blocks, procs, and lambdas can be confusing. In general, they
are all what is called a closure, or a bit of code nestled into the
code around it. The contents of the closure are not known
directly by the code around it. The closure executes its code
and returns the results. Closures are not unique to Ruby, either.
Lots of languages have the construct of a closure.
The lambda is commonly thought of as an anonymous
function. You do not declare the lambda like you do the typical
Ruby method. Instead you assign the code to a variable. That
variable can be called, and it will execute its code. A lambda
can take arguments. One of the things that differentiates a
lambda from a proc is that the lambda checks to make sure it
gets the right number of arguments, where a proc does not.
You may be wondering at this point why not just define a
method for the code in the lambda. The reason is that the
lambda can be passed as an argument to a method. You will see
this in action in Listing 8.1.
def timestamp
[Link].iso8601
end
def batch(sql_lambda:, limit: UPDATE_LIMIT, offset: 0,
count:
0)
sql = sql_lambda.call(limit,offset)
if (cmd_tuples = [Link](sql).cmd_tuples) > 0
count += cmd_tuples
if count % (UPDATE_LIMIT*10) == 0
print "\n#{timestamp} records so far: #{count} "
else
print "."
end
batch(
:sql_lambda => sql_lambda,
:limit => limit,
:offset => offset+=limit,
:count => count
)
else
puts "\n#{timestamp} Total records: #{count}"
end
end
Code Checkpoint
To see the code at this stage, go to
[Link]
Summary
In this chapter we took a slight departure from generating visualizations to
discuss how to feed your reporting needs without creating drag on your
application and users. We accomplish this by separating the normal
transactional data and the reporting data into separate database schemas. I
then discussed using the scenic gem to maintain views and materialized
views in the reporting schema. Finally, I discussed creating a table in the
reporting schema and safely bulk-inserting data into the table.
Part III: Geospatial Rails
So far we’ve talked about querying data using ActiveRecord and also using
raw SQL. We’ve done several different data visualizations using that data.
Those visualizations utilized several different types of graphs. All the
applications we built have an additional component that we have not yet
addressed.
They all have location information.
This next section will discuss how to work with geospatial data in Rails.
In Chapter 9, “Working with Geospatial Data in Rails,” we discuss PostGIS,
the Postgres GIS extension. We discuss geographic data types and how to
make use of them in your Rails apps. You also learn how to import data,
specifically a geospatial data format called a shapefile.
In Chapter 10, “Making Maps with Leaflet and Rails,” we create maps in
all three of the apps we’ve built so that we can visualize the geospatial data.
Finally, in Chapter 11, “Querying Geospatial Data,” we dig a little deeper
into geospatial queries and compare spatial queries to their ActiveRecord
counterparts.
Chapter 9. Working with Geospatial Data in Rails
GIS Primer
We begin our intro to geospatial data with a brief overview of some GIS
concepts. This will give us the foundation for the work we will do toward
the end of this chapter and in the final two chapters when we create maps
and look at geospatial SQL queries.
Datum
Think about how you describe where something is. Do you use a reference
point? The book is on the shelf. My office is on this road at this intersection.
The pass went 27 yards from the line of scrimmage. Those are all examples
of where something is in relation to something else.
A datum is simply a reference used for spatial measurements. A
reference point is set, and something’s location is relative to that point. In
North America there are three main datums used:
• NAD27—North American Datum of 1927
• NAD83—North American Datum of 1983
• WGS84—World Geodetic System of 1984
NAD27 and NAD83 are strictly for North America location. NAD27 uses a
reference point on a ranch in Kansas. NAD83 uses 250,000 points as
reference and is much more accurate. WGS84 covers the entire globe and is
used by the U.S. Department of Defense. GPS was developed by the
Department of Defense, and WGS84 is the default datum used for
recreational and commercial GPS.
Map Projection
Imagine a globe with a map in stained glass and a light shining inside. You
can see the image of the map projected on a nearby wall. A map projection
describes how we take the spherical map and display it on a flat surface.
You’ve seen a map where the longitude and latitude lines are a perfectly
square grid. You’ve also seen a map with cutouts and curved longitude and
latitude lines. The latter had a lot less distortion than the former. These are
different map projections. The different map projections can distort the
globe differently.
Point
The simplest feature is a point (X, Y). Remember from “It’s (Longitude,
Latitude) Not (Latitude, Longitude)” that the X-axis is longitude, so a point
is (longitude, latitude). Your phone’s last known location is a point. If you
have an application that stores location data be prepared for lots of writes
and lots of records because it can accumulate quickly.
Line
A line (also known as linestring or polyline) is a sequence of points. Lines
can curve. When you ask for directions from Google Maps (or whichever
map app you prefer), you are shown a linestring for the route.
You can also have a multiline. As you may have guessed, there are
multiple lines in that feature. The Great Wall of China could be an example
of a multiline because it is considered a single feature but is actually
comprised of several disconnected segments.
Polygon
Polygons have at least three sides and must be closed, otherwise it’s just a
line. The border of your city would be represented by a polygon.
Similar to the multiline, you can also have a multipolygon. The state of
Kentucky could be a stored as a multipolygon. There is a disconnected
piece of Kentucky called Kentucky Bend that is surrounded by Tennessee
and Missouri. This geographic feature is known as an exclave.
PostGIS
PostGIS is a spatial database extender for Postgres. It adds support for
geographic objects and enables location (spatial) queries to be run in SQL.
I mentioned in the intro for this section that all of our apps have location
data. That’s not actually true, yet. Two of the apps have longitude and
latitude information. The Maryland residential sales app has a list of zip
codes but no geographic information for those zip codes. Not to worry,
though, we will address that.
Simply having a latitude and longitude is not enough to make our
database understand them as a location coordinate. We need to teach
Postgres how to handle GIS data specifically. For that we turn to the
PostGIS extension.
Installing PostGIS
As with most software, there are multiple ways to install PostGIS. You can
do it manually and compile everything. I am actually not going to go over
that option, though. There is an easier way to install PostGIS from the
command line. Also, you may not need to install PostGIS, and I will discuss
that as well.
The Manual Way
The easiest way to install PostGIS is with your system’s package
management system (homebrew, yum, or apt). These will generally handle
all the dependencies as well. The installation instructions can be found at:
[Link]
PostGIS Functions
PostGIS has a lot of functions. You can see the subset of the list “which a
user of PostGIS is likely to need” in the PostGIS reference
([Link]
The functions that I use the most tend to center around distance and
whether something is within a geometry. We cover PostGIS functions in
more detail in Chapter 11, “Querying Geospatial Data,” but here is a quick
overview to give you a taste of the sorts of things we can do in PostGIS.
ST_GeomFromText
Databases and languages have data types. GIS also has data types. The
geometry data type is the basis for most, if not all, the GIS calculations
you’ll do. There are a lot of constructors that convert one form of input to a
geometry. ST_GeomFromText is how you convert from a point, polygon,
or line to a geometry.
ST_Centroid
The central point of a polygon is called the centroid. Similarly, the
geometric center of a geometry is called the centroid. For lack of better
information, I will use the centroid of a place to calculate distance from a
point to that place.
SELECT round(CAST(ST_DistanceSphere(ST_Centroid(the_geom),
ST_GeomFromText('POINT(-73.985664 40.748441)',4326)) AS
numeric),2) AS dist_meters
You see how much is packed into such a simple task. Once you get the hang
of casting to a geometry datatype, and working with the right units, it flows
a lot more naturally.
Just having the extension available on the database server is not enough.
You have to add the extension to each app’s database specifically. This
installs the geospatial functions in the database.
And with that in place you are ready to start playing with PostGIS!
Code Checkpoint
To see the code at this stage, go to
[Link]
This gives us a point datatype called lonlat that we also told Postgres to
index. You can look at the ActiveRecord PostGIS adapter documentation to
see all of the field types that are supported. Don’t forget to run the
migration (bundle exec rake db:migrate).
before_create :set_lonlat
private
def set_lonlat
[Link] = Factories::[Link](longitude, latitude)
end
This is how we tell RGeo, which the ActiveRecord PostGIS adapter sits on
top of, how the data is projected.
We don’t really need to imagine this model with longitude and latitude,
though, because our airports table fits the bill. Using the code above as
our template, we can update the Airport model to look like this:
Click here to view code image
class Airport < ActiveRecord::Base
module Factories
GEO = RGeo::Geographic.spherical_factory(:srid => 4326)
end
before_create :set_lonlat
def longitude
long
end
def latitude
lat
end
def lonlat
Factories::[Link](longitude, latitude)
end
private
def set_lonlat
[Link] = Factories::[Link](longitude, latitude)
end
end
Shapefile ETL
I have worked in the travel industry for a few years, and I have never seen
two airport lists have the same airports. Let’s see how well the airports that
we just imported from the shapefile line up with the airports we already
have. To do that we will need a query that does a RIGHT OUTER JOIN.
If you need to brush up on your SQL joins take a look at Appendix C, “SQL
Join Overview.”
The query to see all of the airports from both tables looks like this:
Click here to view code image
SELECT [Link], [Link], [Link], [Link]
FROM airports a1
RIGHT OUTER JOIN [Link] a2 on [Link] = [Link]
WHERE [Link] <> 'NONE' AND cntl_twr = 'Y'
ORDER BY [Link], [Link];
Run that query in your favorite SQL editor, and you will see that there are
some airports that we did not have in our existing airports table. We want
those airports. We need those airports. We will have those airports.
We can INSERT the new airports, but starting in Postgres 9.5 we also
gained the ability to update existing records in the same query. That’s right,
we are going to do an UPSERT! We can do this in a migration, too. Use the
Rails migration generator to create a migration (rails g migration
airports_upsert). We will configure this migration to not be
reversible because once we update the data there is no way to undo it.
def down
raise ActiveRecord::IrreversibleMigration
end
end
I begin the query by defining a subquery, in CTE form, to give us the full
list of airports from the shapefile that we might want to insert. Look back at
Chapter 5, “Window Functions, Subqueries, and Common Table
Expression,” to brush up on Common Table Expression (CTE).
The second phase is your standard INSERT INTO query where you
SELECT FROM a table, or in this case, the CTE. It does not exclude
records that already exist because we are taking advantage of the UPSERT
functionality in the final phase of the query.
The final phase is the UPSERT, and it will only work on Postgres 9.5 and
above. For any record that already has a matching iata value, we will
update three fields. The values from the shapefile record are put into an
EXCLUDED record that we can tap into. We will grab the latitude and
longitude from the shapefile airport, and we will also update the
updated_at field to indicate when the record was updated.
If you have an older version of Postgres you will need to run the version
of the SQL in Listing 9.2.
After running the migration we can confirm that the UPSERT worked by
looking at how many records were updated:
Click here to view code image
# SELECT COUNT(*) FROM airports WHERE created_at <> updated_at;
count
-------
431
(1 row)
Update Missing lonlat Data
It is time to come back to the lonlat field and fill in the data for the
existing records. We do this with a simple migration that executes an
UPDATE query. Since we cannot undo the update once it runs this
migration is also not a reversible migration. The contents of the migration
are:
Click here to view code image
class UpdateAirportLonLat < ActiveRecord::Migration
def up
sql = <<-SQL.strip_heredoc
UPDATE airports
SET lonlat = ST_GeomFromText('POINT(' || long || ' ' ||
lat || ')',4326)
WHERE lonlat IS NULL;
SQL
[Link](sql)
end
def down
raise ActiveRecord::IrreversibleMigration
end
end
Now we have all the airports, and we got to use some fairly new Postgres
functionality.
Code Checkpoint
To see the code at this stage, go to
[Link]
Summary
We covered a lot of ground quickly in this chapter. Like I said in the
beginning, this was not meant to be deep coverage of these topics. I wanted
to give you some key highlights of general GIS concepts so that you could
begin to play with them.
With the general foundation set, we went through some key aspects of
PostGIS: what is it, how do you get it, and what can you do with it. You
saw some strange function names, such as ST_Distance, and then you
saw how to use them.
We incorporated PostGIS into a Rails app with just a few simple tweaks.
Thanks to the hard work that has gone into the ActiveRecord PostGIS
adapter, it is as simple as adding a gem and changing the database adapter
name!
Finally, we loaded geographic data into the app’s database by importing a
shapefile. You could have also loaded a file like we did in previous chapters,
and used the callback in the Airport model to handle the geography.
Chapter 10. Making Maps with Leaflet and Rails
In this chapter you learn about creating maps using a JavaScript library
called Leaflet. We update all three of the apps that we have built throughout
the book.
We begin learning about mapping with Leaflet by updating the weather
app to show the location of all the weather stations that reported temperature
in 1836.
Next we map the location of all the airports in California and implement a
clustering strategy to visually simplify areas with high concentrations of
airports. Once we’ve mapped the airports we will draw a flight path between
two airports at opposite ends of the state.
Finally, we update the Maryland residential sales app by importing a
shapefile for the zip codes so that we can map them. Then we transform the
map into a choropleth by adding color to indicate the median value.
Leaflet
Leaflet is a JavaScript library for creating maps. It defines the functionality
for drawing geographic elements on a web page using Scalable Vector
Graphics (SVG). Leaflet has no dependencies and can be used in conjunction
with popular libraries such as D3 and jQuery. Leaflet also has plug-ins for
additional functionality or easier interaction with map content providers. We
will use a handful of plug-ins from Mapbox.
Map Tiles
Leaflet does a lot of cool stuff, but you can’t see any of it without some map
imagery. You don’t just get a giant image of a map to pan and zoom on,
though. You get several sections of the map that Leaflet stitches together
seamlessly to make the full map. These sections are called tiles. You can see
an example of what these tiles look like in Figure 10.1.
Figure 10.1 Rendered Map Tiles
Fresh tiles are served as you pan around. You also get a new set of tiles
when you zoom in or out. Each zoom level has its own set of map tile
imagery. Leaflet handles fetching the tiles and placing them on the page. The
way that GIS software can seamlessly translate map projections and put
them together still amazes me.
Map Layers
A map layer is a way to organize content in your map. The tiles from the
previous section that are stitched together are usually called the base layer.
This is generally the map without additional features added to it. It could be
satellite view, terrain view, or whatever else the provider offers.
You can add layers on top of the base layer with various features or
groupings of features (e.g., roads, traffic, points of interest). A map could
have dozens of layers. Each layer’s background is transparent so they do not
interfere with each other. You can also add controls to your map that enable
someone to turn layers on and off.
Leaflet does not provide a base layer for you but can be used with many
different map tile providers. I’ll be using Mapbox via the Mapbox Leaflet
plug-in in this chapter. Other providers include Bing, Google, and
OpenStreetMap. You can also run your own tile server.
</body>
</html>
The main difference is that the D3 references are removed and two new
references to Mapbox are included. You’ll also notice the additional data
stored in the body tag. We will have an API token for Mapbox that we do not
want to have exposed in our code or in our JavaScript. We can tap into
environment variables like we would for other services, but getting them into
the JavaScript environment takes a little additional setup. The data attribute
on the body tag is how we can expose that data to JavaScript from the server
side. The easiest way to set environment variables in local development is to
put them in a .env file. See Appendix A, “Ruby and Rails Setup,” for more
information on using the .env file.
Security Note
Putting the API token in the environment and pulling it through
the body tag addresses security from the perspective that we do
not put credentials into source code or source code control. We
still have an access token visible in the page, though. You could
view the source and see the access token. If security is a
concern and you want to try to lock down the visibility of
credentials in JavaScript, then you need to expose that data
through another endpoint over SSL or keep the credential on the
server and not exposed to JavaScript at all.
Map Controller
We could put our map in the same controller as the weather charts, but I
want to have completely separate routes. The cleanest way to do that is to
create a new controller (rails g controller map index --
skip-helper). We will need two actions like we have for all of the other
graphs. The first action will serve the view, and we’ve created the basis for it
with the generator. Once the view is loaded, JavaScript will fire and ask for
the data from the second controller action. The Map controller looks like
this:
Click here to view code image
class MapController < ApplicationController
layout "map"
You can see on the second line where we specify that we want to use a
different layout. The index action does not need to do anything. We could
omit it completely, but I like to leave it in to communicate that it is there.
The map_data action executes an SQL query. I did not put the query in
a model or a database view this time. It is not too long, and it is specific to
the mapping aspect of the application. It feels OK here to me. The purpose
of the query is to give us the list of weather stations that reported a
temperature reading in the year 1836. There are five weather stations that
meet those criteria.
Before we leave this section add the route for the map_data action:
Click here to view code image
get 'map/map_data', :defaults => { :format => 'json' }
Map Index
The map index is very simple. In fact, I copied one of the existing views for
a graph and updated the function. I also made sure that we have a target div
to put the map in. The map div has an ID, and generally it is map, but it can
be whatever you want it to be (so long as it’s valid). Here are the contents of
[Link]:
Click here to view code image
<script>
$(document).on('ready page:load', function(event) {
// apply non-idempotent transformations to the body
makeMap();
});
</script>
[Link] "FeatureCollection"
[Link] @stations do |station|
[Link] "Feature"
[Link] do
[Link]! "marker-color", "#9932CC"
[Link]! "marker-symbol", "circle"
[Link]! "marker-size", "small"
[Link] "#{station['station_id']} (#{station['name']})"
end
[Link] do
[Link] "Point"
[Link] [station['longitude'], station['latitude']]
end
end
The second line iterates on the array of @stations that the controller
fetched from the database. Each weather station will become a feature in the
feature layer we are building. Each feature has properties and a geometry.
The properties are where you define how the feature will look and what
information to make available. Here we set the marker color to purple and
put a white circle in the marker. We include a property named title to
identify the feature. You can have as many properties as you need and are
free to call them whatever you like. They are especially helpful for grouping
and filtering features on the map using D3 selections.
The geometry is what makes this GeoJSON. In Chapter 9, “Working with
Geo-spatial Data in Rails,” I discussed some of the geometry data types.
Here you can see that this is a point. It has a single coordinate (longitude,
latitude).
The formatted JSON response looks like this (except that there are five
weather stations in the array):
Click here to view code image
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"marker-color": "#9932CC",
"marker-symbol": "circle",
"marker-size": "small",
"title": "GM000004204 (JENA STERNWARTE)"
},
"geometry": {
"type": "Point",
"coordinates": [
"11.5842",
"50.9267"
]
}
}
]
}
The GeoJSON payload for this set of five points is not particularly large. The
geometry can potentially be very large though. Imagine listing all of the
points required to draw a line around your city, or your country. It would
take a lot of points to make the line look correct. You could remove some of
the points, which would smooth the line some and reduce the data size.
Smoothing also reduces the fidelity (accuracy) of the geometry. When you
are zoomed out you may not notice, but zoom in on a smoothed data set and
you will definitely notice. Smoothing is not a technique that I will cover in
this book. File this away, and keep it in mind when you see a map slowly
paint on your screen. It’s probably working pretty hard.
Code Checkpoint
To see the code at this stage, go to
[Link]
Visualizing Airports
The next app that we will work with is the flight departures app. We worked
with the departure data in Chapter 6, “The Chord Diagram.” Now we are
going to turn our attention to the airports.
Markers
The setup for this app is very similar to the previous app. We can start by
copying the map layout from the weather app into the layouts folder
(app/views/layouts/[Link]) in this app. Update the title
tag in the layout to Airports.
Next we need a new controller, views, and routes. Call the controller
AirportsController. Go back through the steps from the “Map
Controller” section to create the files and routes that you need. You will need
to update the loadURL() call in
app/assets/javascripts/[Link] to use the
airports/map_data.json route. And don’t forget to copy over the
CSS styles.
There are only two files that we need to change now: the controller and
the GeoJSON.
The map_data action in the controller needs to get airports from the
database. That is a lot of airports. You could pull all of the airports and map
them. Leaflet can handle it. It just takes a while to run the query. We do not
want to put all of the airports on the map, though. The GeoJSON payload
gets pretty large with all those features, too, and chances are nobody would
need to see all of the airports at once.
We will instead just get the California airports:
Click here to view code image
@airports = [Link](:state => "CA").where("iata !~ '[0-
9]'")
The second where condition filters airports with numbers in the IATA code.
That won’t catch all the smaller airports, but it catches a lot of them. The
GeoJSON view for the airports looks like this:
Click here to view code image
[Link] "FeatureCollection"
[Link] @airports do |airport|
[Link] "Feature"
[Link] do
[Link]! "marker-color", "#9932CC"
[Link]! "marker-symbol", "circle"
[Link]! "marker-size", "small"
[Link] "#{[Link]} (#{[Link]})"
end
[Link] do
[Link] "Point"
[Link] [[Link], [Link]]
end
end
Code Checkpoint
To see the code at this stage, go to
[Link]
Marker Cluster
A marker cluster takes several markers that are close together and
consolidates them into a single unit. Generally, you see this as a circle with a
number. Leaflet can handle this for us with the Marker Cluster plug-in. We
just need to update two files.
First we update the map layout file to include the marker plug-in script
and style files. Put these after the Mapbox JavaScript and stylesheet files in
the map layout:
Click here to view code image
<%= javascript_include_tag
'[Link]
markercluster/v0.4.0/[Link]' %>
<%= stylesheet_link_tag
'[Link]
markercluster/v0.4.0/[Link]' %>
<%= stylesheet_link_tag
'[Link]
markercluster/v0.4.0/[Link]' %>
function makeMap() {
// initialize the map on the "map" div with a given center and
zoom
[Link] = $('body').data('mapboxToken');
var map = [Link]('map', '[Link]')
.setView([39.045753, -76.641273], 9);
[Link]('/airports/map_data.json').on('ready',
function(e) {
var clusterGroup = new [Link]({
maxClusterRadius: 35
});
[Link](function(layer) {
[Link](layer);
});
[Link](clusterGroup);
[Link]([Link]());
});
}
I rearranged the script a little and took advantage of the fact that Mapbox
gives us the capability to pass in a file or a route to
[Link] for the GeoJSON. Rather than define a
feature layer for all the markers, we define a marker cluster group. Each
cluster is added to the map as a layer. We then get the bounding box for all
the clusters together in the cluster group to re-center the map.
Refresh your browser and you should see Figure 10.4.
Figure 10.4 California Airport Markers with Marker Clusters
That’s much better. Now we can see where the airports are and how
concentrated they are. When you mouseover one of the clusters you can see
the boundary represented by that cluster highlighted, like in the Los Angeles
area in Figure 10.4.
Code Checkpoint
To see the code at this stage, go to
[Link]
When you refresh the map you should see a slightly curved line that goes
from an airport at the Northern end of California (CEC - Jack McNamara
airport) to the Southern end (BLH - Blythe airport) as in Figure 10.5.
Code Checkpoint
To see the code at this stage, go to
[Link]
Visualizing Zip Codes
The final app we will work with is the Maryland residential sales app. The
last thing that we did with this data was in Chapter 5, “Window Functions,
Subqueries, and Common Table Expression,” where we created a scatter plot
of each zip code and its median sales value. We can also visualize the sales
data by zip code on a map, and we can use color to signify median value by
zip code.
Be sure you use your database username instead of mine, and change the file
path if you put the .shp file elsewhere. You should see INSERT 0 1
printed to your terminal several times if the import works.
When the import finishes you will have a new zip codes table that you do
not have a migration for. The database structure file is out of sync with the
database, so let’s get it caught up by running:
Click here to view code image
bundle exec rake db:structure:dump
We also want a model file so we can make full use of the data in
ActiveRecord and Rails. We use the Rails model generator to accomplish
this, and we tell it to skip the migration because the table already exists.
Click here to view code image
rails g model zipcode --skip-migration
Now our app is ready to work with the geospatial zip code data. To confirm
that the re-projection worked we can look at the centroid of one of the zip
codes:
Click here to view code image
Zipcode.find_by(:zcta5ce10 => "21529").[Link]
I got it wrong a couple of times while working through this and saw
coordinates in the 20,000 range! They did not map well.
View Files
We will use the application layout in this app. Add these two lines after D3
is included:
Click here to view code image
<%= javascript_include_tag
"[Link] %>
<%= stylesheet_link_tag
"[Link] %>
jQuery, D3, and Leaflet can all happily co-exist. Be sure that you also add
the Mapbox token in the body tag of the layout:
Click here to view code image
<body data-mapbox-token="<%= ENV['MAPBOX_TOKEN'] %>">
The Data
Now we need to write the query to retrieve and format the data. You can put
the query in the model or in the controller. In this app I went with a class
method in the SalesFigure model:
Click here to view code image
def self.zipcode_data
sql = <<-SQL
SELECT [Link], [Link], [Link],
sales.total_sales,
sales.median_value, [Link], z.statefp10, z.zcta5ce10,
z.geoid10,
z.classfp10, z.mtfcc10, z.funcstat10,
ST_AsGeoJSON([Link])::JSON AS geometry
FROM sales_figures sales
JOIN zipcodes z ON [Link] = z.zcta5ce10
SQL
[Link](sql)
end
The only other thing to do now is format the data. We will use JBuilder
again. This is what app/views/map/map_data.[Link]
looks like:
Click here to view code image
[Link] "FeatureCollection"
[Link] @zipcodes do |zipcode|
[Link] "Feature"
[Link] Integer(zipcode["id"])
[Link] do
[Link] zipcode["zipcode"]
[Link] zipcode["jurisdiction"]
json.total_sales Integer(zipcode["total_sales"])
json.median_value Integer(zipcode["median_value"])
end
[Link] [Link](zipcode["geometry"])
end
In this response we send a little more data back in the properties section
of the GeoJSON. That gives the front-end more information that it can
display and also more information to determine how each zip code is
rendered. We could define the display coloring in the JSON, but we will do
that in the JavaScript soon. The other thing we do is parse the GeoJSON
geometry that we pulled from the database. Even though we wrote the query
to return JSON, it comes out of the database as a string, and we need to
coerce it back into JSON. Type coercion is one of the magical things that
ActiveRecord handles for you behind the scenes when you use a model to
execute queries. Since we are not using that functionality we need to
perform the coercion ourselves. You can see where we also coerced the id
to an Integer.
Great! Now you should have all the pieces in place to see the Maryland
zip codes. Once they finish loading, they should be grey and look like Figure
10.8.
Figure 10.8 Maryland Zip codes
Code Checkpoint
To see the code at this stage, go to
[Link]
Choropleth
A choropleth map is a thematic map in which areas are shaded or patterned
in proportion to the measurement of the statistical variable being displayed
on the map, such as population density or per-capita income. They can
convey a lot of information in a simple map and also be visually appealing.
Converting our grey zip codes into a colorful choropleth requires updating
the makeMap() function. We will do this in two passes. First let’s add a
splash of color. Here is what makeMap() looks like:
Click here to view code image
function makeMap() {
// initialize the map on the "map" div with a given center and
zoom
[Link] = $('body').data('mapboxToken');
var map = [Link]('map', '[Link]')
.setView([39.045753, -76.641273], 9);
A lot of this should look familiar by now. The getStyle and getColor
functions are new. We grab the median value from the GeoJSON properties
and use that to set the color. The darker the color, the higher the median
value. The choropleth is taking shape! You should see a map that looks like
Figure 10.9.
Figure 10.9 Maryland Zipcode Choropleth
The colors are pretty, and I confess that I could look at these all day long.
We can make it even more interesting, though. Let’s add in some
interactivity with popups that appear when you mouseover each zip code.
We will also add the ability to click on a zip code to zoom in and see the zip
code in closer detail. We should add a legend, too.
I have adapted the Mapbox choropleth example
([Link]
Click here to view code image
function makeMap() {
// initialize the map on the "map" div with a given center and
zoom
[Link] = $('body').data('mapboxToken');
var map = [Link]('map', '[Link]')
.setView([39.045753, -76.641273], 9);
[Link]([Link]);
[Link]('<div class="marker-title">Zipcode: ' +
[Link] + '</div>' +
'Median value: $' +
[Link].median_value);
if (!popup._map) [Link](map);
[Link](closeTooltip);
// highlight feature
[Link]({
weight: 3,
opacity: 0.3,
fillOpacity: 0.9
});
function getLegendHTML() {
var grades = [0, 50000, 100000, 200000, 300000, 400000,
500000, 750000],
labels = [],
from, to;
[Link](
'<li><span class="swatch" style="background:' +
getColor(from + 1) + '"></span> ' +
'$' + from + (to ? '–$' + to : '+')) + '</li>';
}
We also need to add a little more CSS to make the legend and popups look
correct. Add this to [Link]:
Click here to view code image
.map-legend ul {
list-style: none;
padding-left: 0;
}
.map-legend .swatch {
width: 20px;
height: 20px;
float: left;
margin-right: 10px;
}
.leaflet-popup-close-button {
display: none;
}
.leaflet-popup-content-wrapper {
pointer-events: none;
}
Now when you refresh the page you should see a beautiful and interactive
choropleth (Figure 10.10)!
Code Checkpoint
To see the code at this stage, go to
[Link]
Summary
In this chapter we reviewed how to configure an app and database for
PostGIS and we learned about the Leaflet JavaScript mapping library. We
updated all three of the apps that we’ve been working on throughout the
book to draw relevant maps. We put pins on the map to show where the five
weather stations that reported temperature in 1836 are located. We
highlighted where the California airports are located and learned about
marker clusters to handle high concentrations of markers. Finally, we drew
the boundaries of the Maryland zip codes and turned that map into a
choropleth to show the median value of houses sold in each zip code.
Chapter 11. Querying Geospatial Data
You can store a latitude and longitude in your database and make maps
without PostGIS. We cannot, however, ask spatial questions of our data
unless the database understands spatial concepts. That’s where PostGIS and
the spatial SQL functions provided by PostGIS come into play.
In this chapter we look at the two most common spatial questions you
will need to ask of your data: “What exists in this area?” and “What exists
near this point?” Before we can answer the first question we need to discuss
how to define that area. For that we use something called a bounding box.
[Link]([Link]())
It asked the map for the bounding box of all the features in the map, and
then set the zoom level of the map to that bounding box.
Writing a Bounding Box Query
As you have seen through previous chapters, we have a couple of ways that
we can construct a query that calculates a bounding box for us. We can use
PostGIS directly, or we can let the ActiveRecord PostGIS adapter do it for
us.
zcta5ce10 |
bbox_wkt
-----------+----------------------------------------------------
----------------------------------------------------------------
------------------------------------
zcta5ce10
-----------
21529
(1 row)
The other way we can run this query is to ask for any record that has a
geometry that intersects, or overlaps, the bounding box. It just takes a single
point to fall within the bounding box for another geometry to be considered
intersecting.
Click here to view code image
-- && is intersects
SELECT zcta5ce10
FROM zipcodes
WHERE geom &&
ST_Envelope('POLYGON ((
-78.789527 39.67800499999982,
-78.74252300000002 39.67800499999982,
-78.74252300000002 39.72301699999982,
-78.789527 39.72301699999982,
-78.789527 39.67800499999982))'::geometry);
zcta5ce10
-----------
21529
21502
21524
(3 rows)
Here you can see that there are two other zip codes that overlap the source
zip code’s bounding box. Note that we are not comparing each zip code’s
bounding box to the source bounding box. We are looking at each zip
code’s geometry.
We can run the same query in Rails. RGeo does not give us a helper
method, so we need to put the spatial SQL for the WHERE clause into the
ActiveRecord finder method.
First we will run the “contained” query using the && operator in rails
console:
Click here to view code image
>> zipcode = [Link]; nil
Zipcode Load (0.8ms) SELECT "zipcodes".* FROM
"zipcodes" ORDER BY "zipcodes"."gid" ASC LIMIT 1
=> nil
>> zipcodes = [Link]("geom && ?", [Link]);
nil
=> nil
>> [Link](&:zcta5ce10)
Zipcode Load (10.8ms) SELECT "zipcodes".* FROM "zipcodes"
WHERE (geom &&
'0020000003000010e60000000100000005c053b2879c4113c74043d6c8de2ac
309c053af857f3061c94043d6c8de2ac309c053af857f3061c94043dc8bd230b
9c3c053b2879c4113c74043dc8bd230b9c3c053b2879c4113c74043d6c8de2ac
309')
=> ["21529", "21502", "21524"]
The question mark (?) that you see in the query is what ActiveRecord uses
to substitute values into a query. You could put the variable directly in the
query, but using the question mark placeholder offers added security to
avoid SQL injection because ActiveRecord sanitizes the values it
substitutes into your queries.
Next we will run the “includes” query using the @ operator in rails
console:
Click here to view code image
>> zipcodes = [Link]("geom @ ?", [Link]);
nil
=> nil
>> [Link](&:zcta5ce10)
Zipcode Load (10.2ms) SELECT "zipcodes".* FROM "zipcodes"
WHERE (geom @
'0020000003000010e60000000100000005c053b2879c4113c74043d6c8de2ac
309c053af857f3061c94043d6c8de2ac309c053af857f3061c94043dc8bd230b
9c3c053b2879c4113c74043dc8bd230b9c3c053b2879c4113c74043d6c8de2ac
309')
=> ["21529"]
The envelope in the queries was not displayed in WKT format but instead
as a geometry. The WKT representation is for us to be able to see the
attributes of the geometry.
With all the data in place we can ask a question of the database: List the
three airports that are closest to San Francisco.
We cap the search radius at 200 miles so that in less dense areas we do
not recommend an airport that is too far away. Note that this distance is as
the crow flies, not driving distance.
Click here to view code image
SELECT ST_Distance(
lonlat,
ST_GeomFromText('POINT(-122.3748433 37.61900194)',4326)
) AS distance_meters, id, iata, airport, city, state, country
FROM airports
WHERE ST_DWithin(
-- geom 1
lonlat,
-- geom 2
ST_GeomFromText('POINT(-122.3748433 37.61900194)',4326),
-- distance in meters (200 miles)
200 * 1609.34
)
ORDER BY 1
LIMIT 3;
distance_meters | id | iata
| airport | city | state | country
-----------------+------+------+-----------------------------+--
-------------+-------+---------
5.22691384 | 2935 | SFO | San Francisco International |
San Francisco | CA | USA
16144.85676752 | 1689 | HAF | Half Moon Bay |
Half Moon Bay | CA | USA
16248.23050337 | 3007 | SQL | San Carlos |
San Carlos | CA | USA
(3 rows)
The query calculates the distance from each record’s lonlat to a point in
San Francisco using the ST_Distance function. We also sort by that
calculated field, which is the first field in the query. We limit the search
radius to 200 miles by using the ST_DWithin function in the WHERE
clause. Again, for that function we compare each record’s lonlat value to
that point in San Francisco. Distances are in meters with both of these
functions, and a mile is 1609.34 meters. You could, of course, just put
321868 instead of doing the calculation.
Using ActiveRecord
Finding items near a point is another one of those areas where we need to
fall back to using the standard spatial SQL functions. We can make it more
“railsy” by including the SQL from the WHERE clause as a scope in the
Airport model.
Click here to view code image
scope :close_to, -> (lon, lat, distance_in_meters = 200 *
1609.34) {
select(sanitize_sql_array([%{
ST_Distance(
lonlat,
ST_GeomFromText('POINT(? ?)',4326)
) AS distance_meters,
*
}, lon, lat])).
where( [%{
ST_DWithin(
lonlat,
ST_GeographyFromText('SRID=4326;POINT(? ?)'),
?
)
}, lon, lat, distance_in_meters]).
order("1")
}
The syntax in that scope may look a little strange. We use %{} to quote the
text of the query. You could also use a heredoc instead of the quoting literal.
The quote literal is more compact and fits better in the scope. We also use
field order for sorting in this query. Passing the string "1" to
ActiveRecord’s order enables us to use field order sorting like we did in
the raw SQL example.
With the scope in the model we can then run the query like this in
rails console:
Click here to view code image
>> sfo = Airport.find_by(:iata => "SFO"); nil
Airport Load (3.6ms) SELECT "airports".* FROM "airports"
WHERE airports"."iata" = $1 ORDER BY "airports"."id" ASC LIMIT
1
[["iata", "SFO"]]
=> nil
>> airports = Airport.close_to([Link], [Link]).limit(3)
Airport Load (9.7ms) SELECT
ST_Distance(
lonlat,
ST_GeomFromText('POINT(-122.374889 37.618972)',4326)
) AS distance_meters,
*
FROM "airports" WHERE (
ST_DWithin(
lonlat,
ST_GeographyFromText('SRID=4326;POINT(-122.374889
37.618972)'),
321868.0
)
) ORDER BY 1 LIMIT 3
This does everything that we need it to do. We can look at the three airports
we got back from the query to see how far each airport is from SFO:
Click here to view code image
>> [Link](&:distance_meters)
=> [5.22691384, 16139.66651997, 16248.55195813]
The first airport in the array is SFO. The two other airports are HAF and
SQL, same as before when we ran the query using raw SQL.
If the scope option works well for you, then go for it. For a general query
that needs to get the nearest places I think the spatial SQL query is clearer
and easier to implement.
Code Checkpoint
To see the code at this stage, go to
[Link]
Calculating Distance
I first touched on distance calculations in Chapter 9, “Working with
Geospatial Data in Rails.” I used ST_Distance in the query to find the
nearest airports earlier in this chapter. It’s the PostGIS function that I use
the most.
Bear in mind that each projection carries with it some level of distortion.
You’re taking the globe, or some section of it, and projecting it onto a flat
surface. Distance can, therefore, be distorted because the projection is
distorted. It may be less pronounced if you’re using a localized projection.
The precise distance may or may not be a concern. The ST_Distance
value might be good enough. You can always run a comparison with a
sampling of your data to compare ST_Distance against
ST_Distance_Spherical.
For a comparison of the distance methods, adapted from the example in
the PostGIS documentation
([Link] you can run the
following query that calculates the distance to San Francisco from each
airport:
Click here to view code image
SELECT
iata AS origin,
'SFO' AS destination,
ST_DistanceSpheroid(
lonlat::geometry,
ST_GeomFromText('POINT(-122.3748433 37.61900194)',4326),
'SPHEROID["WGS 84",6378137,298.257223563]'
)::numeric AS dist_meters_spheroid,
ST_DistanceSphere(
lonlat::geometry,
ST_GeomFromText('POINT(-122.3748433 37.61900194)',4326)
)::numeric AS dist_meters_sphere,
ST_Distance(
lonlat,
ST_GeomFromText('POINT(-122.3748433 37.61900194)',4326)
)::numeric AS dist_meters
FROM airports
ORDER BY 3, 4, 5;
Summary
This chapter was heavy on queries and ActiveRecord. I went over the two
questions that I need to answer the most when working with geospatial
data: “What exists in this area?” and “What exists near this point?” Before
we could get too far into the first question we first needed to cover the
bounding box.
There is more that we can do with these concepts. You can add
functionality to a map to capture the coordinate location for a click (or
touch) in the map. Given that coordinate you can search for nearby points
of interest. You can also add drawing tools to a map to draw a bounding box
or even create new geometries.
The sky is the limit, and you now have the necessary foundation to go
and create your own fantastic geospatial applications.
Afterword
This book contains most of what I know how to do with data. I told you in
the Preface that I love data. I like playing with it—cleaning the data up and
then letting it tell a story. I hope that enthusiasm came through in my
writing, and I hope that the information is useful to you.
This may be the end of the book, but it is not the end of the story. Take
what you’ve learned and go make your own data dance.
Appendix A. Ruby and Rails Setup
This appendix is a brief overview of all the little things that go into getting
your environment set up to run Rails and create your first Rails app. Feel
free to skip around.
Install Ruby
I like to use RVM to manage my rubies. I really like having project-based
configurations and settings. Some languages lend themselves really well to
this, and some rely more on system-wide configurations.
RVM gemsets are the perfect way to create the isolation that I like. Rails
does not strictly need that isolation. [Link] defines gem
dependencies (with versions) so that when you do have multiple versions of
a gem installed it will pick the right one.
Still, there have been times when I got some gem version messed up. It
was great to be able to just blow away the gemset and reinstall a clean set of
gems for that app.
Once you have RVM installed and at least one ruby, you are ready to start
setting up a project. The steps that I take to do that generally go like this:
Click here to view code image
echo 2.2.3 > .ruby-version
gem install bundler --no-rdoc --no-ri
rvm gemset create APPNAME
rvm gemset use APPNAME
gem install rails --no-rdoc --no-ri
I have a directory for all my projects, and I like to have that .ruby-
version file in that directory so that whenever I change into the directory
I automatically switch to the latest version of Ruby that I have installed.
You can also run rvm use 2.2.3 to change to the version manually.
Create the App
Make sure that you’re still in the right gemset. You can run rvm gemset
list to verify that. You’ve already installed the Rails gems, so you are
ready to create the app. The steps that I generally use to create a new Rails
app are:
Click here to view code image
rails new APPNAME --skip-bundle -d postgresql
echo 2.2.3 > APPNAME/.ruby-version
echo APPNAME > APPNAME/.ruby-gemset
cd APPNAME
git init
git add . && git commit -am'Initial commit'
A new Rails app is created in a subdirectory named for the project. The
.ruby-* files tell RVM to set the version of Ruby and gemset that you
want for that project. I like to do an initial commit before I start making
configuration changes. The --skip-bundle switch tells the Rails app
generator to hold off on installing any gems. Typically, the Rails generator
will run bundle install when it’s finished creating the app. We
discuss this more in the next section.
The default development database is SQLite, so we have to tell Rails that
we want to use Postgres. If you have options, like those, that you always
want the Rails new generator to use you can put those in a configuration file
in your user directory (~/.railsrc).
Here is my .railsrc file:
--skip-bundle
-d postgresql
More Gems
The reason I don’t want to install the gems yet is that I have more that I
want to use, and I have some that I do not want to use.
The file we need to modify is the Gemfile, which is in the app’s root
directory. Here are the modifications that I generally make:
• Add the Ruby version. Heroku needs this. RVM can use this. It also
documents expectations for other developers.
• There are a lot of comments, and you don’t really need them.
• Allow for minor version bumps for the Rails gem. Do this by adding
~> before the version number.
• Comment out coffee-rails. I do not prefer CoffeeScript.
• Add these gems:
• rails_12factor (group: production)—you can omit this if
you’re not using Heroku
• dotenv-rails (groups: development, test)
• puma
• newrelic_rpm
• lograge
With those tweaks the Gemfile is now good enough to install some gems.
Do this by running bundle install. That will create (or update) a
[Link] file. Both of those files should be tracked in your source
code repository.
Config Files
Now it’s time to set up some of the dependencies for the app. We have a
couple of things that need some configuration, like database credentials, and
we have some services that we need to setup.
Puma
Puma is a multi-threaded webserver, and it’s awesome. Listing A.1 shows
the typical puma config file for Heroku. If you are using Rails 5 the Puma
gem is already included.
workers Integer(ENV['WEB_CONCURRENCY'] || 2)
threads_count = Integer(ENV['MAX_THREADS'] || 5)
threads threads_count, threads_count
preload_app!
rackup DefaultRackup
port ENV['PORT'] || 3000
environment ENV['RACK_ENV'] || 'development'
on_worker_boot do
ActiveRecord::Base.establish_connection
end
New Relic
We will set this config up if/when we host. Heroku offers New Relic as an
add-on, and there is a free level. You can also use New Relic on your
localhost in development mode, which can be informative. Go to
[Link] as you use your app to see the dashboard.
Log Rage
This is totally optional, but once I started using it I have not stopped. You
can define the format for your app logs. The configuration is done
separately for each environment. The configuration that I use is below, and
it goes in config/environments/[Link]. You can also
put it in the development config file.
Click here to view code image
# LogRage
[Link] = true
[Link].custom_options = lambda do |event|
params = [Link][:params].reject do |k|
['controller', 'action', 'format'].include? k
end
{
:params => params,
:time => [Link]
}
end
Dot Env
Environment variables are the preferred way to tell your app about
credentials. The dotenv-rails gem can make it easier to set these
locally in development. I’ve also found this is a good way to document
what environment variables an app depends on. Just be sure that you
include .env in your .gitignore. Here is an example .env file with
some database credentials:
POSTGRES_HOST=localhost
POSTGRES_USER=developer
POSTGRES_PASSWORD=password
Database
I like to delete the comments from the boilerplate files ([Link],
[Link]). I also like to set myself up to use environment variables as
much as possible. This makes it really easy to drop your application into a
Docker container. That’s where the dot-env gem comes into play. Listing
A.2 shows my cleaned-up config/[Link] file.
default: &default
adapter: postgresql
encoding: unicode
pool: <%= ENV["DB_POOL"] || ENV['MAX_THREADS'] || 5 %>
host: <%= ENV["POSTGRES_HOST"] || "localhost" %>
username: <%= ENV["POSTGRES_USER"] || "developer" %>
password: <%= ENV["POSTGRES_PASSWORD"] || "password" %>
development:
<<: *default
database: maryland_residential_sales_development
production:
<<: *default
database: maryland_residential_sales_production
README
I like Markdown better than RDoc for formatting documentation, so I
change the [Link] to [Link] (git mv [Link]
[Link]).
Delete the boilerplate, and put in the pertinent information for the project.
You should include things like the following:
• Name of the project
• Things that you are using (PostGIS would be a good thing to
document)
• Steps to get the project running
• Hosting information and steps to host the project
• Anything else that someone would need to know
This is a brief overview of all the little things that go into getting your
environment set up to run Postgres.
Installing Postgres
Chances are your laptop or server does not have Postgres installed on it if
you have not used the Postgres database before. If you’re using OS X, you
have the Postgres client libraries but not the database server itself.
From Source
You can download the source code for Postgres, compile it, and install it.
This gives you the most control. Go to
[Link] to download the source code.
Installation instructions can be found at
[Link]
Package Manager
If you’re on a Linux distro you’ve got access to the Postgres and PostGIS
packages in the apt or yum repos.
On OS X, Homebrew is usually my go-to tool for installing things. You
can use Homebrew to install Postgres (and PostGIS). This was how I
installed Postgres and PostGIS on OS X for a very long time, but it’s not
my preferred method now.
[Link]
The good people at Heroku have made an OS X application that runs the
Postgres server, includes PostGIS, and has all the environmental things like
header files that you need to run Postgres on OS X. This is what I use on
my dev box.
SQL Tools
Once you have a database you might want to interact with it outside the
scope of your application. You have a few choices for how to do that.
Command Line
I love the command line, and on a server this may be your only option. The
Postgres command line client is psql. Here is an example of the psql
command:
Click here to view code image
psql -U developer -d someapp_development -h localhost -p 5432 -W
Your shell username will also be the default user for psql, so you don’t
need to specify username (-U) unless you’re using a different database user.
Tell psql which database you want to connect to with the -d switch. If
you are connecting to a database on the localhost you do not need to specify
the host (-h), but it does not hurt to get in the habit of including it. The
default port (-p) for Postgres is 5432, so you don’t need to specify that
unless you’re trying to access a different port. The -W switch is the same
thing as --password. Both tell psql to ask for a password.
GUI Tool
I like two different GUI applications for interacting with a Postgres
database. They both can connect to your local database(s) as well as
databases in the cloud.
pgAdmin3
pgAdmin is an open source tool for Postgres. You can run pgAdmin on OS
X, Linux, and Windows. The key bindings do not necessarily match up with
what you might be used to for the given OS, so be aware of that.
Navicat
Navicat has several different database clients and several different versions
of their apps to choose from. I like Navicat Essentials for PostgreSQL
enough that I bought the license.
Bulk Importing Data
I touched on importing data in Chapter 1, “D3 and Rails,” and wrote a rake
task to do it one record at a time. Sometimes that is impractical. It generates
a massive log file, for example, and takes a long time with large data files.
There are two other choices for importing data: the COPY SQL statement,
and the \copy psql command. Both require that the data line up exactly
with the table, so the extra fields like created_at and updated_at
will have to be dropped from the table and then added back after the data is
loaded. It may also be helpful to drop all the indices on the table and add
them back once the data is imported.
pg_restore
There is one other option that is a little higher level than bulk importing
data for a table. You can export (dump) the data from a database and load it
into another. The databases have to be the same version, or you have some
additional hoops to jump through.
A simple pg_restore command that assumes you have a dump from
the database that was created using the pg_dump command follows. Here
is an example pg_restore for loading data to a Heroku database.
Click here to view code image
pg_restore --verbose --clean --no-acl --no-owner \
-h [Link] -U developer -d app_production \
-p 34567 ./[Link]
Listing B.1 shows a rake task that I wrote to take the data from a production
Heroku database and load it into a local development database. This can
also be found in a GitHub Gist at
[Link]
namespace :db do
namespace :heroku do
desc "capture DB Backup"
task :capture_backup => :environment do
if [Link] == 'development'
Bundler.with_clean_env do
config =
[Link].database_configuration[[Link]]
system "heroku pg:backups capture"
end
end
end
desc "Load the PROD database from Heroku to the local dev
database"
task :load => :download_backup do
if [Link] == 'development'
Bundler.with_clean_env do
config =
[Link].database_configuration[[Link]]
system <<-CMD
pg_restore --verbose --clean --no-acl --no-owner -h
localhost \
-U #{config["username"]} -d #{config["database"]}
[Link]
rm -rf [Link]
CMD
end
end
end
end
end
Q
UERY PLAN
----------------------------------------------------------------
----------------------------------------------------------------
--------
HashAggregate (cost=16.07..16.30 rows=24 width=14) (actual
time=1.070..1.086 rows=24 loops=1)
Group Key: jurisdictions
-> Seq Scan on maryland_residential_sales_figures
(cost=0.00..13.71 rows=471 width=14) (actual time=0.064..0.256
rows=471 loops=1)
Planning time: 0.190 ms
Execution time: 1.224 ms
(5 rows)
When you see seq scan, it means that the entire table will be scanned in
sequence to find the right data. If you’re using fields in a WHERE clause,
those would be good candidates for indices when you see the sequence
scan.
Appendix C. SQL Join Overview
I don’t know about you, but I have to look a lot of things up. There are a lot
of types of joins, and I don’t use most of them regularly. This appendix is
really here for me, but maybe you’ll find it helpful as well.
\connect join_examples;
Those two lines will create an empty database and then connect to (place
you in) that database. Now that we have a database we can put some sample
data in it.
Click here to view code image
CREATE TABLE t1 (num int, name char(1));
INSERT INTO t1 VALUES (1, 'a'), (2, 'b'), (3, 'c');
Inner Join
This is the default join if you do not specify INNER or OUTER. With an
inner join you get all the records from both tables where the join condition
is satisfied.
Click here to view code image
join_examples=# SELECT * FROM t1 INNER JOIN t2 USING (num);
num | name | value
-----+------+-------
1 | a | xxx
3 | c | yyy
(2 rows)
That query could also be written as:
Click here to view code image
SELECT * FROM t1 JOIN t2 on [Link] = [Link];
Inner and left joins are the mainstay of what you will use, but it’s good to
know what else you have available.
Cross Join
I don’t think that I have ever used this join. It gives you every possible
combination of rows from the two tables. The number of records will be the
record count of t1 * the record count of t2.
Click here to view code image
join_examples=# SELECT * FROM t1 CROSS JOIN t2;
num | name | num | value
-----+------+-----+-------
1 | a | 1 | xxx
1 | a | 3 | yyy
1 | a | 5 | zzz
2 | b | 1 | xxx
2 | b | 3 | yyy
2 | b | 5 | zzz
3 | c | 1 | xxx
3 | c | 3 | yyy
3 | c | 5 | zzz
(9 rows)
Self Join
It may seem like a strange thing to do, but you can join a table on itself.
This comes in handy when you need to compare the data in a table with
itself. For example, you could list all employees for a manager (who is also
an employee). You’d get the manager’s name from the joined version of the
table.
Click here to view code image
join_examples=# SELECT * FROM t2 JOIN t2 t2a USING (value);
value | num | num
-------+-----+-----
xxx | 1 | 1
yyy | 3 | 3
zzz | 5 | 5
(3 rows)
You can see that the num field is brought in a second time thanks to the
join. This also shows the use of a table alias, where we can give a table
name (usually a long table name) an alias. This is generally a shorter,
abbreviated, version of the name that we can use throughout the query to
refer to that table without having the really long table name repeated and
cluttering up the query.
Index
Symbols
|| (double pipe), for concatenation of strings, 118
? (question mark), in ActiveRecord query, 187
A
ActiveRecord
bar chart controller actions, 25
benchmarking and comparing, 63–64
callbacks, 148
creating new schema, 131–132
finding items near a point, 189–190
helper function for benchmarking, 62
importing data, 12
limitations of, 74–75
loading carrier data, 97
maintaining databases, 4
PostGIS and, 146–147
pulling and grouping data, 15–16
scopes, 66–67
in simple multi-line graph, 50
writing bounding box queries, 184–185
Airport model, chord diagram, 95–96
American Statistical Association (ASA), 94
AND queries, materialized views, 110
Apache Bench, for load testing, 68
Application servers
alternatives to Rails, 5
benefits of Rails, 4–5
starting Rails server, 48
ASA (American Statistical Association), 94
B
Bar charts
common uses, 28
controller actions, 25
JavaScript makeBar() function, 26–28
relative views of proportions, 24
views and routes, 24–25
benchmark-ips, 63–64
Benchmarking
ActiveRecord helper function for, 62
comparing and, 63–64
date parsing and, 65
identifying bottlenecks, 68
limitations of, 68–69
locating bottlenecks, 64
Binary files, Git use with, 59
bisectData function, 52–53
Bostock, Mike, 5, 16, 106
Bottlenecks
identifying, 68
locating, 64
Bounding boxes
finding data within, 185–187
markers and, 163
what they are, 183–184
writing bounding box queries, 184–185
Box-and-whisker diagrams. See Box plots
Box plots
JavaScript makeBoxplot() function, 36–40
overview of, 34–35
quartiles in, 35
views, 35–36
BTS (Bureau of Transportation Statistics), 94
Build tools, using Rake as, 9–10
Bulk import, PostgreSQL, 202–203
Bulk insert task, Rake, 136–138
Bureau of Transportation Statistics (BTS), 94
C
Calculating distances, geospatial data, 190–191
cardinal function, D3, 127
Carrier model, chord diagram, 96–97
Cartesian coordinates, applying scatter plots, 28–29
CDN (content delivery network), file storage options, 60
[Link], alternatives to D3, 5
Charts
beware of misleading, 20
box plots. See Box plots
chord diagram. See Chord diagram
pie charts. See Pie charts
scatter plots. See Scatter plots
Chord diagram
adding styles to departures, 105–106
Airport model, 95
applying to flight departures, 94
Carrier model, 96–97
creating views, 104–105
Departures model, 97–98
drawing disjointed city pairs diagram, 111–113
drawing the diagram, 106–108
enforcing referential integrity, 100–101
fetching data, 101
finalizing matrix, 103–104
generating matrix, 101–103
loading airport data, 95–96
loading carrier data, 97
loading departure data, 98–100
materialized view for optimizing slow queries, 110–111
overview of, 93
square matrix format, 93–94
window function for finding empty flight legs, 108–110
Choropleth thematic map, 176–181
Clojure, alternatives to Ruby, 5
Cloud, file storage on, 60
COALESCE function, turning data into time series, 119
Code profiling, identifying bottlenecks, 68
Command line tools, PostgreSQL, 202
Common Table Expressions. See CTE (Common Table Expressions)
Compression, fetching and loading remote compressed file, 61–62
Config files, Ruby on Rails, 197–199
Content delivery network (CDN), file storage options, 60
Contrib modules, PostgreSQL, 144–145
Controllers
for bar charts, 25
for heatmaps, 87
for maps, 158–159, 164
for residential sales app, 15–16
for Scatter plots, 29
using with data fetch, 46–47
for weather app, 45
Coordinates, displaying in scatter plots, 28
\copy, for data import, 12, 203
COPY, for data import, 12, 202–203
create_table macro, 133
create_view macro, 133
Cross joins, SQL, 209
CSS
adding mouseover effects, 20–21
box plot example, 36
defining map div, 159
line graph example, 47
styling choropleth zip code map, 180–181
styling departures model, 105–106
styling heatmaps, 87
tweaking functionality of scatter plot, 33–34
CSV library
loading carrier data, 97
transformation of data types, 96
CTE (Common Table Expressions)
defining subqueries, 152
heatmaps and, 86–90
overview of, 84–86
for queries with multiple parts that build on each other, 109
turning data into time series, 117–121
curl, downloading large files, 60
curved function, D3, 127
D
D3
benefits as graphing library, 5
cardinal and curved functions, 127
creating map layout, 157
documentation of square matrix example, 93
generating timeline in, 122
including D3 JavaScript library in Rail app, 14, 47
Leaflet use with, 155
quantitative scale in, 51
Data
accessing in reporting schema, 134
confirming imported, 13–14
evaluating formats, 6–7
fetching, 101
geospatial. See Geospatial data
importing from shapefiles, 150–151, 170–172
importing/migrating, 8–9, 12–14
iteration and transformation of, 37–39
loading, 95–100
pulling and grouping, 15–16
querying “big data.” See Queries, “big data”
sanitizing, 98
timestamps, 115
turning into time series. See Time series data
Data dictionaries
defining data fields, 7–8
modifying, 8–9
Data fields
creating geospatial table fields, 148
defining in Rails app, 7–8
evaluating data, 6
Data types
evaluating data, 6
PostgreSQL, 3–4
transforming using CSV library, 96
type checking, 10
Databases
alternatives to PostgreSQL, 4
config files, 199
field naming conventions, 7–8
importing/migrating data into, 8–9
portability limits, 73–74
reporting. See Reporting databases
specifying for Rails app, 3
SQL join setup, 207
transactional vs. reporting, 129–130
Datasets, large
benchmarking and, 62–65
fetching and loading remote compressed file, 61–62
file storage on cloud, 60
Git use with, 59–60
hotlinking and, 60
indices, 67–68
limitations of benchmarks and statistics, 68–69
overview of, 59
querying “big data,” 65–66
scope use, 66–67
Date parsing, benchmarking and, 65
daterange datatype, PostgreSQL, 118–119
Datum, reference for spatial measurements, 142–143
DD (decimal degrees), GIS primer, 142
Decimal degrees (DD), GIS primer, 142
Degrees, GIS primer, 142
Degrees, minutes, seconds (DMS), GIS primer, 142
Departures model, chord diagram
adding styles, 105–106
creating views, 104–105
drawing disjointed city pairs diagram, 111–113
loading departure data, 98–100
overview of, 97–98
Diagrams. See Charts
Distances, calculating, 190–191
DMS (degrees, minutes, seconds), GIS primer, 142
Dot Env gem, environment variables, 198
Double pipe (||), for concatenation of strings, 118
E
Effects, in pie charts, 20–21
Elixir, alternatives to Ruby as app server, 5
Enclaves, map features, 144
Environment variables, 198
Error handling, ETL tasks and, 10
ETL (Extract, Transform, Load)
drawing line charts, 48
iteration and transformation of data, 37–39
shapefiles, 151–153
writing for Rails app, 10–11
EXPLAIN ANALYZE, Postgres query optimizer, 204–205
F
Features, on maps, 143–144
Fetching data
for chord diagram, 101
remote compressed files, 61–62
for weather app, 45–47
Fields. See Data fields
File storage
on cloud, 60
Git LFS (large file storage), 59–60
Filters, adding to data in box plot, 39
first_value(), window function, 82–83
fitBounds function, resetting zoom level, 163
Focus circles
connecting time series data, 56–58
highlighting maximum temperature, 52–53
highlighting minimum temperature, 53–54
focusMax variable
connecting focus circles, 56–58
displaying temperature changes in weather app, 55–56
highlighting maximum temperature in line graph, 52–53
focusMin variable
connecting focus circles, 56–58
displaying temperature changes in weather app, 55–56
highlighting minimum temperature in line graph, 53–54
Foreign keys
linking tables, 130
removing for large data load, 100–101
Formats
evaluating data, 6
square matrix format for chord diagram, 93–94
FTP, for data download, 41–42
Full Outer Join, SQL, 208
Functions, JavaScript
makeBar(), 26–28
makeBoxplot(), 36–40
makeChordChart(), 104–108
makeHeatMap(), 87–90
makeLineChart(), 48–51
makeMap(), 161–163, 176–177
makePie(), 21–24
makeScatter(), 30–33
Functions, PostGIS, 146
G
<g> element, SVG, for grouping shapes, 24
gcloud, file storage options, 60
Gemfile, modifying, 196–197
Gems
adding, 197–198
modifying, 196
Gemsets, RVM, 195
generate_series function, 117
Generators
creating flight departures model, 97–98
creating pie chart views, 14–15
Scenic generator, 132–133
Geographical information system. See GIS (geographical information
system)
GeoJSON
formatting data for, 174–175
marker clusters and, 166–167
markers and, 164
view of map data, 159–161
Geospatial data
ActiveRecord and PostGIS, 146–147
GIS primer, 141–144
PostGIS, 144–146
summary, 154
updating missing lonlat field, 153–154
using in Rails, 147–149
working with shapefiles, 150–153
Geospatial data, querying
calculating distances, 190–191
finding data within bounding boxes, 185–187
finding items near a point, 187–190
overview of, 183
summary, 191–192
writing bounding box queries, 184–185
getColor function, Choropleth thematic map, 177
getStyle function, Choropleth thematic map, 177
GHCN (Global Historical Climatology Network). See also Time series data,
41
GIS (geographical information system)
ActiveRecord and PostGIS, 146–147
PostGIS, 144–146
primer, 141–144
zip code shapefile and, 169
Git, for large datasets, 59–60
Git LFS (large file storage), 59–60
GitHub, 59–60
Global Historical Climatology Network (GHCN). See also Time series data,
41
Go, alternatives to Ruby as app server, 5
Google
gcloud, 60
Leaflet maps and, 156
Google Charts, alternatives to D3, 5
Google Maps, 143
Graphical user interface (GUI), postgreSQL, 202
Graphing library
benefits of D3, 5
use with Rails app, 3
Graphs. See Charts
Grimm, Avdi, 10
GUI (graphical user interface), postgreSQL, 202
H
Heatmaps
controller and view, 87
JavaScript, 88–90
queries, 86–87
Heroku
file size limits, 60
New Relic add-on, 197–198
[Link], 201
Highcharts, alternatives to D3, 5
Hotlinking, pros/cons, 60
HTTP, for data download, 41–42
I
Importing/migrating data
alternative approaches, 12
confirming imported data, 13–14
creating new schema, 131–132
into Rails app, 8–9
from shapefiles, 150–151
for table in reporting schema, 135
zip code shapefile, 170–172
Indices
composite, 67–68
individual, 67
map index, 159
removing for large data load, 100
Inner joins, SQL, 207–208
INSERT INTO query, defining subqueries, 152
iqr (interquartile range) function, JavaScript, 36–37
J
Java, app server alternatives, 5
JavaScript
creating legible labels, 19–20
creating/viewing pie chart, 14–18
graphing library. See D3
heatmaps, 87–90
Leaflet library. See Leaflet
makeBar() function, 26–28
makeBoxplot() function, 36–40
makeChordChart() function, 104–108
makeHeatMap() function, 87–90
makeLineChart() function, 48–51
makeMap() function, 161–163, 176–177
makePie() function, 21–24
makeScatter() function, 30–33
Joins. See SQL joins
jQuery, 155
JSON. See also GeoJSON, 159–161
L
Labels
legibility of, 19–20
mouseover effects used with, 20–21
lambda, Ruby, 136
last_value(), window function, 82–83
Latitude
features on maps, 143–144
geospatial data in Rails, 148–149
GIS primer, 141–142
map projections, 143
updating missing data, 153–154
Layers, Leaflet maps, 156
Layouts, creating new layout in Leaflet, 157–158
LEAD, window function, 108–110
Leaflet
choropleth thematic map, 176–181
creating map controller, 158–159
creating new layout, 157–158
drawing flight paths, 167–168
drawing map of weather stations, 161–163
GeoJSON view of map data, 159–161
importing zip code shapefile, 170–172
map index, 159
map layers, 156
map tiles, 155–156
mapping zip codes, 172–176
marker clusters, 165–167
markers, 164–165
overview of, 155
updating residential sales app for PostGIS, 168–170
visualizing airports, 163
Left Outer Join, SQL, 208
Legends
legible labels in pie charts, 19–20
tweaking functionality of scatter plot, 33–34
use with scatter plot, 30
Line charts. See also Multi-line chart
applying simple line graph to weather data, 45
common use of, 28
controller for simple line graph, 45
fetching data for weather app, 45–47
makeLineChart (), 48–50
Lines, features on maps, 143–144
Load testing, identifying bottlenecks, 68
Log Rage, defining app log formats, 198
Logging
defining app log formats, 198
in Rails apps, 12
Longitude
features on maps, 143–144
geospatial data in Rails, 148–149
GIS primer, 141–142
map projections, 143
updating missing data, 153–154
lonlat field
updating missing data, 153–154
writing query for finding items near a point, 187–188
LPAD function, turning data into time series, 118–119
M
makeBar() function, JavaScript, 26–28
makeBoxplot() function, JavaScript, 36–40
makeChordChart() function, JavaScript, 104–106
makeHeatMap() function, JavaScript, 87–90
makeLineChart() function, JavaScript, 48–51
makeMap() function, JavaScript, 161–163, 176–177
makePie() function, JavaScript, 21–24
makeScatter() function, JavaScript, 30–33
Map layers, Leaflet, 156
Map projections, longitude and latitude and, 143
map-reduce, iteration and transformation of data, 37
Mapbox Leaflet plug-in, 156, 161, 167
map_data action
executing queries, 159
GeoJSON view of map data, 159–161
markers and, 164
Maps. See also Leaflet
choropleth thematic map, 176–181
creating controller for, 158–159
drawing map of weather stations, 161–163
GeoJSON view of map data, 159–161
index, 159
layers, 156
with Leaflet and Rails, 155
tiles, 155–156
of zip codes, 172–176
Marker Cluster plug-in, 165
Markers, Leaflet
bounding boxes, 163
clusters of, 165–167
overview of, 164–165
Materialized view
optimizing slow queries, 110–111
use in reporting schema, 132–134
Matrix format. See Square matrix format, for chord diagram
Metz, Sandi, 98
Migration
alternative approaches, 12
creating new schema, 131–132
of data into Rails app, 8–9, 12–14
from shapefiles, 150–151
of table in reporting schema, 135
from zip code shapefile, 170–172
Minutes, GIS primer, 142
mouseover/mouseout events
adding effects to pie charts, 20–21
highlighting maximum temperature in line graph, 51–53
highlighting minimum temperature in line graph, 54–55
tweaking functionality of scatter plot, 33–34
Multi-line chart
adding text for displaying temperature changes, 55–56
applying to weather data, 50–51
connecting focus circles, 56–58
highlighting maximum temperature, 51–53
highlighting minimum temperature, 53–55
MySQL, database alternatives to PostgreSQL, 4
N
Navicat, PostgreSQL GUI tools, 202
New Relic add-on, Heroku, 197–198
NOAA weather data, 41–42
Normalization, comparing reporting with transactional database, 130
Norris, Dr. Jeff, 141
NVD3, alternatives to D3, 5
O
Objects, creating in reporting schema, 132
OpenStreetMap, 156
OS X, PostgreSQL on, 201
Outer joins, SQL, 208–209
Outliers, viewing in box plots, 34–35
P
Package Manager, installing PostreSQL, 201
pgAdmin3, PostgreSQL GUI tools, 202
pg_dump, bulk import and, 203
pg_restore, bulk import and, 203–204
Phoenix, Evan, 63
Pie charts
common uses, 28
creating custom function for, 21–24
creating views, 16–18
legible labels, 19–20
mouseover effects used with slices and labels, 20–21
Points
features on maps, 143–144
finding items near, 187–190
Polygons, features on maps, 143–144
POODR (Practical Object-Oriented Design in Ruby), 98
PostGIS
ActiveRecord and, 146–147
configuring, 147
functions, 146, 174, 188, 190–191
hosting, 147
installing, 145
overview of, 144–146
spatial queries, 183
updating residential sales app for, 168–170
using functions in queries, 149
writing bounding box queries, 184–185
[Link], Heroku, 201
PostgreSQL
alternative databases, 4
benefits for use with Rails, 3–4
bulk import, 202–203
command line and GUI tools, 202
contrib modules, 144–145
converting strings into dates, 117–118
installing, 201
pg_restore command, 203–204
query engine, 67
query optimizer, 204–205
schemas. See Schemas
setting residential sales app, 5–6
time series aggregation. See Time series aggregation, in PostgreSQL
tsrange and daterange datatypes, 118–119
window functions, 81–84
Practical Object-Oriented Design in Ruby (POODR), 98
PSQL \copy command, 203
public, default schema, 131
Puma webserver, 197
Q
QGIS (Quantum GIS), 169–171
Quantitative scale, in D3, 51
Quartiles
in box plots, 35
iqr (interquartile range) function, 36–37
Queries. See also SQL
finding data within bounding boxes, 185–187
finding items near a point, 189–190
geospatial data. See Geospatial data, querying
map_data action executing, 159
materialized view for optimizing, 110–111
subqueries, 84
using PostGIS functions in, 149
when to use CTE, 109
writing bounding box queries, 184–185
Queries, “big data”
indice use, 67–68
overview of, 65–66
scope use, 66–67
Query optimizer, PostgreSQL, 204–205
Question mark (?), in ActiveRecord query, 187
R
Rails app
creating, 196
geospatial data in, 147–149
Rails app, flight departures app
Airport model, 95–96
Carrier model, 96–97
Departures model, 98–100
overview of, 94–95
Rails app, residential sales app
alternative ways to import data, 12
confirming data, 13–14
creating pie chart views, 14–18
customizing repetitive tasks using Rake build tool, 9–10
defining data fields, 7–8
evaluating data, 6–7
importing/migrating data into, 8–9
logging, 12
overview of, 5–6
writing the ETL, 10–11
Rails app, weather app
fetching data, 45–47
importing weather readings data, 42–43
importing weather stations data, 44–45
overview of, 41–42
weather readings model, 41–42
weather stations model, 44
rails console
benchmarking date parsing, 65
confirming imported data, 13–14
writing bounding box queries, 185–187
Rails generators, creating pie chart views, 14–15
Rake
bulk insert task, 136–138
customizing repetitive tasks, 9–10
fetching and loading remote compressed file, 61–62
README documentation, 199
Referential integrity, enforcing, 100–101
Reporting databases
creating objects in reporting schema, 132
isolating reporting activities, 130
materialized view use in reporting schema, 132–134
overview of, 129
summary, 138
table use in reporting schema, 134–138
transactional databases compared with, 129–130
working with multiple schemas, 131–132
RGeo gem, writing bounding box queries, 184–185
Right Join, SQL, 117–121
Right Outer Join, SQL, 208
Roles, viewing database roles, 132
Routes, mapping zip codes, 172
row_number(), window function, 83–84
Ruby on Rails. See also Rails apps
config files, 197–199
creating new Rails app, 196
finalizing setup, 199
installing, 195
lambda, 136
modifying Gemfile, 196–197
overview of, 4–5
ruby-prof
benchmarking to locate bottlemarks, 64
profiling code, 68
Rust, alternatives to Ruby as app server, 5
RVM
creating new Rails app, 196–197
managing rubies, 195
S
Sanitizing data, 98
Scala, alternatives to Ruby as app server, 5
Scalable Vector Graphics. See SVG (Scalable Vector Graphics)
Scatter plots
applied to mortgage payment, 79–81
comparing with other charts, 28–29
controller actions, 29
displaying x, y coordinate pairs, 28
JavaScript makeScatter() function, 30–33
tweaking functionality of, 33–34
views and routes, 29
Scenic generator, creating objects in reporting schema, 132–133
Schemas
creating objects in reporting schema, 132
isolating activities in reporting database, 130
materialized view use in reporting schema, 132–134
shapefile import schema, 150
table use in reporting schema, 134–138
working with multiple schemas, 131–132
Scope, querying “big data,” 66–67
Seconds, GIS primer, 142
SELECT, creating series of values, 117
SELECT FROM, defining subqueries, 152
Self joins, SQL, 209–210
Shapefiles
ETL (Extract, Transform, Load), 151–153
importing from, 150–151
importing zip code shapefile, 170–172
overview of, 150
putting into EXCLUDED record, 153
Slices
creating legible labels, 19–20
mouseover effects used in pie charts, 20–21
Spatial Reference System Identifier (SRID), 143
SQL
bulk import, 202–203
combining data into time series, 117–118
CTE (Common Table Expressions), 84–86
finding data within bounding boxes, 185–187
finding items near a point, 189–190
heatmaps, 86–90
limitations in database portability and, 73–74
limitations of ActiveRecord and, 74–75
PostgreSQL tools, 202
PostgreSQL window functions, 81–84
reasons for using, 73
renaming SQL statement file to include schema, 133
scatter plot applied to mortgage payment, 79–81
spatial queries, 183
subqueries, 84
summary, 90–91
user-defined functions, 75–78
using in Rails, 78–79
writing bounding box queries, 184–185
SQL joins
combining data into time series, 117–121
cross joins, 209
database setup, 207
inner joins, 207–208
outer joins, 208–209
self joins, 209–210
shapefile ETL and, 151–152
SQLite, 196
Square matrix format, for chord diagram
finalizing, 103–104
generating, 101–103
overview of, 93–94
SRID (Spatial Reference System Identifier), 143
ST_AsGeoJSON, PostGIS, 174
Stash, file size limits, 59
Statistics, limitations of, 68–69
ST_Centroid, PostGIS functions, 146
ST_Distance, PostGIS functions, 146, 188, 190–191
ST_DistanceSphere, PostGIS functions, 146
ST_DWithin, PostGIS functions, 188
ST_GeomFromText, PostGIS functions, 146
Strings, converting into dates, 117–118
Subqueries
CTE (Common Table Expressions), 84–86
defining, 152
overview of, 84
SVG (Scalable Vector Graphics)
with D3, 5
focusMax variable, 52–53
focusMin variable, 53–54
<g> element for grouping shapes, 24
Leaflet and, 155
T
Tables
creating geospatial table fields, 148
inserting bulk records, 136
linking using foreign keys, 130
migrating, 135
model for, 135
relationship to schemas, 130
use in reporting schema, 134–135
Text, displaying temperature changes in weather app, 55–56
Tiles, Leaflet maps, 156
Time series aggregation, in PostgreSQL
basic timeline, 121–124
fancy timeline, 124–127
finding flight segments, 115–117
generate_series function, 117
overview of, 115
summary, 127
turning data into time series, 117–121
Time series data
adding text for displaying temperature changes, 55–56
applying multi-line graph, 50–51
applying simple line graph, 45
connecting focus circles, 56–58
creating weather app, 42–45
downloading historic weather data, 41–42
fetching data for weather app, 45–47
GHCN example, 41
highlighting maximum temperature, 51–53
highlighting minimum temperature, 53–55
makeLineChart (), 48–50
weather controller, 45
Timelines, graphical
basic, 121–124
fancy, 124–127
Timestamps
creating time series, 117–118
data, 115
Transactional databases, 129–130
tsrange datatype, PostgreSQL, 118–119
U
“Uber Rides by Neighborhood” (Bostock), 106
UPDATE query
defining subqueries, 152–153
updating missing data, 153–154
User-defined functions, 75–78
V
Variables
applying scatter plot for values with two variables, 28–29
connecting focus circles, 56–58
displaying temperature changes in weather app, 55–56
environment variables, 198
highlighting maximum temperature in line graph, 52–53
highlighting minimum temperature in line graph, 53–54
Views
bar chart, 24–25
box plot, 35–36
chord diagram, 104–105
GeoJSON view of map data, 159–161
heatmap, 87
line graph, 47
Scatter plot, 29
W
weather app. See Rails apps, setting up weather app; Time series data
Weather stations, drawing map of, 161–163
wget, downloading large files, 60
Whiskers (lines), in box plots, 35
Window functions, PostgreSQL
finding empty flight legs, 108–110
first_value() and last_value(), 82–83
lead and lag, 82
overview of, 81–82
PARTITION BY and OVER clauses, 82–83
WITH queries. See CTE (Common Table Expressions)
WKT (Well-Known Text)
bounding box formatted as, 184–185
query formats, 187
translating geometry datatype to, 174–175
Worker process, isolating activities in reporting database, 130
X
x, y coordinate pairs, applying scatter plot for values with two variables,
28–29
X,Y- axis, GIS primer, 141–142
Z
Zip codes
choropleth thematic map, 177–180
finding data within bounding boxes, 185–187
updating residential sales app, 168–170
Zoom levels, map views, 162–163
Code Snippets