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:
parent
5e8215b249
commit
a506ec9245
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
});
|
});
|
||||||
|
|
|
@ -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) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue