Remove unaccessible summary graph a->b functions and add y-axis

fixes CNVS-25736

test plan:
- Go to quiz statistics page
- Access summary graph via keyboard
- Ensure that KO users do not have less functionality than other
  users with the graph
- Ensure that y-axis correctly demonstrates the data presented

Change-Id: Ib971a9e4061f65562f21ef60796afdc046224b89
Reviewed-on: https://gerrit.instructure.com/70073
Tested-by: Jenkins
Reviewed-by: Ryan Taylor <rtaylor@instructure.com>
QA-Review: Deepeeca Soundarrajan <dsoundarrajan@instructure.com>
Product-Review: Dana Danger <dana@instructure.com>
This commit is contained in:
Davis McClellan 2016-01-06 08:40:48 -07:00
parent 5e8215b249
commit a506ec9245
3 changed files with 48 additions and 158 deletions

View File

@ -51,24 +51,13 @@
display: none; display: none;
} }
} }
.y.axis {
fill: $muteFg;
.x.brush { line, path {
fill: rgba($contrastColor, 0.1); fill: none;
} stroke-width: 1px;
stroke: $muteFg;
text.brush-stats {
fill: $primaryFg;
transition: opacity 0.25s ease 0;
&.unused {
opacity: 0;
font-weight: bold;
}
}
&:hover {
text.brush-stats.unused {
opacity: 1;
} }
} }
} }

View File

@ -3,21 +3,14 @@ define(function(require) {
var React = require('react'); var React = require('react');
var ChartMixin = require('../../mixins/chart'); var ChartMixin = require('../../mixins/chart');
var d3 = require('d3'); var d3 = require('d3');
var _ = require('lodash');
var I18n = require('i18n!quiz_statistics.summary'); var I18n = require('i18n!quiz_statistics.summary');
var max = d3.max; var max = d3.max;
var sum = d3.sum; var sum = d3.sum;
var throttle = _.throttle;
var MARGIN_T = 0; var MARGIN_T = 0;
var MARGIN_R = 18; var MARGIN_R = 18;
var MARGIN_B = 60; var MARGIN_B = 60;
var MARGIN_L = 34; var MARGIN_L = 34;
var CHART_BRUSHING_TIP_LABEL = I18n.t(
'chart_brushing_tip',
'Tip: you can focus a specific segment of the chart by making a ' +
'selection using your cursor.'
);
var ScorePercentileChart = React.createClass({ var ScorePercentileChart = React.createClass({
mixins: [ ChartMixin.mixin ], mixins: [ ChartMixin.mixin ],
@ -36,13 +29,13 @@ define(function(require) {
width: 960, width: 960,
height: 220, height: 220,
barPadding: 0.25, barPadding: 0.25,
minBarHeight: 1 minBarHeight: 1,
numTicks: 5
}; };
}, },
createChart: function(node, props) { createChart: function(node, props) {
var svg, width, height, x, xAxis; var svg, width, height, x, xAxis;
var brush, brushCaption, onBrushed;
var barContainer; var barContainer;
width = props.width - MARGIN_L - MARGIN_R; width = props.width - MARGIN_L - MARGIN_R;
@ -53,12 +46,20 @@ define(function(require) {
x = d3.scale.ordinal().rangeRoundBands([0, width], props.barPadding, 0); x = d3.scale.ordinal().rangeRoundBands([0, width], props.barPadding, 0);
x.domain(d3.range(0, 101, 1)); x.domain(d3.range(0, 101, 1));
this.y = d3.scale.linear().range([height, 0]);
xAxis = d3.svg.axis() xAxis = d3.svg.axis()
.scale(x) .scale(x)
.orient("bottom") .orient("bottom")
.tickValues(d3.range(0, 101, 10)) .tickValues(d3.range(0, 101, 10))
.tickFormat(function(d) { return d + '%'; }); .tickFormat(function(d) { return d + '%'; });
this.yAxis = d3.svg.axis()
.scale(this.y)
.orient("left")
.outerTickSize(0)
.ticks(props.numTicks);
svg = d3.select(node) svg = d3.select(node)
.attr('role', 'document') .attr('role', 'document')
.attr('aria-role', 'document') .attr('aria-role', 'document')
@ -77,41 +78,16 @@ define(function(require) {
.attr('transform', "translate(5," + height + ")") .attr('transform', "translate(5," + height + ")")
.call(xAxis); .call(xAxis);
barContainer = svg.append('g'); this.yAxisContainer = svg.append('g')
.attr('class', 'y axis')
brushCaption = svg
.append('text')
.attr('class', 'brush-stats unused')
.attr('text-anchor', 'left')
.attr('aria-hidden', true) .attr('aria-hidden', true)
.attr('dy', '.35em') .call(this.yAxis);
.attr('y', height + 40)
.attr('x', 0)
.text(CHART_BRUSHING_TIP_LABEL);
onBrushed = throttle(this.onBrushed, 100, { barContainer = svg.append('g');
leading: false,
trailing: true
});
brush = d3.svg.brush()
.x(x)
.clamp(true)
.on("brush", onBrushed);
this.brushContainer = svg.append("g")
.attr("class", "x brush")
.call(brush);
this.brushContainer.selectAll("rect")
.attr("y", props.minBarHeight)
.attr("height", height + props.minBarHeight);
this.x = x; this.x = x;
this.height = height; this.height = height;
this.barContainer = barContainer; this.barContainer = barContainer;
this.brushCaption = brushCaption;
this.brush = brush;
this.updateChart(svg, props); this.updateChart(svg, props);
@ -132,15 +108,12 @@ define(function(require) {
})); }));
this.renderBars(this.barContainer, props); this.renderBars(this.barContainer, props);
if (!this.brush.empty()) {
this.onBrushed();
}
}, },
renderBars: function(svg, props) { renderBars: function(svg, props) {
var height, x, y, bars; var height, x, y, bars;
var highest; var highest;
var step;
var visibilityThreshold; var visibilityThreshold;
var data = this.chartData; var data = this.chartData;
@ -148,12 +121,22 @@ define(function(require) {
highest = max(data); highest = max(data);
x = this.x; x = this.x;
y = this.y
y = d3.scale.linear() y.range([0, highest])
.range([0, highest]) .rangeRound([height, 0])
.rangeRound([height, 0]); .domain([0, highest]);
y.domain([0, highest]); step = -Math.ceil((highest + 1) / props.numTicks)
this.yAxis.tickValues(d3.range(highest, 0, step))
.tickFormat(function(d){return Math.floor(d)});
this.yAxisContainer.call(this.yAxis).selectAll('text').attr('dy', '.8em');
this.yAxisContainer
.selectAll('line')
.attr('y1', '.5')
.attr('y2', '.5');
visibilityThreshold = Math.max(highest / 100, props.minBarHeight); visibilityThreshold = Math.max(highest / 100, props.minBarHeight);
@ -170,7 +153,7 @@ define(function(require) {
bars.transition() bars.transition()
.delay(props.animeDelay) .delay(props.animeDelay)
.duration(props.animeDuration) .duration(props.animeDuration)
.attr('y', function(d) { return y(d) + visibilityThreshold; }) .attr('y', function(d) { return y(d) - visibilityThreshold; })
.attr('height', function(d) { .attr('height', function(d) {
return height - y(d) + visibilityThreshold; return height - y(d) + visibilityThreshold;
}); });
@ -233,90 +216,8 @@ define(function(require) {
return set; return set;
}, },
/**
* @private
*
* Update the focus caption with the number of students who are allocated to
* the "brushed" percentile range (which could be just a single point as
* well).
*/
onBrushed: function() {
var i, a, b;
var studentCount;
var message;
var brush = this.brush;
var brushCaption = this.brushCaption;
var data = this.chartData;
var range = this.x.range();
var extent = brush.extent();
// invert the brush extent against the X scale and locate the percentiles
// we're focusing, a and b:
for (i = 0; i < range.length; ++i) {
if (a === undefined && range[i] > extent[0]) {
a = Math.max(i-1, 0);
}
if (b === undefined && range[i] > extent[1]) {
b = Math.max(i-1, 0);
}
if (a !== undefined && b !== undefined) {
break;
}
}
// if at this point we still didn't find the end percentile, it means
// the brush extends beyond the last percentile (100%) so just choose that
if (b === undefined) {
b = range.length - 1;
if (a === undefined) { // single-point brush
a = b;
}
}
if (a - b === 0) { // single percentile
studentCount = data[a] || 0;
message = I18n.t('students_who_got_a_certain_score', {
zero: 'No students have received a score of %{score}%.',
one: 'One student has received a score of %{score}%.',
other: '%{count} students have received a score of %{score}%.',
}, {
count: studentCount,
score: a
});
// redraw the brush to cover the percentile bar:
this.brush.extent([ range[a], range[b] + this.x.rangeBand() ]);
this.brush(this.brushContainer);
}
else { // percentile range
studentCount = 0;
for (i = a; i <= b; ++i) {
studentCount += data[i] || 0;
}
message = I18n.t('students_who_scored_in_a_range', {
zero: 'No students have scored between %{start} and %{end}%.',
one: 'One student has scored between %{start} and %{end}%.',
other: '%{count} students have scored between %{start} and %{end}%.',
}, {
count: studentCount,
start: a,
end: b
});
}
brushCaption.text(message).attr('class', 'brush-stats');
},
render: ChartMixin.defaults.render render: ChartMixin.defaults.render
}); });
return ScorePercentileChart; return ScorePercentileChart;
}); });

View File

@ -52,10 +52,10 @@ define(function(require) {
}); });
testRects([ testRects([
{ i: 15, x: 136, y: 92, h: 92 }, { i: 15, x: 136, y: 88, h: 92 },
{ i: 25, x: 226, y: 92, h: 92 }, { i: 25, x: 226, y: 88, h: 92 },
{ i: 44, x: 397, y: 92, h: 92 }, { i: 44, x: 397, y: 88, h: 92 },
{ i: 59, x: 532, y: 2, h: 182 }, { i: 59, x: 532, y: -2, h: 182 },
], done); ], done);
}); });
@ -69,8 +69,8 @@ define(function(require) {
}); });
testRects([ testRects([
{ i: 15, x: 136, y: 2, h: 182 }, { i: 15, x: 136, y: -2, h: 182 },
{ i: 25, x: 226, y: 182, h: 2 }, { i: 25, x: 226, y: 178, h: 2 },
], function updatePropsAnotherTime() { ], function updatePropsAnotherTime() {
setProps({ setProps({
scores: { scores: {
@ -80,8 +80,8 @@ define(function(require) {
}); });
testRects([ testRects([
{ i: 15, x: 136, y: 2, h: 182 }, { i: 15, x: 136, y: -2, h: 182 },
{ i: 25, x: 226, y: 2, h: 182 }, { i: 25, x: 226, y: -2, h: 182 },
], done); ], done);
}); });
}); });
@ -95,8 +95,8 @@ define(function(require) {
}); });
testRects([ testRects([
{ i: 0, x: 1, y: 146, h: 38 }, { i: 0, x: 1, y: 142, h: 38 },
{ i: 100, x: 901, y: 2, h: 182 }, { i: 100, x: 901, y: -2, h: 182 },
], done); ], done);
}); });
@ -108,4 +108,4 @@ define(function(require) {
}); });
}); });
}); });
}); });