diff --git a/frontend/css/year-in-music.less b/frontend/css/year-in-music.less index 3824fbe585..bded276225 100644 --- a/frontend/css/year-in-music.less +++ b/frontend/css/year-in-music.less @@ -651,7 +651,7 @@ &.yim-2024 { --backgroundColor: #4c6c52; --cardBackgroundColor: #fefff5; - --accentColor: #2B9F7A; + --accentColor: #2b9f7a; --selectedColor: var(--accentColor); --swiper-navigation-color: var(--accentColor); @text-color: var(--accentColor); diff --git a/frontend/js/src/user/year-in-music/2024/YearInMusic2024.tsx b/frontend/js/src/user/year-in-music/2024/YearInMusic2024.tsx index e7cae35343..74039e9d9b 100644 --- a/frontend/js/src/user/year-in-music/2024/YearInMusic2024.tsx +++ b/frontend/js/src/user/year-in-music/2024/YearInMusic2024.tsx @@ -9,6 +9,7 @@ import { } from "swiper"; import { Swiper, SwiperSlide } from "swiper/react"; import { CalendarDatum, ResponsiveCalendar } from "@nivo/calendar"; +import { ResponsiveTreeMap } from "@nivo/treemap"; import Tooltip from "react-tooltip"; import { toast } from "react-toastify"; import { @@ -55,9 +56,26 @@ import { RouteQuery } from "../../../utils/Loader"; import { useBrainzPlayerDispatch } from "../../../common/brainzplayer/BrainzPlayerContext"; import { YearInMusicProps } from "../2023/YearInMusic2023"; +type Node = { + id: string; + loc: number; + name: string; + children?: Node[]; +}; + +type GenreGraphData = { + children: Node[]; + name: string; +}; + +type YearInMusicProps2024 = YearInMusicProps & { + genreGraphData: GenreGraphData; +}; + type YearInMusicLoaderData = { - user: YearInMusicProps["user"]; - data: YearInMusicProps["yearInMusicData"]; + user: YearInMusicProps2024["user"]; + data: YearInMusicProps2024["yearInMusicData"]; + genreGraphData: YearInMusicProps2024["genreGraphData"]; }; const YIM2024Seasons = { @@ -83,14 +101,14 @@ export type YearInMusicState = { }; export default class YearInMusic extends React.Component< - YearInMusicProps, + YearInMusicProps2024, YearInMusicState > { static contextType = GlobalAppContext; declare context: React.ContextType; private buddiesScrollContainer: React.RefObject; - constructor(props: YearInMusicProps) { + constructor(props: YearInMusicProps2024) { super(props); this.state = { mosaics: [], @@ -357,6 +375,7 @@ export default class YearInMusic extends React.Component< topDiscoveriesPlaylist, topMissedRecordingsPlaylist, missingPlaylistData, + genreGraphData, } = this.props; const { selectedMetric, @@ -370,6 +389,14 @@ export default class YearInMusic extends React.Component< const backgroundColor = selectedSeason.background; const cardBackgroundColor = selectedSeason.cardBackground; + const textColors = Object.values(YIM2024Seasons).map( + (season) => season.text + ); + const reorderedColors = [ + ...textColors.slice(textColors.indexOf(selectedSeason.text)), + ...textColors.slice(0, textColors.indexOf(selectedSeason.text)), + ]; + // Some data might not have been calculated for some users // This boolean lets us warn them of that let missingSomeData = missingPlaylistData; @@ -401,6 +428,7 @@ export default class YearInMusic extends React.Component< const isCurrentUser = user.name === currentUser?.name; const youOrUsername = isCurrentUser ? "you" : `${user.name}`; const yourOrUsersName = isCurrentUser ? "your" : `${user.name}'s`; + const hasOrHave = isCurrentUser ? "have" : "has"; /* Most listened years */ let mostListenedYearDataForGraph; @@ -1128,6 +1156,49 @@ export default class YearInMusic extends React.Component< )} + {genreGraphData && ( +
+

+ What genres {hasOrHave} {youOrUsername} explored?{" "} + + + The top genres {youOrUsername} listened to this year + +

+
+
+ +
+
+
+ )}
/year-in-music/", methods=['POST']) @user_bp.route("//year-in-music//", methods=['POST']) def year_in_music(user_name, year: int = 2024): @@ -339,8 +417,26 @@ def year_in_music(user_name, year: int = 2024): yearInMusicData = {} current_app.logger.error(f"Error getting Year in Music data for user {user_name}: {e}") + genreGraphData = {} + if yearInMusicData and year == 2024: + try: + data = cache.get(TAG_HEIRARCHY_CACHE_KEY) + if not data: + with psycopg2.connect(current_app.config["MB_DATABASE_URI"]) as mb_conn,\ + mb_conn.cursor(cursor_factory=DictCursor) as mb_curs: + data = load_genre_with_subgenres(mb_curs) + data = [dict(row) for row in data] if data else [] + cache.set(TAG_HEIRARCHY_CACHE_KEY, data, expirein=TAG_HEIRARCHY_CACHE_EXPIRY) + except Exception as e: + current_app.logger.error("Error loading genre hierarchy: %s", e) + return jsonify({"error": "Failed to load genre hierarchy"}), 500 + + yimTopGenre = yearInMusicData.get("top_genres", []) + genreGraphData = process_genre_data(yimTopGenre, data, user_name) + return jsonify({ "data": yearInMusicData, + **({"genreGraphData": genreGraphData} if year == 2024 else {}), "user": { "id": user.id, "name": user.musicbrainz_id, diff --git a/package-lock.json b/package-lock.json index a000e69a84..16b760ea9a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "@nivo/legends": "^0.81.0", "@nivo/network": "^0.81.0", "@nivo/tooltip": "^0.81.0", + "@nivo/treemap": "^0.81.0", "@react-spring/web": "^9.7.3", "@sentry/react": "^8.33.1", "@tanstack/react-query": "^5.28.4", @@ -3716,6 +3717,22 @@ "@nivo/core": "0.81.0" } }, + "node_modules/@nivo/treemap": { + "version": "0.81.0", + "resolved": "https://registry.npmjs.org/@nivo/treemap/-/treemap-0.81.0.tgz", + "integrity": "sha512-Dt+u15MIS6kdoXDeTYr+EYPeBidPC9JBOF8hFFbo+NvsLPz1jC690wgV++IcMQZS7LjQmMziv7QZmsfzmpDbZw==", + "dependencies": { + "@nivo/colors": "0.81.0", + "@nivo/tooltip": "0.81.0", + "@react-spring/web": "9.4.5 || ^9.7.2", + "d3-hierarchy": "^1.1.8", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "@nivo/core": "0.81.0", + "react": ">= 16.14.0 < 19.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -7070,6 +7087,11 @@ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==" }, + "node_modules/d3-hierarchy": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz", + "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==" + }, "node_modules/d3-interpolate": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-2.0.1.tgz", diff --git a/package.json b/package.json index 4dd4e39fa5..d10f8b5905 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "@nivo/legends": "^0.81.0", "@nivo/network": "^0.81.0", "@nivo/tooltip": "^0.81.0", + "@nivo/treemap": "^0.81.0", "@react-spring/web": "^9.7.3", "@sentry/react": "^8.33.1", "@tanstack/react-query": "^5.28.4",