Traffic Grapher
This is a solution to Ruby Quiz #146, written by Gavin Kistner. The quiz begins:
You have been hired to work for a small city government. The city recently bought a vehicle counter, one of those kinds that uses pneumatic rubber hoses stretched across the road. The company that sells the machine also sells software to interpret the raw data. However, the city has asked you to see if you can interpret it instead, saving them some money.
The quiz asked for the processed data to be sliced and diced in various ways for output. Because there were so many variations, I decided to create a domain-specific language (DSL) to describe the graphs. The DSL allows you to specify:
- filters to determine which data makes it into the graph
- how the data is divided up into bins across the horizontal axis
- one or more series to be shown interleaved with one another
- one or more combiners to determine what the vertical axis is showing (traffic volume, average traffic speed, average inter-vehicular distance, etc.); if more than one combiner is specified, then they act like a series
Here is the main.rb file, which is the entry point to the program, to give you a sense of the DSL:
# This is a solution to Ruby Quiz #146 (see http://www.rubyquiz.com/)
# is Copyright 2007 by J. E. Ivancich, all rights reserved. It is
# released under the Creative Commons Attribution-Share Alike 3.0
# United States License.
# This source code can also be found at:
# http://learnruby.com/examples/ruby-quiz-146.shtml
# This file is the main entry point to the program.
# It creates 109 different graphs slicing and dicing the data in
# various ways. 90 of the graphs are created through two nested loops
# giving various day/direction combinations. 12 of the graphs are
# created through a single loop (the outer of the two nested loops).
# And the remaining 7 are created outside any loop.
require 'traffic_analyzer'
require 'traffic_grapher'
records = analyze "vehicle_counter.data"
graph records, "Maximum Speed by Hour and Direction" do
series WestboundFilter
series EastboundFilter
bins HourSequence
combiner MaxSpeedCombiner
end
graph records, "Minimum Speed by Hour and Direction" do
series WestboundFilter
series EastboundFilter
bins HourSequence
combiner MinSpeedCombiner
end
graph records, "Average Speed by Hour and Direction" do
series WestboundFilter
series EastboundFilter
bins HourSequence
combiner AverageSpeedCombiner
end
graph records, "Speed Distribution by Direction" do
series AllAndEachDirectionSequence
bins SpeedRangeSequence
combiner VolumeCombiner
end
graph records, "Speed Distribution by AM-PM" do
series AM_PM_Sequence
bins SpeedRangeSequence
combiner VolumeCombiner
end
graph records, "Intra-Car Distance in Feet by Hour and Direction" do
series WestboundFilter
series EastboundFilter
bins HourSequence
combiner AverageDistanceCombiner
end
graph records, "Intra-Car Distance in Feet by Quarter Hour" do
series QuarterHourSequence
bins HourSequence
combiner AverageDistanceCombiner
end
#
# Here we'll use nested loops to create a series of graphs that are
# slight variations of one another.
#
# make graphs for all days, Monday, Tuesday, ...
AllAndEachDaySequence.each do |day_filter|
graph records, "%s Volume by Hour and Direction" % day_filter.name do
filter day_filter
series WestboundFilter
series EastboundFilter
bins HourSequence
combiner VolumeCombiner
end
# note: this one uses multiple combiners and a single (default)
# series; there can *either* be mulitple series or multiple
# combiners, but not both
graph records, "%s Speed by Hour" % day_filter.name do
filter day_filter
bins HourSequence
combiner MinSpeedCombiner
combiner AverageSpeedCombiner
combiner MaxSpeedCombiner
end
# make graphs for both directions, Eastbound, and Westbound
AllAndEachDirectionSequence.each do |direction_filter|
graph(records,
"%s %s Volume by AM-PM" %
[day_filter.name, direction_filter.name]) do
filter day_filter
series OneSequence
bins AM_PM_Sequence
combiner VolumeCombiner
end
graph(records,
"%s %s Volume by Hour" % [day_filter.name, direction_filter.name]) do
filter day_filter
series OneSequence
bins HourSequence
combiner VolumeCombiner
end
graph(records,
"%s %s Volume by Half Hour" %
[day_filter.name, direction_filter.name]) do
filter day_filter
series FirstHalfHourFilter
series SecondHalfHourFilter
bins HourSequence
combiner VolumeCombiner
end
graph(records,
"%s %s Volume by Third Hour" %
[day_filter.name, direction_filter.name]) do
filter day_filter
series FirstThirdHourFilter
series SecondThirdHourFilter
series ThirdThirdHourFilter
bins HourSequence
combiner VolumeCombiner
end
graph(records,
"%s %s Volume by Quarter Hour" %
[day_filter.name, direction_filter.name]) do
filter day_filter
series FirstQtrHourFilter
series SecondQtrHourFilter
series ThirdQtrHourFilter
series FourthQtrHourFilter
bins HourSequence
combiner VolumeCombiner
end
end
end
The source code, spread among five source files, and a data file is available in zip format: traffic_grapher.zip.
Note: you will need to install the scruffy RubyGem, which this code relies on to generate the graphs.