mirror of
https://github.com/gabehf/Koito.git
synced 2026-03-07 21:48:18 -08:00
feat: interest over time graph (#127)
* api * ui * test * add margin to prevent clipping
This commit is contained in:
parent
e45099c71a
commit
231eb1b0fb
16 changed files with 1097 additions and 4 deletions
117
client/app/components/InterestGraph.tsx
Normal file
117
client/app/components/InterestGraph.tsx
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getActivity,
|
||||
getInterest,
|
||||
type getActivityArgs,
|
||||
type getInterestArgs,
|
||||
type ListenActivityItem,
|
||||
} from "api/api";
|
||||
import Popup from "./Popup";
|
||||
import { useState } from "react";
|
||||
import { useTheme } from "~/hooks/useTheme";
|
||||
import ActivityOptsSelector from "./ActivityOptsSelector";
|
||||
import type { Theme } from "~/styles/themes.css";
|
||||
import { Area, AreaChart, Line, LineChart, Tooltip } from "recharts";
|
||||
import { RechartsDevtools } from "@recharts/devtools";
|
||||
|
||||
function getPrimaryColor(theme: Theme): string {
|
||||
const value = theme.primary;
|
||||
const rgbMatch = value.match(
|
||||
/^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/
|
||||
);
|
||||
if (rgbMatch) {
|
||||
const [, r, g, b] = rgbMatch.map(Number);
|
||||
return "#" + [r, g, b].map((n) => n.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
interface Props {
|
||||
buckets?: number;
|
||||
artistId?: number;
|
||||
albumId?: number;
|
||||
trackId?: number;
|
||||
}
|
||||
|
||||
export default function InterestGraph({
|
||||
buckets = 14,
|
||||
artistId = 0,
|
||||
albumId = 0,
|
||||
trackId = 0,
|
||||
}: Props) {
|
||||
const { isPending, isError, data, error } = useQuery({
|
||||
queryKey: [
|
||||
"interest",
|
||||
{
|
||||
buckets: buckets,
|
||||
artist_id: artistId,
|
||||
album_id: albumId,
|
||||
track_id: trackId,
|
||||
},
|
||||
],
|
||||
queryFn: ({ queryKey }) => getInterest(queryKey[1] as getInterestArgs),
|
||||
});
|
||||
|
||||
const { theme } = useTheme();
|
||||
const color = getPrimaryColor(theme);
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="w-[500px]">
|
||||
<h3>Interest over time</h3>
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
);
|
||||
} else if (isError) {
|
||||
return (
|
||||
<div className="w-[500px]">
|
||||
<h3>Interest over time</h3>
|
||||
<p className="error">Error: {error.message}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-start w-full max-w-[500px]">
|
||||
<h3>Interest over time</h3>
|
||||
<AreaChart
|
||||
style={{
|
||||
width: "100%",
|
||||
aspectRatio: 3.5,
|
||||
maxWidth: 440,
|
||||
overflow: "visible",
|
||||
}}
|
||||
margin={{ top: 5, right: 0, left: 0, bottom: 10 }}
|
||||
data={data}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="colorGradient" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={color} stopOpacity={0.5} />
|
||||
<stop offset="85%" stopColor={color} stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<Area
|
||||
dataKey="listen_count"
|
||||
type="natural"
|
||||
stroke="none"
|
||||
fill="url(#colorGradient)"
|
||||
animationDuration={750}
|
||||
animationEasing="ease-in-out"
|
||||
activeDot={false}
|
||||
/>
|
||||
<Area
|
||||
dataKey="listen_count"
|
||||
type="natural"
|
||||
stroke={color}
|
||||
fill="none"
|
||||
strokeWidth={2}
|
||||
animationDuration={750}
|
||||
animationEasing="ease-in-out"
|
||||
dot={false}
|
||||
activeDot={false}
|
||||
style={{ filter: `drop-shadow(0px 0px 5px ${color})` }}
|
||||
/>
|
||||
</AreaChart>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import PeriodSelector from "~/components/PeriodSelector";
|
|||
import MediaLayout from "./MediaLayout";
|
||||
import ActivityGrid from "~/components/ActivityGrid";
|
||||
import { timeListenedString } from "~/utils/utils";
|
||||
import InterestGraph from "~/components/InterestGraph";
|
||||
|
||||
export async function clientLoader({ params }: LoaderFunctionArgs) {
|
||||
const res = await fetch(`/apis/web/v1/album?id=${params.id}`);
|
||||
|
|
@ -69,7 +70,10 @@ export default function Album() {
|
|||
<div className="flex flex-wrap gap-20 mt-10">
|
||||
<LastPlays limit={30} albumId={album.id} />
|
||||
<TopTracks limit={12} period={period} albumId={album.id} />
|
||||
<ActivityGrid configurable albumId={album.id} />
|
||||
<div className="flex flex-col items-start gap-4">
|
||||
<ActivityGrid configurable albumId={album.id} />
|
||||
<InterestGraph albumId={album.id} />
|
||||
</div>
|
||||
</div>
|
||||
</MediaLayout>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import MediaLayout from "./MediaLayout";
|
|||
import ArtistAlbums from "~/components/ArtistAlbums";
|
||||
import ActivityGrid from "~/components/ActivityGrid";
|
||||
import { timeListenedString } from "~/utils/utils";
|
||||
import InterestGraph from "~/components/InterestGraph";
|
||||
|
||||
export async function clientLoader({ params }: LoaderFunctionArgs) {
|
||||
const res = await fetch(`/apis/web/v1/artist?id=${params.id}`);
|
||||
|
|
@ -76,7 +77,10 @@ export default function Artist() {
|
|||
<div className="flex gap-15 mt-10 flex-wrap">
|
||||
<LastPlays limit={20} artistId={artist.id} />
|
||||
<TopTracks limit={8} period={period} artistId={artist.id} />
|
||||
<ActivityGrid configurable artistId={artist.id} />
|
||||
<div className="flex flex-col items-start gap-4">
|
||||
<ActivityGrid configurable artistId={artist.id} />
|
||||
<InterestGraph artistId={artist.id} />
|
||||
</div>
|
||||
</div>
|
||||
<ArtistAlbums period={period} artistId={artist.id} name={artist.name} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import PeriodSelector from "~/components/PeriodSelector";
|
|||
import MediaLayout from "./MediaLayout";
|
||||
import ActivityGrid from "~/components/ActivityGrid";
|
||||
import { timeListenedString } from "~/utils/utils";
|
||||
import InterestGraph from "~/components/InterestGraph";
|
||||
|
||||
export async function clientLoader({ params }: LoaderFunctionArgs) {
|
||||
let res = await fetch(`/apis/web/v1/track?id=${params.id}`);
|
||||
|
|
@ -73,7 +74,10 @@ export default function Track() {
|
|||
</div>
|
||||
<div className="flex flex-wrap gap-20 mt-10">
|
||||
<LastPlays limit={20} trackId={track.id} />
|
||||
<ActivityGrid trackId={track.id} configurable />
|
||||
<div className="flex flex-col items-start gap-4">
|
||||
<ActivityGrid configurable trackId={track.id} />
|
||||
<InterestGraph trackId={track.id} />
|
||||
</div>
|
||||
</div>
|
||||
</MediaLayout>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue