CQS: accessible score percentile chart

Closes CNVS-15301

Three enhancements:

  - chart now has a title (and apparently a browser tooltip!)
  - the 0% ... 100% X-Axis ticks are now hidden to the screenreader
  - the chart is equipped with an audible description, something like:
    "10 of the students scored above, or at, the average, and 2 below."

TEST PLAN
---- ----

  - go to CQS page
  - verify you hear the chart's title, "Score percentiles chart",
    labeled as a "group"
  - verify you can enter the group
    + verify that once you enter the group, you can hear the audible
      description of the chart noted above
  - verify you can not reach the 0%...100% X Axis ticks using the
    screen-reader

NVDA NOTES
---- -----

To get to the summary sentence that is now a replacement of the chart,
you can keep pushing DOWNARROW until you pass by all the table cells,
and just before it reads the "Question Breakdown" heading, or
alternatively (faster):

  - press H two times until you get to "Question Breakdown" then UPARROW
  - press T to get to the summary table, press DOWNARROW like 12 times
    to pass by all the cells, then another time

Something else: if the app renders before the data is loaded, e.g, cells
in the first table are still at 0%, NVDA might have already cached the
page. Then, when you get to the table and the app has loaded, it reads
"0%" instead of the actual numbers... you can use INSERT+F5 to tell NVDA
to refresh the page and look for new changes.

This is obviously problematic, but solving it is outside the scope of
this patch. Maybe we should prevent the app from starting until we have
all the data ready? Donno

See "Refresh browse mode document" in this section of their User Guide:
http://community.nvda-project.org/documentation/userGuide.html?#toc40

Change-Id: Ia4912bfb5978bf9c2fc2ae26d9a2967a5392e2f3
Reviewed-on: https://gerrit.instructure.com/41329
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Derek DeVries <ddevries@instructure.com>
QA-Review: Trevor deHaan <tdehaan@instructure.com>
Product-Review: Derek DeVries <ddevries@instructure.com>
This commit is contained in:
Ahmad Amireh 2014-09-18 09:50:40 +03:00 committed by Derek DeVries
parent 885dc223ae
commit bf8ce58549
4 changed files with 71 additions and 3 deletions

View File

@ -25,6 +25,17 @@ define(function(require) {
}
},
addTitle: function(svg, title) {
svg.append('title').text(title);
},
addDescription: function(svg, description) {
svg.append('text')
.attr('fill', 'transparent')
.attr('font-size', '0px')
.text(description);
},
mixin: {
componentWillMount: function() {
if (typeof this.createChart !== 'function') {

View File

@ -112,7 +112,11 @@ define(function(require) {
</tbody>
</table>
<ScorePercentileChart key="chart" scores={this.props.scores} />
<ScorePercentileChart
key="chart"
scores={this.props.scores}
scoreAverage={this.props.scoreAverage}
pointsPossible={this.props.pointsPossible} />
</div>
);
},

View File

@ -3,6 +3,7 @@ define(function(require) {
var React = require('react');
var ChartMixin = require('../../mixins/chart');
var d3 = require('d3');
var I18n = require('i18n!quiz_statistics.summary');
var max = d3.max;
var sum = d3.sum;
@ -19,7 +20,9 @@ define(function(require) {
mixins: [ ChartMixin.mixin ],
propTypes: {
scores: React.PropTypes.object.isRequired
scores: React.PropTypes.object.isRequired,
scoreAverage: React.PropTypes.number.isRequired,
pointsPossible: React.PropTypes.number.isRequired,
},
getDefaultProps: function() {
@ -33,6 +36,8 @@ define(function(require) {
var highest;
var visibilityThreshold;
var data = this.chartData(props);
var avgScore = props.scoreAverage / props.pointsPossible * 100.0;
var labelOptions = this.calculateStudentStatistics(avgScore, data);
width = WIDTH - MARGIN_L - MARGIN_R;
height = HEIGHT - MARGIN_T - MARGIN_B;
@ -52,15 +57,25 @@ define(function(require) {
});
svg = d3.select(node)
.attr('role', 'document')
.attr('aria-role', 'document')
.attr('width', width + MARGIN_L + MARGIN_R)
.attr('height', height + MARGIN_T + MARGIN_B)
.attr('viewBox', "0 0 " + (width + MARGIN_L + MARGIN_R) + " " + (height + MARGIN_T + MARGIN_B))
.attr('preserveAspectRatio', 'xMinYMax')
.append('g')
.attr("transform", "translate(" + MARGIN_L + "," + MARGIN_T + ")");
.attr('transform', "translate(" + MARGIN_L + "," + MARGIN_T + ")")
ChartMixin.addTitle(svg, I18n.t('chart_title', 'Score percentiles chart'));
ChartMixin.addDescription(svg, I18n.t('audible_chart_description',
'%{above_average} students scored above or at the average, and %{below_average} below.', {
above_average: labelOptions.aboveAverage,
below_average: labelOptions.belowAverage
}));
svg.append('g')
.attr('class', 'x axis')
.attr('aria-hidden', true)
.attr('transform', "translate(0," + height + ")")
.call(xAxis);
@ -82,6 +97,36 @@ define(function(require) {
return svg;
},
/**
* Calculate the number of students who scored above, or at, the average
* and those who did lower.
*
* @param {Number} _avgScore
* @param {Number[]} scores
* The flattened score percentile data-set (see #chartData()).
*
* @return {Object} out
* @return {Number} out.aboveAverage
* @return {Number} out.belowAverage
*/
calculateStudentStatistics: function(_avgScore, scores) {
var avgScore = Math.round(_avgScore);
return {
aboveAverage: scores.filter(function(__y, percentile) {
return percentile >= avgScore;
}).reduce(function(count, y) {
return count + y;
}, 0),
belowAverage: scores.filter(function(__y, percentile) {
return percentile < avgScore;
}).reduce(function(count, y) {
return count + y;
}, 0)
};
},
chartData: function(props) {
var percentile, upperBound;
var set = [];

View File

@ -79,5 +79,13 @@ define(function(require) {
{ i: 100, x: 952, y: -2, h: 182 },
]);
});
describe('#calculateStudentStatistics', function() {
it('should work', function() {
var output = subject.calculateStudentStatistics(3, [ 0, 1, 1, 1, 2 ]);
expect(output.aboveAverage).toBe(3); // includes the average
expect(output.belowAverage).toBe(2);
});
});
});
});