diff --git a/app/network/metrics/components/CumulativeMetricCard.tsx b/app/network/metrics/components/CumulativeMetricCard.tsx
index 806d755271..eaeff908f2 100644
--- a/app/network/metrics/components/CumulativeMetricCard.tsx
+++ b/app/network/metrics/components/CumulativeMetricCard.tsx
@@ -9,6 +9,7 @@ import {
formatNumberWithCommas,
formatPercent,
} from "../utils/formatNumbers";
+import MetricSparkline from "./MetricSparkline";
interface CumulativeMetricCardProps {
readonly title: string;
@@ -19,6 +20,9 @@ interface CumulativeMetricCardProps {
readonly accentColor: string;
readonly unit?: string;
readonly href?: string;
+ readonly sparklineData?: number[] | undefined;
+ readonly sparklineColor?: string | undefined;
+ readonly sparklineDates?: number[] | undefined;
}
function ChangeRow({
@@ -69,6 +73,9 @@ export default function CumulativeMetricCard({
accentColor,
unit = "",
href,
+ sparklineData,
+ sparklineColor,
+ sparklineDates,
}: CumulativeMetricCardProps) {
const total = dailyData.current.valueCount;
const daily24hChange =
@@ -78,7 +85,7 @@ export default function CumulativeMetricCard({
const content = (
@@ -143,6 +150,13 @@ export default function CumulativeMetricCard({
/>
+ {sparklineData && sparklineColor && (
+
+ )}
);
@@ -150,7 +164,7 @@ export default function CumulativeMetricCard({
return (
{content}
diff --git a/app/network/metrics/components/MetricCard.tsx b/app/network/metrics/components/MetricCard.tsx
index 39b13c5755..4673cc42cb 100644
--- a/app/network/metrics/components/MetricCard.tsx
+++ b/app/network/metrics/components/MetricCard.tsx
@@ -8,6 +8,7 @@ import {
formatNumberWithCommas,
formatPercent,
} from "../utils/formatNumbers";
+import MetricSparkline from "./MetricSparkline";
interface MetricCardProps {
readonly title: string;
@@ -19,6 +20,9 @@ interface MetricCardProps {
readonly useValueCount?: boolean;
readonly suffix?: string | undefined;
readonly href?: string;
+ readonly sparklineData?: number[] | undefined;
+ readonly sparklineColor?: string | undefined;
+ readonly sparklineDates?: number[] | undefined;
}
function StatBlock({
@@ -97,6 +101,9 @@ export default function MetricCard({
useValueCount = false,
suffix,
href,
+ sparklineData,
+ sparklineColor,
+ sparklineDates,
}: MetricCardProps) {
const getCount = (data: MetricData, period: "current" | "previous") =>
useValueCount ? data[period].valueCount : data[period].eventCount;
@@ -106,7 +113,7 @@ export default function MetricCard({
const content = (
@@ -144,6 +151,13 @@ export default function MetricCard({
suffix={suffix}
/>
+ {sparklineData && sparklineColor && (
+
+ )}
);
@@ -151,7 +165,7 @@ export default function MetricCard({
return (
{content}
diff --git a/app/network/metrics/components/MetricSparkline.tsx b/app/network/metrics/components/MetricSparkline.tsx
new file mode 100644
index 0000000000..11f45acbb5
--- /dev/null
+++ b/app/network/metrics/components/MetricSparkline.tsx
@@ -0,0 +1,49 @@
+import CustomTooltip from "@/components/utils/tooltip/CustomTooltip";
+import { formatCompactNumber } from "../utils/formatNumbers";
+
+interface MetricSparklineProps {
+ readonly data: number[];
+ readonly color: string;
+ readonly dates?: number[];
+}
+
+function formatDate(timestamp: number): string {
+ return new Date(timestamp).toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ });
+}
+
+export default function MetricSparkline({
+ data,
+ color,
+ dates,
+}: MetricSparklineProps) {
+ if (data.length === 0) return null;
+
+ const maxValue = Math.max(...data);
+ const reversed = [...data].reverse();
+ const reversedDates = dates ? [...dates].reverse() : undefined;
+
+ return (
+
+ {reversed.map((value, index) => {
+ const height = maxValue > 0 ? (value / maxValue) * 100 : 0;
+ const date = reversedDates?.[index];
+ const tooltipContent =
+ typeof date === "number"
+ ? `${formatDate(date)}: ${formatCompactNumber(value)}`
+ : formatCompactNumber(value);
+
+ return (
+
+
+
+ );
+ })}
+
+ );
+}
diff --git a/app/network/metrics/page.client.tsx b/app/network/metrics/page.client.tsx
index 19fac3ffd1..255e313215 100644
--- a/app/network/metrics/page.client.tsx
+++ b/app/network/metrics/page.client.tsx
@@ -2,6 +2,7 @@
import { useSetTitle } from "@/contexts/TitleContext";
import { useCommunityMetrics } from "@/hooks/useCommunityMetrics";
+import { useCommunityMetricsSeries } from "@/hooks/useCommunityMetricsSeries";
import { useMintMetrics } from "@/hooks/useMintMetrics";
import CumulativeMetricCard from "./components/CumulativeMetricCard";
import MetricCard from "./components/MetricCard";
@@ -30,10 +31,12 @@ export default function MetricsPageClient() {
const dailyQuery = useCommunityMetrics("DAY");
const weeklyQuery = useCommunityMetrics("WEEK");
const mintQuery = useMintMetrics(50);
+ const seriesQuery = useCommunityMetricsSeries();
const isLoading =
dailyQuery.isLoading || weeklyQuery.isLoading || mintQuery.isLoading;
const error = dailyQuery.error ?? weeklyQuery.error ?? mintQuery.error;
+ const series = seriesQuery.data;
return (
@@ -68,6 +71,9 @@ export default function MetricsPageClient() {
icon={}
iconBgColor="tw-bg-purple-500"
accentColor="tw-text-purple-400"
+ sparklineData={series?.distinctDroppers}
+ sparklineColor="tw-bg-purple-500"
+ sparklineDates={series?.stepsStartTimes}
/>
}
iconBgColor="tw-bg-blue-500"
accentColor="tw-text-blue-400"
+ sparklineData={series?.dropsCreated}
+ sparklineColor="tw-bg-blue-500"
+ sparklineDates={series?.stepsStartTimes}
/>
}
iconBgColor="tw-bg-orange-500"
accentColor="tw-text-orange-400"
+ sparklineData={series?.mainStageSubmissions}
+ sparklineColor="tw-bg-orange-500"
+ sparklineDates={series?.stepsStartTimes}
/>
}
iconBgColor="tw-bg-emerald-500"
accentColor="tw-text-emerald-400"
+ sparklineData={series?.mainStageDistinctVoters}
+ sparklineColor="tw-bg-emerald-500"
+ sparklineDates={series?.stepsStartTimes}
/>
}
iconBgColor="tw-bg-violet-500"
accentColor="tw-text-violet-400"
+ sparklineData={series?.profileCount}
+ sparklineColor="tw-bg-violet-500"
+ sparklineDates={series?.stepsStartTimes}
/>
}
iconBgColor="tw-bg-pink-500"
accentColor="tw-text-pink-400"
+ sparklineData={series?.activeIdentities}
+ sparklineColor="tw-bg-pink-500"
+ sparklineDates={series?.stepsStartTimes}
/>
}
iconBgColor="tw-bg-indigo-500"
accentColor="tw-text-indigo-400"
+ sparklineData={series?.consolidationsFormed}
+ sparklineColor="tw-bg-indigo-500"
+ sparklineDates={series?.stepsStartTimes}
/>
)}
diff --git a/components/react-query-wrapper/ReactQueryWrapper.tsx b/components/react-query-wrapper/ReactQueryWrapper.tsx
index 8be51c9b30..c0eb2855d2 100644
--- a/components/react-query-wrapper/ReactQueryWrapper.tsx
+++ b/components/react-query-wrapper/ReactQueryWrapper.tsx
@@ -102,6 +102,7 @@ export enum QueryKey {
WAVE_OUTCOME_DISTRIBUTION = "WAVE_OUTCOME_DISTRIBUTION",
WAVE_OUTCOME_DISTRIBUTION_PAGE = "WAVE_OUTCOME_DISTRIBUTION_PAGE",
COMMUNITY_METRICS = "COMMUNITY_METRICS",
+ COMMUNITY_METRICS_SERIES = "COMMUNITY_METRICS_SERIES",
MINT_METRICS = "MINT_METRICS",
}
@@ -301,13 +302,11 @@ const createReactQueryContextValue = (
const existingData = queryClient.getQueryData(queryKey);
if (existingData) {
return;
- } else {
- // If there's no existing data, set the initial data
- queryClient.setQueryData>(queryKey, {
+ }
+ queryClient.setQueryData>(queryKey, {
pages: [wavesOverview],
pageParams: [undefined],
});
- }
};
const setWaveDrops = ({
@@ -330,13 +329,11 @@ const createReactQueryContextValue = (
const existingData = queryClient.getQueryData(queryKey);
if (existingData) {
return;
- } else {
- // If there's no existing data, set the initial data
- queryClient.setQueryData>(queryKey, {
- pages: [waveDrops],
- pageParams: [undefined],
- });
}
+ queryClient.setQueryData>(queryKey, {
+ pages: [waveDrops],
+ pageParams: [undefined],
+ });
};
const setProfileProxy = (profileProxy: ApiProfileProxy) => {
@@ -559,7 +556,7 @@ const createReactQueryContextValue = (
rater: rater.toLowerCase(),
});
}
- if (profileProxy?.created_by?.handle && profileProxy.granted_to?.handle) {
+ if (profileProxy?.created_by.handle && profileProxy.granted_to.handle) {
invalidateQueries({
key: QueryKey.PROFILE,
values: [
@@ -571,12 +568,12 @@ const createReactQueryContextValue = (
key: QueryKey.PROFILE_RATERS,
values: [
{
- handleOrWallet: profileProxy.created_by?.handle,
+ handleOrWallet: profileProxy.created_by.handle,
matter: RateMatter.NIC,
given: false,
},
{
- handleOrWallet: profileProxy.granted_to?.handle,
+ handleOrWallet: profileProxy.granted_to.handle,
matter: RateMatter.NIC,
given: false,
},
@@ -643,7 +640,7 @@ const createReactQueryContextValue = (
});
}
- if (profileProxy?.created_by?.handle && profileProxy.granted_to?.handle) {
+ if (profileProxy?.created_by.handle && profileProxy.granted_to.handle) {
invalidateQueries({
key: QueryKey.PROFILE,
values: [
diff --git a/generated/models/ApiCommunityMetricsSeries.ts b/generated/models/ApiCommunityMetricsSeries.ts
new file mode 100644
index 0000000000..fa7078f780
--- /dev/null
+++ b/generated/models/ApiCommunityMetricsSeries.ts
@@ -0,0 +1,114 @@
+// @ts-nocheck
+/**
+ * 6529.io API
+ * This is the API interface description. Brief terminology overview and an authentication example can be found at https://6529.io/about/api.
+ *
+ * OpenAPI spec version: 1.0.0
+ *
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+import { HttpFile } from '../http/http';
+
+export class ApiCommunityMetricsSeries {
+ 'steps_start_times': Array;
+ 'drops_created': Array;
+ 'distinct_droppers': Array;
+ 'main_stage_submissions': Array;
+ 'main_stage_distinct_voters': Array;
+ 'main_stage_votes': Array;
+ 'network_tdh': Array;
+ 'tdh_on_main_stage_submissions': Array;
+ 'consolidations_formed': Array;
+ 'xtdh_granted': Array;
+ 'active_identities': Array;
+ 'profile_count': Array;
+
+ static readonly discriminator: string | undefined = undefined;
+
+ static readonly mapping: {[index: string]: string} | undefined = undefined;
+
+ static readonly attributeTypeMap: Array<{name: string, baseName: string, type: string, format: string}> = [
+ {
+ "name": "steps_start_times",
+ "baseName": "steps_start_times",
+ "type": "Array",
+ "format": "int64"
+ },
+ {
+ "name": "drops_created",
+ "baseName": "drops_created",
+ "type": "Array",
+ "format": "int64"
+ },
+ {
+ "name": "distinct_droppers",
+ "baseName": "distinct_droppers",
+ "type": "Array",
+ "format": "int64"
+ },
+ {
+ "name": "main_stage_submissions",
+ "baseName": "main_stage_submissions",
+ "type": "Array",
+ "format": "int64"
+ },
+ {
+ "name": "main_stage_distinct_voters",
+ "baseName": "main_stage_distinct_voters",
+ "type": "Array",
+ "format": "int64"
+ },
+ {
+ "name": "main_stage_votes",
+ "baseName": "main_stage_votes",
+ "type": "Array",
+ "format": "int64"
+ },
+ {
+ "name": "network_tdh",
+ "baseName": "network_tdh",
+ "type": "Array",
+ "format": "int64"
+ },
+ {
+ "name": "tdh_on_main_stage_submissions",
+ "baseName": "tdh_on_main_stage_submissions",
+ "type": "Array",
+ "format": "int64"
+ },
+ {
+ "name": "consolidations_formed",
+ "baseName": "consolidations_formed",
+ "type": "Array",
+ "format": "int64"
+ },
+ {
+ "name": "xtdh_granted",
+ "baseName": "xtdh_granted",
+ "type": "Array",
+ "format": "int64"
+ },
+ {
+ "name": "active_identities",
+ "baseName": "active_identities",
+ "type": "Array",
+ "format": "int64"
+ },
+ {
+ "name": "profile_count",
+ "baseName": "profile_count",
+ "type": "Array",
+ "format": "int64"
+ } ];
+
+ static getAttributeTypeMap() {
+ return ApiCommunityMetricsSeries.attributeTypeMap;
+ }
+
+ public constructor() {
+ }
+}
diff --git a/generated/models/ObjectSerializer.ts b/generated/models/ObjectSerializer.ts
index 1415ba9d91..1de5ad52dc 100644
--- a/generated/models/ObjectSerializer.ts
+++ b/generated/models/ObjectSerializer.ts
@@ -31,6 +31,7 @@ export * from '../models/ApiCommunityMembersSortOption';
export * from '../models/ApiCommunityMetric';
export * from '../models/ApiCommunityMetricSample';
export * from '../models/ApiCommunityMetrics';
+export * from '../models/ApiCommunityMetricsSeries';
export * from '../models/ApiCompleteMultipartUploadRequest';
export * from '../models/ApiCompleteMultipartUploadRequestPart';
export * from '../models/ApiCompleteMultipartUploadResponse';
@@ -283,6 +284,7 @@ import { ApiCommunityMembersSortOption } from '../models/ApiCommunityMembersSort
import { ApiCommunityMetric } from '../models/ApiCommunityMetric';
import { ApiCommunityMetricSample } from '../models/ApiCommunityMetricSample';
import { ApiCommunityMetrics } from '../models/ApiCommunityMetrics';
+import { ApiCommunityMetricsSeries } from '../models/ApiCommunityMetricsSeries';
import { ApiCompleteMultipartUploadRequest } from '../models/ApiCompleteMultipartUploadRequest';
import { ApiCompleteMultipartUploadRequestPart } from '../models/ApiCompleteMultipartUploadRequestPart';
import { ApiCompleteMultipartUploadResponse } from '../models/ApiCompleteMultipartUploadResponse';
@@ -580,6 +582,7 @@ let typeMap: {[index: string]: any} = {
"ApiCommunityMetric": ApiCommunityMetric,
"ApiCommunityMetricSample": ApiCommunityMetricSample,
"ApiCommunityMetrics": ApiCommunityMetrics,
+ "ApiCommunityMetricsSeries": ApiCommunityMetricsSeries,
"ApiCompleteMultipartUploadRequest": ApiCompleteMultipartUploadRequest,
"ApiCompleteMultipartUploadRequestPart": ApiCompleteMultipartUploadRequestPart,
"ApiCompleteMultipartUploadResponse": ApiCompleteMultipartUploadResponse,
diff --git a/hooks/useCommunityMetricsSeries.ts b/hooks/useCommunityMetricsSeries.ts
new file mode 100644
index 0000000000..5451970649
--- /dev/null
+++ b/hooks/useCommunityMetricsSeries.ts
@@ -0,0 +1,78 @@
+import { useQuery } from "@tanstack/react-query";
+
+import { QueryKey } from "@/components/react-query-wrapper/ReactQueryWrapper";
+import type { ApiCommunityMetricsSeries } from "@/generated/models/ApiCommunityMetricsSeries";
+import { commonApiFetch } from "@/services/api/common-api";
+
+const DAYS_31_MS = 31 * 24 * 60 * 60 * 1000;
+
+export interface CommunityMetricsSeriesData {
+ readonly stepsStartTimes: number[];
+ readonly dropsCreated: number[];
+ readonly distinctDroppers: number[];
+ readonly mainStageSubmissions: number[];
+ readonly mainStageDistinctVoters: number[];
+ readonly mainStageVotes: number[];
+ readonly networkTdh: number[];
+ readonly tdhOnMainStageSubmissions: number[];
+ readonly consolidationsFormed: number[];
+ readonly xtdhGranted: number[];
+ readonly activeIdentities: number[];
+ readonly profileCount: number[];
+ readonly tdhUtilizationPercentage: number[];
+}
+
+function calculateTdhUtilizationPercentage(
+ tdhOnMainStage: number[],
+ networkTdh: number[]
+): number[] {
+ return tdhOnMainStage.map((tdh, i) => {
+ const network = networkTdh[i] ?? 0;
+ return network > 0 ? (tdh / network) * 100 : 0;
+ });
+}
+
+async function fetchCommunityMetricsSeries(): Promise {
+ const now = Date.now();
+ const since = now - DAYS_31_MS;
+
+ const response = await commonApiFetch({
+ endpoint: "community-metrics/series",
+ params: {
+ since: since.toString(),
+ to: now.toString(),
+ },
+ });
+
+ return {
+ stepsStartTimes: response.steps_start_times,
+ dropsCreated: response.drops_created,
+ distinctDroppers: response.distinct_droppers,
+ mainStageSubmissions: response.main_stage_submissions,
+ mainStageDistinctVoters: response.main_stage_distinct_voters,
+ mainStageVotes: response.main_stage_votes,
+ networkTdh: response.network_tdh,
+ tdhOnMainStageSubmissions: response.tdh_on_main_stage_submissions,
+ consolidationsFormed: response.consolidations_formed,
+ xtdhGranted: response.xtdh_granted,
+ activeIdentities: response.active_identities,
+ profileCount: response.profile_count,
+ tdhUtilizationPercentage: calculateTdhUtilizationPercentage(
+ response.tdh_on_main_stage_submissions,
+ response.network_tdh
+ ),
+ };
+}
+
+export function useCommunityMetricsSeries() {
+ return useQuery({
+ queryKey: [QueryKey.COMMUNITY_METRICS_SERIES],
+ queryFn: fetchCommunityMetricsSeries,
+ staleTime: 60_000,
+ gcTime: 300_000,
+ retry: 3,
+ retryDelay: (attemptIndex) => Math.min(1_000 * 2 ** attemptIndex, 30_000),
+ refetchOnWindowFocus: false,
+ refetchOnMount: true,
+ });
+}
diff --git a/openapi.yaml b/openapi.yaml
index d3c51d5451..57755317b3 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -330,6 +330,34 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/ApiCommunityMetrics"
+ /community-metrics/series:
+ get:
+ tags:
+ - CommunityMetrics
+ summary: Get community metrics series.
+ operationId: getCommunityMetricsSeries
+ parameters:
+ - name: since
+ in: query
+ description: Unix millis timestamp for start of series.
+ required: true
+ schema:
+ type: number
+ format: int64
+ - name: to
+ in: query
+ description: Unix millis timestamp for end of series.
+ required: true
+ schema:
+ type: number
+ format: int64
+ responses:
+ "200":
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ApiCommunityMetricsSeries"
/community-metrics/mints:
get:
tags:
@@ -5932,6 +5960,82 @@ components:
type: number
value_count:
type: number
+ ApiCommunityMetricsSeries:
+ type: object
+ required:
+ - steps_start_times
+ - drops_created
+ - distinct_droppers
+ - main_stage_submissions
+ - main_stage_distinct_voters
+ - main_stage_votes
+ - network_tdh
+ - tdh_on_main_stage_submissions
+ - consolidations_formed
+ - xtdh_granted
+ - active_identities
+ - profile_count
+ properties:
+ steps_start_times:
+ type: array
+ items:
+ type: number
+ format: int64
+ drops_created:
+ type: array
+ items:
+ type: number
+ format: int64
+ distinct_droppers:
+ type: array
+ items:
+ type: number
+ format: int64
+ main_stage_submissions:
+ type: array
+ items:
+ type: number
+ format: int64
+ main_stage_distinct_voters:
+ type: array
+ items:
+ type: number
+ format: int64
+ main_stage_votes:
+ type: array
+ items:
+ type: number
+ format: int64
+ network_tdh:
+ type: array
+ items:
+ type: number
+ format: int64
+ tdh_on_main_stage_submissions:
+ type: array
+ items:
+ type: number
+ format: int64
+ consolidations_formed:
+ type: array
+ items:
+ type: number
+ format: int64
+ xtdh_granted:
+ type: array
+ items:
+ type: number
+ format: int64
+ active_identities:
+ type: array
+ items:
+ type: number
+ format: int64
+ profile_count:
+ type: array
+ items:
+ type: number
+ format: int64
ApiCompleteMultipartUploadRequest:
required:
- upload_id