Skip to content

LB-1737: Create top artists graph showing album details #3170

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Mar 20, 2025
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
11ed834
new stats added for user
granth23 Feb 10, 2025
d0ca023
nivo rocks chart bug fix
granth23 Feb 11, 2025
bedff45
Merge branch 'metabrainz:master' into LB-1737-User-Artist-Map
granth23 Feb 11, 2025
c32aecb
db returns fixed
granth23 Feb 14, 2025
38d481e
bugs fixed amd total artist limit added
granth23 Feb 17, 2025
7721189
sitewide activity added
granth23 Feb 17, 2025
f49def5
test component added
granth23 Feb 17, 2025
472bc57
test added
granth23 Feb 18, 2025
e6108a9
album redirect added
granth23 Feb 23, 2025
d7b6c14
all artists names included
granth23 Mar 12, 2025
79e5785
text-wrap and better optimization
granth23 Mar 13, 2025
0ca50ff
Merge branch 'metabrainz:master' into LB-1737-User-Artist-Map
granth23 Mar 13, 2025
eee1df3
key-index bug fix
granth23 Mar 13, 2025
d947652
Rotation added to artist names
granth23 Mar 13, 2025
1991813
minor bug fix
granth23 Mar 13, 2025
fb406dc
Merge branch 'metabrainz:master' into LB-1737-User-Artist-Map
granth23 Mar 13, 2025
bf1d9a7
Update frontend/js/src/user/stats/components/UserArtistActivity.tsx
granth23 Mar 18, 2025
e4749ac
Update UserArtistActivity.tsx
granth23 Mar 18, 2025
03fffdb
Update UserArtistActivity.tsx
granth23 Mar 18, 2025
dddc1cc
Update UserArtistActivity.tsx
granth23 Mar 18, 2025
5829bcf
Update UserArtistActivity.tsx
granth23 Mar 18, 2025
53f80ca
Update UserArtistActivity.tsx
granth23 Mar 18, 2025
e10574e
Update UserArtistActivity.tsx
granth23 Mar 18, 2025
e26a8e8
Merge branch 'metabrainz:master' into LB-1737-User-Artist-Map
granth23 Mar 18, 2025
8c16135
Merge branch 'master' into LB-1737-User-Artist-Map
anshg1214 Mar 20, 2025
3b334ae
feat: Optimize artist activity sorting using heapq
anshg1214 Mar 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions frontend/js/src/user/stats/UserReports.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import UserListeningActivity from "./components/UserListeningActivity";
import UserTopEntity from "./components/UserTopEntity";
import UserDailyActivity from "./components/UserDailyActivity";
import UserArtistMap from "./components/UserArtistMap";
import UserArtistActivity from "./components/UserArtistActivity";
import { getAllStatRanges, isInvalidStatRange } from "./utils";
import GlobalAppContext from "../../utils/GlobalAppContext";
import StatsExplanationsModal from "../../common/stats/StatsExplanationsModal";
Expand Down Expand Up @@ -167,6 +168,10 @@ export default function UserReports() {
<UserDailyActivity range={range} user={user} />
</section>
)}
<section id="artist-activity">
{statsExplanationModalButton}
<UserArtistActivity range={range} user={user} />
</section>
<section id="artist-origin">
{statsExplanationModalButton}
<UserArtistMap range={range} user={user} />
Expand Down
141 changes: 141 additions & 0 deletions frontend/js/src/user/stats/components/UserArtistActivity.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { ResponsiveBar } from "@nivo/bar";
import * as React from "react";
import { faExclamationCircle, faLink } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { useQuery } from "@tanstack/react-query";
import Card from "../../../components/Card";
import Loader from "../../../components/Loader";
import { COLOR_BLACK } from "../../../utils/constants";
import GlobalAppContext from "../../../utils/GlobalAppContext";

export type UserArtistActivityProps = {
range: UserStatsAPIRange;
user?: ListenBrainzUser;
};

export declare type ChartDataItem = {
label: string;
[albumName: string]: number | string;
};

export default function UserArtistActivity(props: UserArtistActivityProps) {
const { APIService } = React.useContext(GlobalAppContext);

// Props
const { user, range } = props;

const { data: loaderData, isLoading: loading } = useQuery({
queryKey: ["userArtistActivity", user?.name, range],
queryFn: async () => {
try {
const queryData = await APIService.getUserArtistActivity(
user?.name,
range
);
return { data: queryData, hasError: false, errorMessage: "" };
} catch (error) {
return {
data: { result: [] } as UserArtistActivityResponse,
hasError: true,
errorMessage: error.message,
};
}
},
});

const {
data: rawData = { result: [] } as UserArtistActivityResponse,
hasError = false,
errorMessage = "",
} = loaderData || {};

const processData = (data?: UserArtistActivityResponse) => {
if (!data || !data.result || data.result.length === 0) {
return [];
}
return data.result.map((artist) => ({
label: artist.name,
...artist.albums.reduce(
(acc, album) => ({ ...acc, [album.name]: album.listen_count }),
{} as Record<string, number>
),
})) as ChartDataItem[];
};

const [chartData, setChartData] = React.useState<ChartDataItem[]>([]);

React.useEffect(() => {
if (rawData && rawData.result.length > 0) {
const processedData = processData(rawData);
setChartData(processedData);
}
}, [rawData]);

return (
<Card className="user-stats-card" data-testid="user-artist-activity">
<div className="row">
<div className="col-xs-10">
<h3 className="capitalize-bold" style={{ marginLeft: 20 }}>
Artist Activity
</h3>
</div>
<div className="col-xs-2 text-right">
<h4 style={{ marginTop: 20 }}>
<a href="#artist-activity">
<FontAwesomeIcon
icon={faLink as IconProp}
size="sm"
color={COLOR_BLACK}
style={{ marginRight: 20 }}
/>
</a>
</h4>
</div>
</div>
<Loader isLoading={loading}>
{hasError ? (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
minHeight: "inherit",
}}
>
<span style={{ fontSize: 24 }}>
<FontAwesomeIcon icon={faExclamationCircle as IconProp} />{" "}
{errorMessage}
</span>
</div>
) : (
<div className="row">
<div className="col-xs-12">
<div
style={{ width: "100%", height: "600px", minHeight: "400px" }}
>
<ResponsiveBar
data={chartData}
keys={Array.from(
new Set(
chartData.flatMap((item) =>
Object.keys(item).filter((key) => key !== "label")
)
)
)}
indexBy="label"
margin={{ top: 20, right: 80, bottom: 60, left: 80 }}
padding={0.2}
layout="vertical"
colors={{ scheme: "nivo" }}
borderColor={{ from: "color", modifiers: [["darker", 1.6]] }}
enableLabel={false}
/>
</div>
</div>
</div>
)}
</Loader>
</Card>
);
}
24 changes: 24 additions & 0 deletions frontend/js/src/utils/APIService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,30 @@ export default class APIService {
return response.json();
};

getUserArtistActivity = async (
userName?: string,
range: UserStatsAPIRange = "all_time"
): Promise<UserArtistActivityResponse> => {
let url;
if (userName) {
url = `${this.APIBaseURI}/stats/user/${userName}/artist-activity`;
} else {
url = `${this.APIBaseURI}/stats/sitewide/artist-activity`;
}
url += `?range=${range}`;
const response = await fetch(url);
await this.checkStatus(response);
if (response.status === 204) {
const error = new APIError(
"There are no statistics available for this user for this period"
);
error.status = response.statusText;
error.response = response;
throw error;
}
return response.json();
};

getUserArtistMap = async (
userName?: string,
range: UserStatsAPIRange = "all_time",
Expand Down
11 changes: 11 additions & 0 deletions frontend/js/src/utils/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,17 @@ declare type UserDailyActivityResponse = {
};
};

declare type UserArtistActivityResponse = {
result: Array<{
name: string;
listen_count: number;
albums: Array<{
name: string;
listen_count: number;
}>;
}>;
};

declare type UserArtistMapArtist = {
artist_name: string;
artist_mbid: string;
Expand Down
130 changes: 130 additions & 0 deletions frontend/js/tests/__mocks__/userArtistActivity.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
{
"result": [
{
"name": "Ed Sheeran",
"listen_count": 87,
"albums": [
{ "listen_count": 17, "name": "=" },
{ "listen_count": 12, "name": "÷" },
{ "listen_count": 11, "name": "+" },
{ "listen_count": 10, "name": "×" },
{ "listen_count": 16, "name": "No.6 Collaborations Project" },
{ "listen_count": 16, "name": "-" },
{ "listen_count": 3, "name": "Autumn Variations" },
{ "listen_count": 2, "name": "Under the tree" }
]
},
{
"name": "AP Dhillon",
"listen_count": 46,
"albums": [
{ "listen_count": 6, "name": "Two Hearts Never Break The Same" },
{ "listen_count": 3, "name": "HIDDEN GEMS" },
{ "listen_count": 8, "name": "Summer High" },
{ "listen_count": 8, "name": "Ma Belle" },
{ "listen_count": 8, "name": "Excuses" },
{ "listen_count": 7, "name": "Insane" },
{ "listen_count": 6, "name": "The Brownprint" }
]
},
{
"name": "Diljit Dosanjh",
"listen_count": 27,
"albums": [
{ "listen_count": 7, "name": "Ghost" },
{ "listen_count": 5, "name": "G.O.A.T." },
{ "listen_count": 3, "name": "Drive Thru" },
{ "listen_count": 6, "name": "Love Ya" },
{ "listen_count": 6, "name": "In Love With Diljit Dosanjh" }
]
},
{
"name": "Honey Singh",
"listen_count": 21,
"albums": [
{ "listen_count": 21, "name": "International Villager" }
]
},
{
"name": "Yo Yo Honey Singh",
"listen_count": 14,
"albums": [
{ "listen_count": 4, "name": "Desi Kalakaar" },
{ "listen_count": 2, "name": "Zorawar" },
{ "listen_count": 8, "name": "Glory" }
]
},
{
"name": "Anuv Jain",
"listen_count": 12,
"albums": [
{ "listen_count": 5, "name": "Ocean" },
{ "listen_count": 7, "name": "Baarishein" }
]
},
{
"name": "Avvy Sra & Karan Aujla",
"listen_count": 8,
"albums": [
{ "listen_count": 8, "name": "White Brown Black" }
]
},
{
"name": "Shubh",
"listen_count": 8,
"albums": [
{ "listen_count": 2, "name": "We Rollin" },
{ "listen_count": 6, "name": "Bandana" }
]
},
{
"name": "Pav Dharia",
"listen_count": 8,
"albums": [
{ "listen_count": 8, "name": "Na Ja" }
]
},
{
"name": "Vishal Mishra",
"listen_count": 8,
"albums": [
{ "listen_count": 8, "name": "Manjha" }
]
},
{
"name": "Aditya Rikhari",
"listen_count": 8,
"albums": [
{ "listen_count": 8, "name": "Jaana Samjho Na (From \"Bhool Bhulaiyaa 3\")" }
]
},
{
"name": "Juss & MixSingh",
"listen_count": 7,
"albums": [
{ "listen_count": 7, "name": "Suniyan Suniyan" }
]
},
{
"name": "Diljit Dosanjh & Sia",
"listen_count": 7,
"albums": [
{ "listen_count": 7, "name": "Hass Hass" }
]
},
{
"name": "Jassa Dhillon",
"listen_count": 6,
"albums": [
{ "listen_count": 6, "name": "VIBIN" }
]
},
{
"name": "Jasleen Royal",
"listen_count": 6,
"albums": [
{ "listen_count": 6, "name": "Sahiba" }
]
}
]
}
Loading
Loading