CQS - Answer distribution charts scaling
Also did some refactoring and added a number of comments in the chart generator to ease future maintenance. Removed "grunt-notify" module because it's too annoying and hardly useful. Closes CNVS-16518 TEST PLAN ---- ---- - create a quiz with two FIMB questions + the first question should have over 10 possible answers (could be right or wrong, doesn't matter) + the other question could have like 3-4, like normal ones - take the quiz by quizard or manually; we'd like to have a number of responses to give the chart some shape - turn on new stats FFlag - go to stats page + verify the chart scales for the first question as described by the ticket and its AC + verify that the chart for the second question looks almost like the one on master/the normal one .. e.g, the bars are 30 pixels wide + make sure you can trigger the tooltips for every bar out there Change-Id: Ifb1ee3e2359f0c3d77d733bba6262a46df8ced0b Reviewed-on: https://gerrit.instructure.com/43404 Tested-by: Jenkins <jenkins@instructure.com> Reviewed-by: Derek DeVries <ddevries@instructure.com> QA-Review: Trevor deHaan <tdehaan@instructure.com> Product-Review: Ahmad Amireh <ahmad@instructure.com>
This commit is contained in:
parent
a038f68301
commit
bacb70e3fc
|
@ -11,7 +11,6 @@ grunt.loadNpmTasks('grunt-connect-proxy');
|
|||
grunt.loadNpmTasks('grunt-contrib-jasmine');
|
||||
grunt.loadNpmTasks('grunt-jsduck');
|
||||
grunt.loadNpmTasks('grunt-contrib-jshint');
|
||||
grunt.loadNpmTasks('grunt-notify');
|
||||
grunt.loadNpmTasks('grunt-newer');
|
||||
grunt.loadNpmTasks('grunt-sass');
|
||||
|
||||
|
|
|
@ -1497,28 +1497,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"grunt-notify": {
|
||||
"version": "0.3.1",
|
||||
"from": "grunt-notify@0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/grunt-notify/-/grunt-notify-0.3.1.tgz",
|
||||
"dependencies": {
|
||||
"semver": {
|
||||
"version": "3.0.1",
|
||||
"from": "semver@>=3.0.1-0 <4.0.0-0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-3.0.1.tgz"
|
||||
},
|
||||
"stack-parser": {
|
||||
"version": "0.0.1",
|
||||
"from": "stack-parser@>=0.0.1-0 <0.1.0-0",
|
||||
"resolved": "https://registry.npmjs.org/stack-parser/-/stack-parser-0.0.1.tgz"
|
||||
},
|
||||
"which": {
|
||||
"version": "1.0.5",
|
||||
"from": "which@>=1.0.5-0 <1.1.0-0",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-1.0.5.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"grunt-react": {
|
||||
"version": "0.9.0",
|
||||
"from": "grunt-react@0.9.0",
|
||||
|
|
|
@ -35,7 +35,6 @@
|
|||
"grunt-contrib-watch": "0.6.1",
|
||||
"grunt-jsduck": "1.0.1",
|
||||
"grunt-newer": "0.7.0",
|
||||
"grunt-notify": "0.3.1",
|
||||
"grunt-sass": "0.14.1",
|
||||
"grunt-template-jasmine-requirejs": "git://github.com/amireh/grunt-template-jasmine-requirejs.git#0.2.0",
|
||||
"jasmine_react": "1.1.0",
|
||||
|
|
|
@ -16,10 +16,6 @@ define(function(require) {
|
|||
DEBUG.update = app.update;
|
||||
});
|
||||
|
||||
DEBUG.expose('react', 'React');
|
||||
DEBUG.expose('util/round', 'round');
|
||||
DEBUG.expose('stores/statistics', 'statisticsStore');
|
||||
|
||||
Root.DEBUG = DEBUG;
|
||||
Root.d = DEBUG;
|
||||
|
||||
|
|
|
@ -9,239 +9,8 @@ define(function(require) {
|
|||
var ScreenReaderContent = require('jsx!../../components/screen_reader_content');
|
||||
var Text = require('jsx!../../components/text');
|
||||
var round = require('../../util/round');
|
||||
|
||||
var mapBy = _.map;
|
||||
var findWhere = _.findWhere;
|
||||
var compact = _.compact;
|
||||
|
||||
var Chart = React.createClass({
|
||||
mixins: [ ChartMixin.mixin, ChartInspectorMixin.mixin ],
|
||||
|
||||
tooltipOptions: {
|
||||
position: {
|
||||
my: 'center+15 bottom',
|
||||
at: 'center top-8'
|
||||
}
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
answers: [],
|
||||
|
||||
/**
|
||||
* @property {Number} [barWidth=30]
|
||||
* Width of the bars in the chart in pixels.
|
||||
*/
|
||||
barWidth: 30,
|
||||
|
||||
/**
|
||||
* @property {Number} [barMargin=1]
|
||||
*
|
||||
* Whitespace to offset the bars by, in pixels.
|
||||
*/
|
||||
barMargin: 1,
|
||||
xOffset: 16,
|
||||
yAxisLabel: '',
|
||||
xAxisLabels: false,
|
||||
linearScale: true,
|
||||
width: 'auto',
|
||||
height: 120
|
||||
};
|
||||
},
|
||||
|
||||
createChart: function(node, props) {
|
||||
var otherAnswers;
|
||||
var data = props.answers;
|
||||
var container = this.getDOMNode();
|
||||
|
||||
var sz = data.reduce(function(sum, item) {
|
||||
return sum + item.y;
|
||||
}, 0);
|
||||
|
||||
var highest = d3.max(mapBy(data, 'y'));
|
||||
|
||||
var width, height;
|
||||
var margin = { top: 0, right: 0, bottom: 0, left: 0 };
|
||||
|
||||
if (props.width === 'auto') {
|
||||
width = container.offsetWidth;
|
||||
}
|
||||
else {
|
||||
width = parseInt(props.width, 10);
|
||||
}
|
||||
|
||||
width -= margin.left - margin.right;
|
||||
height = props.height - margin.top - margin.bottom;
|
||||
|
||||
var barWidth = props.barWidth;
|
||||
var barMargin = props.barMargin;
|
||||
var xOffset = props.xOffset;
|
||||
|
||||
var x = d3.scale.ordinal()
|
||||
.rangeRoundBands([0, barWidth * sz], 0.025);
|
||||
|
||||
var y = d3.scale.linear()
|
||||
.range([height, 0]);
|
||||
|
||||
var visibilityThreshold = Math.max(5, y(highest) / 100.0);
|
||||
|
||||
var svg = d3.select(node)
|
||||
.attr('width', width + margin.left + margin.right)
|
||||
.attr('height', height + margin.top + margin.bottom)
|
||||
.attr('aria-hidden', true)
|
||||
.attr('role', 'presentation')
|
||||
.append('g')
|
||||
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
|
||||
|
||||
var classifyChartBar = this.classifyChartBar;
|
||||
|
||||
x.domain(data.map(function(d, i) { return d.label || i; }));
|
||||
y.domain([ 0, sz ]);
|
||||
|
||||
var bars = svg.selectAll('.bar')
|
||||
.data(data)
|
||||
.enter().append('rect')
|
||||
.attr("class", function(d) {
|
||||
return classifyChartBar(d);
|
||||
})
|
||||
.attr("x", function(d, i) {
|
||||
return i * (barWidth + barMargin) + xOffset;
|
||||
})
|
||||
.attr("width", barWidth)
|
||||
.attr("y", function(d) {
|
||||
return y(d.y) - visibilityThreshold;
|
||||
})
|
||||
.attr("height", function(d) {
|
||||
return height - y(d.y) + visibilityThreshold;
|
||||
});
|
||||
|
||||
ChartInspectorMixin.makeInspectable(bars, this);
|
||||
|
||||
// If the special "No Answer" is present, we represent it as a diagonally-
|
||||
// striped bar, but to do that we need to render the <svg:pattern> that
|
||||
// generates the stripes and use that as a fill pattern, and we also need
|
||||
// to create the <svg:rect> that will be filled with that pattern.
|
||||
otherAnswers = compact([
|
||||
findWhere(data, { id: 'other' }),
|
||||
findWhere(data, { id: 'none' })
|
||||
]);
|
||||
|
||||
if (otherAnswers.length) {
|
||||
this.renderStripePattern(svg);
|
||||
svg.selectAll('.bar.bar-striped')
|
||||
.data(otherAnswers)
|
||||
.enter().append('rect')
|
||||
.attr('class', 'bar bar-striped')
|
||||
// We need to inline the fill style because we are referencing an
|
||||
// inline pattern (#diagonalStripes) which is unreachable from a CSS
|
||||
// directive.
|
||||
//
|
||||
// See this link [StackOverflow] for more info: http://bit.ly/1uDTqyn
|
||||
.attr('style', 'fill: url(#diagonalStripes);')
|
||||
// remove 2 pixels from width and height, and offset it by {1,1} on
|
||||
// both axes to "contain" it inside the margins of the bg rect
|
||||
.attr('x', function(d) {
|
||||
return data.indexOf(d) * (barWidth + barMargin) + xOffset + 1;
|
||||
})
|
||||
.attr('width', barWidth-2)
|
||||
.attr('y', function(d) {
|
||||
return y(d.y + visibilityThreshold) + 1;
|
||||
})
|
||||
.attr('height', function(d) {
|
||||
return height - y(d.y + visibilityThreshold) - 2;
|
||||
});
|
||||
}
|
||||
|
||||
return svg;
|
||||
},
|
||||
|
||||
renderStripePattern: function(svg) {
|
||||
svg.append('pattern')
|
||||
.attr('id', 'diagonalStripes')
|
||||
.attr('width', 5)
|
||||
.attr('height', 5)
|
||||
.attr('patternTransform', 'rotate(45 0 0)')
|
||||
.attr('patternUnits', 'userSpaceOnUse')
|
||||
.append('g')
|
||||
.append('path')
|
||||
.attr('d', 'M0,0 L0,10');
|
||||
},
|
||||
|
||||
classifyChartBar: function(answer) {
|
||||
if (answer.correct) {
|
||||
return 'bar bar-highlighted';
|
||||
} else {
|
||||
return 'bar';
|
||||
}
|
||||
},
|
||||
|
||||
updateChart: ChartMixin.mixin.updateChart,
|
||||
removeChart: ChartMixin.mixin.removeChart,
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div>
|
||||
<div ref="inspector" />
|
||||
<svg ref="chart" className="chart"></svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// A table for screen-readers that provides an alternative view of the data.
|
||||
var Table = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
answers: []
|
||||
};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<table>
|
||||
<caption>
|
||||
<Text phrase="audible_description">
|
||||
This table lists all the answers to the question, along with the
|
||||
number of responses they have received.
|
||||
</Text>
|
||||
</caption>
|
||||
|
||||
<tbody>
|
||||
{this.props.answers.map(this.renderEntry)}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
},
|
||||
|
||||
renderEntry: function(answer, position) {
|
||||
return (
|
||||
<tr key={'answer-'+answer.id}>
|
||||
<td scope="col">
|
||||
{I18n.t('audible_answer_position', 'Answer %{position}:', { position: position+1 }) + ' '}
|
||||
|
||||
{answer.text + '. ' /* make sure there's a sentence delimiter */}
|
||||
|
||||
{answer.correct &&
|
||||
<em>
|
||||
{' '}
|
||||
{I18n.t('audible_correct_answer_indicator', 'This is a correct answer.')}
|
||||
</em>
|
||||
}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{I18n.t('audible_answer_response_count', {
|
||||
zero: 'No responses.',
|
||||
one: 'One response.',
|
||||
other: '%{count} responses.'
|
||||
}, {
|
||||
count: answer.responses
|
||||
})}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
});
|
||||
var Chart = require('jsx!./answer_bars/chart');
|
||||
var Table = require('jsx!./answer_bars/table');
|
||||
|
||||
var AnswerBars = React.createClass({
|
||||
propTypes: {
|
||||
|
|
|
@ -0,0 +1,231 @@
|
|||
/** @jsx React.DOM */
|
||||
define(function(require) {
|
||||
var React = require('react');
|
||||
var d3 = require('d3');
|
||||
var _ = require('lodash');
|
||||
var ChartMixin = require('../../../mixins/chart');
|
||||
var ChartInspectorMixin = require('../../../mixins/components/chart_inspector');
|
||||
|
||||
var mapBy = _.map;
|
||||
var findWhere = _.findWhere;
|
||||
var compact = _.compact;
|
||||
|
||||
var Chart = React.createClass({
|
||||
mixins: [ ChartMixin.mixin, ChartInspectorMixin.mixin ],
|
||||
|
||||
tooltipOptions: {
|
||||
position: {
|
||||
my: 'center+15 bottom',
|
||||
at: 'center top-8'
|
||||
}
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
answers: [],
|
||||
|
||||
/**
|
||||
* @property {Number} [barWidth=30]
|
||||
*
|
||||
* Prefered width of the bars in pixels. This value will be respected
|
||||
* only if the chart's container is wide enough to contain all the
|
||||
* bars with that specified value.
|
||||
*/
|
||||
barWidth: 30,
|
||||
|
||||
/**
|
||||
* @property {Number} [padding=0.05]
|
||||
*
|
||||
* An amount of whitespace/padding to render between each pair of bars.
|
||||
* Value is in the range of [0,1] and represents a percentage of the
|
||||
* bar's *calculated* width.
|
||||
*
|
||||
* So, a padding of 0.5 means a number of pixels equal to half the bar's
|
||||
* width will be rendered as whitespace.
|
||||
*/
|
||||
padding: 0.05,
|
||||
|
||||
/**
|
||||
* @property {Number} [visibilityThreshold=5]
|
||||
*
|
||||
* An amount of pixels to use for a bar's height in the special case
|
||||
* where an answer has received no responses (e.g, y=0).
|
||||
*
|
||||
* Setting this to a positive number would show the bar for such answers
|
||||
* so that the tooltip can be triggered.
|
||||
*/
|
||||
visibilityThreshold: 5,
|
||||
|
||||
/**
|
||||
* @property {String|Number} [width="auto"]
|
||||
*
|
||||
* Width of the chart in pixels. If set to "auto", the width will be
|
||||
* equal to that of the element containing the chart.
|
||||
*/
|
||||
width: 'auto',
|
||||
|
||||
height: 120
|
||||
};
|
||||
},
|
||||
|
||||
createChart: function(node, props) {
|
||||
var otherAnswers;
|
||||
var width;
|
||||
|
||||
var data = props.answers;
|
||||
var barCount = data.length;
|
||||
|
||||
// We need the container element to calculate the chart's width in case
|
||||
// it is set to "auto".
|
||||
var container = this.getDOMNode();
|
||||
|
||||
// The highest response count, for defining the y-axis range boundaries.
|
||||
var highest = d3.max(mapBy(data, 'y'));
|
||||
|
||||
var visibilityThreshold;
|
||||
|
||||
// Uniform width of the bars that will represent answer sets. This amount
|
||||
// will consider @props.barWidth only if the chart is large enough to
|
||||
// accommodate all the bars with the requested width. Otherwise, we'll
|
||||
// use the largest width possible that satisfies rendering *all* the
|
||||
// answer sets.
|
||||
//
|
||||
// Also, a reasonable amount of padding will be rendered between each pair
|
||||
// of bars that is equal to a 1/4 of a bar's calculated width.
|
||||
var effectiveBarWidth;
|
||||
|
||||
// Scales for the x and y axes.
|
||||
var x, y;
|
||||
var xUpperBound;
|
||||
var svg;
|
||||
var bars;
|
||||
|
||||
if (props.width === 'auto') {
|
||||
width = container.offsetWidth;
|
||||
}
|
||||
else {
|
||||
width = parseInt(props.width, 10);
|
||||
}
|
||||
|
||||
// Need to figure out the upper boundary of the range for the x axis scale;
|
||||
// if the container is wide enough to display all the answer bars with the
|
||||
// requested width, we'll clamp the viewport width to be narrow enough to
|
||||
// just fit so that the bars are not widely spaced out. Otherwise, we use
|
||||
// the entire viewport.
|
||||
if (props.barWidth * barCount <= width) {
|
||||
xUpperBound = props.barWidth * barCount;
|
||||
}
|
||||
else {
|
||||
xUpperBound = width;
|
||||
}
|
||||
|
||||
x = d3.scale.ordinal()
|
||||
.domain(d3.range(barCount))
|
||||
.rangeRoundBands([ 0, xUpperBound ], props.padding, 0 /* edge padding */);
|
||||
|
||||
effectiveBarWidth = d3.min([ x.rangeBand(), props.barWidth ]);
|
||||
|
||||
y = d3.scale.linear()
|
||||
.domain([ 0, highest ])
|
||||
.range([ props.height, 0 ]);
|
||||
|
||||
visibilityThreshold = Math.max(props.visibilityThreshold, y(highest) / 100.0);
|
||||
|
||||
svg = d3.select(node)
|
||||
.attr('width', width)
|
||||
.attr('height', props.height)
|
||||
.attr('aria-hidden', true)
|
||||
.attr('role', 'presentation')
|
||||
.append('g');
|
||||
|
||||
bars = svg.selectAll('.bar')
|
||||
.data(data)
|
||||
.enter().append('rect')
|
||||
.attr("class", this.classifyChartBar)
|
||||
.attr("width", effectiveBarWidth)
|
||||
.attr("x", function(d, i) {
|
||||
return x(i);
|
||||
})
|
||||
.attr("y", function(d) {
|
||||
return y(d.y) - visibilityThreshold;
|
||||
})
|
||||
.attr("height", function(d) {
|
||||
return props.height - y(d.y) + visibilityThreshold;
|
||||
});
|
||||
|
||||
ChartInspectorMixin.makeInspectable(bars, this);
|
||||
|
||||
// If the special "No Answer" is present, we represent it as a diagonally-
|
||||
// striped bar, but to do that we need to render the <svg:pattern> that
|
||||
// generates the stripes and use that as a fill pattern, and we also need
|
||||
// to create the <svg:rect> that will be filled with that pattern.
|
||||
otherAnswers = compact([
|
||||
findWhere(data, { id: 'other' }),
|
||||
findWhere(data, { id: 'none' })
|
||||
]);
|
||||
|
||||
if (otherAnswers.length) {
|
||||
this.renderStripePattern(svg);
|
||||
svg.selectAll('.bar.bar-striped')
|
||||
.data(otherAnswers)
|
||||
.enter().append('rect')
|
||||
.attr('class', 'bar bar-striped')
|
||||
// We need to inline the fill style because we are referencing an
|
||||
// inline pattern (#diagonalStripes) which is unreachable from a CSS
|
||||
// directive.
|
||||
//
|
||||
// See this link [StackOverflow] for more info: http://bit.ly/1uDTqyn
|
||||
.attr('style', 'fill: url(#diagonalStripes);')
|
||||
// remove 2 pixels from width and height, and offset it by {1,1} on
|
||||
// both axes to "contain" it inside the margins of the bg rect
|
||||
.attr('x', function(d) {
|
||||
var i = data.indexOf(d); // d is coming from otherAnswers
|
||||
return x(i) + 1;
|
||||
})
|
||||
.attr('width', effectiveBarWidth-2)
|
||||
.attr('y', function(d) {
|
||||
return y(d.y + visibilityThreshold) + 1;
|
||||
})
|
||||
.attr('height', function(d) {
|
||||
return props.height - y(d.y + visibilityThreshold) - 2;
|
||||
});
|
||||
}
|
||||
|
||||
return svg;
|
||||
},
|
||||
|
||||
renderStripePattern: function(svg) {
|
||||
svg.append('pattern')
|
||||
.attr('id', 'diagonalStripes')
|
||||
.attr('width', 5)
|
||||
.attr('height', 5)
|
||||
.attr('patternTransform', 'rotate(45 0 0)')
|
||||
.attr('patternUnits', 'userSpaceOnUse')
|
||||
.append('g')
|
||||
.append('path')
|
||||
.attr('d', 'M0,0 L0,10');
|
||||
},
|
||||
|
||||
classifyChartBar: function(answer) {
|
||||
if (answer.correct) {
|
||||
return 'bar bar-highlighted';
|
||||
} else {
|
||||
return 'bar';
|
||||
}
|
||||
},
|
||||
|
||||
updateChart: ChartMixin.mixin.updateChart,
|
||||
removeChart: ChartMixin.mixin.removeChart,
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div>
|
||||
<div ref="inspector" />
|
||||
<svg ref="chart" className="chart"></svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return Chart;
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
/** @jsx React.DOM */
|
||||
define(function(require) {
|
||||
var React = require('react');
|
||||
var I18n = require('i18n!quiz_statistics.answer_bars_chart');
|
||||
var Text = require('jsx!../../../components/text');
|
||||
|
||||
// A table for screen-readers that provides an alternative view of the data.
|
||||
var Table = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
answers: []
|
||||
};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<table>
|
||||
<caption>
|
||||
<Text phrase="audible_description">
|
||||
This table lists all the answers to the question, along with the
|
||||
number of responses they have received.
|
||||
</Text>
|
||||
</caption>
|
||||
|
||||
<tbody>
|
||||
{this.props.answers.map(this.renderEntry)}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
},
|
||||
|
||||
renderEntry: function(answer, position) {
|
||||
return (
|
||||
<tr key={'answer-'+answer.id}>
|
||||
<td scope="col">
|
||||
{I18n.t('audible_answer_position', 'Answer %{position}:', { position: position+1 }) + ' '}
|
||||
|
||||
{answer.text + '. ' /* make sure there's a sentence delimiter */}
|
||||
|
||||
{answer.correct &&
|
||||
<em>
|
||||
{' '}
|
||||
{I18n.t('audible_correct_answer_indicator', 'This is a correct answer.')}
|
||||
</em>
|
||||
}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{I18n.t('audible_answer_response_count', {
|
||||
zero: 'No responses.',
|
||||
one: 'One response.',
|
||||
other: '%{count} responses.'
|
||||
}, {
|
||||
count: answer.responses
|
||||
})}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return Table;
|
||||
});
|
|
@ -34,17 +34,20 @@ define(function(require) {
|
|||
return answerSet.answers;
|
||||
},
|
||||
|
||||
componentDidUpdate: function(prevProps, prevState) {
|
||||
componentDidMount: function() {
|
||||
// Make sure we always have an active answer set:
|
||||
if (!this.state.answerSetId && this.props.answerSets) {
|
||||
this.setState({ answerSetId: (this.props.answerSets[0] || {}).id });
|
||||
}
|
||||
this.ensureAnswerSetSelection(this.props);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
this.ensureAnswerSetSelection(nextProps);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var crr = calculateResponseRatio(this.getAnswerPool(), this.props.participantCount, {
|
||||
questionType: this.props.questionType
|
||||
});
|
||||
var answerPool = this.getAnswerPool();
|
||||
|
||||
return(
|
||||
<Question expanded={this.props.expanded}>
|
||||
|
@ -68,10 +71,10 @@ define(function(require) {
|
|||
ratio: round(crr * 100.0, 0)
|
||||
})} />
|
||||
|
||||
<AnswerBars answers={this.getAnswerPool()} />
|
||||
<AnswerBars answers={answerPool} />
|
||||
</div>
|
||||
|
||||
{this.props.expanded && <Answers answers={this.getAnswerPool()} />}
|
||||
{this.props.expanded && <Answers answers={answerPool} />}
|
||||
</Question>
|
||||
);
|
||||
},
|
||||
|
@ -91,6 +94,12 @@ define(function(require) {
|
|||
);
|
||||
},
|
||||
|
||||
ensureAnswerSetSelection: function(props) {
|
||||
if (!this.state.answerSetId && props.answerSets.length) {
|
||||
this.setState({ answerSetId: props.answerSets[0].id });
|
||||
}
|
||||
},
|
||||
|
||||
switchAnswerSet: function(answerSetId, e) {
|
||||
e.preventDefault();
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ module.exports = {
|
|||
|
||||
compiled_css: {
|
||||
files: 'dist/*.css',
|
||||
tasks: [ 'noop', 'notify:css' ],
|
||||
tasks: [ 'noop' ],
|
||||
options: {
|
||||
livereload: {
|
||||
port: 9224
|
||||
|
|
298
client_apps/canvas_quiz_statistics/test/fixtures/quiz_statistics_large_question.json
vendored
Normal file
298
client_apps/canvas_quiz_statistics/test/fixtures/quiz_statistics_large_question.json
vendored
Normal file
|
@ -0,0 +1,298 @@
|
|||
{
|
||||
"quiz_statistics": [
|
||||
{
|
||||
"id": "1",
|
||||
"url": "http://localhost:3000/api/v1/courses/1/quizzes/32/statistics",
|
||||
"html_url": "http://localhost:3000/courses/1/quizzes/32/statistics",
|
||||
"generated_at": "2014-08-04T16:32:50Z",
|
||||
"points_possible": 1,
|
||||
"question_statistics": [
|
||||
{
|
||||
"id": "79",
|
||||
"question_type": "fill_in_multiple_blanks_question",
|
||||
"question_text": "<p>Hello. This should force a horizontal scroller cuz it's got MANY MANY MANY answers.</p>",
|
||||
"position": 1,
|
||||
"responses": 14,
|
||||
"answered": 0,
|
||||
"correct": 10,
|
||||
"partially_correct": 0,
|
||||
"incorrect": 4,
|
||||
"answer_sets": [
|
||||
{
|
||||
"id": "bf0109538456c0c93d49334c032c4a0c",
|
||||
"text": "Hair color",
|
||||
"answers": [
|
||||
{
|
||||
"id": "1",
|
||||
"text": "Red",
|
||||
"correct": true,
|
||||
"responses": 1
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"text": "Green",
|
||||
"correct": true,
|
||||
"responses": 1
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"text": "Blue",
|
||||
"correct": false,
|
||||
"responses": 0
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"text": "Teal",
|
||||
"correct": true,
|
||||
"responses": 5
|
||||
},
|
||||
{
|
||||
"id": "5",
|
||||
"text": "Purple",
|
||||
"correct": false,
|
||||
"responses": 0
|
||||
},
|
||||
{
|
||||
"id": "6",
|
||||
"text": "Yellow",
|
||||
"correct": true,
|
||||
"responses": 3
|
||||
},
|
||||
{
|
||||
"id": "7",
|
||||
"text": "Orange",
|
||||
"correct": true,
|
||||
"responses": 0
|
||||
},
|
||||
{
|
||||
"id": "8",
|
||||
"text": "Pink",
|
||||
"correct": false,
|
||||
"responses": 2
|
||||
},
|
||||
{
|
||||
"id": "9",
|
||||
"text": "Pink",
|
||||
"correct": false,
|
||||
"responses": 2
|
||||
},
|
||||
{
|
||||
"id": "10",
|
||||
"text": "A",
|
||||
"correct": false,
|
||||
"responses": 0
|
||||
},
|
||||
{
|
||||
"id": "11",
|
||||
"text": "B",
|
||||
"correct": false,
|
||||
"responses": 0
|
||||
},
|
||||
{
|
||||
"id": "12",
|
||||
"text": "C",
|
||||
"correct": false,
|
||||
"responses": 0
|
||||
},
|
||||
{
|
||||
"id": "13",
|
||||
"text": "D",
|
||||
"correct": false,
|
||||
"responses": 0
|
||||
},
|
||||
{
|
||||
"id": "14",
|
||||
"text": "E",
|
||||
"correct": false,
|
||||
"responses": 0
|
||||
},
|
||||
{
|
||||
"id": "none",
|
||||
"text": "No Answer",
|
||||
"correct": false,
|
||||
"responses": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bf0109538456c0c93d49334c032c4a0d",
|
||||
"text": "Skin color",
|
||||
"answers": [
|
||||
{
|
||||
"id": "1",
|
||||
"text": "Red",
|
||||
"correct": true,
|
||||
"responses": 2
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"text": "Green",
|
||||
"correct": true,
|
||||
"responses": 0
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"text": "Blue",
|
||||
"correct": false,
|
||||
"responses": 2
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"text": "Teal",
|
||||
"correct": true,
|
||||
"responses": 3
|
||||
},
|
||||
{
|
||||
"id": "5",
|
||||
"text": "Purple",
|
||||
"correct": false,
|
||||
"responses": 0
|
||||
},
|
||||
{
|
||||
"id": "6",
|
||||
"text": "Yellow",
|
||||
"correct": true,
|
||||
"responses": 3
|
||||
},
|
||||
{
|
||||
"id": "7",
|
||||
"text": "Orange",
|
||||
"correct": true,
|
||||
"responses": 0
|
||||
},
|
||||
{
|
||||
"id": "8",
|
||||
"text": "Pink",
|
||||
"correct": false,
|
||||
"responses": 0
|
||||
},
|
||||
{
|
||||
"id": "9",
|
||||
"text": "Pink",
|
||||
"correct": false,
|
||||
"responses": 0
|
||||
},
|
||||
{
|
||||
"id": "10",
|
||||
"text": "A",
|
||||
"correct": false,
|
||||
"responses": 3
|
||||
},
|
||||
{
|
||||
"id": "11",
|
||||
"text": "B",
|
||||
"correct": false,
|
||||
"responses": 1
|
||||
},
|
||||
{
|
||||
"id": "12",
|
||||
"text": "C",
|
||||
"correct": false,
|
||||
"responses": 0
|
||||
},
|
||||
{
|
||||
"id": "13",
|
||||
"text": "D",
|
||||
"correct": false,
|
||||
"responses": 2
|
||||
},
|
||||
{
|
||||
"id": "14",
|
||||
"text": "E",
|
||||
"correct": false,
|
||||
"responses": 1
|
||||
},
|
||||
{
|
||||
"id": "none",
|
||||
"text": "No Answer",
|
||||
"correct": false,
|
||||
"responses": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "11",
|
||||
"question_type": "multiple_choice_question",
|
||||
"question_text": "<p>Which?</p>",
|
||||
"position": 1,
|
||||
"responses": 156,
|
||||
"answers": [
|
||||
{
|
||||
"id": "3866",
|
||||
"text": "I am a very long description of an answer that should span multiple lines.",
|
||||
"correct": true,
|
||||
"responses": 129
|
||||
},
|
||||
{
|
||||
"id": "2040",
|
||||
"text": "b",
|
||||
"correct": false,
|
||||
"responses": 1
|
||||
},
|
||||
{
|
||||
"id": "7387",
|
||||
"text": "c",
|
||||
"correct": false,
|
||||
"responses": 26
|
||||
},
|
||||
{
|
||||
"id": "4082",
|
||||
"text": "d",
|
||||
"correct": false,
|
||||
"responses": 0
|
||||
}
|
||||
],
|
||||
"answered_student_count": 156,
|
||||
"top_student_count": 42,
|
||||
"middle_student_count": 72,
|
||||
"bottom_student_count": 42,
|
||||
"correct_student_count": 129,
|
||||
"incorrect_student_count": 27,
|
||||
"correct_student_ratio": 0.8269230769230769,
|
||||
"incorrect_student_ratio": 0.17307692307692307,
|
||||
"correct_top_student_count": 42,
|
||||
"correct_middle_student_count": 72,
|
||||
"correct_bottom_student_count": 15,
|
||||
"variance": 0.14312130177514806,
|
||||
"stdev": 0.3783137610174233,
|
||||
"difficulty_index": 0.8269230769230769,
|
||||
"alpha": null,
|
||||
"point_biserials": [
|
||||
{
|
||||
"answer_id": 3866,
|
||||
"point_biserial": 0.7157094891780442,
|
||||
"correct": true,
|
||||
"distractor": false
|
||||
},
|
||||
{
|
||||
"answer_id": 2040,
|
||||
"point_biserial": -0.0608778335993478,
|
||||
"correct": false,
|
||||
"distractor": true
|
||||
},
|
||||
{
|
||||
"answer_id": 7387,
|
||||
"point_biserial": -0.7134960236217814,
|
||||
"correct": false,
|
||||
"distractor": true
|
||||
},
|
||||
{
|
||||
"answer_id": 4082,
|
||||
"point_biserial": null,
|
||||
"correct": false,
|
||||
"distractor": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"submission_statistics": {
|
||||
"unique_count": 14
|
||||
},
|
||||
"links": {
|
||||
"quiz": "http://localhost:3000/api/v1/courses/1/quizzes/32"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue