From 70695276b625fd91f0aab284a9f51f207f2862de Mon Sep 17 00:00:00 2001 From: Steven Dalamaras Date: Sat, 25 Apr 2026 19:39:32 +1000 Subject: [PATCH 1/7] feat(ai_eff): rename weighting to estimated hours --- src/app/api/models/project.ts | 10 +++++----- src/app/api/models/task-definition.ts | 2 +- src/app/api/services/task-definition.service.ts | 2 +- .../task-definition-general.component.html | 4 ++-- .../unit-tasks-editor/unit-task-editor.component.ts | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/app/api/models/project.ts b/src/app/api/models/project.ts index b799ed01d6..e39e53ff5b 100644 --- a/src/app/api/models/project.ts +++ b/src/app/api/models/project.ts @@ -404,7 +404,7 @@ export class Project extends Entity { const targetTasks = this.unit.taskDefinitionsForGrade(this.targetGrade); // get total value of all tasks assigned to this project - const total = targetTasks.map((td) => td.weighting).reduce((prev, current, idx, array) => prev + current, 0); + const total = targetTasks.map((td) => td.estimated_hours).reduce((prev, current, idx, array) => prev + current, 0); // exit if no tasks or no weights if (targetTasks.length === 0 || total === 0) { @@ -444,7 +444,7 @@ export class Project extends Entity { const weeksElapsed = MappingFunctions.weeksBetween(this.unit.startDate, today); if (weeksElapsed > 0) { const completedTasksWeight = readyOrCompleteTasks - .map((t) => t.definition.weighting) + .map((t) => t.definition.estimated_hours) .reduce((prev, current, idx, arr) => prev + current, 0); completionRate = completedTasksWeight / weeksElapsed; } @@ -465,7 +465,7 @@ export class Project extends Entity { date.getTime(), (targetTasks .filter((taskDef) => taskDef.targetDate >= date) - .map((td) => td.weighting) + .map((td) => td.estimated_hours) .reduce((prev, current) => prev + current, 0) || 0) / total, ]; @@ -475,7 +475,7 @@ export class Project extends Entity { (total - doneTasks .filter((task) => task.submissionDate && task.submissionDate <= date) - .map((task) => task.definition.weighting) + .map((task) => task.definition.estimated_hours) .reduce((prev, current) => prev + current, 0)) / total, ]; @@ -486,7 +486,7 @@ export class Project extends Entity { (total - completedTasks .filter((task) => task.completionDate <= date) - .map((task) => task.definition.weighting) + .map((task) => task.definition.estimated_hours) .reduce((prev, current) => prev + current, 0)) / total, ]; diff --git a/src/app/api/models/task-definition.ts b/src/app/api/models/task-definition.ts index c8f88d1e9e..5ef8671c19 100644 --- a/src/app/api/models/task-definition.ts +++ b/src/app/api/models/task-definition.ts @@ -27,7 +27,7 @@ export class TaskDefinition extends Entity { abbreviation: string; name: string; description: string; - weighting: number; + estimated_hours: number; targetGrade: number; targetDate: Date; dueDate: Date; diff --git a/src/app/api/services/task-definition.service.ts b/src/app/api/services/task-definition.service.ts index d420f2c1fc..4824c68f89 100644 --- a/src/app/api/services/task-definition.service.ts +++ b/src/app/api/services/task-definition.service.ts @@ -34,7 +34,7 @@ export class TaskDefinitionService extends CachedEntityService { 'abbreviation', 'name', 'description', - 'weighting', + 'estimated_hours', 'targetGrade', 'similarityLanguage', 'hasJplagReport', diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-general/task-definition-general.component.html b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-general/task-definition-general.component.html index 5ec2551e03..382ca9943a 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-general/task-definition-general.component.html +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-general/task-definition-general.component.html @@ -14,8 +14,8 @@ - Weight - Effort relative to other tasks. - + Estimated hours required for completion. + diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/unit-task-editor.component.ts b/src/app/units/states/edit/directives/unit-tasks-editor/unit-task-editor.component.ts index f40f563a7c..5c7640a44d 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/unit-task-editor.component.ts +++ b/src/app/units/states/edit/directives/unit-tasks-editor/unit-task-editor.component.ts @@ -278,7 +278,7 @@ export class UnitTaskEditorComponent implements AfterViewInit { task.startDate = new Date(); task.targetDate = addWeeks(new Date(), 2); task.uploadRequirements = []; - task.weighting = 4; + task.estimated_hours = 4; task.targetGrade = 0; task.restrictStatusUpdates = false; task.plagiarismWarnPct = 80; From 3032ef257045ea311471e3a911dad51a62d2477c Mon Sep 17 00:00:00 2001 From: Steven Dalamaras Date: Sun, 26 Apr 2026 20:57:29 +1000 Subject: [PATCH 2/7] feat(ai_eff): add rudimentary front end wirings --- src/app/api/models/task-definition.ts | 4 +++ .../api/services/task-definition.service.ts | 7 ++++ src/app/doubtfire-angular.module.ts | 2 ++ .../task-definition-editor.component.html | 14 ++++++++ .../task-definition-editor.component.ts | 2 ++ .../task-definition-effort.component.html | 32 ++++++++++++++++++ .../task-definition-effort.component.scss | 0 .../task-definition-effort.component.ts | 33 +++++++++++++++++++ 8 files changed, 94 insertions(+) create mode 100644 src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-effort/task-definition-effort.component.html create mode 100644 src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-effort/task-definition-effort.component.scss create mode 100644 src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-effort/task-definition-effort.component.ts diff --git a/src/app/api/models/task-definition.ts b/src/app/api/models/task-definition.ts index 5ef8671c19..42f4d804a8 100644 --- a/src/app/api/models/task-definition.ts +++ b/src/app/api/models/task-definition.ts @@ -28,6 +28,7 @@ export class TaskDefinition extends Entity { name: string; description: string; estimated_hours: number; + predicted_effort: number; targetGrade: number; targetDate: Date; dueDate: Date; @@ -330,4 +331,7 @@ export class TaskDefinition extends Entity { public projectTask(project?: Project): Task | undefined { return project?.tasks?.find((p) => p.definition.id === this.id); } + public get predictEffortUrl(): string { + return `${AppInjector.get(DoubtfireConstants).API_URL}/units/${this.unit.id}/task_definitions/${this.id}/predict_effort`; + } } diff --git a/src/app/api/services/task-definition.service.ts b/src/app/api/services/task-definition.service.ts index 4824c68f89..a673eac2f2 100644 --- a/src/app/api/services/task-definition.service.ts +++ b/src/app/api/services/task-definition.service.ts @@ -35,6 +35,7 @@ export class TaskDefinitionService extends CachedEntityService { 'name', 'description', 'estimated_hours', + 'predicted_effort', 'targetGrade', 'similarityLanguage', 'hasJplagReport', @@ -273,4 +274,10 @@ export class TaskDefinitionService extends CachedEntityService { const httpClient = AppInjector.get(HttpClient); return httpClient.get(url); } + + public predictEffort(taskDefinition: TaskDefinition): Observable { + const http = AppInjector.get(HttpClient); + + return http.post(taskDefinition.predictEffortUrl, {}); + } } diff --git a/src/app/doubtfire-angular.module.ts b/src/app/doubtfire-angular.module.ts index ae030bcc3e..0669758ac2 100644 --- a/src/app/doubtfire-angular.module.ts +++ b/src/app/doubtfire-angular.module.ts @@ -323,6 +323,7 @@ import {TutorNotesModalComponent} from './common/modals/tutor-notes-modal/tutor- import {FeedbackAppealModalComponent} from './tasks/modals/feedback-appeal-modal/feedback-appeal-modal.component'; import {ConfirmModerationModalComponent} from './units/states/tasks/inbox/directives/moderation/confirm-moderation-modal/confirm-moderation-modal.component'; import {TaskClaimComponent} from './units/states/tasks/inbox/directives/task-claim/task-claim.component'; +import { TaskDefinitionEffortComponent } from './units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-effort/task-definition-effort.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 = { @@ -407,6 +408,7 @@ const GANTT_CHART_CONFIG = { TaskDefinitionResourcesComponent, TaskDefinitionOverseerComponent, TaskDefinitionScormComponent, + TaskDefinitionEffortComponent, UnitAnalyticsComponent, StudentTutorialSelectComponent, StudentCampusSelectComponent, diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-editor.component.html b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-editor.component.html index 567758a0ad..84c7fc8a7b 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-editor.component.html +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-editor.component.html @@ -111,6 +111,20 @@

Prerequisite Tasks

+
+

Effort Prediction

+

+ Use a regression model to determine the effort required for a task based on task definition values +

+ + +
+ +
+
+ Enable effort prediction +
+ + +
+ +
+ + +
+ + Predicted Effort + + + +
diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-effort/task-definition-effort.component.scss b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-effort/task-definition-effort.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-effort/task-definition-effort.component.ts b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-effort/task-definition-effort.component.ts new file mode 100644 index 0000000000..c74b5006a4 --- /dev/null +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-effort/task-definition-effort.component.ts @@ -0,0 +1,33 @@ +import { Component, Input } from '@angular/core'; +import { TaskDefinition } from 'src/app/api/models/task-definition'; +import { Unit } from 'src/app/api/models/unit'; +import { TaskDefinitionService } from 'src/app/api/services/task-definition.service'; + +@Component({ + selector: 'f-task-definition-effort', + templateUrl: 'task-definition-effort.component.html', + styleUrls: ['task-definition-effort.component.scss'], +}) +export class TaskDefinitionEffortComponent { + @Input() taskDefinition: TaskDefinition; + @Input() staffView: boolean; + constructor(private TaskDefinitionService: TaskDefinitionService) {} + enablePrediction = true; + + public get unit(): Unit { + return this.taskDefinition?.unit; + } + + runPrediction() { + if (!this.taskDefinition || !this.taskDefinition.id) return; + + this.TaskDefinitionService.predictEffort(this.taskDefinition).subscribe({ + next: () => { + console.log('Prediction queued'); + }, + error: (err) => { + console.error('Prediction failed', err); + }, + }); + } +} From e536c923fe98ec69cfd774c4b4bd4a34d6b4dce7 Mon Sep 17 00:00:00 2001 From: "josh.talev" Date: Tue, 5 May 2026 21:58:04 +1000 Subject: [PATCH 3/7] add allow effort prediction toggle to unit details --- src/app/api/models/unit.ts | 2 ++ src/app/api/services/unit.service.ts | 2 ++ .../unit-details-editor/unit-details-editor.component.html | 5 +++++ 3 files changed, 9 insertions(+) diff --git a/src/app/api/models/unit.ts b/src/app/api/models/unit.ts index e9f48f02f9..61f0e4180e 100644 --- a/src/app/api/models/unit.ts +++ b/src/app/api/models/unit.ts @@ -81,6 +81,8 @@ export class Unit extends Entity { d2lMapping: D2lAssessmentMapping; + allowEffortPredictions: boolean; + public readonly learningOutcomesCache: EntityCache = new EntityCache(); public readonly tutorialStreamsCache: EntityCache = diff --git a/src/app/api/services/unit.service.ts b/src/app/api/services/unit.service.ts index 1d6462727d..5493ef1874 100644 --- a/src/app/api/services/unit.service.ts +++ b/src/app/api/services/unit.service.ts @@ -257,6 +257,7 @@ export class UnitService extends CachedEntityService { // 'groupMemberships', - map to group memberships 'feedbackWarningThresholdDays', 'feedbackOverflowThresholdDays', + 'allowEffortPredictions', ); this.mapping.addJsonKey( @@ -288,6 +289,7 @@ export class UnitService extends CachedEntityService { 'allowStudentChangeTutorial', 'feedbackWarningThresholdDays', 'feedbackOverflowThresholdDays', + 'allowEffortPredictions', ); } diff --git a/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.component.html b/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.component.html index 073c69ce2a..21967465e2 100644 --- a/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.component.html +++ b/src/app/units/states/edit/directives/unit-details-editor/unit-details-editor.component.html @@ -201,6 +201,11 @@

Unit Details

Active

Set to false to hide unit from students and tutors.

+ +
+ Allow AI Effort Prediction +

Set to false to disable running AI effort predictions on tasks belonging to this unit.

+
@if (overseerEnabled.value) { From 0b729f7df25d7e7d054dfb7f816314526b222505 Mon Sep 17 00:00:00 2001 From: "josh.talev" Date: Wed, 6 May 2026 00:33:09 +1000 Subject: [PATCH 4/7] adds polling on prediction run with UI progress notification --- src/app/api/services/sidekiq-job.service.ts | 2 +- .../task-definition-effort.component.html | 77 ++++++++---- .../task-definition-effort.component.ts | 111 ++++++++++++++++-- 3 files changed, 155 insertions(+), 35 deletions(-) diff --git a/src/app/api/services/sidekiq-job.service.ts b/src/app/api/services/sidekiq-job.service.ts index dd5332a258..c8d2b87ece 100644 --- a/src/app/api/services/sidekiq-job.service.ts +++ b/src/app/api/services/sidekiq-job.service.ts @@ -13,7 +13,7 @@ export interface SidekiqJobEntry { @Injectable() export class SidekiqJobService extends CachedEntityService { - protected readonly endpointFormat = 'sidekiq/:id:'; + protected readonly endpointFormat = 'sidekiq/:id'; public jobEntries: Map = new Map(); diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-effort/task-definition-effort.component.html b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-effort/task-definition-effort.component.html index 27309063da..35cf11ae29 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-effort/task-definition-effort.component.html +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-effort/task-definition-effort.component.html @@ -1,32 +1,57 @@ -
+
Enable effort prediction
- -
- -
+
+
+ +
+ +
+ + + + + + {{ predictionStatus === 'queued' ? 'Queued...' : + predictionStatus === 'working' ? 'Running prediction...' : + 'Processing...' }} + + + + ✔ Prediction complete + + + + ✖ Prediction failed + +
+ + + +
+ + Predicted Effort - -
- - Predicted Effort - - - + + +
diff --git a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-effort/task-definition-effort.component.ts b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-effort/task-definition-effort.component.ts index c74b5006a4..c0742bf4a6 100644 --- a/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-effort/task-definition-effort.component.ts +++ b/src/app/units/states/edit/directives/unit-tasks-editor/task-definition-editor/task-definition-effort/task-definition-effort.component.ts @@ -1,33 +1,128 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, OnDestroy } from '@angular/core'; import { TaskDefinition } from 'src/app/api/models/task-definition'; import { Unit } from 'src/app/api/models/unit'; +import { SidekiqJobService } from 'src/app/api/services/sidekiq-job.service'; import { TaskDefinitionService } from 'src/app/api/services/task-definition.service'; +import { Subject, interval, switchMap, takeWhile, Subscription } from 'rxjs'; @Component({ selector: 'f-task-definition-effort', templateUrl: 'task-definition-effort.component.html', styleUrls: ['task-definition-effort.component.scss'], }) -export class TaskDefinitionEffortComponent { +export class TaskDefinitionEffortComponent implements OnDestroy { @Input() taskDefinition: TaskDefinition; @Input() staffView: boolean; - constructor(private TaskDefinitionService: TaskDefinitionService) {} + + constructor( + private TaskDefinitionService: TaskDefinitionService, + private SidekiqJobService: SidekiqJobService, + ) {} + enablePrediction = true; + isPredicting = false; + predictionStatus: 'queued' | 'working' | 'retrying' | 'complete' | 'stopped' | 'failed' | 'interrupted'; + jobId: string | null = null; + + private pollSub?: Subscription; + public get unit(): Unit { return this.taskDefinition?.unit; } runPrediction() { - if (!this.taskDefinition || !this.taskDefinition.id) return; + if (!this.taskDefinition?.id) return; + + this.isPredicting = true; + this.predictionStatus = 'queued'; this.TaskDefinitionService.predictEffort(this.taskDefinition).subscribe({ - next: () => { - console.log('Prediction queued'); + next: (res: any) => { + this.predictionStatus = 'working'; + const jobId = res.job_id; + this.jobId = jobId; + + const resultSubject = new Subject(); + + this.SidekiqJobService.setJob( + jobId, + 'Predicting effort', + resultSubject + ); + + this.pollJob(jobId, resultSubject); + }, + error: () => { + this.predictionStatus = 'failed'; + this.isPredicting = false; }, - error: (err) => { - console.error('Prediction failed', err); + }); + } + + pollJob(jobId: string, resultSubject: Subject) { + this.pollSub = interval(2000).pipe( + switchMap(() => this.SidekiqJobService.getSidekiqJob(jobId)), + takeWhile(job => job.status !== 'complete' && job.status !== 'failed', true) + ).subscribe({ + next: (job) => { + this.predictionStatus = (job.status || 'working').toLowerCase() as any; + + this.SidekiqJobService.setJob( + jobId, + 'Predicting effort', + resultSubject, + job + ); + + if (job.status === 'complete') { + let result: any; + + try { + result = typeof job.result === 'string' + ? JSON.parse(job.result) + : job.result; + } catch { + result = null; + } + + this.taskDefinition.predicted_effort = Number(result?.predicted_effort ?? 0); + + this.predictionStatus = 'complete'; + this.stopPolling(); + } + + if (job.status === 'failed') { + this.predictionStatus = 'failed'; + console.error('Job failed:', job.message || job.result); + this.stopPolling(); + } }, + error: () => { + this.predictionStatus = 'failed'; + this.cleanup(jobId); + } }); } + + stopPolling() { + if (this.pollSub) { + this.pollSub.unsubscribe(); + } + this.isPredicting = false; + } + + cleanup(jobId: string) { + this.isPredicting = false; + if (this.pollSub) { + this.pollSub.unsubscribe(); + } + this.SidekiqJobService.removeJob(jobId); + } + + ngOnDestroy() { + if (this.pollSub) { + this.pollSub.unsubscribe(); + } + } } From 954a7966de7ad074052ee7983ddfec34cd2e4b86 Mon Sep 17 00:00:00 2001 From: Steven Dalamaras Date: Mon, 11 May 2026 19:26:02 +1000 Subject: [PATCH 5/7] fix(ai_eff): revert a change that broke sidekiq jobs --- src/app/api/services/sidekiq-job.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/api/services/sidekiq-job.service.ts b/src/app/api/services/sidekiq-job.service.ts index c8d2b87ece..dd5332a258 100644 --- a/src/app/api/services/sidekiq-job.service.ts +++ b/src/app/api/services/sidekiq-job.service.ts @@ -13,7 +13,7 @@ export interface SidekiqJobEntry { @Injectable() export class SidekiqJobService extends CachedEntityService { - protected readonly endpointFormat = 'sidekiq/:id'; + protected readonly endpointFormat = 'sidekiq/:id:'; public jobEntries: Map = new Map(); From 8bd197de420cf11efbe20a71ec4c2fffa86150f3 Mon Sep 17 00:00:00 2001 From: Steven Dalamaras Date: Sun, 17 May 2026 14:42:31 +1000 Subject: [PATCH 6/7] feat(ai_eff): change burndown to use predicted effort --- src/app/api/models/project.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/api/models/project.ts b/src/app/api/models/project.ts index e39e53ff5b..a5c488ea23 100644 --- a/src/app/api/models/project.ts +++ b/src/app/api/models/project.ts @@ -404,7 +404,7 @@ export class Project extends Entity { const targetTasks = this.unit.taskDefinitionsForGrade(this.targetGrade); // get total value of all tasks assigned to this project - const total = targetTasks.map((td) => td.estimated_hours).reduce((prev, current, idx, array) => prev + current, 0); + const total = targetTasks.map((td) => td.predicted_effort).reduce((prev, current, idx, array) => prev + current, 0); // exit if no tasks or no weights if (targetTasks.length === 0 || total === 0) { @@ -444,7 +444,7 @@ export class Project extends Entity { const weeksElapsed = MappingFunctions.weeksBetween(this.unit.startDate, today); if (weeksElapsed > 0) { const completedTasksWeight = readyOrCompleteTasks - .map((t) => t.definition.estimated_hours) + .map((t) => t.definition.predicted_effort) .reduce((prev, current, idx, arr) => prev + current, 0); completionRate = completedTasksWeight / weeksElapsed; } @@ -465,7 +465,7 @@ export class Project extends Entity { date.getTime(), (targetTasks .filter((taskDef) => taskDef.targetDate >= date) - .map((td) => td.estimated_hours) + .map((td) => td.predicted_effort) .reduce((prev, current) => prev + current, 0) || 0) / total, ]; @@ -475,7 +475,7 @@ export class Project extends Entity { (total - doneTasks .filter((task) => task.submissionDate && task.submissionDate <= date) - .map((task) => task.definition.estimated_hours) + .map((task) => task.definition.predicted_effort) .reduce((prev, current) => prev + current, 0)) / total, ]; @@ -486,7 +486,7 @@ export class Project extends Entity { (total - completedTasks .filter((task) => task.completionDate <= date) - .map((task) => task.definition.estimated_hours) + .map((task) => task.definition.predicted_effort) .reduce((prev, current) => prev + current, 0)) / total, ]; From bea099966c5eae48615a18002770f59d390ba7f5 Mon Sep 17 00:00:00 2001 From: Steven Dalamaras Date: Sun, 17 May 2026 15:43:50 +1000 Subject: [PATCH 7/7] docs(ai_eff): change visualisation axis label to 'effort' --- src/app/visualisations/progress-burndown-chart.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/visualisations/progress-burndown-chart.coffee b/src/app/visualisations/progress-burndown-chart.coffee index c9cf4bc620..dd4d47d5e2 100644 --- a/src/app/visualisations/progress-burndown-chart.coffee +++ b/src/app/visualisations/progress-burndown-chart.coffee @@ -96,7 +96,7 @@ angular.module('doubtfire.visualisations.progress-burndown-chart', []) tickFormat: xAxisTickFormatDateFormat ticks: 8 yAxis: - axisLabel: "Tasks Remaining" + axisLabel: "Effort Remaining" tickFormat: yAxisTickFormatPercentFormat color: colorFunction legendColor: colorFunction