Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
<ng-template #bucketTemplate let-bucket="bucket">
<div class="bucket-card notice-bg-bg">
<h3 class="mat-body-2">
<mat-icon class="mat-18 align-sub" aria-label="Bucket" i18n-aria-label>inventory_2</mat-icon
>&nbsp;<span [innerHTML]="bucket.value"></span>
</h3>
<ul>
@for (choice of bucket.choices; track $index) {
<li>
<div class="choice-row">
<mat-icon class="mat-18 shrink-0 mt-0.25" aria-label="Item" i18n-aria-label
>crop_16_9</mat-icon
>
<span class="flex-1" [innerHTML]="choice.getChoiceValue()"></span>
@if (!isChoiceReuseMatch) {
<span class="shrink-0">
<mat-icon class="mat-18 align-middle">person</mat-icon>{{ choice.getCount() }}
</span>
}
</div>
</li>
}
</ul>
</div>
</ng-template>

<ng-template #choiceTemplate let-choice="choice">
<div class="choice notice-bg-bg" [class.secondary-text]="choice.choiceDataPoints.length === 0">
<div
class="choice-card notice-bg-bg"
[class.secondary-text]="choice.choiceDataPoints.length === 0"
>
<h3 class="mat-body-2">
<mat-icon class="mat-18 align-sub" aria-label="Item" i18n-aria-label>crop_16_9</mat-icon
>&nbsp;<span [innerHTML]="choice.choiceValue"></span>
Expand All @@ -8,50 +37,82 @@ <h3 class="mat-body-2">
<ul>
@for (bucketDataPoint of choice.choiceDataPoints; track $index) {
<li>
<div class="bucket">
<div class="bucket-row">
<mat-icon class="mat-18 shrink-0 mt-0.25" aria-label="Bucket" i18n-aria-label
>inventory_2</mat-icon
>
<div class="flex-1" [innerHTML]="bucketDataPoint.getBucketValue()"></div>
<div class="shrink-0">
<span class="flex-1" [innerHTML]="bucketDataPoint.getBucketValue()"></span>
<span class="shrink-0">
<mat-icon class="mat-18 align-middle">person</mat-icon
>{{ bucketDataPoint.getCount() }}
</div>
</span>
</div>
</li>
}
</ul>
} @else {
<div class="bucket">
<mat-icon class="mat-18" aria-label="Not moved" i18n-aria-label>do_not_disturb</mat-icon>
<div i18n>Not moved by any students</div>
<div class="bucket-row !justify-start">
<mat-icon class="mat-18 mt-0.25">do_not_disturb</mat-icon>
<span i18n>Not moved by any students</span>
</div>
}
</div>
</ng-template>

<div [class.expanded]="expanded">
<h2 class="mat-subtitle-1" i18n>Bucket Frequency</h2>
<div class="max-h-160 overflow-y-auto @container" [class.max-h-none]="expanded">
@if (choiceData.length > 0) {
<h2 class="mat-subtitle-1" i18n>Choice Frequency</h2>
@if (choiceData.length > 0) {
<div class="flex gap-2 flex-wrap justify-between items-center">
<p class="!mt-0" i18n>
Number of times each item <mat-icon class="mat-18 align-sub">crop_16_9</mat-icon> was moved
into the different buckets <mat-icon class="mat-18 align-sub">inventory_2</mat-icon>.
</p>
<div class="flex gap-2 items-center">
<span i18n>Organize by:</span>
<mat-button-toggle-group
[hideSingleSelectionIndicator]="true"
[value]="viewMode"
(change)="viewMode = $event.value"
aria-label="Organize by"
i18n-aria-label
>
<mat-button-toggle value="choice" aria-label="Organize by choice" i18n-aria-label>
<mat-icon class="mat-18">crop_16_9</mat-icon>&nbsp;
<span i18n>Item</span>
</mat-button-toggle>
<mat-button-toggle value="bucket" aria-label="Organize by bucket" i18n-aria-label>
<mat-icon class="mat-18">inventory_2</mat-icon>&nbsp;
<span i18n>Bucket</span>
</mat-button-toggle>
</mat-button-toggle-group>
</div>
</div>
<div class="max-h-160 overflow-y-auto @container" [class.max-h-none]="expanded">
<div class="columns-1 @xl:columns-2 @4xl:columns-3 gap-2 mt-2">
@for (choice of choiceData; track $index) {
<div class="break-inside-avoid">
<ng-container
[ngTemplateOutlet]="choiceTemplate"
[ngTemplateOutletContext]="{ choice: choice }"
/>
</div>
@if (viewMode === 'choice') {
@for (choice of choiceData; track $index) {
<div class="break-inside-avoid">
<ng-container
[ngTemplateOutlet]="choiceTemplate"
[ngTemplateOutletContext]="{ choice: choice }"
/>
</div>
}
} @else {
@for (bucket of bucketData; track $index) {
<div class="break-inside-avoid">
<ng-container
[ngTemplateOutlet]="bucketTemplate"
[ngTemplateOutletContext]="{ bucket: bucket, first: false }"
/>
</div>
}
}
</div>
} @else {
<div class="notice" i18n>
Your students' choices will show up here when they complete the activity.
</div>
}
</div>
</div>
} @else {
<div class="notice" i18n>
Your students' choices will show up here when they complete the activity.
</div>
}
</div>
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
@reference "tailwindcss";

:host {
--mat-button-toggle-divider-color: transparent;
--mat-button-toggle-height: 32px;
.mat-button-toggle-group-appearance-standard, .mat-button-toggle-appearance-standard {
@apply rounded-md;
}
}

h3,
.mat-subtitle-1 {
margin-bottom: 8px;
margin-top: 0;
}

.choice {
.choice-card, .bucket-card {
@apply p-2 mb-2 rounded-md;
}

.bucket {
.choice-row, .bucket-row {
@apply flex gap-1 px-2 py-1 mt-1 rounded-md bg-white border border-neutral-200 text-sm items-start justify-between;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,40 +51,66 @@ describe('MatchSummaryDisplayComponent', () => {
expect(component).toBeTruthy();
});

it('should display one card per unique choice', () => {
expect(fixtureQueryAll(fixture, '.choice').length).toEqual(5);
});
describe('Choice view', () => {
it('should display one card per unique choice', () => {
expect(fixtureQueryAll(fixture, '.choice-card').length).toEqual(5);
});

it('should order choices by total count descending then alphabetically', () => {
const cards = fixtureQueryAll(fixture, '.choice');
const labels = Array.from(cards).map((el) => el.querySelector('h3')?.textContent?.trim());
expect(labels[0]).toContain('Choice B');
expect(labels[1]).toContain('Choice D');
expect(labels[2]).toContain('Choice C');
expect(labels[3]).toContain('Choice E');
expect(labels[4]).toContain('Choice A');
});
it('should order choices by total count descending then alphabetically', () => {
const cards = fixtureQueryAll(fixture, '.choice-card');
const labels = Array.from(cards).map((el) => el.querySelector('h3')?.textContent?.trim());
expect(labels[0]).toContain('Choice B');
expect(labels[1]).toContain('Choice D');
expect(labels[2]).toContain('Choice C');
expect(labels[3]).toContain('Choice E');
expect(labels[4]).toContain('Choice A');
});

it('should show bucket rows sorted by count within each choice', () => {
const cards = fixtureQueryAll(fixture, '.choice');
const choiceDCard = cards[1];
const bucketRows = choiceDCard.querySelectorAll('.bucket');
expect(bucketRows.length).toEqual(2);
expect(bucketRows[0].textContent).toContain('Bucket 2');
expect(bucketRows[0].textContent).toContain('2');
});
it('should show bucket rows sorted by count within each choice', () => {
const cards = fixtureQueryAll(fixture, '.choice-card');
const choiceDCard = cards[1];
const bucketRows = choiceDCard.querySelectorAll('.bucket-row');
expect(bucketRows.length).toEqual(2);
expect(bucketRows[0].textContent).toContain('Bucket 2');
expect(bucketRows[0].textContent).toContain('2');
});

it('should show the correct count for Choice B in Bucket 1', () => {
const cards = fixtureQueryAll(fixture, '.choice-card');
const choiceBCard = cards[0];
expect(choiceBCard.textContent).toContain('3');
});

it('should show the correct count for Choice B in Bucket 1', () => {
const cards = fixtureQueryAll(fixture, '.choice');
const choiceBCard = cards[0];
expect(choiceBCard.textContent).toContain('3');
it('should show "Not moved by any students" for choices left in the source bucket', () => {
const cards = fixtureQueryAll(fixture, '.choice-card');
const choiceACard = cards[4];
expect(choiceACard.textContent).toContain('Not moved by any students');
expect(choiceACard.querySelectorAll('.bucket-row').length).toEqual(1);
});
});

it('should show "Not moved by any students" for choices left in the source bucket', () => {
const cards = fixtureQueryAll(fixture, '.choice');
const choiceACard = cards[4];
expect(choiceACard.textContent).toContain('Not moved by any students');
expect(choiceACard.querySelectorAll('.bucket').length).toEqual(1);
describe('Bucket view', () => {
beforeEach(() => {
component.viewMode = 'bucket';
fixture.detectChanges();
});

it('should display one card per unique non-source bucket', () => {
expect(fixtureQueryAll(fixture, '.bucket-card').length).toBe(2);
});

it('should show choices sorted by count within each bucket', () => {
const cards = fixtureQueryAll(fixture, '.bucket-card');
const bucket1Card = cards[0];
const choiceRows = bucket1Card.querySelectorAll('.choice-row');
expect(choiceRows.length).toBe(3);
expect(choiceRows[0].textContent).toContain('Choice B');
expect(choiceRows[0].textContent).toContain('3');
expect(choiceRows[1].textContent).toContain('Choice C');
expect(choiceRows[1].textContent).toContain('2');
expect(choiceRows[2].textContent).toContain('Choice D');
expect(choiceRows[2].textContent).toContain('1');
});
});
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { CommonModule } from '@angular/common';
import { Component, Input, OnInit } from '@angular/core';
import { ChoiceData, MatchSummaryData } from '../summary-data/MatchSummaryData';
import { MatchContent } from '../../../components/match/MatchContent';
import { BucketData, ChoiceData, MatchSummaryData } from '../summary-data/MatchSummaryData';
import { MatchSummaryDataPoint } from '../summary-data/MatchSummaryDataPoint';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatIconModule } from '@angular/material/icon';
import { TeacherSummaryDisplayComponent } from '../teacher-summary-display.component';

export type SummaryViewMode = 'choice' | 'bucket';

@Component({
imports: [CommonModule, MatIconModule],
imports: [CommonModule, MatButtonToggleModule, MatIconModule],
selector: 'match-summary-display',
styleUrls: [
'./match-summary-display.component.scss',
Expand All @@ -15,29 +19,41 @@ import { TeacherSummaryDisplayComponent } from '../teacher-summary-display.compo
templateUrl: './match-summary-display.component.html'
})
export class MatchSummaryDisplayComponent extends TeacherSummaryDisplayComponent implements OnInit {
protected bucketData: { value: string; choices: MatchSummaryDataPoint[] }[] = [];
protected choiceData: ChoiceData[] = [];
@Input() expanded: boolean;
protected isChoiceReuseMatch: boolean;
private matchSummaryData: MatchSummaryData;
viewMode: SummaryViewMode = 'choice';

ngOnInit(): void {
this.setIsChoiceReuseMatch();
this.generateSummary();
}

private setIsChoiceReuseMatch(): void {
this.isChoiceReuseMatch = (
this.projectService.getComponent(this.nodeId, this.componentId) as MatchContent
).choiceReuseEnabled;
}

private generateSummary(): void {
this.getLatestWork().subscribe((componentStates) => {
this.bucketData = [];
this.choiceData = [];
this.matchSummaryData = new MatchSummaryData(
this.projectService.injectAssetPaths(componentStates)
);
this.setChoiceData();
this.setBucketData();
});
}

protected setChoiceData(): void {
this.matchSummaryData.getChoicesData().forEach((choice) => {
this.choiceData.push({
choiceValue: choice.choiceValue,
choiceDataPoints: choice.choiceDataPoints.sort(this.sortBuckets)
choiceDataPoints: choice.choiceDataPoints.sort(this.sortByCount)
});
});
this.choiceData.sort(this.sortChoices);
Expand All @@ -52,7 +68,16 @@ export class MatchSummaryDisplayComponent extends TeacherSummaryDisplayComponent
return countDiff !== 0 ? countDiff : a.choiceValue.localeCompare(b.choiceValue);
};

private sortBuckets(a: MatchSummaryDataPoint, b: MatchSummaryDataPoint): number {
protected setBucketData(): void {
this.matchSummaryData.getBucketsData().forEach((bucket: BucketData) => {
this.bucketData.push({
value: bucket.bucketValue,
choices: bucket.bucketDataPoints.sort(this.sortByCount)
});
});
}

private sortByCount(a: MatchSummaryDataPoint, b: MatchSummaryDataPoint): number {
return b.getCount() - a.getCount();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { MatchSummaryDataPoint } from './MatchSummaryDataPoint';
import { SummaryData } from '../../summary-display/summary-data/SummaryData';

export type ChoiceData = { choiceValue: string; choiceDataPoints: MatchSummaryDataPoint[] };
export type BucketData = { bucketValue: string; bucketDataPoints: MatchSummaryDataPoint[] };

/**
* Summary data for all choices, each with a breakdown per bucket
Expand All @@ -19,14 +20,34 @@ export class MatchSummaryData extends SummaryData {
return this.choicesData;
}

getBucketsData(): BucketData[] {
const bucketsMap = new Map<string, BucketData>();
this.choicesData.forEach((choice) => {
choice.choiceDataPoints.forEach((point) => {
const bucketValue = point.getBucketValue();
if (!bucketsMap.has(bucketValue)) {
bucketsMap.set(bucketValue, { bucketValue, bucketDataPoints: [] });
}
bucketsMap.get(bucketValue).bucketDataPoints.push(point);
});
});
return Array.from(bucketsMap.values()).sort(
(a, b) => this.getTotalCount(b) - this.getTotalCount(a)
);
}

private getTotalCount(bucket: BucketData): number {
return bucket.bucketDataPoints.reduce((sum, point) => sum + point.getCount(), 0);
}

private extractChoiceData(componentStates: ComponentState[]): void {
componentStates.forEach((componentState) => {
componentState.studentData.buckets.forEach((bucketStudentData, index) => {
if (index === 0) {
bucketStudentData.items.forEach((item) => this.registerChoice(item.value));
bucketStudentData.items.forEach((choice) => this.registerChoice(choice.value));
} else {
bucketStudentData.items.forEach((item) => {
this.extractBucketDataPerChoice(item.value, bucketStudentData.value);
bucketStudentData.items.forEach((choice) => {
this.extractBucketDataPerChoice(choice.value, bucketStudentData.value);
});
}
});
Expand Down
Loading