From 020cc27e0f9a1f220a27a4678d531f4e36c8c3e5 Mon Sep 17 00:00:00 2001 From: lachlan-robinson Date: Sun, 10 Aug 2025 09:50:26 +1000 Subject: [PATCH 01/11] chore: create outcome.service.ts --- src/app/api/services/outcome.service.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/app/api/services/outcome.service.ts diff --git a/src/app/api/services/outcome.service.ts b/src/app/api/services/outcome.service.ts new file mode 100644 index 0000000000..6f512bdcff --- /dev/null +++ b/src/app/api/services/outcome.service.ts @@ -0,0 +1,9 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class OutcomeService { + + constructor() { } +} From 401b9eb3e83521ce9cd454ce8de59a4e7f271e67 Mon Sep 17 00:00:00 2001 From: lachlan-robinson Date: Sun, 10 Aug 2025 09:58:38 +1000 Subject: [PATCH 02/11] chore: implement outcome service --- src/app/api/services/outcome.service.ts | 145 +++++++++++++++++++++++- 1 file changed, 142 insertions(+), 3 deletions(-) diff --git a/src/app/api/services/outcome.service.ts b/src/app/api/services/outcome.service.ts index 6f512bdcff..76afcc43eb 100644 --- a/src/app/api/services/outcome.service.ts +++ b/src/app/api/services/outcome.service.ts @@ -1,9 +1,148 @@ -import { Injectable } from '@angular/core'; +import {Injectable} from '@angular/core'; +import {DomSanitizer} from '@angular/platform-browser'; +import {GradeService} from '../models/doubtfire-model'; +import {TaskService} from '../models/doubtfire-model'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class OutcomeService { + alignmentLabels: string[] = [ + 'The task is not related to this outcome at all', + 'The task is slightly related to this outcome', + 'The task is related to this outcome', + 'The task is a reasonable example for this outcome', + 'The task is a strong example of this outcome', + 'The task is the best example of this outcome', + ]; - constructor() { } + constructor( + private sanitizer: DomSanitizer, + private gradeService: GradeService, + private taskService: TaskService, + ) {} + + individualTaskStatusFactor(project: any, task: any) { + return (taskDefinitionId: any) => { + if (task.definition.id === taskDefinitionId) { + return this.taskService.learningWeight.get( + project.findTaskForDefinition(taskDefinitionId).status, + ); + } else { + return 0; + } + }; + } + + individualTaskPotentialFactor(project: any, task: any) { + return (taskDefinitionId: any) => { + return task.definition.id === taskDefinitionId ? 1 : 0; + }; + } + + calculateTargets(unit: any, source: any, taskStatusFactor: (td: any) => number) { + const outcomes: any = {}; + + unit.ilos.forEach((outcome: any) => { + outcomes[outcome.id] = {0: [], 1: [], 2: [], 3: []}; + }); + + source.taskOutcomeAlignments.forEach((align: any) => { + const td = unit.taskDef(align.taskDefinition.id); + outcomes[align.learningOutcome.id][td.targetGrade].push(align.rating * taskStatusFactor(td)); + }); + + Object.keys(outcomes).forEach((key) => { + const outcome = outcomes[key]; + Object.keys(outcome).forEach((gradeKey) => { + const scale = Math.pow(2, parseInt(gradeKey, 10)); + outcome[gradeKey] = + outcome[gradeKey].reduce((memo: number, num: number) => memo + num, 0) * scale; + }); + }); + + return outcomes; + } + + calculateTaskContribution(unit: any, project: any, task: any) { + const outcomeSet: any[] = []; + outcomeSet[0] = this.calculateTargets( + unit, + unit, + this.individualTaskStatusFactor(project, task), + ); + + Object.keys(outcomeSet[0]).forEach((key) => { + outcomeSet[0][key] = Object.values(outcomeSet[0][key] as any).reduce( + (memo: number, num: any) => memo + (num as number), + 0, + ); + }); + + outcomeSet[0].title = 'Current Task Contribution'; + return outcomeSet; + } + + calculateTaskPotentialContribution(unit: any, project: any, task: any) { + const outcomes = this.calculateTargets( + unit, + unit, + this.individualTaskPotentialFactor(project, task), + ); + + Object.keys(outcomes).forEach((key) => { + outcomes[key] = Object.values(outcomes[key]).reduce( + (memo: number, num: any) => memo + (num as number), + 0, + ); + }); + + outcomes['title'] = 'Potential Task Contribution'; + return outcomes; + } + + calculateProgress(unit: any, project: any) { + const outcomeSet: any[] = []; + outcomeSet[0] = this.calculateTargets(unit, unit, project.taskStatusFactor.bind(project)); + + outcomeSet.forEach((outcomes) => { + Object.keys(outcomes).forEach((key) => { + outcomes[key] = Object.values(outcomes[key]).reduce( + (memo: number, num: any) => memo + (num as number), + 0, + ); + }); + }); + + outcomeSet[0].title = 'Your Progress'; + return outcomeSet; + } + + targetsByGrade(unit: any, source: any) { + const result: any[] = []; + const outcomes = this.calculateTargets(unit, source, unit.taskStatusFactor); + + const values: Record = {'0': [], '1': [], '2': [], '3': []}; + + Object.keys(outcomes).forEach((key) => { + const outcome = outcomes[key]; + Object.keys(outcome).forEach((gradeKey) => { + values[gradeKey].push({ + label: this.sanitizer.bypassSecurityTrustHtml( + unit.outcome(parseInt(key, 10)).abbreviation, + ), + value: outcome[gradeKey], + }); + }); + }); + + Object.keys(values).forEach((gradeKey) => { + result.push({ + key: this.gradeService.grades[gradeKey], + values: values[gradeKey], + }); + }); + + return result; + } } From 5c0465e6f155f5106c8303745d175449227732b7 Mon Sep 17 00:00:00 2001 From: lachlan-robinson Date: Sun, 10 Aug 2025 10:08:48 +1000 Subject: [PATCH 03/11] chore: update imports and downgrade injectable --- src/app/doubtfire-angular.module.ts | 70 ++++++++++---------- src/app/doubtfire-angularjs.module.ts | 92 ++++++++++++++------------- 2 files changed, 87 insertions(+), 75 deletions(-) diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index c714ff0e5a..42199cce26 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -1,13 +1,13 @@ import {interval} from 'rxjs'; import {take} from 'rxjs/operators'; -import { NgModule, Injector, DoBootstrap } from '@angular/core'; -import { BrowserModule, DomSanitizer, Title } from '@angular/platform-browser'; -import { UpgradeModule } from '@angular/upgrade/static'; -import { AppInjector, setAppInjector } from './app-injector'; -import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { NgxChartsModule } from '@swimlane/ngx-charts'; +import {NgModule, Injector, DoBootstrap} from '@angular/core'; +import {BrowserModule, DomSanitizer, Title} from '@angular/platform-browser'; +import {UpgradeModule} from '@angular/upgrade/static'; +import {AppInjector, setAppInjector} from './app-injector'; +import {HttpClientModule, HTTP_INTERCEPTORS} from '@angular/common/http'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {NgxChartsModule} from '@swimlane/ngx-charts'; // Lottie animation module // import {LottieModule, LottieCacheModule} from 'ngx-lottie'; @@ -98,12 +98,16 @@ import {ExtensionModalComponent} from './common/modals/extension-modal/extension import {CalendarModalComponent} from './common/modals/calendar-modal/calendar-modal.component'; import {MatRadioModule} from '@angular/material/radio'; import {MatButtonToggleModule} from '@angular/material/button-toggle'; -import {DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE, MatOptionModule} from '@angular/material/core'; +import { + DateAdapter, + MAT_DATE_FORMATS, + MAT_DATE_LOCALE, + MatOptionModule, +} from '@angular/material/core'; import {MatDatepickerModule} from '@angular/material/datepicker'; -import { DateFnsAdapter } from '@angular/material-date-fns-adapter'; -import { enAU } from 'date-fns/locale'; - +import {DateFnsAdapter} from '@angular/material-date-fns-adapter'; +import {enAU} from 'date-fns/locale'; import {doubtfireStates} from './doubtfire.states'; import {MatTableModule} from '@angular/material/table'; @@ -226,23 +230,23 @@ import { TeachingPeriodUnitImportDialogComponent, TeachingPeriodUnitImportService, } from './admin/states/teaching-periods/teaching-period-unit-import/teaching-period-unit-import.dialog'; -import { UnauthorisedComponent } from './errors/states/unauthorised/unauthorised.component'; -import { AcceptEulaComponent } from './eula/accept-eula/accept-eula.component'; -import { TiiActionLogComponent } from './admin/tii-action-log/tii-action-log.component'; -import { TiiActionService } from './api/services/tii-action.service'; -import { FUnitsComponent } from './admin/states/units/units.component'; -import { FUnitTaskListComponent } from './units/task-viewer/directives/unit-task-list/unit-task-list.component'; -import { FTaskDetailsViewComponent } from './units/task-viewer/directives/task-details-view/task-details-view.component'; -import { FTaskSheetViewComponent } from './units/task-viewer/directives/task-sheet-view/task-sheet-view.component'; -import { UnitCodeComponent } from './common/unit-code/unit-code.component'; -import { GradeService } from './common/services/grade.service'; -import { UnitRootStateComponent } from './units/unit-root-state.component'; -import { TaskViewerStateComponent } from './units/task-viewer/task-viewer-state.component'; -import { ProjectRootStateComponent } from './projects/states/project-root-state.component'; -import { ProjectProgressDashboardComponent } from './projects/project-progress-dashboard/project-progress-dashboard.component'; -import { ProgressBurndownChartComponent } from './visualisations/progress-burndown-chart/progressburndownchart.component'; -import { TaskVisualisationComponent } from './visualisations/task-visualisation/taskvisualisation.component'; -import { ChartBaseComponent } from './common/chart-base/chart-base-component/chart-base-component.component'; +import {UnauthorisedComponent} from './errors/states/unauthorised/unauthorised.component'; +import {AcceptEulaComponent} from './eula/accept-eula/accept-eula.component'; +import {TiiActionLogComponent} from './admin/tii-action-log/tii-action-log.component'; +import {TiiActionService} from './api/services/tii-action.service'; +import {FUnitsComponent} from './admin/states/units/units.component'; +import {FUnitTaskListComponent} from './units/task-viewer/directives/unit-task-list/unit-task-list.component'; +import {FTaskDetailsViewComponent} from './units/task-viewer/directives/task-details-view/task-details-view.component'; +import {FTaskSheetViewComponent} from './units/task-viewer/directives/task-sheet-view/task-sheet-view.component'; +import {UnitCodeComponent} from './common/unit-code/unit-code.component'; +import {GradeService} from './common/services/grade.service'; +import {UnitRootStateComponent} from './units/unit-root-state.component'; +import {TaskViewerStateComponent} from './units/task-viewer/task-viewer-state.component'; +import {ProjectRootStateComponent} from './projects/states/project-root-state.component'; +import {ProjectProgressDashboardComponent} from './projects/project-progress-dashboard/project-progress-dashboard.component'; +import {ProgressBurndownChartComponent} from './visualisations/progress-burndown-chart/progressburndownchart.component'; +import {TaskVisualisationComponent} from './visualisations/task-visualisation/taskvisualisation.component'; +import {ChartBaseComponent} from './common/chart-base/chart-base-component/chart-base-component.component'; import {ScormPlayerComponent} from './common/scorm-player/scorm-player.component'; import {ScormAdapterService} from './api/services/scorm-adapter.service'; import {ScormCommentComponent} from './tasks/task-comments-viewer/scorm-comment/scorm-comment.component'; @@ -250,9 +254,9 @@ import {TaskScormCardComponent} from './projects/states/dashboard/directives/tas import {TestAttemptService} from './api/services/test-attempt.service'; import {ScormExtensionCommentComponent} from './tasks/task-comments-viewer/scorm-extension-comment/scorm-extension-comment.component'; import {ScormExtensionModalComponent} from './common/modals/scorm-extension-modal/scorm-extension-modal.component'; -import { GradeIconComponent } from './common/grade-icon/grade-icon.component'; -import { GradeTaskModalComponent } from './tasks/modals/grade-task-modal/grade-task-modal.component'; -import { PrivacyPolicy } from './config/privacy-policy/privacy-policy'; +import {GradeIconComponent} from './common/grade-icon/grade-icon.component'; +import {GradeTaskModalComponent} from './tasks/modals/grade-task-modal/grade-task-modal.component'; +import {PrivacyPolicy} from './config/privacy-policy/privacy-policy'; // See https://stackoverflow.com/questions/55721254/how-to-change-mat-datepicker-date-format-to-dd-mm-yyyy-in-simplest-way/58189036#58189036 const MY_DATE_FORMAT = { @@ -266,7 +270,8 @@ const MY_DATE_FORMAT = { monthYearA11yLabel: 'MMMM yyyy', }, }; -import { UnitStudentEnrolmentModalComponent } from './units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.component'; +import {UnitStudentEnrolmentModalComponent} from './units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.component'; +import {OutcomeService} from './api/services/outcome.service'; @NgModule({ // Components we declare @@ -421,6 +426,7 @@ import { UnitStudentEnrolmentModalComponent } from './units/modals/unit-student- FileDownloaderService, CheckForUpdateService, TaskOutcomeAlignmentService, + OutcomeService, visualisationsProvider, commentsModalProvider, rootScopeProvider, diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index d9dc6955d5..d962ee963c 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -119,7 +119,6 @@ import 'build/src/app/common/modals/modals.js'; import 'build/src/app/common/file-uploader/file-uploader.js'; import 'build/src/app/common/common.js'; import 'build/src/app/common/services/listener-service.js'; -import 'build/src/app/common/services/outcome-service.js'; import 'build/src/app/common/services/services.js'; import 'build/src/app/common/services/recorder-service.js'; import 'build/src/app/common/services/media-service.js'; @@ -181,49 +180,50 @@ import { UnitService, UserService, } from './api/models/doubtfire-model'; -import { UnauthorisedComponent } from './errors/states/unauthorised/unauthorised.component'; -import { FileDownloaderService } from './common/file-downloader/file-downloader.service'; -import { CheckForUpdateService } from './sessions/service-worker-updater/check-for-update.service'; -import { TaskSubmissionService } from './common/services/task-submission.service'; -import { TaskAssessmentModalService } from './common/modals/task-assessment-modal/task-assessment-modal.service'; -import { TaskSubmissionHistoryComponent } from './tasks/task-submission-history/task-submission-history.component'; -import { HeaderComponent } from './common/header/header.component'; -import { SplashScreenComponent } from './home/splash-screen/splash-screen.component'; -import { GlobalStateService } from './projects/states/index/global-state.service'; -import { TransitionHooksService } from './sessions/transition-hooks.service'; -import { GradeIconComponent } from './common/grade-icon/grade-icon.component'; -import { GradeTaskModalService } from './tasks/modals/grade-task-modal/grade-task-modal.service'; -import { AuthenticationService } from './api/services/authentication.service'; -import { ProjectService } from './api/services/project.service'; -import { ObjectSelectComponent } from './common/obect-select/object-select.component'; -import { TaskDefinitionService } from './api/services/task-definition.service'; -import { EditProfileDialogService } from './common/modals/edit-profile-dialog/edit-profile-dialog.service'; -import { GroupService } from './api/services/group.service'; -import { UserBadgeComponent } from './common/user-badge/user-badge.component'; -import { TaskStatusCardComponent } from './projects/states/dashboard/directives/task-dashboard/directives/task-status-card/task-status-card.component'; -import { TaskDueCardComponent } from './projects/states/dashboard/directives/task-dashboard/directives/task-due-card/task-due-card.component'; -import { FooterComponent } from './common/footer/footer.component'; -import { TaskAssessmentCardComponent } from './projects/states/dashboard/directives/task-dashboard/directives/task-assessment-card/task-assessment-card.component'; -import { TaskSubmissionCardComponent } from './projects/states/dashboard/directives/task-dashboard/directives/task-submission-card/task-submission-card.component'; -import { InboxComponent } from './units/states/tasks/inbox/inbox.component'; -import { TaskDefinitionEditorComponent } from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-editor.component'; -import { UnitAnalyticsComponent } from './units/states/analytics/unit-analytics-route.component'; -import { UnitTaskEditorComponent } from './units/states/edit/directives/unit-tasks-editor/unit-task-editor.component'; -import { CreateNewUnitModal } from './admin/modals/create-new-unit-modal/create-new-unit-modal.component'; -import { FUsersComponent } from './admin/states/users/users.component'; -import { FUnitTaskListComponent } from './units/task-viewer/directives/unit-task-list/unit-task-list.component'; -import { FTaskDetailsViewComponent } from './units/task-viewer/directives/task-details-view/task-details-view.component'; -import { FTaskSheetViewComponent } from './units/task-viewer/directives/task-sheet-view/task-sheet-view.component'; -import { ProgressBurndownChartComponent } from './visualisations/progress-burndown-chart/progressburndownchart.component'; -import { TaskVisualisationComponent } from './visualisations/task-visualisation/taskvisualisation.component'; +import {UnauthorisedComponent} from './errors/states/unauthorised/unauthorised.component'; +import {FileDownloaderService} from './common/file-downloader/file-downloader.service'; +import {CheckForUpdateService} from './sessions/service-worker-updater/check-for-update.service'; +import {TaskSubmissionService} from './common/services/task-submission.service'; +import {TaskAssessmentModalService} from './common/modals/task-assessment-modal/task-assessment-modal.service'; +import {TaskSubmissionHistoryComponent} from './tasks/task-submission-history/task-submission-history.component'; +import {HeaderComponent} from './common/header/header.component'; +import {SplashScreenComponent} from './home/splash-screen/splash-screen.component'; +import {GlobalStateService} from './projects/states/index/global-state.service'; +import {TransitionHooksService} from './sessions/transition-hooks.service'; +import {GradeIconComponent} from './common/grade-icon/grade-icon.component'; +import {GradeTaskModalService} from './tasks/modals/grade-task-modal/grade-task-modal.service'; +import {AuthenticationService} from './api/services/authentication.service'; +import {ProjectService} from './api/services/project.service'; +import {ObjectSelectComponent} from './common/obect-select/object-select.component'; +import {TaskDefinitionService} from './api/services/task-definition.service'; +import {EditProfileDialogService} from './common/modals/edit-profile-dialog/edit-profile-dialog.service'; +import {GroupService} from './api/services/group.service'; +import {UserBadgeComponent} from './common/user-badge/user-badge.component'; +import {TaskStatusCardComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-status-card/task-status-card.component'; +import {TaskDueCardComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-due-card/task-due-card.component'; +import {FooterComponent} from './common/footer/footer.component'; +import {TaskAssessmentCardComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-assessment-card/task-assessment-card.component'; +import {TaskSubmissionCardComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-submission-card/task-submission-card.component'; +import {InboxComponent} from './units/states/tasks/inbox/inbox.component'; +import {TaskDefinitionEditorComponent} from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-editor.component'; +import {UnitAnalyticsComponent} from './units/states/analytics/unit-analytics-route.component'; +import {UnitTaskEditorComponent} from './units/states/edit/directives/unit-tasks-editor/unit-task-editor.component'; +import {CreateNewUnitModal} from './admin/modals/create-new-unit-modal/create-new-unit-modal.component'; +import {FUsersComponent} from './admin/states/users/users.component'; +import {FUnitTaskListComponent} from './units/task-viewer/directives/unit-task-list/unit-task-list.component'; +import {FTaskDetailsViewComponent} from './units/task-viewer/directives/task-details-view/task-details-view.component'; +import {FTaskSheetViewComponent} from './units/task-viewer/directives/task-sheet-view/task-sheet-view.component'; +import {ProgressBurndownChartComponent} from './visualisations/progress-burndown-chart/progressburndownchart.component'; +import {TaskVisualisationComponent} from './visualisations/task-visualisation/taskvisualisation.component'; import {FUnitsComponent} from './admin/states/units/units.component'; import {AlertService} from './common/services/alert.service'; import {GradeService} from './common/services/grade.service'; import {TaskScormCardComponent} from './projects/states/dashboard/directives/task-dashboard/directives/task-scorm-card/task-scorm-card.component'; -import { UnitStudentEnrolmentModalService } from './units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.service'; -import { PrivacyPolicy } from './config/privacy-policy/privacy-policy'; +import {UnitStudentEnrolmentModalService} from './units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.service'; +import {PrivacyPolicy} from './config/privacy-policy/privacy-policy'; +import {OutcomeService} from './api/services/outcome.service'; export const DoubtfireAngularJSModule = angular.module('doubtfire', [ 'doubtfire.config', @@ -276,6 +276,7 @@ DoubtfireAngularJSModule.factory( 'newLearningOutcomeService', downgradeInjectable(LearningOutcomeService), ); +DoubtfireAngularJSModule.factory('outcomeService', downgradeInjectable(OutcomeService)); DoubtfireAngularJSModule.factory('emojiService', downgradeInjectable(EmojiService)); DoubtfireAngularJSModule.factory('gradeService', downgradeInjectable(GradeService)); DoubtfireAngularJSModule.factory( @@ -302,7 +303,10 @@ DoubtfireAngularJSModule.factory( ); DoubtfireAngularJSModule.factory('CreateNewUnitModal', downgradeInjectable(CreateNewUnitModal)); DoubtfireAngularJSModule.factory('GradeTaskModal', downgradeInjectable(GradeTaskModalService)); -DoubtfireAngularJSModule.factory('UnitStudentEnrolmentModal', downgradeInjectable(UnitStudentEnrolmentModalService)); +DoubtfireAngularJSModule.factory( + 'UnitStudentEnrolmentModal', + downgradeInjectable(UnitStudentEnrolmentModalService), +); DoubtfireAngularJSModule.factory('PrivacyPolicy', downgradeInjectable(PrivacyPolicy)); // directive -> component @@ -470,7 +474,10 @@ DoubtfireAngularJSModule.directive( ); DoubtfireAngularJSModule.directive('newFUnits', downgradeComponent({component: FUnitsComponent})); -DoubtfireAngularJSModule.directive('unauthorised', downgradeComponent({ component: UnauthorisedComponent })); +DoubtfireAngularJSModule.directive( + 'unauthorised', + downgradeComponent({component: UnauthorisedComponent}), +); // Global configuration @@ -485,13 +492,12 @@ const otherwiseConfigBlock = [ ]; DoubtfireAngularJSModule.config(otherwiseConfigBlock); - DoubtfireAngularJSModule.directive( 'fProgressBurndownChart', - downgradeComponent({ component: ProgressBurndownChartComponent }) + downgradeComponent({component: ProgressBurndownChartComponent}), ); DoubtfireAngularJSModule.directive( 'fTaskVisualisation', - downgradeComponent({ component: TaskVisualisationComponent }) + downgradeComponent({component: TaskVisualisationComponent}), ); From 441c33b74ee80c91239e6e72f26d28225df6de28 Mon Sep 17 00:00:00 2001 From: lachlan-robinson Date: Sun, 10 Aug 2025 10:09:07 +1000 Subject: [PATCH 04/11] chore: remove outcome-service.coffee --- .../common/services/outcome-service.coffee | 144 ------------------ src/app/common/services/services.coffee | 1 - 2 files changed, 145 deletions(-) delete mode 100644 src/app/common/services/outcome-service.coffee diff --git a/src/app/common/services/outcome-service.coffee b/src/app/common/services/outcome-service.coffee deleted file mode 100644 index e8c19ca445..0000000000 --- a/src/app/common/services/outcome-service.coffee +++ /dev/null @@ -1,144 +0,0 @@ -angular.module("doubtfire.common.services.outcome-service", []) - -# -# Services for handling Outcomes -# -.factory("outcomeService", (gradeService, newTaskService) -> - outcomeService = {} - - # outcomeService.unitTaskStatusFactor = -> - # (taskDefinitionId) -> 1 - - # outcomeService.projectTaskStatusFactor = (project) -> - # (taskDefinitionId) -> - # task = project.findTaskForDefinition(taskDefinitionId) - # if task? - # newTaskService.learningWeight.get(task.status) - # else - # 0 - - outcomeService.alignmentLabels = [ - "The task is not related to this outcome at all", - "The task is slightly related to this outcome", - "The task is related to this outcome", - "The task is a reasonable example for this outcome", - "The task is a strong example of this outcome", - "The task is the best example of this outcome", - ] - - outcomeService.individualTaskStatusFactor = (project, task) -> - (taskDefinitionId) -> - if task.definition.id == taskDefinitionId - newTaskService.learningWeight.get(project.findTaskForDefinition(taskDefinitionId).status) - else - 0 - - outcomeService.individualTaskPotentialFactor = (project, task) -> - (taskDefinitionId) -> - if task.definition.id == taskDefinitionId then 1 else 0 - - outcomeService.calculateTargets = (unit, source, taskStatusFactor) -> - outcomes = {} - # For each learning outcome (LO) -- produce a map with grades containing task scores - # calculated from alignment details, task target grade, and task status factor. - # - # The Task Status Factor for projects will be a value between 0 and 1 - # In the unit the taskStatusFactor will always be 1 (100%) to show potential values - # for the unit -- in effect removing the task status from unit calculations - _.each unit.ilos, (outcome) -> - # Add grade map for this LO to outcomes map - outcomes[outcome.id] = { - # Using 0..3 so that it can be used to calculate the grade scale below - 0: [] # Pass grade... -- will contain scores for pass grade tasks - 1: [] # Credit grade... -- etc. - 2: [] - 3: [] - } - - # For each outcome / task alignment... - _.each source.taskOutcomeAlignments, (align) -> - # Get the task definition - td = unit.taskDef(align.taskDefinition.id) - # Store a partial score for this task in the relevant outcomes ( outcomes[outcome id][grade] << score ) - # At this stage it is just rating * taskFactor (1 to 5 times 0 to 1) - outcomes[align.learningOutcome.id][td.targetGrade].push align.rating * taskStatusFactor(td) - - # Finally reduce all of these into one score for each outcome / grade - _.each outcomes, (outcome, key) -> - # For this outcome - _.each outcome, (tmp, key1) -> - # get a scale for the grade - scale = Math.pow(2, parseInt(key1,10)) - # Reduce all task partial scores and * grade scale -- replace array with single value - outcome[key1] = _.reduce(tmp, ((memo, num) -> memo + num), 0) * scale - - # Returns map of... - # { - # : { - # 0: - # 1: ... - # }, - # 86: { <--- OutcomeID 86 --> "Programming Principles" - # 0: 27 <--- Pass: 27 score (from rating * task status factor * scale reduced) - # 1: 53 ... - # }, - # } - outcomes - - outcomeService.calculateTaskContribution = (unit, project, task) -> - outcome_set = [] - outcome_set[0] = outcomeService.calculateTargets(unit, unit, outcomeService.individualTaskStatusFactor(project, task)) - - _.each outcome_set[0], (outcome, key) -> - outcome_set[0][key] = _.reduce(outcome, ((memo, num) -> memo + num), 0) - - outcome_set[0].title = 'Current Task Contribution' - outcome_set - - outcomeService.calculateTaskPotentialContribution = (unit, project, task) -> - outcomes = outcomeService.calculateTargets(unit, unit, outcomeService.individualTaskPotentialFactor(project, task)) - - _.each outcomes, (outcome, key) -> - outcomes[key] = _.reduce(outcome, ((memo, num) -> memo + num), 0) - - outcomes['title'] = 'Potential Task Contribution' - outcomes - - outcomeService.calculateProgress = (unit, project) -> - outcome_set = [] - - outcome_set[0] = outcomeService.calculateTargets(unit, unit, project.taskStatusFactor.bind(project)) - # outcome_set[1] = outcomeService.calculateTargets(unit, project, outcomeService.projectTaskStatusFactor(project)) - - _.each outcome_set, (outcomes, key) -> - _.each outcomes, (outcome, key) -> - outcomes[key] = _.reduce(outcome, ((memo, num) -> memo + num), 0) - - outcome_set[0].title = "Your Progress" # - Staff Suggestion" - # outcome_set[1].title = "Your Progress - Your Reflection" - - outcome_set - - - outcomeService.targetsByGrade = (unit, source) -> - result = [] - outcomes = outcomeService.calculateTargets(unit, source, unit.taskStatusFactor) - - values = { - '0': [] - '1': [] - '2': [] - '3': [] - } - - _.each outcomes, (outcome, key) -> - _.each outcome, (tmp, key1) -> - values[key1].push { label: $sce.getTrustedHtml(unit.outcome(parseInt(key,10)).abbreviation), value: tmp } - - _.each values, (vals, idx) -> - result.push { key: gradeService.grades[idx], values: vals } - - result - - outcomeService -) diff --git a/src/app/common/services/services.coffee b/src/app/common/services/services.coffee index e88e7a7bdc..589ec34110 100644 --- a/src/app/common/services/services.coffee +++ b/src/app/common/services/services.coffee @@ -1,5 +1,4 @@ angular.module("doubtfire.common.services", [ - 'doubtfire.common.services.outcome-service' 'doubtfire.common.services.analytics' 'doubtfire.common.services.dates' 'doubtfire.common.services.listener' From 8b275424212c5f8d020cf0fb79c9f6337ed30d6d Mon Sep 17 00:00:00 2001 From: lachlan-robinson Date: Wed, 10 Sep 2025 11:29:07 +1000 Subject: [PATCH 05/11] chore: create new component files --- .../achievement-custom-bar-chart.component.html | 1 + .../achievement-custom-bar-chart.component.scss | 0 .../achievement-custom-bar-chart.component.ts | 12 ++++++++++++ 3 files changed, 13 insertions(+) create mode 100644 src/app/visualisations/achievement-custom-bar-chart/achievement-custom-bar-chart/achievement-custom-bar-chart.component.html create mode 100644 src/app/visualisations/achievement-custom-bar-chart/achievement-custom-bar-chart/achievement-custom-bar-chart.component.scss create mode 100644 src/app/visualisations/achievement-custom-bar-chart/achievement-custom-bar-chart/achievement-custom-bar-chart.component.ts diff --git a/src/app/visualisations/achievement-custom-bar-chart/achievement-custom-bar-chart/achievement-custom-bar-chart.component.html b/src/app/visualisations/achievement-custom-bar-chart/achievement-custom-bar-chart/achievement-custom-bar-chart.component.html new file mode 100644 index 0000000000..27e5e868a4 --- /dev/null +++ b/src/app/visualisations/achievement-custom-bar-chart/achievement-custom-bar-chart/achievement-custom-bar-chart.component.html @@ -0,0 +1 @@ +

achievement-custom-bar-chart works!

diff --git a/src/app/visualisations/achievement-custom-bar-chart/achievement-custom-bar-chart/achievement-custom-bar-chart.component.scss b/src/app/visualisations/achievement-custom-bar-chart/achievement-custom-bar-chart/achievement-custom-bar-chart.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/visualisations/achievement-custom-bar-chart/achievement-custom-bar-chart/achievement-custom-bar-chart.component.ts b/src/app/visualisations/achievement-custom-bar-chart/achievement-custom-bar-chart/achievement-custom-bar-chart.component.ts new file mode 100644 index 0000000000..3073c0c068 --- /dev/null +++ b/src/app/visualisations/achievement-custom-bar-chart/achievement-custom-bar-chart/achievement-custom-bar-chart.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'f-achievement-custom-bar-chart', + standalone: true, + imports: [], + templateUrl: './achievement-custom-bar-chart.component.html', + styleUrl: './achievement-custom-bar-chart.component.css' +}) +export class AchievementCustomBarChartComponent { + +} From b8f82dcce90052222059d64f2c28082d8ef4186c Mon Sep 17 00:00:00 2001 From: lachlan-robinson Date: Wed, 10 Sep 2025 11:32:10 +1000 Subject: [PATCH 06/11] chore: update module to include new component reference --- src/app/doubtfire-angular.module.ts | 2 ++ src/app/doubtfire-angularjs.module.ts | 5 +++++ .../achievement-custom-bar-chart.component.ts | 10 +++------- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index 42199cce26..9db328d4c8 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -257,6 +257,7 @@ import {ScormExtensionModalComponent} from './common/modals/scorm-extension-moda import {GradeIconComponent} from './common/grade-icon/grade-icon.component'; import {GradeTaskModalComponent} from './tasks/modals/grade-task-modal/grade-task-modal.component'; import {PrivacyPolicy} from './config/privacy-policy/privacy-policy'; +import {AchievementCustomBarChartComponent} from './visualisations/achievement-custom-bar-chart/achievement-custom-bar-chart/achievement-custom-bar-chart.component'; // See https://stackoverflow.com/questions/55721254/how-to-change-mat-datepicker-date-format-to-dd-mm-yyyy-in-simplest-way/58189036#58189036 const MY_DATE_FORMAT = { @@ -276,6 +277,7 @@ import {OutcomeService} from './api/services/outcome.service'; @NgModule({ // Components we declare declarations: [ + AchievementCustomBarChartComponent, AlertComponent, UnitStudentEnrolmentModalComponent, AboutDoubtfireModalContent, diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index d962ee963c..45484326f6 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -224,6 +224,7 @@ import {TaskScormCardComponent} from './projects/states/dashboard/directives/tas import {UnitStudentEnrolmentModalService} from './units/modals/unit-student-enrolment-modal/unit-student-enrolment-modal.service'; import {PrivacyPolicy} from './config/privacy-policy/privacy-policy'; import {OutcomeService} from './api/services/outcome.service'; +import {AchievementCustomBarChartComponent} from './visualisations/achievement-custom-bar-chart/achievement-custom-bar-chart/achievement-custom-bar-chart.component'; export const DoubtfireAngularJSModule = angular.module('doubtfire', [ 'doubtfire.config', @@ -310,6 +311,10 @@ DoubtfireAngularJSModule.factory( DoubtfireAngularJSModule.factory('PrivacyPolicy', downgradeInjectable(PrivacyPolicy)); // directive -> component +DoubtfireAngularJSModule.directive( + 'fAchievementCustomBarChart', + downgradeComponent({component: AchievementCustomBarChartComponent}), +); DoubtfireAngularJSModule.directive( 'fProjectTasksList', downgradeComponent({component: ProjectTasksListComponent}), diff --git a/src/app/visualisations/achievement-custom-bar-chart/achievement-custom-bar-chart/achievement-custom-bar-chart.component.ts b/src/app/visualisations/achievement-custom-bar-chart/achievement-custom-bar-chart/achievement-custom-bar-chart.component.ts index 3073c0c068..f1d1becd62 100644 --- a/src/app/visualisations/achievement-custom-bar-chart/achievement-custom-bar-chart/achievement-custom-bar-chart.component.ts +++ b/src/app/visualisations/achievement-custom-bar-chart/achievement-custom-bar-chart/achievement-custom-bar-chart.component.ts @@ -1,12 +1,8 @@ -import { Component } from '@angular/core'; +import {Component} from '@angular/core'; @Component({ selector: 'f-achievement-custom-bar-chart', - standalone: true, - imports: [], templateUrl: './achievement-custom-bar-chart.component.html', - styleUrl: './achievement-custom-bar-chart.component.css' + styleUrl: './achievement-custom-bar-chart.component.scss', }) -export class AchievementCustomBarChartComponent { - -} +export class AchievementCustomBarChartComponent {} From a7a458b732614049ba68f7beddb6a49ee8116b78 Mon Sep 17 00:00:00 2001 From: lachlan-robinson Date: Wed, 10 Sep 2025 15:18:17 +1000 Subject: [PATCH 07/11] chore: include d3 modules for graph creation --- package.json | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index cc4a67fd36..315be768cc 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "build:angular18": "ng build", "lint:fix": "ng lint --fix", "lint": "ng lint", - "serve:angular18": "export NODE_OPTIONS=--max_old_space_size=4096 && ng serve --configuration $NODE_ENV --proxy-config proxy.conf.json", + "serve:angular18": "export NODE_OPTIONS=--max_old_space_size=4096 && ng serve --poll=2000 --configuration $NODE_ENV --proxy-config proxy.conf.json", "start": "npm-run-all -l -s build:angular1 -p watch:angular1 serve:angular18", "watch:angular1": "grunt delta", "deploy:build2api": "ng build --delete-output-path=true --optimization=true --configuration production --output-path dist", @@ -41,9 +41,9 @@ "@angular/upgrade": "^18.0", "@ctrl/ngx-emoji-mart": "^9.2.0", "@ngneat/hotkeys": "^4.0.0", + "@swimlane/ngx-charts": "^20.5.0", "@uirouter/angular": "^14.0", "@uirouter/angular-hybrid": "^18.0", - "@swimlane/ngx-charts": "^20.5.0", "@uirouter/angularjs": "^1.0.30", "@uirouter/core": "^6.1.0", "@uirouter/rx": "^1.0.0", @@ -70,6 +70,9 @@ "codemirror": "5.65.0", "core-js": "^3.21.1", "d3": "3.5.17", + "d3-axis": "^3.0.0", + "d3-scale": "^4.0.2", + "d3-selection": "^3.0.0", "date-fns": "^3.6.0", "es5-shim": "^4.5.12", "file-saver": "^2.0.5", @@ -82,7 +85,7 @@ "ng-csv": "0.2.3", "ng-file-upload": "~5.0.9", "ng-flex-layout": "^17.3.7-beta.1", - "ng2-pdf-viewer": "^10.2", + "ng2-pdf-viewer": "10.2.2", "ngx-bootstrap": "^6.1.0", "ngx-entity-service": "^0.0.39", "ngx-lottie": "^11.0.2", @@ -102,14 +105,17 @@ "@angular-eslint/eslint-plugin-template": "18.0.1", "@angular-eslint/schematics": "18.0.1", "@angular-eslint/template-parser": "18.0.1", - "@angular/compiler-cli": "^18.0.3", "@angular/cli": "^18.0.4", + "@angular/compiler-cli": "^18.0.3", "@angular/language-service": "^18.0.3", "@commitlint/cli": "^16.0.1", "@commitlint/config-conventional": "^17", "@types/angular": "1.5.11", "@types/canvas-confetti": "^1.6.0", "@types/d3": "^3.5.17", + "@types/d3-axis": "^3.0.6", + "@types/d3-scale": "^4.0.9", + "@types/d3-selection": "^3.0.11", "@types/file-saver": "^2.0.1", "@types/jasmine": "~3.9.1", "@types/jasminewd2": "~2.0.3", From e3aa2f51cfd793896622a6fb5f69f877191dcf4b Mon Sep 17 00:00:00 2001 From: lachlan-robinson Date: Wed, 10 Sep 2025 15:19:06 +1000 Subject: [PATCH 08/11] chore: implement new component functionality and styling --- ...chievement-custom-bar-chart.component.html | 11 +- ...chievement-custom-bar-chart.component.scss | 33 +++ .../achievement-custom-bar-chart.component.ts | 263 +++++++++++++++++- 3 files changed, 303 insertions(+), 4 deletions(-) diff --git a/src/app/visualisations/achievement-custom-bar-chart/achievement-custom-bar-chart/achievement-custom-bar-chart.component.html b/src/app/visualisations/achievement-custom-bar-chart/achievement-custom-bar-chart/achievement-custom-bar-chart.component.html index 27e5e868a4..7e91b7a953 100644 --- a/src/app/visualisations/achievement-custom-bar-chart/achievement-custom-bar-chart/achievement-custom-bar-chart.component.html +++ b/src/app/visualisations/achievement-custom-bar-chart/achievement-custom-bar-chart/achievement-custom-bar-chart.component.html @@ -1 +1,10 @@ -

achievement-custom-bar-chart works!

+
+ +
+
diff --git a/src/app/visualisations/achievement-custom-bar-chart/achievement-custom-bar-chart/achievement-custom-bar-chart.component.scss b/src/app/visualisations/achievement-custom-bar-chart/achievement-custom-bar-chart/achievement-custom-bar-chart.component.scss index e69de29bb2..e92197819e 100644 --- a/src/app/visualisations/achievement-custom-bar-chart/achievement-custom-bar-chart/achievement-custom-bar-chart.component.scss +++ b/src/app/visualisations/achievement-custom-bar-chart/achievement-custom-bar-chart/achievement-custom-bar-chart.component.scss @@ -0,0 +1,33 @@ +.achievement-chart-container { + position: relative; + width: 100%; + height: 600px; // or use 100vh for full viewport height +} +svg { + display: block; + width: 100%; + height: 100%; + z-index: 1; +} +.achievement-tooltip { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 8px; + position: absolute; + pointer-events: none; + background: #fff; + border: 1px solid #333; + border-radius: 4px; + padding: 8px; + font-size: 13px; + color: #222; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + z-index: 10; + min-width: 120px; + max-width: 220px; +} +.target-group rect { + transition: opacity 0.5s; +} diff --git a/src/app/visualisations/achievement-custom-bar-chart/achievement-custom-bar-chart/achievement-custom-bar-chart.component.ts b/src/app/visualisations/achievement-custom-bar-chart/achievement-custom-bar-chart/achievement-custom-bar-chart.component.ts index f1d1becd62..fbaa9bcf52 100644 --- a/src/app/visualisations/achievement-custom-bar-chart/achievement-custom-bar-chart/achievement-custom-bar-chart.component.ts +++ b/src/app/visualisations/achievement-custom-bar-chart/achievement-custom-bar-chart/achievement-custom-bar-chart.component.ts @@ -1,8 +1,265 @@ -import {Component} from '@angular/core'; +import { + Component, + Input, + OnChanges, + SimpleChanges, + AfterViewInit, + ElementRef, + NgZone, +} from '@angular/core'; +import {DomSanitizer} from '@angular/platform-browser'; +import {select, Selection} from 'd3-selection'; +import * as d3Scale from 'd3-scale'; +import * as d3Axis from 'd3-axis'; +import {OutcomeService} from 'src/app/api/services/outcome.service'; +import {GradeService} from 'src/app/common/services/grade.service'; +import {SafeHtml} from '@angular/platform-browser'; + +interface Target { + offset: number; + height: number; + color: string; +} + +interface AchievementValue { + label: string; + value: number; + targets: Target[]; +} @Component({ selector: 'f-achievement-custom-bar-chart', templateUrl: './achievement-custom-bar-chart.component.html', - styleUrl: './achievement-custom-bar-chart.component.scss', + styleUrls: ['./achievement-custom-bar-chart.component.scss'], }) -export class AchievementCustomBarChartComponent {} +export class AchievementCustomBarChartComponent implements OnChanges, AfterViewInit { + @Input() unit: any; + @Input() project: any; + @Input() showValues = false; + @Input() width = 960; + @Input() height = 600; + + tooltip = { + visible: false, + x: 0, + y: 0, + content: '' as SafeHtml, + }; + + private overlayHoverIndex: number | null = null; + private svg!: Selection; + private achievementData: AchievementValue[] = []; + private maxValue = 0; + + constructor( + private el: ElementRef, + private sanitizer: DomSanitizer, + private outcomeService: OutcomeService, + private gradeService: GradeService, + private ngZone: NgZone, + ) {} + + private resizeObserver?: ResizeObserver; + + ngAfterViewInit() { + this.updateChartSize(); + this.createSvg(); + this.updateDataAndRender(); + + // Responsive: update chart size and re-render on resize + this.resizeObserver = new ResizeObserver(() => { + this.updateChartSize(); + this.createSvg(); + this.updateDataAndRender(); + }); + const container = this.el.nativeElement.querySelector('.achievement-chart-container'); + if (container) this.resizeObserver.observe(container); + } + + ngOnDestroy() { + if (this.resizeObserver) this.resizeObserver.disconnect(); + } + + private updateChartSize() { + const container = this.el.nativeElement.querySelector('.achievement-chart-container'); + if (container) { + this.width = container.offsetWidth; + this.height = container.offsetHeight; + } + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.unit || changes.project) { + this.updateDataAndRender(); + } + } + + private createSvg() { + const svgElement = this.el.nativeElement.querySelector('svg') as SVGSVGElement; + this.svg = select(svgElement) + .attr('width', this.width) + .attr('height', this.height); + } + + private updateDataAndRender() { + if (!this.unit || !this.project) return; + const targets = this.outcomeService.calculateTargets( + this.unit, + this.unit, + this.unit.taskStatusFactor, + ); + const currentProgress = this.outcomeService.calculateProgress(this.unit, this.project); + this.achievementData = []; + this.maxValue = 0; + for (const ilo of this.unit.ilos) { + const iloTargets: Target[] = [ + {offset: 0, height: targets[ilo.id][0], color: this.gradeService.gradeColors.P}, + { + offset: targets[ilo.id][0], + height: targets[ilo.id][1], + color: this.gradeService.gradeColors.C, + }, + { + offset: targets[ilo.id][0] + targets[ilo.id][1], + height: targets[ilo.id][2], + color: this.gradeService.gradeColors.D, + }, + { + offset: targets[ilo.id][0] + targets[ilo.id][1] + targets[ilo.id][2], + height: targets[ilo.id][3], + color: this.gradeService.gradeColors.HD, + }, + ]; + const lastOffset = iloTargets[3].offset + iloTargets[3].height; + if (lastOffset > this.maxValue) this.maxValue = lastOffset; + this.achievementData.push({ + label: this.sanitizer.sanitize(1, ilo.name) || ilo.name, + value: currentProgress[0][ilo.id], + targets: iloTargets, + }); + } + this.renderChart(); + } + + private renderChart() { + if (!this.achievementData || this.achievementData.length === 0) return; + const margin = {top: 20, right: 20, bottom: 50, left: 60}; + const w = this.width - margin.left - margin.right; + const h = this.height - margin.top - margin.bottom; + this.svg.selectAll('*').remove(); + const chart = this.svg.append('g').attr('transform', `translate(${margin.left},${margin.top})`); + const x = d3Scale + .scaleBand() + .domain(this.achievementData.map((d) => d.label)) + .range([0, w]) + .padding(0.2); + const y = d3Scale.scaleLinear().domain([0, this.maxValue]).range([h, 0]); + + // Draw stacked target bars with tooltip + const groups = chart + .selectAll('g.target-group') + .data(this.achievementData) + .enter() + .append('g') + .attr('class', 'target-group') + .attr('transform', (d) => `translate(${x(d.label)},0)`); + + groups.each((d, i, nodes) => { + d.targets.forEach((t, idx) => { + select(nodes[i]) + .append('rect') + .attr('y', y(t.offset + t.height)) + .attr('height', y(t.offset) - y(t.offset + t.height)) + .attr('width', x.bandwidth()) + .attr('fill', t.color) + .attr('opacity', 0.2) + .style('pointer-events', 'all') + .on('mouseenter', (event) => { + const gradeLabels = ['Pass', 'Credit', 'Distinction', 'High Distinction']; + this.showTargetTooltip(event, gradeLabels[idx], t.color); + }) + .on('mousemove', (event) => { + const gradeLabels = ['Pass', 'Credit', 'Distinction', 'High Distinction']; + this.showTargetTooltip(event, gradeLabels[idx], t.color); + }) + .on('mouseleave', () => this.hideTooltip()); + }); + }); + + groups + .append('rect') + .attr('y', (d) => y(d.value)) + .attr('height', (d) => h - y(d.value)) + .attr('width', x.bandwidth() * 0.5) + .attr('fill', '#373737') + .attr('x', x.bandwidth() * 0.25) + .attr('opacity', 0.5) // always start at 0.5 + .style('transition', 'opacity 0.5s') + .style('pointer-events', 'all') + .on('mouseenter', (event, d) => { + select(event.target).attr('opacity', 1); + this.showProgressTooltip(event, d.label, '#373737'); + }) + .on('mousemove', (event, d) => { + this.showProgressTooltip(event, d.label, '#373737'); + }) + .on('mouseleave', (event, d) => { + select(event.target).attr('opacity', 0.5); + this.hideTooltip(); + }); + + // Optional value labels + if (this.showValues) { + groups + .append('text') + .text((d) => d.value) + .attr('x', x.bandwidth() / 2) + .attr('y', (d) => y(d.value) - 5) + .attr('text-anchor', 'middle') + .attr('font-size', '14px'); + } + chart + .append('g') + .attr('transform', `translate(0,${h})`) + .call(d3Axis.axisBottom(x)) + .selectAll('text') + .style('font-size', '16px'); + } + + showTargetTooltip(event: MouseEvent, grade: string, color: string) { + const containerRect = this.el.nativeElement + .querySelector('.achievement-chart-container') + .getBoundingClientRect(); + this.ngZone.run(() => { + // <-- wrap in NgZone.run + this.tooltip.content = this.sanitizer.bypassSecurityTrustHtml( + `${grade} task range`, + ); + this.tooltip.x = event.clientX - containerRect.left + 20; + this.tooltip.y = event.clientY - containerRect.top - 10; + this.tooltip.visible = true; + }); + } + + showProgressTooltip(event: MouseEvent, iloName: string, color: string) { + const containerRect = this.el.nativeElement + .querySelector('.achievement-chart-container') + .getBoundingClientRect(); + this.ngZone.run(() => { + // <-- wrap in NgZone.run + this.tooltip.content = this.sanitizer.bypassSecurityTrustHtml( + `Your progress with ${iloName}`, + ); + this.tooltip.x = event.clientX - containerRect.left + 20; + this.tooltip.y = event.clientY - containerRect.top - 10; + this.tooltip.visible = true; + }); + } + + hideTooltip() { + this.ngZone.run(() => { + // <-- wrap in NgZone.run + this.tooltip.visible = false; + }); + } +} From b8544dbc41235421100524e47d165175573ba326 Mon Sep 17 00:00:00 2001 From: lachlan-robinson Date: Wed, 10 Sep 2025 15:20:05 +1000 Subject: [PATCH 09/11] chore: remove template reference to old directive --- .../project-outcome-alignment.tpl.html | 57 +++++++++---------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/src/app/projects/project-outcome-alignment/project-outcome-alignment.tpl.html b/src/app/projects/project-outcome-alignment/project-outcome-alignment.tpl.html index 66de997bd9..9e8ae05e95 100644 --- a/src/app/projects/project-outcome-alignment/project-outcome-alignment.tpl.html +++ b/src/app/projects/project-outcome-alignment/project-outcome-alignment.tpl.html @@ -1,47 +1,44 @@ - - Outcome Achievement - + Outcome Achievement - - Outcome Alignment - - - + Outcome Alignment +
-

- Outcome Achievement -

-
-
- Overall progress on unit outcome are shown below +

Outcome Achievement

+
Overall progress on unit outcome are shown below
-
+ summary-only="true" + > + +
-

- Visualise Achievement -

-
-
- Your achievement with all ILOs are visualised below +

Visualise Achievement

- Your achievement with all ILOs are visualised below
+ + + + +
+ - -
-
- -
+ > + + From 2549c455a3f3e9d99a084c7ec84c80089259d82f Mon Sep 17 00:00:00 2001 From: lachlan-robinson Date: Wed, 10 Sep 2025 15:22:38 +1000 Subject: [PATCH 10/11] chore: remove existing directive files and references --- src/app/doubtfire-angularjs.module.ts | 1 - .../achievement-custom-bar-chart.coffee | 677 ------------------ src/app/visualisations/visualisations.coffee | 1 - 3 files changed, 679 deletions(-) delete mode 100644 src/app/visualisations/achievement-custom-bar-chart.coffee diff --git a/src/app/doubtfire-angularjs.module.ts b/src/app/doubtfire-angularjs.module.ts index 45484326f6..499fea1f10 100644 --- a/src/app/doubtfire-angularjs.module.ts +++ b/src/app/doubtfire-angularjs.module.ts @@ -39,7 +39,6 @@ import 'build/src/app/visualisations/task-status-pie-chart.js'; import 'build/src/app/visualisations/task-completion-box-plot.js'; import 'build/src/app/visualisations/visualisations.js'; import 'build/src/app/visualisations/alignment-bullet-chart.js'; -import 'build/src/app/visualisations/achievement-custom-bar-chart.js'; import 'build/src/app/visualisations/alignment-bar-chart.js'; import 'build/src/app/visualisations/achievement-box-plot.js'; import 'build/src/app/tasks/modals/upload-submission-modal/upload-submission-modal.js'; diff --git a/src/app/visualisations/achievement-custom-bar-chart.coffee b/src/app/visualisations/achievement-custom-bar-chart.coffee deleted file mode 100644 index 6eafad4c6d..0000000000 --- a/src/app/visualisations/achievement-custom-bar-chart.coffee +++ /dev/null @@ -1,677 +0,0 @@ -angular.module('doubtfire.visualisations.achievement-custom-bar-chart', []) -.directive 'achievementCustomBarChart', -> - replace: true - restrict: 'E' - templateUrl: 'visualisations/visualisation.tpl.html' - scope: - project: '=' - unit: '=' - - controller: ($scope, Visualisation, outcomeService, gradeService, $sce) -> - $scope.showLegend = if $scope.showLegend? then $scope.showLegend else true - unless nv.models.achievementBar? - nv.models.achievementBar = -> - chart = (selection) -> - renderWatch.reset() - selection.each (data) -> - availableWidth = width - (margin.left) - (margin.right) - availableHeight = height - (margin.top) - (margin.bottom) - container = d3.select(this) - nv.utils.initSVG container - #add series index to each data point for reference - data.forEach (series, i) -> - series.values.forEach (point) -> - point.series = i - return - return - # Setup Scales - # remap and flatten the data for use in calculating the scales' domains - seriesData = if xDomain and yDomain then [] else data.map(((d) -> - d.values.map ((d, i) -> - { - x: getX(d, i) - y: getY(d, i) - y0: d.y0 - } - ) - )) - x.domain(xDomain or d3.merge(seriesData).map((d) -> - d.x - )).rangeBands xRange or [ - 0 - availableWidth - ], .1 - y.domain yDomain or d3.extent(d3.merge(seriesData).map((d) -> - d.y - ).concat(forceY)) - # If showValues, pad the Y axis range to account for label height - if showValues - y.range yRange or [ - availableHeight - (if y.domain()[0] < 0 then 12 else 0) - if y.domain()[1] > 0 then 12 else 0 - ] - else - y.range yRange or [ - availableHeight - 0 - ] - #store old scales if they exist - x0 = x0 or x - y0 = y0 or y.copy().range([ - y(0) - y(0) - ]) - # Setup containers and skeleton of chart - wrap = container.selectAll('g.nv-wrap.nv-discretebar').data([ data ]) - wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-discretebar') - gEnter = wrapEnter.append('g') - g = wrap.select('g') - gEnter.append('g').attr 'class', 'nv-groups' - wrap.attr 'transform', 'translate(' + margin.left + ',' + margin.top + ')' - # TODO: (@macite) by definition, the discrete bar should not have multiple groups, will modify/remove later - groups = wrap.select('.nv-groups').selectAll('.nv-group').data(((d) -> - d - ), (d) -> - d.key - ) - groups.enter().append('g').style('stroke-opacity', 1e-6).style 'fill-opacity', 1e-6 - groups.exit().watchTransition(renderWatch, 'discreteBar: exit groups').style('stroke-opacity', 1e-6).style('fill-opacity', 1e-6).remove() - groups.attr('class', (d, i) -> - 'nv-group nv-series-' + i - ) - groups.watchTransition(renderWatch, 'discreteBar: groups').style('stroke-opacity', 1).style 'fill-opacity', .75 - - if targets? && _.size(targets) > 0 - # Create the background bars... match one set per ILO - backBarSeries = groups.selectAll('g.nv-backBarSeries').data((d) -> - d.values - ) - backBarSeries.exit().remove() - backBarSeries.enter().append('g') - backBarSeries.attr('class', (d, i) -> - 'nv-backBarSeries nv-backSeries-' + i - ).classed 'hover', (d) -> - d.hover - - backBars = backBarSeries.selectAll('g.nv-backBar').data( (d,i) -> _.map(_.values(d.targets), (v) -> { value: v, key: d.label } ) ) - backBars.exit().remove() - backBarsEnter = backBars.enter().append('g') - - backBarsEnter.append('rect').attr('height', 10).attr 'width', x.rangeBand() * 0.9 / data.length - backBarsEnter.on('mouseover', (d, i) -> - d3.select(this).classed 'hover', true - dispatch.elementMouseover - data: gradeService.grades[i] - index: i - color: d.value.color - return - ).on('mouseout', (d, i) -> - d3.select(this).classed 'hover', false - dispatch.elementMouseout - data: gradeService.grades[i] - index: i - color: d.value.color - return - ).on('mousemove', (d, i) -> - dispatch.elementMousemove - data: gradeService.grades[i] - index: i - color: d.value.color - return - ).on('click', (d, i) -> - element = this - dispatch.elementClick - data: gradeService.grades[i] - index: i - color: d.value.color - event: d3.event - element: element - d3.event.stopPropagation() - return - ).on('dblclick', (d, i) -> - dispatch.elementDblClick - data: gradeService.grades[i] - index: i - color: d.value.color - d3.event.stopPropagation() - return - ) - - backBars.attr('class', (d, i, j) -> - if d.value.height < 0 then 'nv-backBar negative' else 'nv-backBar positive' - ).select('rect').attr('class', rectClass).style('fill-opacity', '0.2'). - style('fill', (d) -> d.value.color). - watchTransition(renderWatch, 'discreteBar: backBars rect').attr 'width', x.rangeBand() * .9 / data.length - backBars.watchTransition(renderWatch, 'discreteBar: backBars').attr('transform', (d, i, j) -> - left = x(d.key) + x.rangeBand() * .05 - top = y(d.value.height + d.value.offset) - 'translate(' + left + ', ' + top + ')' - ).select('rect').attr 'height', (d, i, j) -> - Math.max Math.abs(y(d.value.height) - y(0)), 1 - - bars = groups.selectAll('g.nv-bar').data((d) -> - d.values - ) - bars.exit().remove() - barsEnter = bars.enter().append('g').on('mouseover', (d, i) -> - # TODO: (@macite) figure out why j works above, but not here - d3.select(this).classed 'hover', true - dispatch.elementMouseover - data: d - index: i - color: d3.select(this).style('fill') - return - ).on('mouseout', (d, i) -> - d3.select(this).classed 'hover', false - dispatch.elementMouseout - data: d - index: i - color: d3.select(this).style('fill') - return - ).on('mousemove', (d, i) -> - dispatch.elementMousemove - data: d - index: i - color: d3.select(this).style('fill') - return - ).on('click', (d, i) -> - element = this - dispatch.elementClick - data: d - index: i - color: d3.select(this).style('fill') - event: d3.event - element: element - d3.event.stopPropagation() - return - ).on('dblclick', (d, i) -> - dispatch.elementDblClick - data: d - index: i - color: d3.select(this).style('fill') - d3.event.stopPropagation() - return - ) - - barsEnter.append('rect').attr('height', 0).attr 'width', x.rangeBand() * .5 / data.length - if showValues - barsEnter.append('text').attr 'text-anchor', 'middle' - bars.select('text').text((d, i) -> - valueFormat getY(d, i) - ).watchTransition(renderWatch, 'discreteBar: bars text').attr('x', x.rangeBand() * .5 / 2).attr 'y', (d, i) -> - if getY(d, i) < 0 then y(getY(d, i)) - y(0) + 12 else -4 - else - bars.selectAll('text').remove() - bars.attr('class', (d, i) -> - if getY(d, i) < 0 then 'nv-bar negative' else 'nv-bar positive' - ).style('fill', (d, i) -> - d.color or color(d, i) - ).style('stroke', (d, i) -> - d.color or color(d, i) - ).select('rect').attr('class', rectClass).watchTransition(renderWatch, 'discreteBar: bars rect').attr 'width', x.rangeBand() * .5 / data.length - bars.watchTransition(renderWatch, 'discreteBar: bars').attr('transform', (d, i) -> - left = x(getX(d, i)) + x.rangeBand() * .25 - top = if getY(d, i) < 0 then y(0) else if y(0) - y(getY(d, i)) < 1 then y(0) - 1 else y(getY(d, i)) - 'translate(' + left + ', ' + top + ')' - ).select('rect').attr 'height', (d, i) -> - Math.max Math.abs(y(getY(d, i)) - y(0)), 1 - #store old scales for use in transitions on update - x0 = x.copy() - y0 = y.copy() - return - renderWatch.renderEnd 'achievementBar immediate' - chart - - 'use strict' - #============================================================ - # Public Variables with Default Settings - #------------------------------------------------------------ - margin = - top: 0 - right: 0 - bottom: 0 - left: 0 - width = 960 - height = 500 - id = Math.floor(Math.random() * 10000) - container = undefined - x = d3.scale.ordinal() - y = d3.scale.linear() - - getX = (d) -> - d.x - - getY = (d) -> - d.y - - forceY = [ 0 ] - color = nv.utils.defaultColor() - showValues = false - valueFormat = d3.format(',.2f') - xDomain = undefined - yDomain = undefined - xRange = undefined - yRange = undefined - dispatch = d3.dispatch('chartClick', 'elementClick', 'elementDblClick', 'elementMouseover', 'elementMouseout', 'elementMousemove', 'renderEnd') - rectClass = 'discreteBar' - duration = 250 - #============================================================ - # Private Variables - #------------------------------------------------------------ - x0 = undefined - y0 = undefined - renderWatch = nv.utils.renderWatch(dispatch, duration) - #============================================================ - # Expose Public Variables - #------------------------------------------------------------ - chart.dispatch = dispatch - chart.options = nv.utils.optionsFunc.bind(chart) - chart._options = Object.create({}, - width: - get: -> - width - set: (_) -> - width = _ - return - height: - get: -> - height - set: (_) -> - height = _ - return - forceY: - get: -> - forceY - set: (_) -> - forceY = _ - return - showValues: - get: -> - showValues - set: (_) -> - showValues = _ - return - x: - get: -> - getX - set: (_) -> - getX = _ - return - y: - get: -> - getY - set: (_) -> - getY = _ - return - xScale: - get: -> - x - set: (_) -> - x = _ - return - yScale: - get: -> - y - set: (_) -> - y = _ - return - xDomain: - get: -> - xDomain - set: (_) -> - xDomain = _ - return - yDomain: - get: -> - yDomain - set: (_) -> - yDomain = _ - return - xRange: - get: -> - xRange - set: (_) -> - xRange = _ - return - yRange: - get: -> - yRange - set: (_) -> - yRange = _ - return - valueFormat: - get: -> - valueFormat - set: (_) -> - valueFormat = _ - return - id: - get: -> - id - set: (_) -> - id = _ - return - rectClass: - get: -> - rectClass - set: (_) -> - rectClass = _ - return - margin: - get: -> - margin - set: (_) -> - margin.top = if _.top != undefined then _.top else margin.top - margin.right = if _.right != undefined then _.right else margin.right - margin.bottom = if _.bottom != undefined then _.bottom else margin.bottom - margin.left = if _.left != undefined then _.left else margin.left - return - color: - get: -> - color - set: (_) -> - color = nv.utils.getColor(_) - return - duration: - get: -> - duration - set: (_) -> - duration = _ - renderWatch.reset duration - return - ) - nv.utils.initOptions chart - chart - - # - # Chart that contains stacked bars in the background, overlaid by other bars... - # - nv.models.achievementBarChart = -> - chart = (selection) -> - renderWatch.reset() - renderWatch.models achievementbar - if showXAxis - renderWatch.models xAxis - if showYAxis - renderWatch.models yAxis - selection.each (data) -> - container = d3.select(this) - that = this - nv.utils.initSVG container - availableWidth = nv.utils.availableWidth(width, container, margin) - availableHeight = nv.utils.availableHeight(height, container, margin) - - chart.update = -> - dispatch.beforeUpdate() - container.transition().duration(duration).call chart - return - - chart.container = this - - # Display No Data message if there's nothing to show. - if (!data) or (!data.length) or data.filter(((d) -> d.values.length)).length <= 0 - nv.utils.noData chart, container - return chart - else - container.selectAll('.nv-noData').remove() - - # Setup Scales - x = achievementbar.xScale() - y = achievementbar.yScale().clamp(true) - # Setup containers and skeleton of chart - wrap = container.selectAll('g.nv-wrap.nv-discreteBarWithAxes').data([ data ]) - gEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-discreteBarWithAxes').append('g') - defsEnter = gEnter.append('defs') - g = wrap.select('g') - gEnter.append('g').attr 'class', 'nv-x nv-axis' - gEnter.append('g').attr('class', 'nv-y nv-axis').append('g').attr('class', 'nv-zeroLine').append 'line' - gEnter.append('g').attr 'class', 'nv-barsWrap' - gEnter.append('g').attr 'class', 'nv-legendWrap' - g.attr 'transform', 'translate(' + margin.left + ',' + margin.top + ')' - if showLegend - legend.width availableWidth - g.select('.nv-legendWrap').datum(data).call legend - if margin.top != legend.height() - margin.top = legend.height() - availableHeight = nv.utils.availableHeight(height, container, margin) - wrap.select('.nv-legendWrap').attr 'transform', 'translate(0,' + -margin.top + ')' - if rightAlignYAxis - g.select('.nv-y.nv-axis').attr 'transform', 'translate(' + availableWidth + ',0)' - if rightAlignYAxis - g.select('.nv-y.nv-axis').attr 'transform', 'translate(' + availableWidth + ',0)' - # Main Chart Component(s) - achievementbar.width(availableWidth).height availableHeight - barsWrap = g.select('.nv-barsWrap').datum(data.filter((d) -> - !d.disabled - )) - barsWrap.transition().call achievementbar - defsEnter.append('clipPath').attr('id', 'nv-x-label-clip-' + achievementbar.id()).append 'rect' - g.select('#nv-x-label-clip-' + achievementbar.id() + ' rect').attr('width', x.rangeBand() * (if staggerLabels then 2 else 1)).attr('height', 16).attr 'x', -x.rangeBand() / (if staggerLabels then 1 else 2) - # Setup Axes - if showXAxis - xAxis.scale(x)._ticks(nv.utils.calcTicksX(availableWidth / 100, data)).tickSize -availableHeight, 0 - g.select('.nv-x.nv-axis').attr 'transform', 'translate(0,' + (y.range()[0] + (if achievementbar.showValues() and y.domain()[0] < 0 then 16 else 0)) + ')' - g.select('.nv-x.nv-axis').call xAxis - xTicks = g.select('.nv-x.nv-axis').selectAll('g') - if staggerLabels - xTicks.selectAll('text').attr 'transform', (d, i, j) -> - 'translate(0,' + (if j % 2 == 0 then '5' else '17') + ')' - if rotateLabels - xTicks.selectAll('.tick text').attr('transform', 'rotate(' + rotateLabels + ' 0,0)').style 'text-anchor', if rotateLabels > 0 then 'start' else 'end' - if wrapLabels - g.selectAll('.tick text').call nv.utils.wrapTicks, chart.xAxis.rangeBand() - if showYAxis - yAxis.scale(y)._ticks(nv.utils.calcTicksY(availableHeight / 36, data)).tickSize -availableWidth, 0 - g.select('.nv-y.nv-axis').call yAxis - # Zero line - g.select('.nv-zeroLine line').attr('x1', 0).attr('x2', if rightAlignYAxis then -availableWidth else availableWidth).attr('y1', y(0)).attr 'y2', y(0) - return - renderWatch.renderEnd 'achievementBar chart immediate' - chart - - 'use strict' - #============================================================ - # Public Variables with Default Settings - #------------------------------------------------------------ - achievementbar = nv.models.achievementBar() - xAxis = nv.models.axis() - yAxis = nv.models.axis() - legend = nv.models.legend() - tooltip = nv.models.tooltip() - margin = - top: 15 - right: 10 - bottom: 50 - left: 60 - width = null - height = null - color = nv.utils.getColor() - showLegend = false - showXAxis = true - showYAxis = true - rightAlignYAxis = false - staggerLabels = false - wrapLabels = false - rotateLabels = 0 - x = undefined - y = undefined - noData = null - dispatch = d3.dispatch('beforeUpdate', 'renderEnd') - duration = 250 - xAxis.orient('bottom').showMaxMin(false).tickFormat (d) -> - d - yAxis.orient(if rightAlignYAxis then 'right' else 'left').tickFormat d3.format(',.1f') - tooltip.duration(0).headerEnabled(false).keyFormatter (d, i) -> - xAxis.tickFormat() d, i - #============================================================ - # Private Variables - #------------------------------------------------------------ - renderWatch = nv.utils.renderWatch(dispatch, duration) - #============================================================ - # Event Handling/Dispatching (out of chart's scope) - #------------------------------------------------------------ - achievementbar.dispatch.on 'elementMouseover.tooltip', (evt) -> - key = chart.x()(evt.data) - unless key? - key = "#{evt.data} task range" - else - key = "Your progress with #{key}" - evt['series'] = - key: key - # value: chart.y()(evt.data) - color: evt.color - tooltip.data(evt).hidden false - return - achievementbar.dispatch.on 'elementMouseout.tooltip', (evt) -> - tooltip.hidden true - return - achievementbar.dispatch.on 'elementMousemove.tooltip', (evt) -> - tooltip() - return - #============================================================ - # Expose Public Variables - #------------------------------------------------------------ - chart.dispatch = dispatch - chart.achievementbar = achievementbar - chart.legend = legend - chart.xAxis = xAxis - chart.yAxis = yAxis - chart.tooltip = tooltip - chart.options = nv.utils.optionsFunc.bind(chart) - chart._options = Object.create({}, - width: - get: -> - width - set: (_) -> - width = _ - return - height: - get: -> - height - set: (_) -> - height = _ - return - showLegend: - get: -> - showLegend - set: (_) -> - showLegend = _ - return - staggerLabels: - get: -> - staggerLabels - set: (_) -> - staggerLabels = _ - return - rotateLabels: - get: -> - rotateLabels - set: (_) -> - rotateLabels = _ - return - wrapLabels: - get: -> - wrapLabels - set: (_) -> - wrapLabels = ! !_ - return - showXAxis: - get: -> - showXAxis - set: (_) -> - showXAxis = _ - return - showYAxis: - get: -> - showYAxis - set: (_) -> - showYAxis = _ - return - noData: - get: -> - noData - set: (_) -> - noData = _ - return - margin: - get: -> - margin - set: (_) -> - margin.top = if _.top != undefined then _.top else margin.top - margin.right = if _.right != undefined then _.right else margin.right - margin.bottom = if _.bottom != undefined then _.bottom else margin.bottom - margin.left = if _.left != undefined then _.left else margin.left - return - duration: - get: -> - duration - set: (_) -> - duration = _ - renderWatch.reset duration - achievementbar.duration duration - xAxis.duration duration - yAxis.duration duration - return - color: - get: -> - color - set: (_) -> - color = nv.utils.getColor(_) - achievementbar.color color - legend.color color - return - rightAlignYAxis: - get: -> - rightAlignYAxis - set: (_) -> - rightAlignYAxis = _ - yAxis.orient if _ then 'right' else 'left' - return - ) - nv.utils.inheritOptions chart, achievementbar - nv.utils.initOptions chart - chart - - - # - # Get the data and options for the chart... - # - targets = outcomeService.calculateTargets($scope.unit, $scope.unit, $scope.unit.taskStatusFactor) - currentProgress = outcomeService.calculateProgress($scope.unit, $scope.project) - - achievementData = { - key: "Learning Achievement" - values: [] - } - - max = 0 - - _.each $scope.unit.ilos, (ilo) -> - iloTargets = { } - iloTargets[0] = { offset: 0, height: targets[ilo.id][0], color: gradeService.gradeColors.P } - iloTargets[1] = { offset: iloTargets[0].offset + iloTargets[0].height, height: targets[ilo.id][1], color: gradeService.gradeColors.C } - iloTargets[2] = { offset: iloTargets[1].offset + iloTargets[1].height, height: targets[ilo.id][2], color: gradeService.gradeColors.D } - iloTargets[3] = { offset: iloTargets[2].offset + iloTargets[2].height, height: targets[ilo.id][3], color: gradeService.gradeColors.HD } - - if iloTargets[3].offset + iloTargets[3].height > max - max = iloTargets[3].offset + iloTargets[3].height - - achievementData.values.push { - label: $sce.getTrustedHtml(ilo.name) - value: currentProgress[0][ilo.id] # 0 = staff value - targets: iloTargets - } - - [$scope.options, $scope.config] = Visualisation 'achievementBarChart', 'ILO Achievement Bar Chart', { - height: 600 - duration: 500 - yDomain: [0, max] - showValues: false - showYAxis: false - showLegend: false - x: (d) -> d.label - y: (d) -> d.value - color: (d) -> '#373737' - }, {} - - $scope.data = [ achievementData ] diff --git a/src/app/visualisations/visualisations.coffee b/src/app/visualisations/visualisations.coffee index f7882be92e..7bf61ea302 100644 --- a/src/app/visualisations/visualisations.coffee +++ b/src/app/visualisations/visualisations.coffee @@ -8,7 +8,6 @@ angular.module('doubtfire.visualisations', [ 'doubtfire.visualisations.target-grade-pie-chart' 'doubtfire.visualisations.task-completion-box-plot' 'doubtfire.visualisations.achievement-box-plot' - 'doubtfire.visualisations.achievement-custom-bar-chart' ]) .factory('Visualisation', ($interval, analyticsService) -> From 62f41b7019b953757b9a614fc031f2e1898ea750 Mon Sep 17 00:00:00 2001 From: lachlan-robinson Date: Mon, 15 Sep 2025 17:27:02 +1000 Subject: [PATCH 11/11] fix: add guard clause to this.svg to resolve selectAll error --- .../achievement-custom-bar-chart.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/visualisations/achievement-custom-bar-chart/achievement-custom-bar-chart/achievement-custom-bar-chart.component.ts b/src/app/visualisations/achievement-custom-bar-chart/achievement-custom-bar-chart/achievement-custom-bar-chart.component.ts index fbaa9bcf52..837a508cf5 100644 --- a/src/app/visualisations/achievement-custom-bar-chart/achievement-custom-bar-chart/achievement-custom-bar-chart.component.ts +++ b/src/app/visualisations/achievement-custom-bar-chart/achievement-custom-bar-chart/achievement-custom-bar-chart.component.ts @@ -142,6 +142,7 @@ export class AchievementCustomBarChartComponent implements OnChanges, AfterViewI } private renderChart() { + if (!this.svg) return; if (!this.achievementData || this.achievementData.length === 0) return; const margin = {top: 20, right: 20, bottom: 50, left: 60}; const w = this.width - margin.left - margin.right;