Skip to content

Commit

Permalink
feat: Add rubric summary tag table [PT-188404153]
Browse files Browse the repository at this point in the history
  • Loading branch information
dougmartin committed Oct 28, 2024
1 parent d272820 commit 46c2010
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 75 deletions.
8 changes: 7 additions & 1 deletion css/portal-dashboard/feedback/rubric-summary-modal.less
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.35);
height: auto;
left: 50px;
top: 135px;
// top: 135px;
width: 777px;

.lightboxHeader {
Expand All @@ -30,6 +30,12 @@
.contentArea {
padding: 0 20px 10px 20px;

.contents {
display: flex;
gap: 10px;
flex-direction: column;
}

.modalOption {
margin: 15px 0 10px 0;

Expand Down
17 changes: 16 additions & 1 deletion css/portal-dashboard/feedback/rubric-table.less
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,13 @@

.rubricDescriptionTitle {
justify-content: center;
width: 75%
width: 75%;

&.fullWidth {
width: 100%;
text-align: left;
margin-left: 10px;
}
}
}
.rubricScoreHeader {
Expand Down Expand Up @@ -111,6 +117,10 @@
flex: 1;
align-items: stretch;

p {
margin-top: 0;
}

&:first-child {
border: none;
}
Expand All @@ -120,6 +130,11 @@
font-weight: normal;
flex: auto;

&.separateImage {
display: flex;
align-items: center;
}

img {
padding-right: 10px;
}
Expand Down
235 changes: 172 additions & 63 deletions js/components/portal-dashboard/feedback/rubric-summary-modal.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React, { PureComponent } from "react";
import { Modal } from "react-bootstrap";
import Markdown from "markdown-to-jsx";
import { Rubric, RubricCriterion, RubricRating, getFeedbackColor, getFeedbackTextColor } from "./rubric-utils";
import classNames from "classnames";
import { Rubric, RubricRating, getFeedbackColor, getFeedbackTextColor } from "./rubric-utils";
import { ICriteriaCount } from "./rubric-summary-icon";
import LaunchIcon from "../../../../img/svg-icons/launch-icon.svg";
import { ScoringSettings } from "../../../util/scoring";
Expand All @@ -18,9 +19,110 @@ interface IProps {
scoringSettings: ScoringSettings;
}

interface IRubricSummaryRowRating {
score: number;
percentage: number;
description: string;
isApplicableRating: boolean;
}

interface IRubricSummaryRow {
text: string;
ratings: IRubricSummaryRowRating[];
iconUrl?: string;
iconPhrase?: string;
criteriaGroupIndex?: number;
}

interface IRubricSummary {
tableRows: IRubricSummaryRow[];
tagSummaryRows: IRubricSummaryRow[];
tagSummaryTitle: string;
}

export const getRubricSummaryRowRatings = (rows: IRubricSummaryRow[], ratings: RubricRating[]) => {
const summaryRowRatings: IRubricSummaryRowRating[] = [];

ratings.forEach(({score}, index) => {
const {sum, count, hasApplicableRating} = rows.reduce((acc, row) => {
const {isApplicableRating, percentage} = row.ratings[index];
acc.count++;
if (isApplicableRating) {
acc.sum += percentage;
acc.hasApplicableRating = true;
}
return acc;
}, {sum: 0, count: 0, hasApplicableRating: false});
if (count > 0 && hasApplicableRating) {
summaryRowRatings.push({isApplicableRating: true, description: "", percentage: Math.round(sum / count), score});
} else {
summaryRowRatings.push({isApplicableRating: false, description: "Not Applicable", percentage: 0, score});
}
});

return summaryRowRatings;
};

export const getRubricSummary = (rubric: Rubric, criteriaCounts: ICriteriaCount[]): IRubricSummary => {
const {criteriaGroups, ratings} = rubric;
const tableRows: IRubricSummaryRow[] = [];
const tagSummaryRows: IRubricSummaryRow[] = [];
const tags = new Map<string, string>();

criteriaGroups.forEach((criteriaGroup, criteriaGroupIndex) => {
criteriaGroup.criteria.forEach(criterion => {
const {iconUrl, iconPhrase} = criterion;
const critId = criterion.id;
const row: IRubricSummaryRow = {criteriaGroupIndex, ratings: [], iconUrl, iconPhrase, text: criterion.description};
ratings.forEach(rating => {
const ratingId = rating.id;
const isApplicableRating = criterion.nonApplicableRatings === undefined || criterion.nonApplicableRatings.indexOf(ratingId) < 0;
const description = isApplicableRating ? criterion.ratingDescriptions?.[rating.id] : "Not Applicable";
let percentage = 0;
if (isApplicableRating) {
const criteriaCount = criteriaCounts.find(cc => cc.id === critId);
if (criteriaCount && criteriaCount?.numStudents > 0) {
const ratingCount = criteriaCount.ratings[ratingId];
if (ratingCount) {
percentage = Math.round((ratingCount / criteriaCount.numStudents) * 100);
}
}
}
row.ratings.push({score: rating.score, percentage, description, isApplicableRating});
});
tableRows.push(row);

if (iconUrl && !tags.has(iconUrl) && iconPhrase) {
tags.set(iconUrl, iconPhrase);
}
});
});

const hasTags = tags.size > 0;
const tagSummaryTitle = hasTags ? "Average Result by Tag" : "Summary of Rubric Score";

if (hasTags) {
const sortedKeys = Array.from(tags.keys()).sort();
sortedKeys.forEach(iconUrl => {
const tagRows = tableRows.filter(r => r.iconUrl === iconUrl);
const iconPhrase = tags.get(iconUrl) ?? "";
tagSummaryRows.push({text: iconPhrase, ratings: getRubricSummaryRowRatings(tagRows, ratings), iconUrl, iconPhrase});
});
const nonTagRows = tableRows.filter(r => !r.iconUrl);
if (nonTagRows.length > 0) {
tagSummaryRows.push({text: "No tag applied", ratings: getRubricSummaryRowRatings(nonTagRows, ratings)});
}
} else {
tagSummaryRows.push({text: "Class Results", ratings: getRubricSummaryRowRatings(tableRows, ratings)});
}

return {tableRows, tagSummaryRows, tagSummaryTitle};
};

export class RubricSummaryModal extends PureComponent<IProps> {

render() {
const summary = getRubricSummary(this.props.rubric, this.props.criteriaCounts);

return (
<Modal animation={false} centered dialogClassName={css.lightbox} onHide={this.handleClose} show={true} data-cy="rubric-summary-modal">
Expand All @@ -31,7 +133,9 @@ export class RubricSummaryModal extends PureComponent<IProps> {
</Modal.Header>
<Modal.Body className={css.lightboxBody}>
<div className={css.contentArea} data-cy="rubric-summary-modal-content-area">
{this.renderTable()}
<div className={css.contents}>
{this.renderSummary(summary)}
</div>
<div className={css.buttonContainer}>
<div className={css.closeButton} onClick={this.handleClose} data-cy="rubric-summary-modal-close-button">
Close
Expand All @@ -43,49 +147,69 @@ export class RubricSummaryModal extends PureComponent<IProps> {
);
}

private renderTable() {
private renderSummary(summary: IRubricSummary) {
switch (this.props.rubric.tagSummaryDisplay) {
case "above":
return <>{this.renderTagSummary(summary)}{this.renderCriteriaTable(summary)}</>;
case "below":
return <>{this.renderCriteriaTable(summary)}{this.renderTagSummary(summary)}</>;
case "onlySummary":
return this.renderTagSummary(summary);
case "none":
default:
return this.renderCriteriaTable(summary);
}
}

private renderTagSummary({tagSummaryRows, tagSummaryTitle}: IRubricSummary) {
const { rubric, rubricDocUrl } = this.props;

return (
<div className={tableCss.rubricContainer} data-cy="rubric-table">
{this.renderColumnHeaders(rubric, rubricDocUrl, {title: tagSummaryTitle, hideScoringGuide: true})}
<div className={tableCss.rubricTable}>
<div className={tableCss.rubricTableGroup}>
{this.renderRows(tagSummaryRows)}
</div>
</div>
</div>
);
}

private renderCriteriaTable({tableRows}: IRubricSummary) {
const { rubric, rubricDocUrl } = this.props;
const { criteriaGroups } = rubric;

return (
<div className={tableCss.rubricContainer} data-cy="rubric-table">
{this.renderColumnHeaders(rubric, rubricDocUrl)}
{this.renderColumnHeaders(rubric, rubricDocUrl, {title: rubric.criteriaLabel})}
<div className={tableCss.rubricTable}>
{criteriaGroups.map((criteriaGroup, index) => (
<div className={tableCss.rubricTableGroup} key={index}>
{criteriaGroup.label.length > 0 && <div className={tableCss.rubricTableGroupLabel}>{criteriaGroup.label}</div>}
<div className={tableCss.rubricTableRows}>
{criteriaGroup.criteria.map(criterion =>
<div className={tableCss.rubricTableRow} key={criterion.id} id={criterion.id}>
<div className={tableCss.rubricDescription}>
{criterion.iconUrl && <img src={criterion.iconUrl} title={criterion.iconPhrase} />}
<Markdown>{criterion.description}</Markdown>
</div>
{this.renderRatings(criterion)}
</div>
)}
</div>
{this.renderRows(tableRows.filter(r => r.criteriaGroupIndex === index))}
</div>
))}
</div>
</div>
);
}

private renderColumnHeaders = (rubric: Rubric, rubricDocUrl: string) => {
private renderColumnHeaders = (rubric: Rubric, rubricDocUrl: string, options: {title: string; hideScoringGuide?: boolean}) => {
const hasRubricDocUrl = rubricDocUrl.trim().length > 0;

const showScore = this.props.scoringSettings.scoreType === RUBRIC_SCORE;
const titleClassName = classNames(tableCss.rubricDescriptionTitle, tableCss.fullWidth);

return (
<div className={tableCss.columnHeaders}>
<div className={tableCss.rubricDescriptionHeader}>
{hasRubricDocUrl && <div className={tableCss.scoringGuideArea}>
{hasRubricDocUrl && !options.hideScoringGuide && <div className={tableCss.scoringGuideArea}>
<a className={tableCss.launchButton} href={rubricDocUrl} target="_blank" data-cy="scoring-guide-launch-icon">
<LaunchIcon className={tableCss.icon} />
</a>
Scoring Guide
</div>}
<div className={tableCss.rubricDescriptionTitle}>{rubric.criteriaLabel}</div>
<div className={titleClassName}>{options.title}</div>
</div>
{rubric.ratings.map((rating: any) =>
<div className={tableCss.rubricScoreHeader} key={rating.id}>
Expand All @@ -97,55 +221,40 @@ export class RubricSummaryModal extends PureComponent<IProps> {
);
}

private renderRatings = (crit: RubricCriterion) => {
const { rubric } = this.props;
const { ratings } = rubric;
return (
<div className={tableCss.ratingsGroup}>
{ratings.map(rating => this.renderRating(crit, rating))}
</div>
);
}

private renderRating = (criterion: RubricCriterion, rating: RubricRating) => {
const critId = criterion.id;
const ratingId = rating.id;
const key = `${critId}-${ratingId}`;
const ratingDescription = criterion.ratingDescriptions?.[ratingId];
const isApplicableRating = criterion.nonApplicableRatings === undefined || criterion.nonApplicableRatings.indexOf(ratingId) < 0;
const style: React.CSSProperties = {
color: getFeedbackTextColor({rubric: this.props.rubric, score: rating.score}),
backgroundColor: getFeedbackColor({rubric: this.props.rubric, score: rating.score})
};
private renderRows(rows: IRubricSummaryRow[]) {
const {rubric} = this.props;

return (
<div className={tableCss.rubricScoreBox} style={style} key={key}>
<div className={tableCss.rubricButton} title={(isApplicableRating) ? ratingDescription : "Not Applicable"}>
{ !isApplicableRating
? <span className={tableCss.noRating}>N/A</span>
: this.renderPercentage(critId, ratingId)
}
</div>
<div className={tableCss.rubricTableRows}>
{rows.map(({text, ratings, iconUrl, iconPhrase}, index) => (
<div className={tableCss.rubricTableRow} key={index}>
<div className={classNames(tableCss.rubricDescription, tableCss.separateImage)}>
{iconUrl && <img src={iconUrl} title={iconPhrase} /> }
<Markdown>{text}</Markdown>
</div>
<div className={tableCss.ratingsGroup}>
{ratings.map(({score, isApplicableRating, description, percentage}, index) => {
const style: React.CSSProperties = {
color: getFeedbackTextColor({rubric, score}),
backgroundColor: getFeedbackColor({rubric, score})
};
return (
<div className={tableCss.rubricScoreBox} style={style} key={index}>
<div className={tableCss.rubricButton} title={description}>
{ !isApplicableRating
? <span className={tableCss.noRating}>N/A</span>
: <>{percentage}%</>
}
</div>
</div>
);
})}
</div>
</div>))}
</div>
);
}

private renderPercentage = (critId: string, ratingId: string) => {
let percentage = 0;

const criteriaCount = this.props.criteriaCounts.find(cc => cc.id === critId);
if (criteriaCount && criteriaCount?.numStudents > 0) {
const ratingCount = criteriaCount.ratings[ratingId];
if (ratingCount) {
percentage = Math.round((ratingCount / criteriaCount.numStudents) * 100);
}
}

return (
<>{percentage}%</>
);
}

private handleClose = (e: React.MouseEvent) => this.props.onClose();

}

3 changes: 2 additions & 1 deletion js/components/portal-dashboard/feedback/rubric-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from "react";
import { Map } from "immutable";
import Markdown from "markdown-to-jsx";
import ReactTooltip from "react-tooltip";
import classNames from "classnames";
import LaunchIcon from "../../../../img/svg-icons/launch-icon.svg";
import { Rubric, RubricCriterion, RubricRating, getFeedbackColor } from "./rubric-utils";
import { ScoringSettings } from "../../../util/scoring";
Expand Down Expand Up @@ -36,7 +37,7 @@ export class RubricTableContainer extends React.PureComponent<IProps> {
<div className={css.rubricTableRows}>
{criteriaGroup.criteria.map(criterion =>
<div className={css.rubricTableRow} key={criterion.id} id={criterion.id}>
<div className={css.rubricDescription}>
<div className={classNames(css.rubricDescription, css.separateImage)}>
{criterion.iconUrl && <img src={criterion.iconUrl} />}
<Markdown>{criterion.description}</Markdown>
</div>
Expand Down
Loading

0 comments on commit 46c2010

Please sign in to comment.