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;
}
}
.y.axis {
fill: $muteFg;
.x.brush {
fill: rgba($contrastColor, 0.1);
}
text.brush-stats {
fill: $primaryFg;
transition: opacity 0.25s ease 0;
&.unused {
opacity: 0;
font-weight: bold;
}
}
&:hover {
text.brush-stats.unused {
opacity: 1;
line, path {
fill: none;
stroke-width: 1px;
stroke: $muteFg;
}
}
}

View File

@ -3,21 +3,14 @@ define(function(require) {
var React = require('react');
var ChartMixin = require('../../mixins/chart');
var d3 = require('d3');
var _ = require('lodash');
var I18n = require('i18n!quiz_statistics.summary');
var max = d3.max;
var sum = d3.sum;
var throttle = _.throttle;
var MARGIN_T = 0;
var MARGIN_R = 18;
var MARGIN_B = 60;
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({
mixins: [ ChartMixin.mixin ],
@ -36,13 +29,13 @@ define(function(require) {
width: 960,
height: 220,
barPadding: 0.25,
minBarHeight: 1
minBarHeight: 1,
numTicks: 5
};
},
createChart: function(node, props) {
var svg, width, height, x, xAxis;
var brush, brushCaption, onBrushed;
var barContainer;
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.domain(d3.range(0, 101, 1));
this.y = d3.scale.linear().range([height, 0]);
xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.tickValues(d3.range(0, 101, 10))
.tickFormat(function(d) { return d + '%'; });
this.yAxis = d3.svg.axis()
.scale(this.y)
.orient("left")
.outerTickSize(0)
.ticks(props.numTicks);
svg = d3.select(node)
.attr('role', 'document')
.attr('aria-role', 'document')
@ -77,41 +78,16 @@ define(function(require) {
.attr('transform', "translate(5," + height + ")")
.call(xAxis);
barContainer = svg.append('g');
brushCaption = svg
.append('text')
.attr('class', 'brush-stats unused')
.attr('text-anchor', 'left')
this.yAxisContainer = svg.append('g')
.attr('class', 'y axis')
.attr('aria-hidden', true)
.attr('dy', '.35em')
.attr('y', height + 40)
.attr('x', 0)
.text(CHART_BRUSHING_TIP_LABEL);
.call(this.yAxis);
onBrushed = throttle(this.onBrushed, 100, {
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);
barContainer = svg.append('g');
this.x = x;
this.height = height;
this.barContainer = barContainer;
this.brushCaption = brushCaption;
this.brush = brush;
this.updateChart(svg, props);
@ -132,15 +108,12 @@ define(function(require) {
}));
this.renderBars(this.barContainer, props);
if (!this.brush.empty()) {
this.onBrushed();
}
},
renderBars: function(svg, props) {
var height, x, y, bars;
var highest;
var step;
var visibilityThreshold;
var data = this.chartData;
@ -148,12 +121,22 @@ define(function(require) {
highest = max(data);
x = this.x;
y = this.y
y = d3.scale.linear()
.range([0, highest])
.rangeRound([height, 0]);
y.range([0, highest])
.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);
@ -170,7 +153,7 @@ define(function(require) {
bars.transition()
.delay(props.animeDelay)
.duration(props.animeDuration)
.attr('y', function(d) { return y(d) + visibilityThreshold; })
.attr('y', function(d) { return y(d) - visibilityThreshold; })
.attr('height', function(d) {
return height - y(d) + visibilityThreshold;
});
@ -233,90 +216,8 @@ define(function(require) {
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
});
return ScorePercentileChart;
});
});

View File

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