Visualize your Outlook meetings using Ruby and NVD3
The other day my co-worker lamented that he has so many meetings he couldn't get any actual work done. His frustration lead me to think of how I could visualize how much time meetings take up during the workday.
We use Microsoft Outlook, and I knew I could programmatically access the calendar information. This could be a small, fun project to visualize.
Goals & Criteria
After ruminating a bit, I came up with these goals:
- Write a ruby script which visualizes the amount of time meetings take up for each workday. I want to keep it compact, so everything is going in one source code file.
- Calendar data will be extracted from local Outlook running on Windows.
- The Ruby script will extract Outlook calendar items using Microsoft OLE Automation via the win32ole module.
- The data will be saved in a temporary SQLite3 database.
- Appointments will be selected, and summed, from the database with the following criteria:
- Only if less than 1 day
- Ignore lunch block
- Only workdays (Mon-Fri)
- start / end on same day
- The data will be normalized, and converted to JSON for use in a NVD3 MultiBarChart.
- An index.html file will be generated which renders the NVD3 MultiBarChart.
Requirements
This ruby script only runs on Windows where Outlook is installed. It has been tested using Ruby 1.9.3-p545 on Windows 7 x64.
Retrieving source
From the Windows command-line, clone the project from GitLab:
git clone https://gitlab.com/ejstembler/outlook-meetings-visualizer.git
Installing dependencies
Install bundler 1.6.3 if you don't already have it installed:
gem install bundler
Run bundler before attempting to run the script:
bundle install
Running the script
ruby outlook_meetings_visualizer.rb
The Output
After it has successfully completed, it will automatically open the index.html
file in the output
directory in the default web browser.
Here's what it looks like for my calendar:
How it Works
The script extracts Outlook calendar items using Microsoft OLE Automation via the win32ole module. The data is saved in a temporary SQLite3 database at db/calendar.sqlite3.
Appointments are selected, and summed, from the database with the following criteria:
- Only if less than 1 day
- Ignore lunch block
- Only workdays (Mon-Fri)
- start / end on same day
The data is normalized, and converted to JSON for use in a NVD3 MultiBarChart. An index.html file is generated in output/index.html which renders the NVD3 MultiBarChart.
The main run
method performs the following steps:
open_database
create_calendar_table
insert_appointments
select_data
close_database
save_index_html
open_index_html
Here's what outlook_meetings_visualizer.rb
looks like:
require 'sqlite3'
require 'win32ole'
require 'json'
require 'launchy'
require 'fileutils'
class OutlookMeetingsVisualizer
DB_DATE = '%Y-%m-%d'
DB_DATETIME = DB_DATE + ' %H:%M:%S.%L'
INSERT_SQL = 'INSERT INTO appointments (subject, location, start_datetime, duration, end_datetime, body) VALUES (?, ?, ?, ?, ?, ?)'
def open_database
db_path = File.join(File.dirname(__FILE__), 'db/calendar.sqlite3')
FileUtils.rm(db_path) if File.exist?(db_path)
@db = SQLite3::Database.new(db_path)
end
def close_database
@db.close
end
def create_calendar_table
@db.execute <<-SQL
CREATE TABLE appointments (
subject VARCHAR(255),
location VARCHAR(255),
start_datetime VARCHAR(23),
duration INT,
end_datetime VARCHAR(23),
body BLOB
);
SQL
end
def insert_appointments
# http://rubyonwindows.blogspot.com/2007/07/automating-outlook-with-ruby-calendar.html
outlook = WIN32OLE.new('Outlook.Application')
mapi = outlook.GetNameSpace('MAPI')
calendar = mapi.GetDefaultFolder(9)
calendar.Items.each do |appointment|
subject = appointment.Subject
location = appointment.Location
start_datetime = appointment.Start.strftime(DB_DATETIME)
duration = appointment.Duration
end_datetime = appointment.End.strftime(DB_DATETIME)
body = appointment.Body
@db.execute(INSERT_SQL, [subject, location, start_datetime, duration, end_datetime, body])
end
end
def select_data
# http://www.sqlite.org/lang_datefunc.html
sql = <<-SQL
SELECT
start_date,
SUM(duration)
FROM
(
SELECT
duration,
strftime('#{DB_DATE}', start_datetime) start_date
FROM
appointments
WHERE
duration < 1440 -- Only if less than 1 day
AND subject <> 'Lunch' -- Ignore lunch block
AND strftime('%w', start_datetime) in ('1', '2', '3' ,'4', '5') -- Only workdays (Mon-Fri)
AND strftime('%w', end_datetime) in ('1', '2', '3' ,'4', '5') -- Only workdays (Mon-Fri)
AND strftime('#{DB_DATE}', start_datetime) = strftime('#{DB_DATE}', end_datetime) -- start/end on same day
)
GROUP BY
start_date
SQL
@data = @db.execute(sql)
end
def save_index_html
File.open('output/index.html', 'wt') {|f| f.write(index_html)}
end
def open_index_html
Launchy.open 'output/index.html'
end
def run
open_database
create_calendar_table
insert_appointments
select_data
close_database
save_index_html
open_index_html
end
def data_for_nvd3
meetings = { key: 'Meetings', values: @data.map {|row| { x: row[0], y: row[1] }} }
workday = { key: 'Workday', values: @data.map {|row| { x: row[0], y: 480 - row[1] }} }
"#{meetings.to_json},\n#{workday.to_json}"
end
def nvd3_chart_javascript
<<-JAVASCRIPT
var data = [
#{data_for_nvd3}
];
nv.addGraph(function() {
var chart = nv.models.multiBarChart()
.transitionDuration(350)
.reduceXTicks(true) // If 'false', every single x-axis tick label will be rendered.
.rotateLabels(0) // Angle to rotate x-axis labels.
.showControls(false) // Allow user to switch between 'Grouped' and 'Stacked' mode.
.groupSpacing(0.1) // Distance between each group of bars.
.stacked(true);
chart.yAxis
.tickFormat(d3.format(',d'));
d3.select('#chart svg')
.datum(data)
.call(chart);
nv.utils.windowResize(chart.update);
return chart;
});
JAVASCRIPT
end
def index_html
<<-HTML
Meetings Visualization
HTML
end
end
if __FILE__ == $PROGRAM_NAME
OutlookMeetingsVisualizer.new.run
end
Full Source Code
ejstembler/outlook-meetings-visualizerMore Ruby Articles
- How to connect to Impala + Kerberos with Ruby
- Learning Hadoop
- Microsoft Message Queue (MSMQ) in Ruby
- Netflix OData Episodes
- Authenticates a Ruby on Rails User model via LDAP and saves their LDAP photo if they have one
- Select table names from SQL Server from a Mac via ActiveRecord and TinyTDS driver
- Retrieving Twitter account from Mac OS X Lion’s Address Book via MacRuby
- Using .NET 4.0’s Task Parallel Library with IronRuby 1.1
- Convert XML to JSON using Ruby and ActiveSupport