@@ -134,92 +187,83 @@ export default function UserEntityChart() {
-
-
- - - Artists - - - - - Albums - - - - - Tracks - - -
-
-
-
-

- Top{" "} - - {terminology ? `${terminology}s` : ""} - {" "} - of {range !== "all_time" ? "the" : ""} - - -
    - {Array.from(ranges, ([stat_type, stat_name]) => { - return ( -
  • - - {stat_name} - -
  • - ); - })} -
-
- {range !== "all_time" && - !hasError && - `(${startDate?.toLocaleString("en-us", { - day: "2-digit", - month: "long", - year: "numeric", - })} - ${endDate?.toLocaleString("en-us", { - day: "2-digit", - month: "long", - year: "numeric", - })})`} -

-
+
+ + + Artists + + + + + Albums + + + + + Tracks + +
+

+ Top{" "} + + {terminology ? `${terminology}s` : ""} + {" "} + of {range !== "all_time" ? "the" : ""} + + +
    + {Array.from(ranges, ([stat_type, stat_name]) => { + return ( +
  • + + {stat_name} + +
  • + ); + })} +
+
+ {range !== "all_time" && + !hasError && + `(${startDate?.toLocaleString("en-us", { + day: "2-digit", + month: "long", + year: "numeric", + })} - ${endDate?.toLocaleString("en-us", { + day: "2-digit", + month: "long", + year: "numeric", + })})`} +

{hasError && ( -
-
+
+
{" "} {errorMessage} @@ -229,60 +273,90 @@ export default function UserEntityChart() { )} {!hasError && ( <> -
-
-

- {terminology} count - {entityCount} -

-
-
-
-
- {data - ?.slice() - .reverse() - .map((datum, index) => { - const listen = listenableItems[index]; - const listenDetails = getChartEntityDetails(datum); - return ( - - ); - })} -
-
+ + {terminology} + +  count - {entityCount} total + {entityCount > 1000 && ( + +  (showing the first 1000) + + )} + + )} +
+ - -
+ defs={[ + { + id: "barGradient", + type: "linearGradient", + colors: [ + { + offset: 10, + color: "antiquewhite", + }, + { + offset: 90, + color: COLOR_LB_ORANGE, + }, + ], + y2: "90vw", + gradientTransform: "rotate(-90)", + gradientUnits: "userSpaceOnUse", + }, + ]} + fill={[{ match: "*", id: "barGradient" }]} + // labelPosition="start" // Upcoming nivo release, see https://github.com/plouc/nivo/pull/2585 + />
- {terminology === "album" && ( -
-
- - *The listen count denotes the number of times - you have listened to a recording from the release group. - -
-
- )} -
-
-
    + {totalPages > 1 && ( +
    +
    • 0) ? "disabled" : "" }`} + title="First page" + > + + + +
    • +
    • 0) ? "disabled" : "" + }`} + title="Previous page" > - ← Previous + {" "} + Previous
    • + {currPage > 3 && ( +
    • + ... +
    • + )} + {currPage > 2 && currPage - 2 < totalPages && ( +
    • + + {currPage - 2} + +
    • + )} + {currPage > 1 && currPage - 1 < totalPages && ( +
    • + + {currPage - 1} + +
    • + )} +
    • + page {currPage} +
    • + {currPage + 1 <= totalPages && ( +
    • + + {currPage + 1} + +
    • + )} + {currPage + 2 <= totalPages && ( +
    • + + {currPage + 2} + +
    • + )} + {currPage + 2 < totalPages && ( +
    • + ... +
    • + )}
    • - Next → + Next{" "} + + +
    • +
    • + +
    -
+ )} + + {(entity === "artist" || entity === "recording") && ( +
+ {data?.slice().map((datum, index) => { + const listen = listenableItems[index]; + const listenDetails = getChartEntityDetails(datum); + const listenCountComponent = ( + + {datum.count} +   + + + ); + return ( + + ); + })} +
+ )} + + {(entity === "release" || entity === "release-group") && ( + <> +

+ *The listen count denotes the number of times you + have listened to a recording from the release group. +

+
+ {data?.slice().map((datum, index) => { + return ( + a.artist_mbid) ?? + [] + } + artistCredits={datum.artists} + artistCreditName={datum.artist as string} + listenCount={datum.count} + caaID={datum.caaID ?? null} + caaReleaseMBID={datum.caaReleaseMBID ?? null} + showListens + showReleaseTitle + showArtist + /> + ); + })} +
+ + )} )} diff --git a/frontend/js/src/user/charts/components/Bar.tsx b/frontend/js/src/user/charts/components/Bar.tsx index b980e4770c..9db4ff1d20 100644 --- a/frontend/js/src/user/charts/components/Bar.tsx +++ b/frontend/js/src/user/charts/components/Bar.tsx @@ -1,94 +1,88 @@ import * as React from "react"; import { ResponsiveBar, - LabelFormatter, BarDatum, BarTooltipProps, + BarSvgProps, } from "@nivo/bar"; -import type { AxisTickProps } from "@nivo/axes"; -import { omit } from "lodash"; -import { BasicTooltip } from "@nivo/tooltip"; +import { TooltipWrapper } from "@nivo/tooltip"; import { COLOR_LB_ORANGE } from "../../../utils/constants"; export type BarProps = { data: UserEntityData; maxValue: number; -}; + isMobileSize?: boolean; +} & Partial>; export default function Bar(props: BarProps) { - const { data, maxValue } = props; - - const renderTickValue = (tick: AxisTickProps): JSX.Element => { - const datum: UserEntityDatum = data?.[tick.tickIndex]; - const { idx } = datum; - return ( - - {idx}. - - ); - }; - - const labelFormatter = (((label: string) => { - return ( - - {label} - - ); - }) as unknown) as LabelFormatter; + const { data, maxValue, isMobileSize, ...barProps } = props; const customTooltip = (tooltipProps: BarTooltipProps) => { - const { data: datum, value, color } = tooltipProps; + const { data: datum, value } = tooltipProps; return ( - + +
+ #{datum.idx} {datum.entity} + :  + + {value} {Number(value) === 1 ? "listen" : "listens"} + + {datum.artist &&
{datum.artist}
} +
+
); }; const theme = { labels: { text: { - fontSize: "14px", + fontSize: "15px", + fontFamily: "'Sintony', sans-serif", }, }, }; - const typescriptCompliantData: BarDatum[] = data?.map((datum) => - omit(datum, [ - "entityMBID", - "artist", - "artistMBID", - "release", - "releaseMBID", - "artists", - ]) - ); + const numberOfTicks = isMobileSize + ? Math.min(5, maxValue) + : Math.min(9, maxValue); + + const horizontalAxis = { + tickSize: 5, + tickValues: numberOfTicks, + tickPadding: 5, + legend: "Number of listens", + legendOffset: 30, + }; + return ( x.data.entity} labelSkipWidth={0} tooltip={customTooltip} margin={{ - top: -12, - left: 35, + bottom: 40, + left: 15, + right: 15, }} - axisLeft={{ - tickSize: 0, - tickValues: data?.length, - tickPadding: 5, - renderTick: renderTickValue, + axisBottom={{ ...horizontalAxis, legendPosition: "middle" }} + axisTop={{ + ...horizontalAxis, + legendPosition: "middle", + legendOffset: -30, }} + axisLeft={null} theme={theme} keys={["count"]} animate={false} + {...barProps} /> ); } diff --git a/frontend/js/src/user/charts/utils.tsx b/frontend/js/src/user/charts/utils.tsx index 4536aa104f..5f98ed3aa2 100644 --- a/frontend/js/src/user/charts/utils.tsx +++ b/frontend/js/src/user/charts/utils.tsx @@ -24,33 +24,42 @@ export const getData = async ( rowsPerPage ); let maxListens = 0; + // We only calculate the top 1000 entities nowadays, + // so we don't want to use the total_{entity}_count value directly + const maxNumberOfPages = 1000 / rowsPerPage; let totalPages = 0; let entityCount = 0; if (entity === "artist") { entityData = entityData as UserArtistsResponse; maxListens = entityData.payload.artists?.[0]?.listen_count; - totalPages = Math.ceil(entityData.payload.total_artist_count / rowsPerPage); + totalPages = Math.min( + maxNumberOfPages, + Math.ceil(entityData.payload.total_artist_count / rowsPerPage) + ); entityCount = entityData.payload.total_artist_count; } else if (entity === "release") { entityData = entityData as UserReleasesResponse; maxListens = entityData.payload.releases?.[0]?.listen_count; - totalPages = Math.ceil( - entityData.payload.total_release_count / rowsPerPage + totalPages = Math.min( + maxNumberOfPages, + Math.ceil(entityData.payload.total_release_count / rowsPerPage) ); entityCount = entityData.payload.total_release_count; } else if (entity === "recording") { entityData = entityData as UserRecordingsResponse; maxListens = entityData.payload.recordings?.[0]?.listen_count; - totalPages = Math.ceil( - entityData.payload.total_recording_count / rowsPerPage + totalPages = Math.min( + maxNumberOfPages, + Math.ceil(entityData.payload.total_recording_count / rowsPerPage) ); entityCount = entityData.payload.total_recording_count; } else if (entity === "release-group") { entityData = entityData as UserReleaseGroupsResponse; maxListens = entityData.payload.release_groups?.[0]?.listen_count; - totalPages = Math.ceil( - entityData.payload.total_release_group_count / rowsPerPage + totalPages = Math.min( + maxNumberOfPages, + Math.ceil(entityData.payload.total_release_group_count / rowsPerPage) ); entityCount = entityData.payload.total_release_group_count; } @@ -78,8 +87,8 @@ export const processData = ( return result; } if (entity === "artist") { - result = (data as UserArtistsResponse).payload.artists - ?.map((elem, idx: number) => { + result = (data as UserArtistsResponse).payload.artists?.map( + (elem, idx: number) => { const entityMBID = elem.artist_mbid ?? undefined; return { id: idx.toString(), @@ -89,11 +98,11 @@ export const processData = ( count: elem.listen_count, entityMBID, }; - }) - .reverse(); + } + ); } else if (entity === "release") { - result = (data as UserReleasesResponse).payload.releases - ?.map((elem, idx: number) => { + result = (data as UserReleasesResponse).payload.releases?.map( + (elem, idx: number) => { return { id: idx.toString(), entity: elem.release_name, @@ -107,11 +116,11 @@ export const processData = ( caaReleaseMBID: elem.caa_release_mbid, artists: elem.artists, }; - }) - .reverse(); + } + ); } else if (entity === "recording") { - result = (data as UserRecordingsResponse).payload.recordings - ?.map((elem, idx: number) => { + result = (data as UserRecordingsResponse).payload.recordings?.map( + (elem, idx: number) => { return { id: idx.toString(), entity: elem.track_name, @@ -121,17 +130,18 @@ export const processData = ( artistMBID: elem.artist_mbids, release: elem.release_name, releaseMBID: elem.release_mbid, + recordingMSID: elem.recording_msid, idx: offset + idx + 1, count: elem.listen_count, caaID: elem.caa_id, caaReleaseMBID: elem.caa_release_mbid, artists: elem.artists, }; - }) - .reverse(); + } + ); } else if (entity === "release-group") { - result = (data as UserReleaseGroupsResponse).payload.release_groups - ?.map((elem, idx: number) => { + result = (data as UserReleaseGroupsResponse).payload.release_groups?.map( + (elem, idx: number) => { return { id: idx.toString(), entity: elem.release_group_name, @@ -145,8 +155,8 @@ export const processData = ( caaReleaseMBID: elem.caa_release_mbid, artists: elem.artists, }; - }) - .reverse(); + } + ); } return result; diff --git a/frontend/js/src/user/stats/utils.tsx b/frontend/js/src/user/stats/utils.tsx index 6f7aa7734f..5041784ef8 100644 --- a/frontend/js/src/user/stats/utils.tsx +++ b/frontend/js/src/user/stats/utils.tsx @@ -131,7 +131,7 @@ export function getChartEntityDetails(datum: UserEntityDatum): JSX.Element { return ( <> -
+
{getEntityLink(entityType, entityName, entityMBID)}