Commit 4afde4dd by Miles Steele

add proxied analytics graphs, refactor analytics

parent fb8c84a5
......@@ -142,5 +142,6 @@ def _section_analytics(course_id):
'section_key': 'analytics',
'section_display_name': 'Analytics',
'get_distribution_url': reverse('get_distribution', kwargs={'course_id': course_id}),
'proxy_legacy_analytics_url': reverse('proxy_legacy_analytics', kwargs={'course_id': course_id}),
}
return section_data
......@@ -6,60 +6,32 @@
plantTimeout = -> window.InstructorDashboard.util.plantTimeout.apply this, arguments
std_ajax_err = -> window.InstructorDashboard.util.std_ajax_err.apply this, arguments
# Analytics Section
class Analytics
constructor: (@$section) ->
@$section.data 'wrapper', @
# gather elements
@$display = @$section.find '.distribution-display'
@$display_text = @$display.find '.distribution-display-text'
@$display_graph = @$display.find '.distribution-display-graph'
@$display_table = @$display.find '.distribution-display-table'
@$distribution_select = @$section.find 'select#distributions'
@$request_response_error = @$display.find '.request-response-error'
@populate_selector => @$distribution_select.change => @on_selector_change()
class ProfileDistributionWidget
constructor: ({@$container, @feature, title, @endpoint}) ->
# render template
template_params =
title: title
feature: @feature
endpoint: @endpoint
template_html = $("#profile-distribution-widget-template").html()
@$container.html Mustache.render template_html, template_params
reset_display: ->
@$display_text.empty()
@$display_graph.empty()
@$display_table.empty()
@$request_response_error.empty()
# fetch and list available distributions
# `cb` is a callback to be run after
populate_selector: (cb) ->
# ask for no particular distribution to get list of available distribuitions.
@get_profile_distributions undefined,
# on error, print to console and dom.
error: std_ajax_err => @$request_response_error.text "Error getting available distributions."
success: (data) =>
# replace loading text in drop-down with "-- Select Distribution --"
@$distribution_select.find('option').eq(0).text "-- Select Distribution --"
@$container.find('.display-errors').empty()
@$container.find('.display-text').empty()
@$container.find('.display-graph').empty()
@$container.find('.display-table').empty()
# add all fetched available features to drop-down
for feature in data.available_features
opt = $ '<option/>',
text: data.feature_display_names[feature]
data:
feature: feature
@$distribution_select.append opt
# call callback if one was supplied
cb?()
show_error: (msg) ->
@$container.find('.display-errors').text msg
# display data
on_selector_change: ->
opt = @$distribution_select.children('option:selected')
feature = opt.data 'feature'
load: ->
@reset_display()
# only proceed if there is a feature attached to the selected option.
return unless feature
@get_profile_distributions feature,
error: std_ajax_err => @$request_response_error.text "Error getting distribution for '#{feature}'."
@get_profile_distributions @feature,
error: std_ajax_err => @show_error "Error fetching distribution."
success: (data) =>
feature_res = data.feature_results
if feature_res.type is 'EASY_CHOICE'
......@@ -70,9 +42,9 @@ class Analytics
forceFitColumns: true
columns = [
id: feature
field: feature
name: feature
id: @feature
field: @feature
name: data.feature_display_names[@feature]
,
id: 'count'
field: 'count'
......@@ -81,16 +53,16 @@ class Analytics
grid_data = _.map feature_res.data, (value, key) ->
datapoint = {}
datapoint[feature] = feature_res.choices_display_names[key]
datapoint[@feature] = feature_res.choices_display_names[key]
datapoint['count'] = value
datapoint
table_placeholder = $ '<div/>', class: 'slickgrid'
@$display_table.append table_placeholder
@$container.find('.display-table').append table_placeholder
grid = new Slick.Grid(table_placeholder, grid_data, columns, options)
else if feature_res.feature is 'year_of_birth'
graph_placeholder = $ '<div/>', class: 'year-of-birth'
@$display_graph.append graph_placeholder
graph_placeholder = $ '<div/>', class: 'graph-placeholder'
@$container.find('.display-graph').append graph_placeholder
graph_data = _.map feature_res.data, (value, key) -> [parseInt(key), value]
......@@ -99,7 +71,7 @@ class Analytics
]
else
console.warn("unable to show distribution #{feature_res.type}")
@$display_text.text 'Unavailable Metric Display\n' + JSON.stringify(feature_res)
@show_error 'Unavailable metric display.'
# fetch distribution data from server.
# `handler` can be either a callback for success
......@@ -107,7 +79,7 @@ class Analytics
get_profile_distributions: (feature, handler) ->
settings =
dataType: 'json'
url: @$distribution_select.data 'endpoint'
url: @endpoint
data: feature: feature
if typeof handler is 'function'
......@@ -117,13 +89,138 @@ class Analytics
$.ajax settings
# slickgrid's layout collapses when rendered
# in an invisible div. use this method to reload
# the AuthList widget
class GradeDistributionDisplay
constructor: ({@$container, @endpoint}) ->
template_params = {}
template_html = $('#grade-distributions-widget-template').html()
@$container.html Mustache.render template_html, template_params
@$problem_selector = @$container.find '.problem-selector'
reset_display: ->
@$container.find('.display-errors').empty()
@$container.find('.display-text').empty()
@$container.find('.display-graph').empty()
show_error: (msg) ->
@$container.find('.display-errors').text msg
load: ->
@get_grade_distributions
error: std_ajax_err => @show_error "Error fetching grade distributions."
success: (data) =>
@$container.find('.last-updated').text "Last Updated: #{data.time}"
# populate selector
@$problem_selector.empty()
for {module_id, grade_info} in data.data
I4X_PROBLEM = /i4x:\/\/.*\/.*\/problem\/(.*)/
label = (I4X_PROBLEM.exec module_id)?[1]
label ?= module_id
@$problem_selector.append $ '<option/>',
text: label
data:
module_id: module_id
grade_info: grade_info
@$problem_selector.change =>
$opt = @$problem_selector.children('option:selected')
return unless $opt.length > 0
@reset_display()
@render_distribution
module_id: $opt.data 'module_id'
grade_info: $opt.data 'grade_info'
# one-time first selection of first list item.
@$problem_selector.change()
render_distribution: ({module_id, grade_info}) ->
$display_graph = @$container.find('.display-graph')
graph_data = grade_info.map ({grade, max_grade, num_students}) -> [grade, num_students]
total_students = _.reduce ([0].concat grade_info),
(accum, {grade, max_grade, num_students}) -> accum + num_students
# show total students
@$container.find('.display-text').text "#{total_students} students scored."
# render to graph
graph_placeholder = $ '<div/>', class: 'graph-placeholder'
$display_graph.append graph_placeholder
graph_data = graph_data
$.plot graph_placeholder, [
data: graph_data
bars: show: true
color: '#1d9dd9'
]
# `handler` can be either a callback for success
# or a mapping e.g. {success: ->, error: ->, complete: ->}
#
# the data passed to the success handler takes this form:
# {
# "aname": "ProblemGradeDistribution",
# "time": "2013-07-31T20:25:56+00:00",
# "course_id": "MITx/6.002x/2013_Spring",
# "options": {
# "course_id": "MITx/6.002x/2013_Spring",
# "_id": "6fudge2b49somedbid1e1",
# "data": [
# {
# "module_id": "i4x://MITx/6.002x/problem/Capacitors_and_Energy_Storage",
# "grade_info": [
# {
# "grade": 0.0,
# "max_grade": 100.0,
# "num_students": 3
# }, ... for each grade number between 0 and max_grade
# ],
# }
get_grade_distributions: (handler) ->
settings =
dataType: 'json'
url: @endpoint
data: aname: 'ProblemGradeDistribution'
if typeof handler is 'function'
_.extend settings, success: handler
else
_.extend settings, handler
$.ajax settings
# Analytics Section
class Analytics
constructor: (@$section) ->
@$section.data 'wrapper', @
@$pd_containers = @$section.find '.profile-distribution-widget-container'
@$gd_containers = @$section.find '.grade-distributions-widget-container'
@pdws = _.map (@$pd_containers), (container) =>
new ProfileDistributionWidget
$container: $(container)
feature: $(container).data 'feature'
title: $(container).data 'title'
endpoint: $(container).data 'endpoint'
@gdws = _.map (@$gd_containers), (container) =>
new GradeDistributionDisplay
$container: $(container)
endpoint: $(container).data 'endpoint'
refresh: ->
@on_selector_change()
for pdw in @pdws
pdw.load()
for gdw in @gdws
gdw.load()
# handler for when the section title is clicked.
onClickTitle: ->
@refresh()
......
......@@ -33,6 +33,46 @@ CSS_INSTRUCTOR_NAV = 'instructor-nav'
# prefix for deep-linking
HASH_LINK_PREFIX = '#view-'
# helper class for queueing and fault isolation.
# Will execute functions marked by waiter.after only after all functions marked by
# waiter.waitFor have been called.
class SafeWaiter
constructor: ->
@after_handlers = []
@waitFor_handlers = []
@fired = false
after: (f) ->
if @fired
f()
else
@after_handlers.push f
waitFor: (f) ->
return if @fired
@waitFor_handlers.push f
# wrap the function so that it notifies the waiter
# and can fire the after handlers.
=>
@waitFor_handlers = @waitFor_handlers.filter (g) -> g isnt f
if @waitFor_handlers.length is 0
plantTimeout 0, =>
@fired = true
for cb in @after_handlers
cb()
f.apply this, arguments
# waiter for dashboard sections.
# Will only execute after all sections have at least attempted to load.
# This is here to facilitate section constructors isolated by setTimeout
# while still being able to interact with them under the guarantee
# that the sections will be initialized at call time.
sections_have_loaded = new SafeWaiter
# once we're ready, check if this page is the instructor dashboard
$ =>
instructor_dashboard_content = $ ".#{CSS_INSTRUCTOR_CONTENT}"
......@@ -45,9 +85,9 @@ $ =>
# handles hiding and showing sections
setup_instructor_dashboard = (idash_content) =>
# clickable section titles
links = idash_content.find(".#{CSS_INSTRUCTOR_NAV}").find('a')
$links = idash_content.find(".#{CSS_INSTRUCTOR_NAV}").find('a')
for link in ($ link for link in links)
for link in ($ link for link in $links)
link.click (e) ->
e.preventDefault()
......@@ -70,24 +110,24 @@ setup_instructor_dashboard = (idash_content) =>
# write to url
location.hash = "#{HASH_LINK_PREFIX}#{section_name}"
plantTimeout 0, -> section.data('wrapper')?.onClickTitle?()
# plantTimeout 0, -> section.data('wrapper')?.onExit?()
sections_have_loaded.after ->
section.data('wrapper')?.onClickTitle?()
# TODO enable onExit handler
# activate an initial section by 'clicking' on it.
# check for a deep-link, or click the first link.
click_first_link = ->
link = links.eq(0)
link = $links.eq(0)
link.click()
link.data('wrapper')?.onClickTitle?()
if (new RegExp "^#{HASH_LINK_PREFIX}").test location.hash
rmatch = (new RegExp "^#{HASH_LINK_PREFIX}(.*)").exec location.hash
section_name = rmatch[1]
link = links.filter "[data-section='#{section_name}']"
link = $links.filter "[data-section='#{section_name}']"
if link.length == 1
link.click()
link.data('wrapper')?.onClickTitle?()
else
click_first_link()
else
......@@ -98,9 +138,14 @@ setup_instructor_dashboard = (idash_content) =>
# enable sections
setup_instructor_dashboard_sections = (idash_content) ->
# see fault isolation NOTE at top of file.
# an error thrown in one section will not block other sections from exectuing.
plantTimeout 0, -> new window.InstructorDashboard.sections.CourseInfo idash_content.find ".#{CSS_IDASH_SECTION}#course_info"
plantTimeout 0, -> new window.InstructorDashboard.sections.DataDownload idash_content.find ".#{CSS_IDASH_SECTION}#data_download"
plantTimeout 0, -> new window.InstructorDashboard.sections.Membership idash_content.find ".#{CSS_IDASH_SECTION}#membership"
plantTimeout 0, -> new window.InstructorDashboard.sections.StudentAdmin idash_content.find ".#{CSS_IDASH_SECTION}#student_admin"
plantTimeout 0, -> new window.InstructorDashboard.sections.Analytics idash_content.find ".#{CSS_IDASH_SECTION}#analytics"
# If an error thrown in one section, it will not stop other sections from exectuing.
plantTimeout 0, sections_have_loaded.waitFor ->
new window.InstructorDashboard.sections.CourseInfo idash_content.find ".#{CSS_IDASH_SECTION}#course_info"
plantTimeout 0, sections_have_loaded.waitFor ->
new window.InstructorDashboard.sections.DataDownload idash_content.find ".#{CSS_IDASH_SECTION}#data_download"
plantTimeout 0, sections_have_loaded.waitFor ->
new window.InstructorDashboard.sections.Membership idash_content.find ".#{CSS_IDASH_SECTION}#membership"
plantTimeout 0, sections_have_loaded.waitFor ->
new window.InstructorDashboard.sections.StudentAdmin idash_content.find ".#{CSS_IDASH_SECTION}#student_admin"
plantTimeout 0, sections_have_loaded.waitFor ->
new window.InstructorDashboard.sections.Analytics idash_content.find ".#{CSS_IDASH_SECTION}#analytics"
......@@ -36,6 +36,11 @@ section.instructor-dashboard-content-2 {
color: $error-red;
}
.display-errors {
line-height: 3em;
color: $error-red;
}
.slickgrid {
margin-left: 1px;
color:#333333;
......@@ -320,25 +325,40 @@ section.instructor-dashboard-content-2 {
}
.instructor-dashboard-wrapper-2 section.idash-section#analytics {
.distribution-display {
margin-top: 1.2em;
.profile-distribution-widget {
margin-bottom: $baseline * 2;
.distribution-display-graph {
.year-of-birth {
width: 750px;
height: 250px;
}
}
.display-text {}
.distribution-display-table {
.slickgrid {
height: 400px;
}
.display-graph .graph-placeholder {
width: 750px;
height: 250px;
}
.display-table {
.slickgrid {
height: 250px;
}
}
}
.grade-distributions-widget {
margin-bottom: $baseline * 2;
.last-updated {
line-height: 2.2em;
font-size: 10pt;
}
.display-graph .graph-placeholder {
width: 750px;
height: 200px;
}
.display-text {
line-height: 2em;
}
}
.member-list-widget {
$width: 20 * $baseline;
......
<%page args="section_data"/>
<h2>Distributions</h2>
<select id="distributions" data-endpoint="${ section_data['get_distribution_url'] }">
<option> Getting available distributions... </option>
</select>
<div class="distribution-display">
<div class="distribution-display-text"></div>
<div class="distribution-display-graph"></div>
<div class="distribution-display-table"></div>
<div class="request-response-error"></div>
</div>
<script type="text/template" id="profile-distribution-widget-template">
<div class="profile-distribution-widget">
<div class="header">
<h2 class="title"> {{title}} </h2>
</div>
<div class="view">
<div class="display-errors"></div>
<div class="display-text"></div>
<div class="display-graph"></div>
<div class="display-table"></div>
</div>
</div>
</script>
<script type="text/template" id="grade-distributions-widget-template">
<div class="grade-distributions-widget">
<div class="header">
<h2 class="title"> Grade Distribution </h2>
Problem: <select class="problem-selector">
<option> Loading problem list... </option>
</select>
<div class="last-updated"></div>
</div>
<div class="view">
<div class="display-errors"></div>
<div class="display-text"></div>
<div class="display-graph"></div>
</div>
</div>
</script>
<div class="grade-distributions-widget-container"
data-endpoint="${ section_data['proxy_legacy_analytics_url'] }"
></div>
<div class="profile-distribution-widget-container"
data-title="Year of Birth"
data-feature="year_of_birth"
data-endpoint="${ section_data['get_distribution_url'] }"
></div>
<div class="profile-distribution-widget-container"
data-title="Gender Distribution"
data-feature="gender"
data-endpoint="${ section_data['get_distribution_url'] }"
></div>
<div class="profile-distribution-widget-container"
data-title="Level of Education"
data-feature="level_of_education"
data-endpoint="${ section_data['get_distribution_url'] }"
></div>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment