Merge pull request #55 from differentreality/stats_charts

Stats charts for registrations (Admin)
This commit is contained in:
Henne Vogelsang 2013-09-26 08:38:44 -07:00
commit 5c719afbdf
13 changed files with 9349 additions and 110 deletions

View file

@ -19,3 +19,5 @@
//= require cocoon
//= require bootstrap
//= require osem
//= require d3.v3
//= require d3.v3.min

8774
app/assets/javascripts/d3.v3.js vendored Normal file

File diff suppressed because it is too large Load diff

5
app/assets/javascripts/d3.v3.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -11,8 +11,10 @@ class Admin::StatsController < ApplicationController
@registered_with_partner = @conference.registrations.where("attending_with_partner = ?", true).count
@attended_with_partner = @conference.registrations.where("attending_with_partner = ? AND attended = ?", true, true).count
@handicapped_access = @conference.registrations.where("handicapped_access_required = ?", true).count
@handicapped_access = @conference.registrations.where(handicapped_access_required: true).count
@handicapped_access_attended = @conference.registrations.where(handicapped_access_required: true)
@handicapped_access_attended = @handicapped_access_attended.where(attended: true).count
@suggested_hotel_stay = @conference.registrations.where("using_affiliated_lodging = ?", true).count
@speakers = Person.joins(:events).where("events.conference_id = ? AND events.state LIKE ?", @conference.id, 'confirmed').uniq
@ -21,27 +23,53 @@ class Admin::StatsController < ApplicationController
@supporter_levels = @conference.supporter_levels
@tickets = @conference.registrations.joins(:supporter_registration => :supporter_level)
@tickets = @tickets.where("registrations.conference_id = ? AND supporter_levels.title NOT LIKE ? ", @conference, "%Free%").count
@tickets = @tickets.where("supporter_levels.title NOT LIKE ? ", "%Free%")
@mydays = []
@mytickets = []
(@conference.registration_start_date..@conference.end_date).each do |day|
day_reg_count = @registrations.where("created_at LIKE '%#{day}%' ").count
day_ticket_count = @tickets.where("supporter_registrations.created_at LIKE '%#{day}%' ").count
if day_reg_count != 0
@mydays << {"status" => "#{day}", "value" => day_reg_count}
end
@conference.supporter_levels.each do |level|
day_ticket_count = @tickets.where("supporter_registrations.created_at LIKE ? AND supporter_levels.title LIKE ?", "%#{day}%", "%#{level.title}%").count
if day_ticket_count !=0
@mytickets << {"status" => "#{day}", "ticket" => "#{level}", "value" => day_ticket_count}
end
end
end
@other_info = [
{"status" => "with partner (registered)", "value" => @registered_with_partner},
{"status" => "with partner (attended)", "value" => @attended_with_partner},
{"status" => "handicapped (registered)", "value" => @handicapped_access},
{"status" => "handicapped (attended)", "value" => @handicapped_access_attended},
{"status" => "stay at suggested hotel", "value" => @suggested_hotel_stay}
]
end
def speaker_reg(speaker)
speaker.registrations.where("conference_id = ? AND person_id = ?", @conference.id, speaker.id).first
end
def speaker_diet(reg)
@conference.dietary_choices.find(reg.dietary_choice_id)
end
def diet_count(diet)
@conference.registrations.where("dietary_choice_id = ?", diet).count
@conference.registrations.where("dietary_choice_id = ?", diet)
end
def social_event_count(event)
@conference.registrations.joins(:social_events).where("registrations_social_events.social_event_id = ?", event).count
end
helper_method :speaker_reg
helper_method :speaker_diet
helper_method :diet_count
helper_method :social_event_count
end
end

View file

@ -0,0 +1,70 @@
<script>
var registered = '<%= h @registrations.count%>';
var data = <%=raw @other_info.to_json %>;
var valueLabelWidth = 70; // space reserved for value labels (right)
var barHeight = 24; // height of one bar
var barLabelWidth = 210; // space reserved for bar labels
var barLabelPadding = 8; // padding between bar and bar labels (left)
var gridLabelHeight = 18; // space reserved for gridline labels
var gridChartOffset = 3; // space between start of grid and first bar
var maxBarWidth = 350; // width of the bar with the max value
var color = d3.scale.category20();
// accessor functions
var barLabel = function(d) { return d['status']; };
var barValue = function(d) { return d['value'] / registered * 100; };
var barValues = function(d) { return d['value']; };
// scales
var yScale = d3.scale.ordinal().domain(d3.range(0, data.length)).rangeBands([0, data.length * barHeight]);
var y = function(d, i) { return yScale(i); };
var yText = function(d, i) { return y(d, i) + yScale.rangeBand() / 2; };
var yMax = d3.max(data, function(d){ return barValue(d) + 15; });
var x = d3.scale.linear().domain([0, yMax]).range([0, maxBarWidth]);
// svg container element
var chart = d3.select('#other_info').append("svg")
.attr('width', maxBarWidth + barLabelWidth + valueLabelWidth)
.attr('height', gridLabelHeight + gridChartOffset + data.length * barHeight);
// bar labels
var labelsContainer = chart.append('g')
.attr('transform', 'translate(' + (barLabelWidth - barLabelPadding) + ',' + (gridLabelHeight + gridChartOffset) + ')');
labelsContainer.selectAll('text').data(data).enter().append('text')
.attr('y', yText)
.attr('stroke', 'none')
.attr('fill', 'black')
.attr("dy", ".35em") // vertical-align: middle
.attr('text-anchor', 'end')
.text(barLabel);
// bars
var barsContainer = chart.append('g')
.attr('transform', 'translate(' + barLabelWidth + ',' + (gridLabelHeight + gridChartOffset) + ')');
barsContainer.selectAll("rect").data(data).enter().append("rect")
.attr('y', y)
.attr('height', yScale.rangeBand())
.attr('width', function(d) { return x(barValue(d)); })
.attr('stroke', 'white')
.attr('fill', function(d) { return color(barLabel(d)); });
// bar value labels
barsContainer.selectAll("text").data(data).enter().append("text")
.attr("x", 5)
.attr("y", yText)
.attr("dx", 3) // padding-left
.attr("dy", ".35em") // vertical-align: middle
.attr("text-anchor", "start") // text-align: right
.attr("fill", "black")
.attr("stroke", "none")
.text(function(d) { return d3.round(barValues(d), 2) + " (" + d3.round(barValue(d), 0) + "%)"; });
// start line
barsContainer.append("line")
.attr("y1", -gridChartOffset)
.attr("y2", yScale.rangeExtent()[1] + gridChartOffset)
.style("stroke", "#000");
</script>

View file

@ -0,0 +1,85 @@
<style>
.arc path {
stroke: #fff;
}
</style>
<script>
var width = 250,
height = 250,
radius = Math.min(width, height) / 2;
var color = d3.scale.ordinal()
.range(["#9ECAE1", "#6baed6", "#DEEBF7"]);
var arc = d3.svg.arc()
.outerRadius(radius - 10)
.innerRadius(0);
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d.value; });
// Where to put the chart
var svg = d3.select("#preregistered_onsite").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var total = '<%= h @registrations.count %>';
var pre_registered = '<%= h @pre_registered %>';
var onsite = '<%= h @registrations.count - @pre_registered %>';
var data = [
{"status":"onsite","value": onsite, "percent": d3.round(onsite / total * 100)},
{"status":"pre_registered","value":pre_registered, "percent": d3.round(pre_registered / total * 100)}
];
var g = svg.selectAll(".arc")
.data(pie(data))
.enter().append("g")
.attr("class", "arc");
g.append("path")
.attr("d", arc)
.style("fill", function(d) { return color(d.data.status); });
// Put text inside every piece of the pie-chart
g.append("text")
.attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; })
.attr("dy", ".35em")
.style("text-anchor", "middle")
.text(function(d) { return d.data.percent +"%"; });
// Title of chart
svg.append("text")
.attr("class", "title")
.attr("x", radius)
.attr("y", -radius+10)
.text("% of pre-registered");
// Create & position legend
var legend = d3.select("#preregistered_onsite").append("svg")
.attr("class", "legend")
.attr("width", radius * 2)
.attr("height", radius)
.selectAll("g")
.data(color.domain().slice().reverse())
.enter().append("g")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
legend.append("rect")
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("x", 24)
.attr("y", 9)
.attr("dy", ".15em")
.text(function(d) { return d + " (" + data.filter(function(v){return v.status === d;})[0].value + " or " + data.filter(function(v){return v.status === d;})[0].percent + "%)" ; });
</script>

View file

@ -0,0 +1,85 @@
<style>
.arc path {
stroke: #fff;
}
</style>
<script>
var width = 250,
height = 250,
radius = Math.min(width, height) / 2;
var color = d3.scale.ordinal()
.range(["#9ECAE1", "#6baed6", "#DEEBF7"]);
var arc = d3.svg.arc()
.outerRadius(radius - 10)
.innerRadius(0);
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d.value; });
// Where to put the chart
var svg = d3.select("#registered_attended").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var registered = '<%= h @pre_registered%>';
var attended = '<%= h @pre_registered_attended %>';
var not_attended = '<%= h @pre_registered - @pre_registered_attended %>';
var data = [
{"status":"did not attend","value":not_attended, "percent": d3.round(not_attended / registered * 100)},
{"status":"attended","value": attended, "percent": d3.round(attended / registered * 100)}
];
var g = svg.selectAll(".arc")
.data(pie(data))
.enter().append("g")
.attr("class", "arc");
g.append("path")
.attr("d", arc)
.style("fill", function(d) { return color(d.data.status); });
// Put text inside every piece of the pie-chart
g.append("text")
.attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; })
.attr("dy", ".35em")
.style("text-anchor", "middle")
.text(function(d) { return d.data.percent +"%"; });
// Title of chart
svg.append("text")
.attr("class", "title")
.attr("x", radius)
.attr("y", -radius+10)
.text("% of pre-registered");
// Create & position legend
var legend = d3.select("#registered_attended").append("svg")
.attr("class", "legend")
.attr("width", radius*2)
.attr("height", radius)
.selectAll("g")
.data(color.domain().slice().reverse())
.enter().append("g")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
legend.append("rect")
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("x", 24)
.attr("y", 9)
.attr("dy", ".15em")
.text(function(d) { return d + " (" + data.filter(function(v){return v.status === d;})[0].value + " or " + data.filter(function(v){return v.status === d;})[0].percent + "%)" ; });
</script>

View file

@ -0,0 +1,71 @@
<script>
var registered = '<%= h @registrations.count%>';
var data = <%=raw @mydays.to_json %>;
var valueLabelWidth = 10; // space reserved for value labels (right)
var barHeight = 24; // height of one bar
var barLabelWidth = 100; // space reserved for bar labels
var barLabelPadding = 8; // padding between bar and bar labels (left)
var gridLabelHeight = 18; // space reserved for gridline labels
var gridChartOffset = 3; // space between start of grid and first bar
var maxBarWidth = 420; // width of the bar with the max value
var color = d3.scale.category20();
// accessor functions
var barLabel = function(d) { return d['status']; };
var barValue = function(d) { return d['value'] / registered * 100; };
var barValues = function(d) { return d['value']; };
// scales
var yScale = d3.scale.ordinal().domain(d3.range(0, data.length)).rangeBands([0, data.length * barHeight]);
var y = function(d, i) { return yScale(i); };
var yText = function(d, i) { return y(d, i) + yScale.rangeBand() / 2; };
var yMax = d3.max(data, function(d){ return barValue(d) + 15; });
var x = d3.scale.linear().domain([0, yMax]).range([0, maxBarWidth]);
var width = maxBarWidth + barLabelWidth + valueLabelWidth;
// svg container element
var chart = d3.select('#registered_time').append("svg")
.attr('width', maxBarWidth + barLabelWidth + valueLabelWidth)
.attr('height', gridLabelHeight + gridChartOffset + data.length * barHeight);
// bar labels
var labelsContainer = chart.append('g')
.attr('transform', 'translate(' + (barLabelWidth - barLabelPadding) + ',' + (gridLabelHeight + gridChartOffset) + ')');
labelsContainer.selectAll('text').data(data).enter().append('text')
.attr('y', yText)
.attr('stroke', 'none')
.attr('fill', 'black')
.attr("dy", ".35em") // vertical-align: middle
.attr('text-anchor', 'end')
.text(barLabel);
// bars
var barsContainer = chart.append('g')
.attr('transform', 'translate(' + barLabelWidth + ',' + (gridLabelHeight + gridChartOffset) + ')');
barsContainer.selectAll("rect").data(data).enter().append("rect")
.attr('y', y)
.attr('height', yScale.rangeBand())
.attr('width', function(d) { return x(barValue(d)); })
.attr('stroke', 'white')
.attr('fill', function(d) { return color(barValue(d)); });
// bar value labels
barsContainer.selectAll("text").data(data).enter().append("text")
.attr("x", 5)
.attr("y", yText)
.attr("dx", 3) // padding-left
.attr("dy", ".35em") // vertical-align: middle
.attr("text-anchor", "start") // text-align: right
.attr("fill", "black")
.attr("stroke", "none")
.text(function(d) { return d3.round(barValues(d), 2) + " (" + d3.round(barValue(d), 0) + "%)"; });
// start line
barsContainer.append("line")
.attr("y1", -gridChartOffset)
.attr("y2", yScale.rangeExtent()[1] + gridChartOffset)
.style("stroke", "#000");
</script>

View file

@ -1,72 +1,32 @@
.row-fluid
.span12
%h3
Total Registrations
- if @registered
= "(#{@registered})"
- if @attendees
= " - Attendees (#{@attendees})"
.row-fluid
.span3
%table.table.table-bordered.table-striped
%thead
%th General Stats
%th #
%tr
%td Registered attended
%td= @pre_registered_attended
%tr
%td Registered not attended
%td= @pre_registered - @pre_registered_attended
%tr
%td Attended with partner
%td= @attended_with_partner
%tr
%td Registred to attend with partner
%td= @registered_with_partner
%tr
%td Handicapped Access for
%td= @handicapped_access
%tr
%td Staying at suggested hotel
%td= @suggested_hotel_stay
.span4
%h4{:style => "text-indent: 25px"}= "Registrations - Attendees (#{(@attendees.to_f / @registered * 100).round}%)"
.row-fluid
.span12#registrations_attendees
.span3
- if @conference.use_dietary_choices == true
- diet_choices = []
- @conference.dietary_choices.each do |d|
- count = diet_count(d.id)
- if count > 0
- diet_choices << [d.title, count]
- if diet_choices.any?
%table.table.table-bordered.table-striped
%thead
%th Dietary Choice
%th #
%th Percentage
- diet_choices.each do |d|
%tr
%td= d[0]
%td= d[1]
%td
- if @registered and @registered > 0
= "#{d[1] * 100 / @registered}% of registered"
- if @attendees and @attendees > 0
%br
= "#{d[1] * 100 / @attendees}% of attendees"
.span3
- if @conference.social_events.count > 0
- social_events = []
- @conference.social_events.each do |social_event|
- count = social_event_count(social_event)
- if count > 0
- social_events << [social_event.title, count]
- if social_events.any?
%table.table.table-bordered.table-striped
%thead
%th Social Event
%th #
- social_events.each do |s|
%tr
%td= s[0]
%td= s[1]
.span4
%h4{:style => "text-indent: 10px"}= "Attendance of pre-registered"
.row-fluid
.span12#registered_attended
.span4
%h4{:style => "text-indent: 25px"}= "% of pre-registered"
.row-fluid
.span12#preregistered_onsite
.row-fluid
.span6
%h4= "Other Info"
.row-fluid
.span12#other_info
.span6
%h4{:style => "text-indent: 98px"}= "Registrations over time"
.row-fluid
.span12#registered_time
= render :partial => "registered_attended"
= render :partial => "registered_time"
= render :partial => "preregistered_onsite"
= render :partial => "registrations_attendees"
= render :partial => "other_info"

View file

@ -0,0 +1,96 @@
<script>
var w = 250,
h = 250,
r = Math.min(w, h) / 2 - 10,
inner = 80,
color = d3.scale.category20c();
var registrations = '<%= h @registered %>';
var attendees = '<%= h @attendees %>';
var data = [
{"status":"Registrations","value": registrations, "percent": ""},
{"status":"Attendees","value": attendees, "percent": d3.round(attendees/registrations * 100)+"%"}
];
var vis = d3.select("#registrations_attendees")
.append("svg:svg")
.data([data])
.attr("width", w)
.attr("height", h)
.append("svg:g")
.attr("transform", "translate(" + r * 1.1 + "," + r * 1.1 + ")")
var textTop = vis.append("text")
.attr("dy", ".35em")
.style("text-anchor", "middle")
.attr("class", "textTop")
.text( "Registered: " + registrations)
.attr("y", -10),
textBottom = vis.append("text")
.attr("dy", ".35em")
.style("text-anchor", "middle")
.attr("class", "textBottom")
.text("Attended: " + attendees)
.attr("y", 10);
var arc = d3.svg.arc()
.innerRadius(inner)
.outerRadius(r);
var arcOver = d3.svg.arc()
.innerRadius(inner + 5)
.outerRadius(r + 5);
var pie = d3.layout.pie()
.value(function(d) { return d.value; });
var arcs = vis.selectAll("g.slice")
.data(pie)
.enter()
.append("svg:g")
.attr("class", "slice")
.on("mouseover", function(d) {
d3.select(this).select("path").transition()
.duration(200)
.attr("d", arcOver)
textTop.text(d3.select(this).datum().data.status)
.attr("y", -10);
textBottom.text( d3.select(this).datum().data.value)
.attr("y", 10);
})
.on("mouseout", function(d) {
d3.select(this).select("path").transition()
.duration(100)
.attr("d", arc);
textTop.text("Registered: " + registrations)
.attr("y", -10);
textBottom.text("Attended: " + attendees);
});
arcs.append("svg:path")
.attr("fill", function(d, i) { return color(i); } )
.attr("d", arc);
var legend = d3.select("#registrations_attendees").append("svg")
.attr("class", "legend")
.attr("width", r * 2)
.attr("height", r)
.selectAll("g")
.data(data)
.enter().append("g")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
legend.append("rect")
.attr("width", 18)
.attr("height", 18)
.style("fill", function(d, i) { return color(i); });
legend.append("text")
.attr("x", 24)
.attr("y", 9)
.attr("dy", ".35em")
.text(function(d) { return "Total " + d.status; });
</script>

View file

@ -1,28 +1,5 @@
.row-fluid
.span12
%h3
Non-free Tickets (#{@tickets})
.row-fluid
.span5
- if @supporter_levels.count > 0
%table.table.table-bordered.table-striped
%thead
%th Support Level
%th #
%th %
- @supporter_levels.each do |level|
- reg_count = level.supporter_registrations.where(conference_id: @conference).count
- if reg_count > 0
%tr
%td= level.title
%td
= reg_count
%td
- if @registered and @registered > 0
= "#{reg_count * 100 / @registered}% of registered"
- if @attendees and @attendees > 0
%br
= "#{reg_count * 100 / @attendees}% of attendees"
- else
%p
No tickets sold yet
- if @tickets
%h2
= "Non-Free Tickets(#{@tickets.count})"
.span4#tickets_time
= render :partial => "tickets_time"

View file

@ -0,0 +1,86 @@
<script>
var registered = '<%= h @registrations.count%>';
var data = <%=raw @mytickets.to_json %>;
var valueLabelWidth = 40; // space reserved for value labels (right)
var barHeight = 24; // height of one bar
var barLabelWidth = 100; // space reserved for bar labels
var barLabelPadding = 8; // padding between bar and bar labels (left)
var gridLabelHeight = 18; // space reserved for gridline labels
var gridChartOffset = 3; // space between start of grid and first bar
var maxBarWidth = 420; // width of the bar with the max value
// accessor functions
var barLabel = function(d) { return d['status']; };
var barValue = function(d) { return d['value'] / registered * 100; };
var barValues = function(d) { return d['value']; };
// scales
var yScale = d3.scale.ordinal().domain(d3.range(0, data.length)).rangeBands([0, data.length * barHeight]);
var y = function(d, i) { return yScale(i); };
var yText = function(d, i) { return y(d, i) + yScale.rangeBand() / 2; };
var x = d3.scale.linear().domain([0, 100]).range([0, maxBarWidth]);
// svg container element
var chart = d3.select('#tickets_time').append("svg")
.attr('width', maxBarWidth + barLabelWidth + valueLabelWidth)
.attr('height', gridLabelHeight + gridChartOffset + data.length * barHeight);
/*
// grid line labels
var gridContainer = chart.append('g')
.attr('transform', 'translate(' + barLabelWidth + ',' + gridLabelHeight + ')');
gridContainer.selectAll("text").data(x.ticks(10)).enter().append("text")
.attr("x", x)
.attr("dy", -3)
.attr("text-anchor", "middle")
.text(String);
// vertical grid lines
gridContainer.selectAll("line").data(x.ticks(10)).enter().append("line")
.attr("x1", x)
.attr("x2", x)
.attr("y1", 0)
.attr("y2", yScale.rangeExtent()[1] + gridChartOffset)
.style("stroke", "#ccc");*/
// bar labels
var labelsContainer = chart.append('g')
.attr('transform', 'translate(' + (barLabelWidth - barLabelPadding) + ',' + (gridLabelHeight + gridChartOffset) + ')');
labelsContainer.selectAll('text').data(data).enter().append('text')
.attr('y', yText)
.attr('stroke', 'none')
.attr('fill', 'black')
.attr("dy", ".35em") // vertical-align: middle
.attr('text-anchor', 'end')
.text(barLabel);
// bars
var barsContainer = chart.append('g')
.attr('transform', 'translate(' + barLabelWidth + ',' + (gridLabelHeight + gridChartOffset) + ')');
barsContainer.selectAll("rect").data(data).enter().append("rect")
.attr('y', y)
.attr('height', yScale.rangeBand())
.attr('width', function(d) { return x(barValue(d)); })
.attr('stroke', 'white')
.attr('fill', 'steelblue');
// bar value labels
barsContainer.selectAll("text").data(data).enter().append("text")
.attr("x", function(d) { return x(barValue(d)); })
.attr("y", yText)
.attr("dx", 3) // padding-left
.attr("dy", ".35em") // vertical-align: middle
.attr("text-anchor", "start") // text-align: right
.attr("fill", "black")
.attr("stroke", "none")
.text(function(d) { return d3.round(barValues(d), 2) + " registrations (" + d3.round(barValue(d), 0) + "%)"; });
// start line
barsContainer.append("line")
.attr("y1", -gridChartOffset)
.attr("y2", yScale.rangeExtent()[1] + gridChartOffset)
.style("stroke", "#000");
</script>

View file

@ -15,8 +15,8 @@
= render :partial => 'speakers'
.tab-pane#tickets
= render :partial => 'tickets'
:javascript
:javascript
$('#myTab a').click(function (e) {
e.preventDefault();
$(this).tab('show');