Skip to content

Commit 636b484

Browse files
committed
Use piechart
1 parent 2fde054 commit 636b484

File tree

5 files changed

+218
-82
lines changed

5 files changed

+218
-82
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"prettier": "3.0.0"
1111
},
1212
"dependencies": {
13+
"chartjs-plugin-labels": "^1.1.0",
1314
"exec": "^0.2.1"
1415
},
1516
"packageManager": "pnpm@8.14.0+sha1.bb42032ff80dba5f9245bc1b03470d2fa0b7fb2f"

pnpm-lock.yaml

Lines changed: 49 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import "chart.js/auto";
2+
import type { FC } from "react";
3+
import { Pie } from "react-chartjs-2";
4+
import type { TemplateAppUsage } from "api/typesGenerated";
5+
import { formatTime } from "utils/time";
6+
7+
type AppUsageChartProps = {
8+
usage: TemplateAppUsage[];
9+
colors: string[];
10+
};
11+
12+
export const AppUsageChart: FC<AppUsageChartProps> = ({ usage, colors }) => {
13+
const totalUsageInSeconds = usage.reduce((acc, u) => acc + u.seconds, 0);
14+
return (
15+
<Pie
16+
data={{
17+
datasets: [
18+
{
19+
data: usage.map((u) => u.seconds),
20+
backgroundColor: colors,
21+
borderWidth: 0,
22+
},
23+
],
24+
}}
25+
options={{
26+
plugins: {
27+
tooltip: {
28+
padding: 12,
29+
boxPadding: 6,
30+
usePointStyle: true,
31+
callbacks: {
32+
title: (context) => {
33+
return usage[context[0].dataIndex].display_name;
34+
},
35+
label: (context) => {
36+
const appUsage = usage[context.dataIndex];
37+
const percentage = Math.round(
38+
(appUsage.seconds / totalUsageInSeconds) * 100,
39+
);
40+
return `${formatTime(
41+
usage[context.dataIndex].seconds,
42+
)} (${percentage}%)`;
43+
},
44+
labelPointStyle: () => {
45+
return {
46+
pointStyle: "circle",
47+
rotation: 0,
48+
};
49+
},
50+
},
51+
},
52+
},
53+
}}
54+
/>
55+
);
56+
};

site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx

Lines changed: 80 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { useTheme } from "@emotion/react";
22
import CancelOutlined from "@mui/icons-material/CancelOutlined";
33
import CheckCircleOutlined from "@mui/icons-material/CheckCircleOutlined";
44
import LinkOutlined from "@mui/icons-material/LinkOutlined";
5-
import LinearProgress from "@mui/material/LinearProgress";
65
import Link from "@mui/material/Link";
76
import Tooltip from "@mui/material/Tooltip";
87
import chroma from "chroma-js";
@@ -58,6 +57,7 @@ import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
5857
import { useTemplateLayoutContext } from "pages/TemplatePage/TemplateLayout";
5958
import { getLatencyColor } from "utils/latency";
6059
import { getTemplatePageTitle } from "../utils";
60+
import { AppUsageChart } from "./AppUsageChart";
6161
import { DateRange as DailyPicker, type DateRangeValue } from "./DateRange";
6262
import { type InsightsInterval, IntervalMenu } from "./IntervalMenu";
6363
import { lastWeeks } from "./utils";
@@ -419,15 +419,15 @@ const TemplateUsagePanel: FC<TemplateUsagePanelProps> = ({
419419
...panelProps
420420
}) => {
421421
const theme = useTheme();
422-
const validUsage = data?.filter((u) => u.seconds > 0);
423-
const totalInSeconds =
424-
validUsage?.reduce((total, usage) => total + usage.seconds, 0) ?? 1;
425-
const usageColors = chroma
426-
.scale([theme.roles.success.fill.solid, theme.roles.notice.fill.solid])
427-
.mode("lch")
428-
.colors(validUsage?.length ?? 0);
422+
const usage = data
423+
?.filter((u) => u.seconds > 0)
424+
.sort((a, b) => b.seconds - a.seconds);
425+
const colors = chroma
426+
.scale([theme.palette.primary.dark, "#FFF"])
427+
.classes(usage?.length ?? 0)
428+
.colors(usage?.length ?? 0);
429429
// The API returns a row for each app, even if the user didn't use it.
430-
const hasDataAvailable = validUsage && validUsage.length > 0;
430+
const hasDataAvailable = usage && usage.length > 0;
431431

432432
return (
433433
<Panel {...panelProps} css={{ overflowY: "auto" }}>
@@ -438,88 +438,86 @@ const TemplateUsagePanel: FC<TemplateUsagePanelProps> = ({
438438
{!data && <Loader css={{ height: "100%" }} />}
439439
{data && !hasDataAvailable && <NoDataAvailable />}
440440
{data && hasDataAvailable && (
441-
<div
442-
css={{
443-
display: "flex",
444-
flexDirection: "column",
445-
gap: 24,
446-
}}
447-
>
448-
{validUsage
449-
.sort((a, b) => b.seconds - a.seconds)
450-
.map((usage, i) => {
451-
const percentage = (usage.seconds / totalInSeconds) * 100;
452-
return (
453-
<div
454-
key={usage.slug}
455-
css={{ display: "flex", gap: 24, alignItems: "center" }}
456-
>
441+
<Stack direction="row" spacing={4} alignItems="center">
442+
<div
443+
css={{
444+
padding: "0 16px",
445+
width: 360,
446+
}}
447+
>
448+
<AppUsageChart usage={usage} colors={colors} />
449+
</div>
450+
<div
451+
css={{ flex: 1, display: "grid", gridAutoRows: "1fr", gap: 8 }}
452+
>
453+
{usage.map((usage, i) => (
454+
<Stack
455+
key={usage.slug}
456+
direction="row"
457+
alignItems="center"
458+
justifyContent="space-between"
459+
>
460+
<div css={{ display: "flex", alignItems: "center" }}>
457461
<div
458-
css={{ display: "flex", alignItems: "center", gap: 8 }}
459-
>
460-
<div
461-
css={{
462-
width: 20,
463-
height: 20,
464-
display: "flex",
465-
alignItems: "center",
466-
justifyContent: "center",
467-
}}
468-
>
469-
<img
470-
src={usage.icon}
471-
alt=""
472-
style={{
473-
objectFit: "contain",
474-
width: "100%",
475-
height: "100%",
476-
}}
477-
/>
478-
</div>
479-
<div css={{ fontSize: 13, fontWeight: 500, width: 200 }}>
480-
{usage.display_name}
481-
</div>
482-
</div>
483-
<LinearProgress
484-
value={percentage}
485-
variant="determinate"
486462
css={{
487-
width: "100%",
463+
width: 8,
488464
height: 8,
489-
backgroundColor: theme.palette.divider,
490-
"& .MuiLinearProgress-bar": {
491-
backgroundColor: usageColors[i],
492-
borderRadius: 999,
493-
},
465+
borderRadius: 999,
466+
backgroundColor: colors[i],
467+
marginRight: 16,
494468
}}
495469
/>
496-
<Stack
497-
spacing={0}
470+
<div
498471
css={{
499-
fontSize: 13,
500-
color: theme.palette.text.secondary,
501-
width: 120,
502-
flexShrink: 0,
503-
lineHeight: "1.5",
472+
width: 20,
473+
height: 20,
474+
display: "flex",
475+
alignItems: "center",
476+
justifyContent: "center",
477+
marginRight: 8,
504478
}}
505479
>
506-
{formatTime(usage.seconds)}
507-
{usage.times_used > 0 && (
508-
<span
509-
css={{
510-
fontSize: 12,
511-
color: theme.palette.text.disabled,
512-
}}
513-
>
514-
Opened {usage.times_used.toLocaleString()}{" "}
515-
{usage.times_used === 1 ? "time" : "times"}
516-
</span>
517-
)}
518-
</Stack>
480+
<img
481+
src={usage.icon}
482+
alt=""
483+
style={{
484+
objectFit: "contain",
485+
width: "100%",
486+
height: "100%",
487+
}}
488+
/>
489+
</div>
490+
<div css={{ fontSize: 13, fontWeight: 500, width: 200 }}>
491+
{usage.display_name}
492+
</div>
519493
</div>
520-
);
521-
})}
522-
</div>
494+
<Stack
495+
spacing={0}
496+
css={{
497+
fontSize: 13,
498+
color: theme.palette.text.secondary,
499+
width: 120,
500+
flexShrink: 0,
501+
lineHeight: "1.5",
502+
}}
503+
>
504+
{formatTime(usage.seconds)}
505+
{usage.times_used > 0 && (
506+
<span
507+
css={{
508+
fontSize: 12,
509+
color: theme.palette.text.disabled,
510+
}}
511+
>
512+
Opened {usage.times_used.toLocaleString()}{" "}
513+
{usage.times_used === 1 ? "time" : "times"}
514+
</span>
515+
)}
516+
</Stack>
517+
</Stack>
518+
))}
519+
</div>
520+
</Stack>
523521
)}
524522
</PanelContent>
525523
</Panel>

site/src/utils/time.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,35 @@ export function durationInHours(duration: number): number {
2929
export function durationInDays(duration: number): number {
3030
return duration / 1000 / 60 / 60 / 24;
3131
}
32+
33+
export function formatTime(seconds: number): string {
34+
let value: {
35+
amount: number;
36+
unit: "seconds" | "minutes" | "hours";
37+
} = {
38+
amount: seconds,
39+
unit: "seconds",
40+
};
41+
42+
if (seconds >= 60 && seconds < 3600) {
43+
value = {
44+
amount: Math.floor(seconds / 60),
45+
unit: "minutes",
46+
};
47+
} else {
48+
value = {
49+
amount: seconds / 3600,
50+
unit: "hours",
51+
};
52+
}
53+
54+
if (value.amount === 1) {
55+
const singularUnit = value.unit.slice(0, -1);
56+
return `${value.amount} ${singularUnit}`;
57+
}
58+
59+
return `${value.amount.toLocaleString(undefined, {
60+
maximumFractionDigits: 1,
61+
minimumFractionDigits: 0,
62+
})} ${value.unit}`;
63+
}

0 commit comments

Comments
 (0)