Skip to content

Commit

Permalink
Merge pull request #2470 from studentinsights/feature/star-debug
Browse files Browse the repository at this point in the history
Reading: Debug page for STAR coverage and distributions
  • Loading branch information
kevinrobinson committed May 16, 2019
2 parents 7ee0448 + e6a1eae commit 3e56b53
Show file tree
Hide file tree
Showing 14 changed files with 3,588 additions and 90 deletions.
26 changes: 24 additions & 2 deletions app/assets/javascripts/components/BoxAndWhisker.js
Expand Up @@ -14,7 +14,7 @@ function percentiles(values) {
}


export default function BoxAndWhisker({values, boxStyle, whiskerStyle, labelStyle, style}) {
export default function BoxAndWhisker({values, boxStyle, whiskerStyle, labelStyle, showQuartiles, quartileLabelStyle, style}) {
const [min, p25, p50, p75, max] = percentiles(values);
const labelWidth = 50; // arbitrary

Expand Down Expand Up @@ -67,6 +67,26 @@ export default function BoxAndWhisker({values, boxStyle, whiskerStyle, labelStyl
top: midTop+barHeight/2,
color: '#333',
...labelStyle}}>{p50}</div>
{showQuartiles && <div>
<div style={{
position: 'absolute',
left: scale(p25 - labelWidth/2),
width: scale(labelWidth),
textAlign: 'center',
fontSize: 10,
top: midTop+barHeight/2,
color: '#333',
...quartileLabelStyle}}>{p25}</div>
<div style={{
position: 'absolute',
left: scale(p75 - labelWidth/2),
width: scale(labelWidth),
textAlign: 'center',
fontSize: 10,
top: midTop+barHeight/2,
color: '#333',
...quartileLabelStyle}}>{p75}</div>
</div>}
</div>
</div>
);
Expand All @@ -76,5 +96,7 @@ BoxAndWhisker.propTypes = {
style: PropTypes.object,
boxStyle: PropTypes.object,
whiskerStyle: PropTypes.object,
labelStyle: PropTypes.object
labelStyle: PropTypes.object,
showQuartiles: PropTypes.bool,
quartileLabelStyle: PropTypes.object
};
64 changes: 64 additions & 0 deletions app/assets/javascripts/components/Histogram.js
@@ -0,0 +1,64 @@
import PropTypes from 'prop-types';
import React from 'react';
import _ from 'lodash';

// Visual component showing a small histogram, initially made for
// percentiles.
export default class Histogram extends React.Component {
scale(count) {
const {values} = this.props;
return `${Math.ceil(100 * count / values.length)}%`;
}

render() {
const {range, values, bucketSize, height, style = {}} = this.props;
const buckets = _.groupBy(values, value => bucketSize * Math.floor(value / bucketSize));
const bucketValues = _.range(range[0], range[1], bucketSize);
return (
<div className="Histogram" style={{height, ...style}}>
<div style={{position: 'relative', width: '100%', height}}>
{bucketValues.map(bucketFloor => {
const countInBucket = (buckets[bucketFloor] || []).length;
return this.renderBucket(bucketFloor, bucketValues.length, countInBucket);
})}
</div>
</div>
);
}


renderBucket(bucketFloor, bucketsCount, countInBucket) {
if (countInBucket === 0) return null;

const {bucketSize, values, range, height, innerStyle} = this.props;
const width = (range[1] - range[0]) / bucketsCount;
const yScale = this.props.yScale || 1;
const x = (range[1] - range[0]) * (bucketFloor / range[1]);
const y = Math.ceil(height * countInBucket / values.length) / yScale;
const title = `${bucketFloor}-${bucketFloor+bucketSize} has ${countInBucket} values, ${Math.round(100* countInBucket/values.length)}%`;

return (
<div key={bucketFloor} title={title}>
<div style={{
position: 'absolute',
background: 'blue',
left: `${x}%`,
width: `${width}%`,
bottom: 0,
height: y,
...innerStyle
}}>{'\u00A0'}</div>
</div>
);
}
}

Histogram.propTypes = {
bucketSize: PropTypes.number.isRequired,
range: PropTypes.arrayOf(PropTypes.number).isRequired,
values: PropTypes.arrayOf(PropTypes.number).isRequired,
height: PropTypes.number.isRequired,
style: PropTypes.object,
innerStyle: PropTypes.object,
yScale: PropTypes.number
};
26 changes: 25 additions & 1 deletion app/assets/javascripts/reading/readingData.js
@@ -1,9 +1,16 @@
import _ from 'lodash';
import moment from 'moment';
import {
high,
medium,
low
} from '../helpers/colors';
import {
toSchoolYear,
firstDayOfSchool,
lastDayOfSchool
} from '../helpers/schoolYear';


export const DIBELS_DORF_WPM = 'dibels_dorf_wpm';
export const DIBELS_DORF_ACC = 'dibels_dorf_acc';
Expand Down Expand Up @@ -128,4 +135,21 @@ export function interpretFAndPEnglish(text) {

export function orderedFAndPLevels() {
return _.sortBy(Object.keys(ORDERED_F_AND_P_ENGLISH_LEVELS), level => ORDERED_F_AND_P_ENGLISH_LEVELS[level]);
}
}

export function benchmarkPeriodKeyFor(timeMoment) {
const year = toSchoolYear(timeMoment);
const fallStart = firstDayOfSchool(year);
const winterStart = toMoment([year+1, 1, 1]);
const springStart = toMoment([year+1, 5, 1]);
const summerStart = lastDayOfSchool(year);

if (timeMoment.isBetween(fallStart, winterStart)) return 'fall';
if (timeMoment.isBetween(winterStart, springStart)) return 'winter';
if (timeMoment.isBetween(springStart, summerStart)) return 'spring';
return 'summer';
}

function toMoment(triple) {
return moment.utc([triple[0], triple[1], triple[2]].join('-'), 'YYYY-M-D');
}
132 changes: 132 additions & 0 deletions app/assets/javascripts/reading_debug/GradeTimeGrid.js
@@ -0,0 +1,132 @@
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
import {gradeText} from '../helpers/gradeText';

// Render a grid of grade by time, with selection.
export default class GradeTimeGrid extends React.Component {
constructor(props) {
super(props);
this.state = {
selection: null
};
}

render() {
const {isFlipped} = this.props;
return (isFlipped)
? this.renderFlipped()
: this.renderNormal();
}

renderNormal() {
const {selection, grades, intervals, onSelectionChanged, renderCellFn} = this.props;

return (
<div>
<table style={{width: '100%'}}>
<thead>
<tr>
<th style={styles.firstColumnCell}></th>
{intervals.map(interval => {
const [year, period] = interval;
return (
<th style={styles.headCell} key={interval.join('-')}>
<div>{year}</div>
<div>{period}</div>
</th>
);
})}
</tr>
</thead>
<tbody>
{grades.map(grade => (
<tr key={grade}>
<td style={styles.firstColumnCell}>{gradeText(grade)} now</td>
{intervals.map(interval => {
const [year, period] = interval;
return (
<td
key={interval.join('-')}>
<div
style={_.isEqual(selection, {year, period, grade})
? {cursor: 'pointer', padding: 10, border: `2px solid ${selection}`}
: {cursor: 'pointer', padding: 10, border: `2px solid white`}
}
onClick={() => onSelectionChanged({year, period, grade})}>
{renderCellFn({year, period, grade})}
</div>
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
);
}

renderFlipped() {
const {selection, grades, intervals, onSelectionChanged, renderCellFn} = this.props;

return (
<div>
<table style={{width: '100%'}}>
<thead>
<tr>
<th style={styles.firstColumnCell}></th>
{grades.map(grade => (
<th style={styles.headCell} key={grade}>
<td>{gradeText(grade)} now</td>
</th>
))}
</tr>
</thead>
<tbody>
{intervals.map(interval => {
const [year, period] = interval;
return (
<tr key={interval.join('-')}>
<td style={styles.firstColumnCell}><div>{year} {period}</div></td>
{grades.map(grade => {
return (
<td
style={{textAlign: 'center'}}
key={grade}>
<div
style={_.isEqual(selection, {year, period, grade})
? {cursor: 'pointer', padding: 10, border: `2px solid ${selection}`}
: {cursor: 'pointer', padding: 10, border: `2px solid white`}
}
onClick={() => onSelectionChanged({year, period, grade})}>
{renderCellFn({year, period, grade})}
</div>
</td>
);
})}
</tr>
);
})}
</tbody>
</table>
</div>
);
}
}
GradeTimeGrid.propTypes = {
grades: PropTypes.arrayOf(PropTypes.string).isRequired,
intervals: PropTypes.array.isRequired,
renderCellFn: PropTypes.func.isRequired,
selection: PropTypes.object,
onSelectionChanged: PropTypes.func.isRequired,
isFlipped: PropTypes.bool
};

const styles = {
firstColumnCell: {
textAlign: 'left'
},
headCell: {
}
};

0 comments on commit 3e56b53

Please sign in to comment.