add chart.js for random charts

This commit is contained in:
hzrd149 2024-01-08 17:31:28 +00:00
parent b17c0825c6
commit 9eb219917f
13 changed files with 185 additions and 14 deletions

1
.gitignore vendored
View File

@ -1,4 +1,3 @@
dist
node_modules
stats.html
relay

View File

@ -2,4 +2,3 @@ node_modules
dist
public/lib
stats.html
relay

View File

@ -35,6 +35,7 @@
"@webscopeio/react-textarea-autocomplete": "^4.9.2",
"bech32": "^2.0.0",
"blurhash": "^2.0.5",
"chart.js": "^4.4.1",
"cheerio": "^1.0.0-rc.12",
"chroma-js": "^2.4.2",
"dayjs": "^1.11.9",
@ -55,6 +56,7 @@
"ngeohash": "^0.6.3",
"nostr-tools": "^1.17.0",
"react": "^18.2.0",
"react-chartjs-2": "^5.2.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.0.11",
"react-force-graph-2d": "^1.25.1",

View File

@ -199,7 +199,7 @@ export default class Relay {
}
} catch (e) {
console.log(`Relay: Failed to parse event from ${this.url}`);
console.log(event.data);
console.log(event.data, e);
}
}
}

View File

@ -8,7 +8,10 @@ import ReactionGroupButton from "./reaction-group-button";
import { useAddReaction } from "./common-hooks";
import { ButtonProps } from "@chakra-ui/react";
export default function SimpleDislikeButton({ event, ...props }: Omit<ButtonProps, "children"> & { event: NostrEvent }) {
export default function SimpleDislikeButton({
event,
...props
}: Omit<ButtonProps, "children"> & { event: NostrEvent }) {
const account = useCurrentAccount();
const reactions = useEventReactions(event.id) ?? [];
const grouped = useMemo(() => groupReactions(reactions), [reactions]);

View File

@ -19,7 +19,7 @@ import {
} from "../icons";
import useCurrentAccount from "../../hooks/use-current-account";
import accountService from "../../services/account";
import { useKeyPressEvent, useLocalStorage } from "react-use";
import { useLocalStorage } from "react-use";
import ZapModal from "../event-zap-modal";
import PuzzlePiece01 from "../icons/puzzle-piece-01";
import Package from "../icons/package";

View File

@ -1,14 +1,14 @@
import dayjs from "dayjs";
import SuperMap from "../classes/super-map";
import { NostrEvent } from "../types/nostr-event";
import { getReferences, sortByDate } from "./nostr/events";
export function groupByDay(events: NostrEvent[]) {
const DAY_IN_SECONDS = 60 * 60 * 24;
export function groupByTime(events: NostrEvent[], time = DAY_IN_SECONDS) {
const grouped = new SuperMap<number, NostrEvent[]>(() => []);
for (const event of events) {
const day = dayjs.unix(event.created_at).startOf("day").unix();
grouped.get(day).push(event);
const slot = Math.floor(event.created_at / time) * time;
grouped.get(slot).push(event);
}
return Array.from(grouped.entries()).sort((a, b) => b[0] - a[0]);

View File

@ -8,7 +8,7 @@ export type Kind0ParsedContent = {
display_name?: string;
about?: string;
/** @deprecated */
image?: string
image?: string;
picture?: string;
banner?: string;
website?: string;

View File

@ -46,7 +46,7 @@ function Feed({ list, ...props }: { list: NostrEvent } & Omit<CardProps, "childr
);
}
export default function FeedsCard({...props}: Omit<CardProps,'children'>) {
export default function FeedsCard({ ...props }: Omit<CardProps, "children">) {
const account = useCurrentAccount();
const contacts = useUserContactList(account?.pubkey);
const myLists = useUserLists(account?.pubkey).filter((list) => list.kind === PEOPLE_LIST_KIND);

View File

@ -18,7 +18,7 @@ import VerticalPageLayout from "../../components/vertical-page-layout";
import NotificationItem from "./notification-item";
import NotificationTypeToggles from "./notification-type-toggles";
import { NostrEvent } from "../../types/nostr-event";
import { groupByDay } from "../../helpers/notification";
import { groupByTime } from "../../helpers/notification";
import DayGroup from "./components/day-group";
import { useThrottle } from "react-use";
import TimelineLoader from "../../classes/timeline-loader";
@ -75,7 +75,7 @@ const NotificationsTimeline = memo(
}),
[throttledEvents, peoplePubkeys, showReplies, showMentions, showReactions, showReposts, showZaps],
);
const sortedDays = useMemo(() => groupByDay(filteredEvents), [filteredEvents]);
const sortedDays = useMemo(() => groupByTime(filteredEvents), [filteredEvents]);
return (
<>

View File

@ -1,3 +1,4 @@
import { lazy } from "react";
import { useParams } from "react-router-dom";
import {
Button,
@ -25,6 +26,7 @@ import PeopleListProvider from "../../../providers/local/people-list-provider";
import PeopleListSelection from "../../../components/people-list-selection/people-list-selection";
import { RelayFavicon } from "../../../components/relay-favicon";
import VerticalPageLayout from "../../../components/vertical-page-layout";
const RelayDetailsTab = lazy(() => import("./relay-details"));
function RelayPage({ relay }: { relay: string }) {
const { info } = useRelayInfo(relay);
@ -69,6 +71,7 @@ function RelayPage({ relay }: { relay: string }) {
<TabList overflowX="auto" overflowY="hidden" flexShrink={0}>
<Tab>Reviews</Tab>
<Tab>Notes</Tab>
<Tab>Details</Tab>
</TabList>
<TabPanels>
@ -87,6 +90,9 @@ function RelayPage({ relay }: { relay: string }) {
<TabPanel py="2" px="0">
<RelayNotes relay={relay} />
</TabPanel>
<TabPanel py="2" px="0">
<RelayDetailsTab relay={relay} />
</TabPanel>
</TabPanels>
</Tabs>
</VerticalPageLayout>

View File

@ -0,0 +1,145 @@
import { Button, Card, Flex, Heading, Text, useColorModeValue, useForceUpdate, useTheme } from "@chakra-ui/react";
import {
Chart as ChartJS,
ArcElement,
Tooltip,
Legend,
ChartData,
Colors,
Title,
LineElement,
PointElement,
LinearScale,
CategoryScale,
} from "chart.js";
import { Line, Pie } from "react-chartjs-2";
import dayjs from "dayjs";
import _throttle from "lodash.throttle";
import { useAppTitle } from "../../../hooks/use-app-title";
import VerticalPageLayout from "../../../components/vertical-page-layout";
import { NostrEvent } from "../../../types/nostr-event";
import { groupByTime } from "../../../helpers/notification";
import { useEffect, useMemo } from "react";
import EventStore from "../../../classes/event-store";
import NostrRequest from "../../../classes/nostr-request";
import { sortByDate } from "../../../helpers/nostr/events";
ChartJS.register(
ArcElement,
Tooltip,
Legend,
Colors,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
);
function buildPieChartData(events: NostrEvent[]) {
const countByKind: Record<number, number> = {};
for (const event of events) {
if (countByKind[event.kind] === undefined) countByKind[event.kind] = 0;
countByKind[event.kind]++;
}
const sortedKinds = Object.entries(countByKind)
.map(([kind, count]) => ({ kind, count }))
.sort((a, b) => b.count - a.count);
const data: ChartData<"pie", number[], string> = {
labels: sortedKinds.map(({ kind }) => String(kind)),
datasets: [{ label: "# of events", data: sortedKinds.map(({ count }) => count) }],
};
return data;
}
function buildLineChartData(events: NostrEvent[], timeBlock = 60 * 60): ChartData<"line", number[], string> {
let minDate = Infinity;
let maxDate = -Infinity;
const byKind: Record<number, NostrEvent[]> = {};
for (const event of events) {
byKind[event.kind] = byKind[event.kind] || [];
byKind[event.kind].push(event);
if (event.created_at < minDate) minDate = Math.floor(event.created_at / timeBlock) * timeBlock;
if (event.created_at > maxDate) maxDate = Math.ceil(event.created_at / timeBlock) * timeBlock;
}
if (minDate === Infinity || maxDate === -Infinity) return { labels: [], datasets: [] };
const byKindAndDate: Record<string, Record<number, NostrEvent[]>> = {};
for (const [kind, eventsByKind] of Object.entries(byKind)) {
const byTime: Record<number, NostrEvent[]> = groupByTime(eventsByKind, timeBlock).reduce(
(dir, group) => ({ ...dir, [group[0]]: group[1] }),
{},
);
for (let i = minDate; i < maxDate; i += timeBlock) {
if (!byTime[i]) byTime[i] = [];
}
byKindAndDate[kind] = byTime;
}
const sorted = Object.entries(byKindAndDate)
.map(([kind, data]) => ({ kind: parseInt(kind), data }))
.sort((a, b) => b.kind - a.kind);
return {
labels: Array((maxDate - minDate) / timeBlock).fill(""),
datasets: sorted.map(({ data, kind }) => ({
label: String(kind),
data: Object.entries(data)
.sort((a, b) => parseInt(b[0]) - parseInt(a[0]))
.map((d) => d[1].length),
})),
};
}
export default function RelayDetailsTab({ relay }: { relay: string }) {
useAppTitle(`${relay} - Details`);
const theme = useTheme();
const token = theme.semanticTokens.colors["chakra-body-text"];
const color = useColorModeValue(token._light, token._dark) as string;
const update = useForceUpdate();
const store = useMemo(() => new EventStore(), []);
useEffect(() => {
const request = new NostrRequest([relay]);
request.onEvent.subscribe(store.addEvent, store);
const throttle = _throttle(update, 100);
request.onEvent.subscribe(() => throttle());
request.start({ limit: 500 });
}, [relay, update]);
const events = Array.from(store.events.values()).sort(sortByDate);
return (
<VerticalPageLayout>
<Flex gap="2" alignItems="center">
<Text>Events loaded: {events.length}</Text>
</Flex>
<Flex wrap="wrap" gap="4">
<Card p="2" maxW="sm">
<Heading size="sm">Events by kind</Heading>
<Pie
data={buildPieChartData(events)}
options={{
color,
}}
/>
</Card>
<Card p="2" w="full" aspectRatio={16 / 9}>
<Heading size="sm">Event kinds over time</Heading>
<Line data={buildLineChartData(events)} options={{ color, responsive: true }} />
</Card>
</Flex>
</VerticalPageLayout>
);
}

View File

@ -2319,6 +2319,11 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@kurkle/color@^0.3.0":
version "0.3.2"
resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.2.tgz#5acd38242e8bde4f9986e7913c8fdf49d3aa199f"
integrity sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==
"@lightninglabs/lnc-core@0.2.8-alpha":
version "0.2.8-alpha"
resolved "https://registry.yarnpkg.com/@lightninglabs/lnc-core/-/lnc-core-0.2.8-alpha.tgz#78272c04a5ec95a9ccb830f75ab9b5ca227f0801"
@ -3353,6 +3358,13 @@ chardet@^0.7.0:
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
chart.js@^4.4.1:
version "4.4.1"
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.4.1.tgz#ac5dc0e69a7758909158a96fe80ce43b3bb96a9f"
integrity sha512-C74QN1bxwV1v2PEujhmKjOZ7iUM4w6BWs23Md/6aOZZSlwMzeCIDGuZay++rBgChYru7/+QFeoQW0fQoP534Dg==
dependencies:
"@kurkle/color" "^0.3.0"
cheerio-select@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4"
@ -5553,6 +5565,11 @@ rdndmb-html5-to-touch@^8.0.0:
react-dnd-html5-backend "^16.0.1"
react-dnd-touch-backend "^16.0.1"
react-chartjs-2@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz#43c1e3549071c00a1a083ecbd26c1ad34d385f5d"
integrity sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==
react-clientside-effect@^1.2.6:
version "1.2.6"
resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz#29f9b14e944a376b03fb650eed2a754dd128ea3a"