Skip to content

Commit

Permalink
enhance(apps/analytics): add illustrations for asynchronous activity …
Browse files Browse the repository at this point in the history
…progress on performance dashboard (#4396)
  • Loading branch information
sjschlapbach authored Dec 13, 2024
1 parent 69cc23b commit 1310b0c
Show file tree
Hide file tree
Showing 20 changed files with 736 additions and 43 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { H1, UserNotification } from '@uzh-bf/design-system'
import { useTranslations } from 'next-intl'
import React from 'react'
import Layout from '~/components/Layout'

function AnalyticsErrorView({
title,
navigation,
}: {
title: string
navigation: React.ReactNode
}) {
const t = useTranslations()

return (
<Layout displayName={title}>
{navigation}
<H1>{title}</H1>
<UserNotification
message={t('manage.analytics.analyticsLoadingFailed')}
type="error"
className={{ root: 'mx-auto my-auto w-max max-w-full text-base' }}
/>
</Layout>
)
}

export default AnalyticsErrorView
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Loader from '@klicker-uzh/shared-components/src/Loader'
import { useTranslations } from 'next-intl'
import Layout from '~/components/Layout'

function AnalyticsLoadingView({
title,
navigation,
}: {
title: string
navigation: React.ReactNode
}) {
const t = useTranslations()

return (
<Layout displayName={title}>
{navigation}
<div className="flex h-full w-full flex-row items-center justify-center gap-4 text-lg">
{t('manage.analytics.analyticsLoadingWait')}
<Loader basic />
</div>
</Layout>
)
}

export default AnalyticsLoadingView
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { ActivityProgress, ActivityType } from '@klicker-uzh/graphql/dist/ops'
import { H2, H4 } from '@uzh-bf/design-system'
import { useTranslations } from 'next-intl'
import { Legend } from 'recharts'
import StackedProgress from './StackedProgress'

function ActivityProgressPlot({
activityProgresses,
participants,
}: {
activityProgresses: ActivityProgress[]
participants: number
}) {
const t = useTranslations()
const pqProgresses = activityProgresses.filter(
(progress) => progress.activityType === ActivityType.PracticeQuiz
)
const mlProgresses = activityProgresses.filter(
(progress) => progress.activityType === ActivityType.MicroLearning
)

const chartColors = {
started: '#4ade80',
completed: '#15803d',
repeated: '#064e3b',
}

return (
<div className="border-uzh-grey-80 rounded-xl border border-solid p-3">
<div className="relative">
<H2>{t('manage.analytics.asynchronousActivityProgress')}</H2>
<Legend
payload={[
{
value: t('manage.analytics.started'),
color: chartColors.started,
type: 'rect',
},
{
value: t('manage.analytics.completed'),
color: chartColors.completed,
type: 'rect',
},
{
value: t('manage.analytics.repeated'),
color: chartColors.repeated,
type: 'rect',
},
]}
wrapperStyle={{ bottom: 0, right: 0 }}
/>
</div>
<div className="flex flex-col gap-6">
{pqProgresses.length > 0 && (
<div>
<H4>{t('shared.generic.practiceQuizzes')}</H4>
<div className="max-h-[13rem] overflow-y-scroll">
{pqProgresses.map((progress, idx) => (
<StackedProgress
key={`activity-progress-pq-${idx}`}
progress={progress}
participants={participants}
colors={chartColors}
/>
))}
</div>
</div>
)}
{mlProgresses.length > 0 && (
<div>
<H4>{t('shared.generic.microlearnings')}</H4>
<div className="max-h-[13rem] overflow-y-scroll">
{mlProgresses.map((progress, idx) => (
<StackedProgress
key={`activity-progress-ml-${idx}`}
progress={progress}
participants={participants}
colors={chartColors}
/>
))}
</div>
</div>
)}
</div>
</div>
)
}

export default ActivityProgressPlot
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import ActivityDashboardLabel from '../overview/ActivityDashboardLabel'
import AnalyticsNavigation from '../overview/AnalyticsNavigation'
import QuizDashboardLabel from '../overview/QuizDashboardLabel'

function PerformanceAnalyticsNavigation({ courseId }: { courseId: string }) {
return (
<AnalyticsNavigation
hrefLeft={`/analytics/${courseId}/activity`}
labelLeft={<ActivityDashboardLabel />}
hrefRight={`/analytics/${courseId}/quizzes`}
labelRight={<QuizDashboardLabel />}
/>
)
}

export default PerformanceAnalyticsNavigation
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { ActivityProgress } from '@klicker-uzh/graphql/dist/ops'
import { useTranslations } from 'next-intl'
import {
Bar,
BarChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from 'recharts'

function StackedProgress({
progress,
participants,
colors,
}: {
progress: ActivityProgress
participants: number
colors: {
started: string
completed: string
repeated: string
}
}) {
const t = useTranslations()
const repeatedSet =
progress.repeatedCount !== null &&
typeof progress.repeatedCount !== 'undefined'
const repeatedPercent = repeatedSet
? (progress.repeatedCount! / participants) * 100
: 0
const completedPercent = (progress.completedCount / participants) * 100
const startedPercent = (progress.startedCount / participants) * 100

const data = [
{
repeated: repeatedPercent,
completed: completedPercent - repeatedPercent,
started: startedPercent - completedPercent,
full: 100 - startedPercent,
},
]

return (
<div className="flex h-8 items-center gap-4">
<div className="w-48 overflow-hidden overflow-ellipsis whitespace-nowrap">
{progress.activityName}
</div>
<div className="flex-1">
<ResponsiveContainer width="100%" height={35}>
<BarChart data={data} layout="vertical">
<XAxis type="number" domain={[0, 100]} hide />
<YAxis type="category" hide />
<Bar dataKey="repeated" stackId="a" fill={colors.repeated} />
<Bar dataKey="completed" stackId="a" fill={colors.completed} />
<Bar dataKey="started" stackId="a" fill={colors.started} />
<Bar dataKey="full" stackId="a" fill="#f0f0f0" />
<Tooltip
wrapperStyle={{ zIndex: 20 }}
content={({ payload }) => {
if (!payload?.length) return null

return (
<div className="flex flex-col rounded border bg-white p-2 shadow-md">
<div
style={{ color: colors.started }}
>{`${t('manage.analytics.started')}: ${startedPercent.toFixed(1)} %`}</div>
<div
style={{ color: colors.completed }}
>{`${t('manage.analytics.completed')}: ${completedPercent.toFixed(1)} %`}</div>
{repeatedSet ? (
<div
style={{ color: colors.repeated }}
>{`${t('manage.analytics.repeated')}: ${repeatedPercent.toFixed(1)} %`}</div>
) : null}
</div>
)
}}
/>
</BarChart>
</ResponsiveContainer>
</div>
</div>
)
}

export default StackedProgress
36 changes: 15 additions & 21 deletions apps/frontend-manage/src/pages/analytics/[courseId]/activity.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useQuery } from '@apollo/client'
import { GetCourseActivityAnalyticsDocument } from '@klicker-uzh/graphql/dist/ops'
import Loader from '@klicker-uzh/shared-components/src/Loader'
import { H1, UserNotification } from '@uzh-bf/design-system'
import { H1 } from '@uzh-bf/design-system'
import { GetStaticPropsContext } from 'next'
import { useTranslations } from 'next-intl'
import { useRouter } from 'next/router'
Expand All @@ -10,6 +9,8 @@ import DailyActivityPlot from '~/components/analytics/activity/DailyActivityPlot
import DailyActivityTimeSeries from '~/components/analytics/activity/DailyActivityTimeSeries'
import TotalStudentActivityPlot from '~/components/analytics/activity/TotalStudentActivityPlot'
import WeeklyActivityTimeSeries from '~/components/analytics/activity/WeeklyActivityTimeSeries'
import AnalyticsErrorView from '~/components/analytics/AnalyticsErrorView'
import AnalyticsLoadingView from '~/components/analytics/AnalyticsLoadingView'
import Layout from '~/components/Layout'

function ActivityDashboard() {
Expand All @@ -25,40 +26,33 @@ function ActivityDashboard() {
}
)
const course = data?.getCourseActivityAnalytics
const navigation = (
<ActivityAnalyticsNavigation courseId={courseId as string} />
)

// TODO: extract to separate component with variable names / navigation
// loading state
if (loading || !courseId) {
return (
<Layout displayName={t('manage.analytics.activityDashboard')}>
<ActivityAnalyticsNavigation courseId={courseId as string} />
<div className="flex h-full w-full flex-row items-center justify-center gap-4 text-lg">
{t('manage.analytics.analyticsLoadingWait')}
<Loader basic />
</div>
</Layout>
<AnalyticsLoadingView
title={t('manage.analytics.activityDashboard')}
navigation={navigation}
/>
)
}

// TODO: extract to separate component for re-use
// error state
if (course === null || typeof course === 'undefined' || error) {
return (
<Layout displayName={t('manage.analytics.activityDashboard')}>
<ActivityAnalyticsNavigation courseId={courseId as string} />
<H1>{t('manage.analytics.activityDashboard')}</H1>
<UserNotification
message={t('manage.analytics.analyticsLoadingFailed')}
type="error"
className={{ root: 'mx-auto my-auto w-max max-w-full text-base' }}
/>
</Layout>
<AnalyticsErrorView
title={t('manage.analytics.activityDashboard')}
navigation={navigation}
/>
)
}

return (
<Layout displayName={t('manage.analytics.activityDashboard')}>
<ActivityAnalyticsNavigation courseId={courseId as string} />
{navigation}
<div className="mb-3 flex w-full flex-row items-end justify-between font-bold">
<H1 className={{ root: 'mb-0' }}>
{t('manage.analytics.activityDashboard')}: {course.name}
Expand Down
Loading

0 comments on commit 1310b0c

Please sign in to comment.