Skip to content

Commit

Permalink
Refactor piechart to use generics
Browse files Browse the repository at this point in the history
  • Loading branch information
20041540MichaelKelly committed Aug 1, 2024
1 parent 1678214 commit 87e1009
Show file tree
Hide file tree
Showing 7 changed files with 332 additions and 91 deletions.
122 changes: 122 additions & 0 deletions src/lib/ui/time/supabase/analytics/piechart/base-pie-chart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import * as echarts from "echarts/core";
import { TooltipComponent, LegendComponent, GridComponent } from "echarts/components";
import { CanvasRenderer } from "echarts/renderers";
import { PieChart, BarChart } from "echarts/charts";
import { LabelLayout } from "echarts/features";
import type { Course, Lo } from "$lib/services/models/lo-types";
import { backgroundPattern, textureBackground } from "../../charts/tutors-charts-background-url";
import type { Session } from "@supabase/supabase-js";
import type { DrilledDownData } from "$lib/services/types/supabase-metrics";

echarts.use([TooltipComponent, LegendComponent, PieChart, BarChart, GridComponent, CanvasRenderer, LabelLayout]);

const bgTexture = textureBackground;
const bgPatternSrc = backgroundPattern;

const piePatternImg = new Image();
piePatternImg.src = bgTexture;
const bgPatternImg = new Image();
bgPatternImg.src = bgPatternSrc;

export class BasePieChart<T> {
myChart: any;
course: Course;
session: Session;
totalTimesMap: Map<string, DrilledDownData[]>;
titleTimesMap: Map<string, T>;

constructor(course: Course, session: Session) {
this.myChart = null;
this.course = course;
this.session = session;
this.titleTimesMap = new Map();
this.totalTimesMap = new Map();
}

initChart() {
if (!this.myChart) {
// Create a new chart instance if it doesn't exist
this.myChart = echarts.init(document.getElementById("chart"));
} else {
// Clear the previous chart to prevent aggregation issues
this.myChart.clear();
}
}

handlePieClick() {
if (this.myChart !== null) {
// Remove any existing click listeners to prevent multiple handlers
this.myChart.off("click");
this.myChart.on("click", (params: { seriesName: string; name: string }) => {
if (params.seriesName === "Inner Pie") {
const outerPieData: DrilledDownData[] = []; // Reset outerPieData array

// Find the corresponding data for the clicked inner pie slice
this.totalTimesMap.forEach((steps, title) => {
if (title === params.name) {
steps.forEach((step) => {
if (step.value !== 0) {
outerPieData.push({ value: step.value, name: step.name, type: step.type });
}
});
}
});
this.populateOuterPieData(outerPieData);
}
});
}
}

populateOuterPieData(outerPieData: DrilledDownData[]) {
// Update the data for the outer pie chart
const element = document.getElementById("chart");
if (element) {
const chartInstance = echarts.getInstanceByDom(element);
if (chartInstance) {
chartInstance.setOption({
series: [
{
name: "Outer Pie",
data: outerPieData
}
]
});
}
}
}

updateMaps(lo: Lo, timeActive: number, getTitle: (lo: Lo) => string) {
const title = getTitle(lo);
const loTitle = lo.title;
// Add timeActive to the total time for the title
if (this.titleTimesMap.has(title)) {
this.titleTimesMap.set(title, ((this.titleTimesMap.get(title)! as number) + timeActive) as unknown as T);
} else {
this.titleTimesMap.set(title, timeActive as unknown as T);
}

if (!this.totalTimesMap.has(title)) {
this.totalTimesMap.set(title, []);
}

const existingEntries = this.totalTimesMap.get(title)!;
const existingEntry = existingEntries.find((entry) => entry.name === loTitle);

if (existingEntry) {
existingEntry.value += timeActive;
} else {
existingEntries.push({ value: timeActive, name: loTitle, type: lo.type });
}
}

renderChart() {
this.initChart();
this.handlePieClick();
}

setOption(option: any) {
if (this.myChart) {
this.myChart.setOption(option);
}
}
}
68 changes: 68 additions & 0 deletions src/lib/ui/time/supabase/analytics/piechart/lab-pie-chart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import * as echarts from "echarts/core";
import { TooltipComponent, LegendComponent, GridComponent } from "echarts/components";
import { CanvasRenderer } from "echarts/renderers";
import { PieChart, BarChart } from "echarts/charts";
import { LabelLayout } from "echarts/features";
import type { Course } from "$lib/services/models/lo-types";
import { backgroundPattern, textureBackground } from "../../charts/tutors-charts-background-url";
import type { Session } from "@supabase/supabase-js";
import { filterByType } from "$lib/services/models/lo-utils";
import { piechart } from "../../charts/piechart";
import type { DrilledDownData } from "$lib/services/types/supabase-metrics";
import { BasePieChart } from "./base-pie-chart";
echarts.use([TooltipComponent, LegendComponent, PieChart, BarChart, GridComponent, CanvasRenderer, LabelLayout]);

const bgTexture = textureBackground;
const bgPatternSrc = backgroundPattern;

const piePatternImg = new Image();
piePatternImg.src = bgTexture;
const bgPatternImg = new Image();
bgPatternImg.src = bgPatternSrc;

export class LabPieChart extends BasePieChart<number> {
labs: string[];

constructor(course: Course, session: Session) {
super(course, session);
this.labs = [];
}

renderChart() {
super.renderChart(); // Initialize and set up click handlers

this.titleTimesMap.clear();
this.totalTimesMap.clear();

let labs = filterByType(this.course.los, "lab");
let steps = filterByType(this.course.los, "step");

const allLabSteps = [...labs, ...steps];

allLabSteps.forEach((lo) => {
const timeActive = lo.learningRecords?.get(this.session.user.user_metadata.user_name)?.timeActive || 0;
this.updateMaps(lo, timeActive, (lo) => (lo.type === "lab" ? lo.title : lo.parentLo!.title));
});

const singleUserInnerData = Array.from(this.titleTimesMap.entries()).map(([title, timeActive]) => ({
name: title,
value: timeActive
}));

const singleUserOuterData: DrilledDownData[] = [];
this.totalTimesMap.forEach((steps, title) => {
steps.forEach((step) => {
if (step.type !== undefined) {
singleUserOuterData.push({
name: step.name,
value: step.value,
type: step.type
});
}
});
});

const option = piechart(bgPatternImg, [], singleUserInnerData, singleUserOuterData);
super.setOption(option);
}
}
88 changes: 88 additions & 0 deletions src/lib/ui/time/supabase/analytics/piechart/topic-pie-chart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import * as echarts from "echarts/core";
import { TooltipComponent, LegendComponent, GridComponent } from "echarts/components";
import { CanvasRenderer } from "echarts/renderers";
import { PieChart, BarChart } from "echarts/charts";
import { LabelLayout } from "echarts/features";
import type { Course, Lo } from "$lib/services/models/lo-types";
import { backgroundPattern, textureBackground } from "../../charts/tutors-charts-background-url";
import type { Session } from "@supabase/supabase-js";
import type { DrilledDownData } from "$lib/services/types/supabase-metrics";
import { BasePieChart } from "./base-pie-chart";
import { piechart } from "../../charts/piechart";
import { getCompositeValues, getSimpleTypesValues } from "$lib/services/utils/supabase-utils";


echarts.use([TooltipComponent, LegendComponent, PieChart, BarChart, GridComponent, CanvasRenderer, LabelLayout]);

const bgTexture = textureBackground;
const bgPatternSrc = backgroundPattern;

const piePatternImg = new Image();
piePatternImg.src = bgTexture;
const bgPatternImg = new Image();
bgPatternImg.src = bgPatternSrc;

export class TopicPieChart extends BasePieChart<number> {
topics: string[];
userIds: string[];
multipleUsers: boolean;

constructor(course: Course, session: Session, userIds: string[], multipleUsers: boolean) {
super(course, session);
this.topics = [];
this.userIds = userIds;
this.multipleUsers = multipleUsers;
}

getOuterPieDataForMultipleUsers(): DrilledDownData[] {
const outerPieData: { value: number; name: string }[] = [];
this.titleTimesMap.forEach((value, key) => {
const existing = outerPieData.find((data) => data.name === key);
if (existing) {
existing.value += value;
} else {
outerPieData.push({ value, name: key });
}
});

return outerPieData;
}

renderChart() {
super.renderChart(); // Initialize and set up click handlers

const allComposites = getCompositeValues(this.course.los);
const allSimpleTypes = getSimpleTypesValues(this.course.los);
const allTypes = [...allComposites, ...allSimpleTypes];

allTypes.forEach((lo) => {
if (this.userIds && this.userIds.length > 0) {
this.userIds.forEach((userId) => {
const timeActive = lo.learningRecords?.get(userId)?.timeActive || 0;
this.updateMaps(lo, timeActive, (lo) =>
lo.parentTopic?.type === "topic" ? lo.parentTopic.title : lo.parentLo?.parentTopic?.type === "topic" ? lo.parentLo?.parentTopic?.title : lo.title
);
});
} else {
const timeActive = lo.learningRecords?.get(this.session.user.user_metadata.user_name)?.timeActive || 0;
this.updateMaps(lo, timeActive, (lo) =>
lo.parentTopic?.type === "topic" ? lo.parentTopic.title : lo.parentLo?.parentTopic?.type === "topic" ? lo.parentLo?.parentTopic?.title : lo.title
);
}
});

if (this.multipleUsers === false) {
const singleUserInnerData = Array.from(this.titleTimesMap.entries()).map(([title, timeActive]) => ({
name: title,
value: timeActive
}));

const option = piechart(bgPatternImg, [], singleUserInnerData, []);
super.setOption(option);
} else {
const allUsersTopicActivity = this.getOuterPieDataForMultipleUsers();
const option = piechart(bgPatternImg, allUsersTopicActivity, [], []);
super.setOption(option);
}
}
}
42 changes: 42 additions & 0 deletions src/lib/ui/time/supabase/views/Chart.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<script lang="ts">
import { onMount, onDestroy } from "svelte";
import type { Course } from "$lib/services/models/lo-types";
import { LabPieChart } from "../analytics/piechart/lab-pie-chart";
import { TopicPieChart } from "../analytics/piechart/topic-pie-chart";
import type { Session } from "@supabase/supabase-js";
export let chartType: 'LabPieChart' | 'TopicPieChart';
export let course: Course;
export let session: Session;
export let userIds: string[] = [];
export let multipleUsers: boolean = false;
let chartInstance: LabPieChart | TopicPieChart | null = null;
onMount(() => {
if (chartType === 'LabPieChart') {
chartInstance = new LabPieChart(course, session);
} else if (chartType === 'TopicPieChart') {
chartInstance = new TopicPieChart(course, session, userIds, multipleUsers);
}
renderChart();
});
const renderChart = () => {
if (chartInstance) {
chartInstance.renderChart();
}
};
onDestroy(() => {
if (chartInstance) {
chartInstance = null;
}
});
window.addEventListener("focus", renderChart);
</script>

<div class="h-screen">
<div id="chart" style="height: 100%; width: 100%;"></div>
</div>
32 changes: 4 additions & 28 deletions src/lib/ui/time/supabase/views/InstructorTopicViewPieChart.svelte
Original file line number Diff line number Diff line change
@@ -1,36 +1,12 @@
<script lang="ts">
import { onMount, onDestroy } from "svelte";
import type { Course, Topic } from "$lib/services/models/lo-types";
import { TopicPieChart } from "../analytics/topic-pie";
import type { Session } from "@supabase/supabase-js";
import type { Course } from "$lib/services/models/lo-types";
import type { Session } from "@supabase/supabase-js";
import Chart from './Chart.svelte';
export let course: Course;
export let session: Session;
export let userIds: string[];
const multipleUsers = true;
let topicPieChart: TopicPieChart | null;
onMount(() => {
topicPieChart = new TopicPieChart(course, session, userIds, multipleUsers);
renderChart();
});
const renderChart = () => {
if (topicPieChart) {
topicPieChart.renderChart(userIds);
}
};
onDestroy(() => {
if (topicPieChart) {
topicPieChart = null;
}
});
window.addEventListener("focus", renderChart);
</script>

<div class="h-screen">
<div id={"chart"} style="height: 100%; width:100%"></div>
</div>
<Chart chartType="TopicPieChart" {course} {session} {userIds} {multipleUsers} />
Loading

0 comments on commit 87e1009

Please sign in to comment.