mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-09 20:29:17 +02:00
add chart.js for random charts
This commit is contained in:
parent
b17c0825c6
commit
9eb219917f
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,3 @@
|
||||
dist
|
||||
node_modules
|
||||
stats.html
|
||||
relay
|
||||
|
@ -2,4 +2,3 @@ node_modules
|
||||
dist
|
||||
public/lib
|
||||
stats.html
|
||||
relay
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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]);
|
||||
|
@ -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";
|
||||
|
@ -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]);
|
||||
|
@ -8,7 +8,7 @@ export type Kind0ParsedContent = {
|
||||
display_name?: string;
|
||||
about?: string;
|
||||
/** @deprecated */
|
||||
image?: string
|
||||
image?: string;
|
||||
picture?: string;
|
||||
banner?: string;
|
||||
website?: string;
|
||||
|
@ -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);
|
||||
|
@ -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 (
|
||||
<>
|
||||
|
@ -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>
|
||||
|
145
src/views/relays/relay/relay-details.tsx
Normal file
145
src/views/relays/relay/relay-details.tsx
Normal 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>
|
||||
);
|
||||
}
|
17
yarn.lock
17
yarn.lock
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user