From bf8ce58549ced4e1b456b769b51192c71740fcf0 Mon Sep 17 00:00:00 2001 From: Ahmad Amireh Date: Thu, 18 Sep 2014 09:50:40 +0300 Subject: [PATCH] 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 Reviewed-by: Derek DeVries QA-Review: Trevor deHaan Product-Review: Derek DeVries --- .../src/js/mixins/chart.js | 11 +++++ .../src/js/views/summary.jsx | 6 ++- .../views/summary/score_percentile_chart.jsx | 49 ++++++++++++++++++- .../components/score_percentile_chart_test.js | 8 +++ 4 files changed, 71 insertions(+), 3 deletions(-) diff --git a/client_apps/canvas_quiz_statistics/src/js/mixins/chart.js b/client_apps/canvas_quiz_statistics/src/js/mixins/chart.js index 15808df8c4e..dcae98bbf4e 100644 --- a/client_apps/canvas_quiz_statistics/src/js/mixins/chart.js +++ b/client_apps/canvas_quiz_statistics/src/js/mixins/chart.js @@ -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') { diff --git a/client_apps/canvas_quiz_statistics/src/js/views/summary.jsx b/client_apps/canvas_quiz_statistics/src/js/views/summary.jsx index e226633b3b5..b52db6a2615 100644 --- a/client_apps/canvas_quiz_statistics/src/js/views/summary.jsx +++ b/client_apps/canvas_quiz_statistics/src/js/views/summary.jsx @@ -112,7 +112,11 @@ define(function(require) { - + ); }, diff --git a/client_apps/canvas_quiz_statistics/src/js/views/summary/score_percentile_chart.jsx b/client_apps/canvas_quiz_statistics/src/js/views/summary/score_percentile_chart.jsx index 7e63b7f608a..1440c7209a2 100644 --- a/client_apps/canvas_quiz_statistics/src/js/views/summary/score_percentile_chart.jsx +++ b/client_apps/canvas_quiz_statistics/src/js/views/summary/score_percentile_chart.jsx @@ -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 = []; diff --git a/client_apps/canvas_quiz_statistics/test/unit/components/score_percentile_chart_test.js b/client_apps/canvas_quiz_statistics/test/unit/components/score_percentile_chart_test.js index 6be01cba8b1..a9300bc665f 100644 --- a/client_apps/canvas_quiz_statistics/test/unit/components/score_percentile_chart_test.js +++ b/client_apps/canvas_quiz_statistics/test/unit/components/score_percentile_chart_test.js @@ -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); + }); + }); }); }); \ No newline at end of file