From 4c9760e71d9a4fa682e68883d946c08af79ef62c Mon Sep 17 00:00:00 2001 From: Manish Gupta Date: Thu, 20 Feb 2025 12:26:54 -0800 Subject: [PATCH] [wip] apps - details agg data bar tooltip --- .../applications/[owner_project]/page.tsx | 174 ++++++++++++++++-- .../applications/_components/GTPChart.tsx | 151 ++++++++------- 2 files changed, 237 insertions(+), 88 deletions(-) diff --git a/app/(layout)/applications/[owner_project]/page.tsx b/app/(layout)/applications/[owner_project]/page.tsx index a84fd883..fe5555b2 100644 --- a/app/(layout)/applications/[owner_project]/page.tsx +++ b/app/(layout)/applications/[owner_project]/page.tsx @@ -26,6 +26,7 @@ import { useRouter } from "next/navigation"; import { AggregatedDataRow, useApplicationsData } from "../_contexts/ApplicationsDataContext"; import useDragScroll from "@/hooks/useDragScroll"; import { Sources } from "@/lib/datasources"; +import moment from "moment"; type Props = { params: { owner_project: string }; @@ -35,6 +36,7 @@ export default function Page({ params: { owner_project } }: Props) { const { ownerProjectToProjectData } = useProjectsMetadata(); const { data: master } = useMaster(); const { selectedMetrics } = useMetrics(); + const { selectedTimespan, timespans } = useTimespan(); const SourcesDisplay = useMemo(() => { if (!master) @@ -94,7 +96,7 @@ export default function Page({ params: { owner_project } }: Props) {
Most Active Contracts
- See the most active contracts within the selected timeframe (Maximum) for 1inch. + See the most active contracts within the selected timeframe ({timespans[selectedTimespan].label}){ownerProjectToProjectData[owner_project] ? ` for ${ownerProjectToProjectData[owner_project].display_name}` : ""}.
@@ -209,6 +211,8 @@ const MetricSection = ({ metric, owner_project }: { metric: string; owner_projec interface FloatingTooltipProps { content: React.ReactNode; + containerClassName?: string; + // width?: number; offsetX?: number; offsetY?: number; children: React.ReactNode; @@ -216,8 +220,10 @@ interface FloatingTooltipProps { const FloatingTooltip: React.FC = ({ content, - offsetX = 20, - offsetY = 20, + containerClassName, + // width = 280, + offsetX = 10, + offsetY = 10, children, }) => { const [visible, setVisible] = useState(false); @@ -240,19 +246,30 @@ const FloatingTooltip: React.FC = ({ let newY = coords.y; // Prevent overflow on the right edge. if (coords.x + tooltipRect.width > window.innerWidth) { - newX = window.innerWidth - tooltipRect.width - 10; + newX = window.innerWidth - tooltipRect.width - 20; } // Prevent overflow on the bottom edge. if (coords.y + tooltipRect.height > window.innerHeight) { - newY = window.innerHeight - tooltipRect.height - 10; + newY = window.innerHeight - tooltipRect.height - 20; } + + // Prevent overflow on the left edge. + if (coords.x < 0) { + newX = 0 + 20; + } + + // Prevent overflow on the top edge. + if (coords.y < 0) { + newY = 0 + 20; + } + setAdjustedCoords({ x: newX, y: newY }); } }, [coords, visible]); return (
setVisible(true)} onMouseLeave={() => setVisible(false)} onMouseMove={handleMouseMove} @@ -264,8 +281,9 @@ const FloatingTooltip: React.FC = ({ style={{ left: adjustedCoords.x, top: adjustedCoords.y, + // width: width, }} - className="fixed mt-3 mr-3 mb-3 w-52 md:w-60 text-xs font-raleway bg-[#2A3433EE] text-white rounded-[17px] px-3 py-2 shadow-lg pointer-events-none z-50" + className="fixed mt-3 mr-3 mb-3 text-xs font-raleway bg-[#2A3433EE] text-white rounded-[17px] shadow-lg pointer-events-none z-50" > {content}
@@ -307,7 +325,7 @@ const blendColors = (color1: string, color2: string, percentage: number): string const MetricChainBreakdownBar = ({ metric }: { metric: string }) => { const { data, owner_project } = useApplicationDetailsData(); const { ownerProjectToProjectData } = useProjectsMetadata(); - const { selectedTimespan } = useTimespan(); + const { selectedTimespan, timespans } = useTimespan(); const { AllChainsByKeys } = useMaster(); const { metricsDef } = useMetrics(); const [showUsd, setShowUsd] = useLocalStorage("showUsd", true); @@ -368,6 +386,7 @@ const MetricChainBreakdownBar = ({ metric }: { metric: string }) => { const metricData = data.metrics[metric] as MetricData; + const firstSeenOn = data.first_seen; // filter out chains with 0 value const chainsData = Object.entries(metricData.aggregated.data).filter(([chain, valsByTimespan]) => valsByTimespan[selectedTimespan][metricData.aggregated.types.indexOf(valueKey)] > 0) // sort by chain asc @@ -381,6 +400,10 @@ const MetricChainBreakdownBar = ({ metric }: { metric: string }) => { return [...acc, prev + v]; }, [] as number[]); + const maxUnix = Math.max(...Object.values(metricData.over_time).map((chainData) => chainData.daily.data[chainData.daily.data.length - 1][0])); + const minUnix = Math.min(...Object.values(metricData.over_time).map((chainData) => chainData.daily.data[0][0])); + const maxAggValue = Math.max(...Object.values(metricData.aggregated.data).map((chainData) => chainData[selectedTimespan][metricData.aggregated.types.indexOf(valueKey)])); + const getBarColor = useCallback((chain: string) => { @@ -402,6 +425,74 @@ const MetricChainBreakdownBar = ({ metric }: { metric: string }) => { console.log("cumulativePercentages", cumulativePercentages); + // show all chains in the tooltip + const allTooltipContent = useMemo(() => { + const maxDate = moment.unix(maxUnix/1000).utc().toDate().toLocaleString("en-GB", { + year: "numeric", + month: "short", + day: "numeric", + }); + + let minDate = moment.unix(maxUnix/1000).subtract(timespans[selectedTimespan].value, "days").utc().toDate().toLocaleString("en-GB", { + year: "numeric", + month: "short", + day: "numeric", + }); + + if(selectedTimespan === "max"){ + minDate = moment.unix(minUnix/1000).utc(false).toDate().toLocaleString("en-GB", { + year: "numeric", + month: "short", + day: "numeric", + }); + } + + return ( +
+
+ {/*
On {AllChainsByKeys[chain].label}
*/} +
{minDate} - {maxDate}
+
{metricsDef[metric].name}
+
+
+ {[...chainsData].sort( + ([, a], [, b]) => b[selectedTimespan][metricData.aggregated.types.indexOf(valueKey)] - a[selectedTimespan][metricData.aggregated.types.indexOf(valueKey)] + ).map(([chain, valsByTimespan], i) => { + const value = valsByTimespan[selectedTimespan][metricData.aggregated.types.indexOf(valueKey)]; + + return ( + <> +
+
+
{AllChainsByKeys[chain].label}
+
+ {prefix}{valsByTimespan[selectedTimespan][metricData.aggregated.types.indexOf(valueKey)].toLocaleString("en-GB", { maximumFractionDigits: decimals })} +
+
+
+
+
+
+ + ) + })} +
+
+
Total
+
+ {prefix}{total.toLocaleString("en-GB", { maximumFractionDigits: decimals })} +
+
+
+
+ ); + + }, [chainsData, metricData, selectedTimespan, valueKey, prefix, decimals, ownerProjectToProjectData, maxUnix, AllChainsByKeys]); + if (!metricData) { return null; } @@ -410,6 +501,7 @@ const MetricChainBreakdownBar = ({ metric }: { metric: string }) => {
+
@@ -417,6 +509,7 @@ const MetricChainBreakdownBar = ({ metric }: { metric: string }) => {
{ownerProjectToProjectData[owner_project] && ownerProjectToProjectData[owner_project].display_name || ""}
+
{chainsData.map(([chain, values], i) => { // Determine whether this bar is hovered. @@ -440,21 +533,62 @@ const MetricChainBreakdownBar = ({ metric }: { metric: string }) => { let thisPercentageWidth = thisPercentage + (i === 0 ? 0 : lastPercentagesTotal); const thisRenderWidth = (thisPercentageWidth / 100) * (containerWidth - 200); + //convert incoming date (UTC) to timestamp + const firstSeen = moment.utc(firstSeenOn[chain]); + const maxDate = moment.unix(maxUnix/1000).utc().toDate().toLocaleString("en-GB", { + year: "numeric", + month: "short", + day: "numeric", + }); + + let minDate = (moment.unix(maxUnix/1000).subtract(timespans[selectedTimespan].value, "days")).utc().toDate().toLocaleString("en-GB", { + year: "numeric", + month: "short", + day: "numeric", + }); + + if(selectedTimespan === "max"){ + minDate = moment.unix(minUnix/1000).utc().toDate().toLocaleString("en-GB", { + year: "numeric", + month: "short", + day: "numeric", + }); + } + const tooltipContent = ( - <> -
-
-
{AllChainsByKeys[chain].label}
-
-
- {prefix}{values[selectedTimespan][metricData.aggregated.types.indexOf(valueKey)].toLocaleString("en-GB", { maximumFractionDigits: 2 })} -
+
+
+ {/*
On {AllChainsByKeys[chain].label}
*/} +
{minDate} - {maxDate}
+
{metricsDef[metric].name}
+
+
+ {/*
Timeframe: {minDate} - {maxDate}
*/} +
+
+
{AllChainsByKeys[chain].label}
+
+ {prefix}{values[selectedTimespan][metricData.aggregated.types.indexOf(valueKey)].toLocaleString("en-GB", { maximumFractionDigits: 2 })} +
+
+
+
+ {/*
On {AllChainsByKeys[chain].label}
*/} +
+ First seen on {firstSeen.utc().toDate().toLocaleString("en-GB", { + year: "numeric", + month: "short", + day: "numeric", + // hour: "numeric", + // minute: "numeric", + // second: "numeric", + })}
- +
); return ( diff --git a/app/(layout)/applications/_components/GTPChart.tsx b/app/(layout)/applications/_components/GTPChart.tsx index 3c0fdbfe..0148f46a 100644 --- a/app/(layout)/applications/_components/GTPChart.tsx +++ b/app/(layout)/applications/_components/GTPChart.tsx @@ -50,6 +50,8 @@ import { baseOptions, tooltipFormatter } from "@/lib/chartUtils"; import { useTimespan } from "../_contexts/TimespanContext"; import { useChartScale } from "../_contexts/ChartScaleContext"; import { useMetrics } from "../_contexts/MetricsContext"; +import { MetricData, useApplicationDetailsData } from "../_contexts/ApplicationDetailsDataContext"; +import moment from "moment"; const COLORS = { GRID: "rgb(215, 223, 222)", @@ -72,84 +74,94 @@ type SeriesData = { export const ApplicationDetailsChart = ({ seriesData, seriesTypes, metric, prefix, suffix, decimals}: { seriesData: SeriesData[], seriesTypes: string[], metric: string, prefix: string, suffix: string, decimals: number }) => { const { selectedScale, selectedYAxisScale } = useChartScale(); + const { data } = useApplicationDetailsData(); const { metricsDef } = useMetrics(); const [showUsd] = useLocalStorage("showUsd", true); const [showGwei] = useLocalStorage("showGwei", false); + const { selectedTimespan, timespans } = useTimespan(); -const formatNumber = useCallback((value: number | string, options: { - isAxis: boolean; - // prefix: string; - // suffix: string - // seriesTypes: string[]; - selectedScale: string; -}) => { - const { isAxis, selectedScale } = options; - // let prefix = valuePrefix; - // let suffix = ""; - let val = parseFloat(value as string); - const metricDef = metricsDef[metric]; - const units = metricDef.units; - const unitKeys = Object.keys(units); - const unitKey = - unitKeys.find((unit) => unit !== "usd" && unit !== "eth") || - (showUsd ? "usd" : "eth"); - - let prefix = metricDef.units[unitKey].prefix - ? metricDef.units[unitKey].prefix - : ""; - let suffix = metricDef.units[unitKey].suffix - ? metricDef.units[unitKey].suffix - : ""; - - if ( - !showUsd && - seriesTypes.includes("eth") && - selectedScale !== "percentage" - ) { - if (showGwei) { - prefix = ""; - suffix = " Gwei"; + // const metricData = data.metrics[metric] as MetricData; + + // const chainsData = Object.entries(metricData.aggregated.data); + // const maxUnix = Math.max(...Object.values(metricData.over_time).map((chainData) => chainData.daily.data[chainData.daily.data.length - 1][0])); + // const minUnix = Math.min(...Object.values(metricData.over_time).map((chainData) => chainData.daily.data[0][0])); + + const maxUnix = Math.max(...seriesData.map((series) => series.data[series.data.length - 1][0])) + + const formatNumber = useCallback((value: number | string, options: { + isAxis: boolean; + // prefix: string; + // suffix: string + // seriesTypes: string[]; + selectedScale: string; + }) => { + const { isAxis, selectedScale } = options; + // let prefix = valuePrefix; + // let suffix = ""; + let val = parseFloat(value as string); + const metricDef = metricsDef[metric]; + const units = metricDef.units; + const unitKeys = Object.keys(units); + const unitKey = + unitKeys.find((unit) => unit !== "usd" && unit !== "eth") || + (showUsd ? "usd" : "eth"); + + let prefix = metricDef.units[unitKey].prefix + ? metricDef.units[unitKey].prefix + : ""; + let suffix = metricDef.units[unitKey].suffix + ? metricDef.units[unitKey].suffix + : ""; + + if ( + !showUsd && + seriesTypes.includes("eth") && + selectedScale !== "percentage" + ) { + if (showGwei) { + prefix = ""; + suffix = " Gwei"; + } } - } - let number = d3Format(`.2~s`)(val).replace(/G/, "B"); - - let absVal = Math.abs(val); - - // let formatStringPrefix = units[unitKey].currency ? "." : "~." - - if (isAxis) { - if (selectedScale === "percentage") { - number = d3Format(".2~s")(val).replace(/G/, "B") + "%"; - } else { - if (prefix || suffix) { - // for small USD amounts, show 2 decimals - if (absVal === 0) number = "0"; - else if (absVal < 1) number = val.toFixed(2); - else if (absVal < 10) - number = units[unitKey].currency ? val.toFixed(2) : - d3Format(`~.3s`)(val).replace(/G/, "B"); - else if (absVal < 100) - number = units[unitKey].currency ? d3Format(`s`)(val).replace(/G/, "B") : - d3Format(`~.4s`)(val).replace(/G/, "B") - else - number = units[unitKey].currency ? d3Format(`s`)(val).replace(/G/, "B") : - d3Format(`~.2s`)(val).replace(/G/, "B"); + let number = d3Format(`.2~s`)(val).replace(/G/, "B"); + + let absVal = Math.abs(val); + + // let formatStringPrefix = units[unitKey].currency ? "." : "~." + + if (isAxis) { + if (selectedScale === "percentage") { + number = d3Format(".2~s")(val).replace(/G/, "B") + "%"; } else { - if (absVal === 0) number = "0"; - else if (absVal < 1) number = val.toFixed(2); - else if (absVal < 10) - d3Format(`.2s`)(val).replace(/G/, "B") - else number = d3Format(`s`)(val).replace(/G/, "B"); + if (prefix || suffix) { + // for small USD amounts, show 2 decimals + if (absVal === 0) number = "0"; + else if (absVal < 1) number = val.toFixed(2); + else if (absVal < 10) + number = units[unitKey].currency ? val.toFixed(2) : + d3Format(`~.3s`)(val).replace(/G/, "B"); + else if (absVal < 100) + number = units[unitKey].currency ? d3Format(`s`)(val).replace(/G/, "B") : + d3Format(`~.4s`)(val).replace(/G/, "B") + else + number = units[unitKey].currency ? d3Format(`s`)(val).replace(/G/, "B") : + d3Format(`~.2s`)(val).replace(/G/, "B"); + } else { + if (absVal === 0) number = "0"; + else if (absVal < 1) number = val.toFixed(2); + else if (absVal < 10) + d3Format(`.2s`)(val).replace(/G/, "B") + else number = d3Format(`s`)(val).replace(/G/, "B"); + } + // for negative values, add a minus sign before the prefix + number = `${prefix}${number} ${suffix}`.replace(`${prefix}-`, `\u2212${prefix}`); } - // for negative values, add a minus sign before the prefix - number = `${prefix}${number} ${suffix}`.replace(`${prefix}-`, `\u2212${prefix}`); } - } - return number; -}, [showUsd, showGwei, seriesTypes]); + return number; + }, [showUsd, showGwei, seriesTypes]); const getPlotOptions: (scale: string) => Highcharts.PlotOptions = (scale) => { @@ -503,6 +515,8 @@ const formatNumber = useCallback((value: number | string, options: { color: "rgb(215, 223, 222)", }, }} + min={(maxUnix - timespans[selectedTimespan].value * 24 * 3600 * 1000) / 1000} + max={maxUnix/1000} /> {seriesData.map((series, i) => { @@ -855,7 +870,7 @@ const GTPChartTooltip = ({props, metric_id} : {props?: TooltipProps, metric_id: const showOthers = points.length > 10 && metric_id !== "txcosts"; - const date = new Date(x); + const date = moment.utc(x).utc().toDate(); const dateString = date.toLocaleDateString("en-GB", { timeZone: "UTC", month: "short",