diff --git a/Epsilon.Abstractions/Epsilon.Abstractions.csproj b/Epsilon.Abstractions/Epsilon.Abstractions.csproj
index d46fbc80..d034d6a3 100644
--- a/Epsilon.Abstractions/Epsilon.Abstractions.csproj
+++ b/Epsilon.Abstractions/Epsilon.Abstractions.csproj
@@ -23,5 +23,4 @@
-
diff --git a/Epsilon.Abstractions/Model/CompetenceOutcomeResult.cs b/Epsilon.Abstractions/Model/CompetenceOutcomeResult.cs
new file mode 100644
index 00000000..7d6e8cc5
--- /dev/null
+++ b/Epsilon.Abstractions/Model/CompetenceOutcomeResult.cs
@@ -0,0 +1,7 @@
+namespace Epsilon.Abstractions.Model;
+
+public record CompetenceOutcomeResult(
+ int OutcomeId,
+ double Grade,
+ DateTime SubmittedAt
+);
\ No newline at end of file
diff --git a/Epsilon.Abstractions/Model/DecayingAverage.cs b/Epsilon.Abstractions/Model/DecayingAverage.cs
new file mode 100644
index 00000000..0de50cae
--- /dev/null
+++ b/Epsilon.Abstractions/Model/DecayingAverage.cs
@@ -0,0 +1,7 @@
+namespace Epsilon.Abstractions.Model;
+
+public record DecayingAverage(
+ double Score,
+ int ArchitectureLayer,
+ int Activity
+);
\ No newline at end of file
diff --git a/Epsilon.Abstractions/Model/DecayingAveragePerActivity.cs b/Epsilon.Abstractions/Model/DecayingAveragePerActivity.cs
deleted file mode 100644
index 0c7c7645..00000000
--- a/Epsilon.Abstractions/Model/DecayingAveragePerActivity.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace Epsilon.Abstractions.Model;
-
-public record DecayingAveragePerActivity(
- int Activity,
- double DecayingAverage
-);
\ No newline at end of file
diff --git a/Epsilon.Abstractions/Model/DecayingAveragePerLayer.cs b/Epsilon.Abstractions/Model/DecayingAveragePerLayer.cs
deleted file mode 100644
index 7c3d648e..00000000
--- a/Epsilon.Abstractions/Model/DecayingAveragePerLayer.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace Epsilon.Abstractions.Model;
-
-public record DecayingAveragePerLayer(
- int ArchitectureLayer,
- IEnumerable LayerActivities
-);
\ No newline at end of file
diff --git a/Epsilon.Abstractions/Model/DecayingAveragePerSkill.cs b/Epsilon.Abstractions/Model/DecayingAveragePerSkill.cs
deleted file mode 100644
index 90eef853..00000000
--- a/Epsilon.Abstractions/Model/DecayingAveragePerSkill.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace Epsilon.Abstractions.Model;
-
-public record DecayingAveragePerSkill(
- int Skill,
- double DecayingAverage
-);
\ No newline at end of file
diff --git a/Epsilon.Abstractions/Model/ProfessionalSkillResult.cs b/Epsilon.Abstractions/Model/ProfessionalSkillResult.cs
index 8cd48934..505b4f3b 100644
--- a/Epsilon.Abstractions/Model/ProfessionalSkillResult.cs
+++ b/Epsilon.Abstractions/Model/ProfessionalSkillResult.cs
@@ -1,8 +1,9 @@
namespace Epsilon.Abstractions.Model;
public record ProfessionalSkillResult(
+ int OutcomeId,
int Skill,
int MasteryLevel,
double Grade,
DateTime AssessedAt
-);
\ No newline at end of file
+) : CompetenceOutcomeResult(OutcomeId, Grade, AssessedAt);
\ No newline at end of file
diff --git a/Epsilon.Abstractions/Model/ProfessionalTaskResult.cs b/Epsilon.Abstractions/Model/ProfessionalTaskResult.cs
index 55998383..a83e9c83 100644
--- a/Epsilon.Abstractions/Model/ProfessionalTaskResult.cs
+++ b/Epsilon.Abstractions/Model/ProfessionalTaskResult.cs
@@ -1,9 +1,10 @@
namespace Epsilon.Abstractions.Model;
public record ProfessionalTaskResult(
+ int OutcomeId,
int ArchitectureLayer,
int Activity,
int MasteryLevel,
double Grade,
DateTime AssessedAt
-);
\ No newline at end of file
+) : CompetenceOutcomeResult(OutcomeId, Grade, AssessedAt);
\ No newline at end of file
diff --git a/Epsilon.Host.Frontend/src/components/CompetenceGraph.vue b/Epsilon.Host.Frontend/src/components/CompetenceGraph.vue
index 176fac24..44185abb 100644
--- a/Epsilon.Host.Frontend/src/components/CompetenceGraph.vue
+++ b/Epsilon.Host.Frontend/src/components/CompetenceGraph.vue
@@ -1,18 +1,22 @@
+ :series="series"
+ height="350"
+ type="bar"
+ width="750" />
diff --git a/Epsilon.Host.Frontend/src/components/PersonalDevelopmentGraph.vue b/Epsilon.Host.Frontend/src/components/PersonalDevelopmentGraph.vue
index 61b89581..6190ea02 100644
--- a/Epsilon.Host.Frontend/src/components/PersonalDevelopmentGraph.vue
+++ b/Epsilon.Host.Frontend/src/components/PersonalDevelopmentGraph.vue
@@ -1,23 +1,28 @@
+ :series="series"
+ height="350"
+ type="bar"
+ width="200" />
diff --git a/Epsilon.Host.Frontend/src/logic/DecayingAverageLogic.ts b/Epsilon.Host.Frontend/src/logic/DecayingAverageLogic.ts
new file mode 100644
index 00000000..186262b9
--- /dev/null
+++ b/Epsilon.Host.Frontend/src/logic/DecayingAverageLogic.ts
@@ -0,0 +1,199 @@
+import {
+ IHboIDomain,
+ ProfessionalSkillResult,
+ ProfessionalTaskResult,
+} from "/@/logic/Api"
+
+export interface DecayingAveragePerActivity {
+ outcome: number
+ activity: number
+ decayingAverage: number
+}
+
+export interface DecayingAveragePerLayer {
+ architectureLayer: number
+ layerActivities: DecayingAveragePerActivity[]
+}
+
+export interface DecayingAveragePerSkill {
+ skill: number
+ decayingAverage: number
+ masteryLevel: number
+}
+
+export class DecayingAverageLogic {
+ /**
+ * Calculate the averages for each skill type
+ * @param taskResults
+ * @param domain
+ * @constructor
+ */
+ public static getAverageSkillOutcomeScores(
+ taskResults: ProfessionalSkillResult[],
+ domain: IHboIDomain
+ ): DecayingAveragePerSkill[] {
+ const listOfResults = Object.entries(
+ this.groupBy(taskResults, (r) => r.outcomeId as unknown as string)
+ ).map(([, j]) => {
+ return {
+ decayingAverage: this.getDecayingAverageFromOneOutcomeType(j),
+ skill: j.at(0)?.skill,
+ masteryLevel: j
+ .sort((a) => a.masteryLevel as never as number)
+ .at(0)?.masteryLevel,
+ } as DecayingAveragePerSkill
+ })
+
+ return domain.professionalSkills?.map((s) => {
+ let score = 0.0
+ const filteredResults = listOfResults.filter(
+ (r) => r.skill === s.id
+ )
+ filteredResults.map((result) => {
+ score += result.decayingAverage
+ })
+ return {
+ decayingAverage: score / filteredResults.length,
+ skill: s.id,
+ masteryLevel: filteredResults
+ .sort((a) => a.masteryLevel as never as number)
+ .at(0)?.masteryLevel,
+ } as DecayingAveragePerSkill
+ }) as DecayingAveragePerSkill[]
+ }
+
+ /**
+ * Calculate the averages for each task type divided in architecture layers.
+ * @param taskResults
+ * @param domain
+ * @constructor
+ */
+ public static getAverageTaskOutcomeScores(
+ taskResults: ProfessionalTaskResult[],
+ domain: IHboIDomain
+ ): DecayingAveragePerLayer[] {
+ const canvasDecaying = this.getDecayingAverageForAllOutcomes(
+ taskResults,
+ domain
+ )
+ return domain.architectureLayers?.map((layer) => {
+ return {
+ architectureLayer: layer.id,
+ layerActivities: domain.activities?.map((activity) => {
+ let totalScoreActivity = 0
+ let totalScoreArchitectureActivity = 0
+ let amountOfActivities = 0
+
+ //Calculate the total score from activity
+ canvasDecaying.map((l) =>
+ l.layerActivities
+ ?.filter((la) => la.activity === activity.id)
+ .map(
+ (la) =>
+ (totalScoreActivity +=
+ la.decayingAverage &&
+ amountOfActivities++)
+ )
+ )
+
+ //Calculate the total score from activity inside this architecture layer
+ canvasDecaying
+ .filter((l) => l.architectureLayer === layer.id)
+ .map((l) =>
+ l.layerActivities
+ ?.filter((la) => la.activity === activity.id)
+ .map(
+ (la) =>
+ (totalScoreArchitectureActivity +=
+ la.decayingAverage)
+ )
+ )
+
+ return {
+ activity: activity.id,
+ decayingAverage:
+ ((totalScoreActivity / amountOfActivities) *
+ totalScoreArchitectureActivity) /
+ totalScoreActivity,
+ } as DecayingAveragePerActivity
+ }),
+ }
+ }) as DecayingAveragePerLayer[]
+ }
+
+ /**
+ * Calculate average of given tasks divided in architectural layers
+ * @param taskResults
+ * @param domain
+ * @constructor
+ * @private
+ */
+ private static getDecayingAverageForAllOutcomes(
+ taskResults: ProfessionalTaskResult[],
+ domain: IHboIDomain
+ ): DecayingAveragePerLayer[] {
+ return domain.architectureLayers?.map((l) => {
+ return {
+ architectureLayer: l.id,
+ layerActivities: Object.entries(
+ this.groupBy(
+ //Ensure that given results are only relined on the architecture that is currently being used.
+ taskResults.filter(
+ (layer) => layer.architectureLayer === l.id
+ ),
+ (r) => r.outcomeId as unknown as string
+ )
+ ).map(([i, j]) => {
+ //From all selected outcomes calculate the decaying average, Give outcome id and activity layer.
+ return {
+ outcome: i,
+ activity: j.at(0)?.activity,
+ decayingAverage:
+ this.getDecayingAverageFromOneOutcomeType(j),
+ }
+ }) as unknown as DecayingAveragePerActivity[],
+ }
+ }) as DecayingAveragePerLayer[]
+ }
+
+ /**
+ * Calculate decaying average described by Canvas: https://community.canvaslms.com/t5/Canvas-Basics-Guide/What-are-Outcomes/ta-p/75#decaying_average
+ * !IMPORTANT, The list of results will always have to be a list of the same outcome id. Not a list of equal activity and architecture layer.
+ * @param results
+ * @constructor
+ * @private
+ */
+ private static getDecayingAverageFromOneOutcomeType(
+ results: ProfessionalTaskResult[] | ProfessionalSkillResult[]
+ ): number {
+ let totalGradeScore = 0.0
+
+ const recentResult = results.reverse().pop()
+ if (recentResult && recentResult.grade) {
+ if (results.length > 0) {
+ results.forEach(
+ (r) => (totalGradeScore += r.grade ? r.grade : 0)
+ )
+ totalGradeScore =
+ (totalGradeScore / results.length) * 0.35 +
+ recentResult.grade * 0.65
+ } else {
+ totalGradeScore = recentResult.grade
+ }
+ }
+
+ return totalGradeScore
+ }
+
+ private static groupBy(
+ arr: T[],
+ fn: (item: T) => number | string
+ ): Record {
+ return arr.reduce>((prev, curr) => {
+ const groupKey = fn(curr)
+ const group = prev[groupKey] || []
+ group.push(curr)
+ return { ...prev, [groupKey]: group }
+ }, {})
+ }
+}
diff --git a/Epsilon.Host.Frontend/src/views/PerformanceDashboard.vue b/Epsilon.Host.Frontend/src/views/PerformanceDashboard.vue
index 3e5989d3..e487b248 100644
--- a/Epsilon.Host.Frontend/src/views/PerformanceDashboard.vue
+++ b/Epsilon.Host.Frontend/src/views/PerformanceDashboard.vue
@@ -4,16 +4,16 @@
:terms="data.terms"
@on-term-selected="setTermFilter" />
+ :data="filteredProfessionalTaskOutcomes"
+ :domain="data.hboIDomain" />
-
+ :data="filteredProfessionalTaskOutcomes"
+ :domain="data.hboIDomain" />
+
@@ -21,17 +21,17 @@