diff --git a/backend/api/src/get-bets.ts b/backend/api/src/get-bets.ts index 106aff7d01..c06a859d47 100644 --- a/backend/api/src/get-bets.ts +++ b/backend/api/src/get-bets.ts @@ -8,8 +8,9 @@ import { getUserIdFromUsername } from 'shared/supabase/users' import { getBetsWithFilter } from 'shared/supabase/bets' import { convertBet, NON_POINTS_BETS_LIMIT } from 'common/supabase/bets' import { filterDefined } from 'common/util/array' +import { ValidatedAPIParams } from 'common/api/schema' -export const getBets: APIHandler<'bets'> = async (props) => { +export const getBetsInternal = async (props: ValidatedAPIParams<'bets'>) => { const { limit, username, @@ -90,6 +91,15 @@ export const getBets: APIHandler<'bets'> = async (props) => { return await getBetsWithFilter(pg, opts) } +export const getBets: APIHandler<'bets'> = async (props) => + getBetsInternal(props) + +export const getBetPointsBetween: APIHandler<'bet-points'> = async (props) => + getBetsInternal({ + ...props, + points: true, + }) + async function getBetTime(pg: SupabaseDirectClient, id: string) { const created = await pg.oneOrNone( ` diff --git a/backend/api/src/routes.ts b/backend/api/src/routes.ts index 781744f393..ba0f629acc 100644 --- a/backend/api/src/routes.ts +++ b/backend/api/src/routes.ts @@ -27,7 +27,7 @@ import { pinComment } from './pin-comment' import { getManagrams } from './get-managrams' import { getGroups } from './get-groups' import { getComments } from './get-comments' -import { getBets } from './get-bets' +import { getBetPointsBetween, getBets } from './get-bets' import { getLiteUser, getUser } from './get-user' import { getUsers } from './get-users' import { getUserBalancesByIds, getUsersByIds } from './get-users-by-ids' @@ -181,6 +181,7 @@ export const handlers: { [k in APIPath]: APIHandler } = { 'bet/cancel/:betId': cancelBet, 'market/:contractId/sell': sellShares, bets: getBets, + 'bet-points': getBetPointsBetween, 'get-notifications': getNotifications, 'get-channel-memberships': getChannelMemberships, 'get-channel-messages': getChannelMessages, diff --git a/common/src/api/schema.ts b/common/src/api/schema.ts index f32e505d80..6656233c9f 100644 --- a/common/src/api/schema.ts +++ b/common/src/api/schema.ts @@ -388,6 +388,24 @@ export const API = (_apiTypeCheck = { }) .strict(), }, + 'bet-points': { + method: 'GET', + visibility: 'public', + authed: false, + cache: 'public, max-age=600, stale-while-revalidate=60', + returns: [] as Bet[], + props: z + .object({ + contractId: z.string(), + answerId: z.string().optional(), + limit: z.coerce.number().gte(0).lte(50000).default(50000), + beforeTime: z.coerce.number(), + afterTime: z.coerce.number(), + filterRedemptions: coerceBoolean.optional(), + includeZeroShareRedemptions: coerceBoolean.optional(), + }) + .strict(), + }, 'unique-bet-group-count': { method: 'GET', visibility: 'undocumented', diff --git a/common/src/bets.ts b/common/src/bets.ts index 5818a9ebd7..af0921f398 100644 --- a/common/src/bets.ts +++ b/common/src/bets.ts @@ -44,3 +44,28 @@ export const getBetPoints = async ( })) ) } + +// gets random bets - 50,000 by default +export const getBetPointsBetween = async (options: APIParams<'bet-points'>) => { + const data = await unauthedApi('bet-points', options) + + const sorted = sortBy(data, 'createdTime') + + if (sorted.length === 0) return [] + + // we need to include previous prob for binary in case the prob shifted from something + const includePrevProb = !sorted[0].answerId + + return buildArray( + includePrevProb && { + x: sorted[0].createdTime - 1, + y: sorted[0].probBefore, + answerId: sorted[0].answerId, + }, + sorted.map((r) => ({ + x: r.createdTime, + y: r.probAfter, + answerId: r.answerId, + })) + ) +} diff --git a/common/src/contract-params.ts b/common/src/contract-params.ts index 9fef9dc9fc..13d22f24fc 100644 --- a/common/src/contract-params.ts +++ b/common/src/contract-params.ts @@ -29,7 +29,7 @@ import { sortAnswers, } from './answer' import { getDashboardsToDisplayOnContract } from './supabase/dashboards' -import { getBetPoints, getTotalBetCount } from './bets' +import { getBetPointsBetween, getTotalBetCount } from './bets' export async function getContractParams( contract: Contract, @@ -72,9 +72,12 @@ export async function getContractParams( }) : ([] as Bet[]), hasMechanism && !shouldHideGraph(contract) - ? getBetPoints(contract.id, { + ? getBetPointsBetween({ + contractId: contract.id, filterRedemptions: !includeRedemptions, includeZeroShareRedemptions: includeRedemptions, + beforeTime: contract.lastBetTime ?? contract.createdTime, + afterTime: contract.createdTime, }) : [], getRecentTopLevelCommentsAndReplies(db, contract.id, 25), diff --git a/web/components/charts/contract/zoom-utils.ts b/web/components/charts/contract/zoom-utils.ts index b32565d8f3..a7645c82f3 100644 --- a/web/components/charts/contract/zoom-utils.ts +++ b/web/components/charts/contract/zoom-utils.ts @@ -3,19 +3,20 @@ import { buildArray } from 'common/util/array' import { debounce } from 'lodash' import { useCallback, useEffect, useLayoutEffect, useState } from 'react' import { ScaleTime } from 'd3-scale' -import { getBetPoints } from 'common/bets' +import { getBetPointsBetween } from 'common/bets' import { getMultiBetPoints } from 'common/contract-params' import { MultiContract } from 'common/contract' export async function getPointsBetween( contractId: string, - min?: number, - max?: number + min: number, + max: number ) { - const points = await getBetPoints(contractId, { - filterRedemptions: true, + const points = await getBetPointsBetween({ + contractId, beforeTime: max, afterTime: min, + filterRedemptions: true, }) const compressed = maxMinBin(points, 500) @@ -26,60 +27,67 @@ export async function getPointsBetween( // only for single value contracts export const useDataZoomFetcher = (props: { contractId: string + createdTime: number + lastBetTime: number viewXScale?: ScaleTime points: HistoryPoint[] }) => { - const [data, setData] = useState(props.points) + const { contractId, createdTime, lastBetTime, viewXScale, points } = props + const [data, setData] = useState(points) const [loading, setLoading] = useState(false) const onZoomData = useCallback( - debounce(async (min?: number, max?: number) => { + debounce(async (min: number, max: number) => { if (min && max) { setLoading(true) - const points = await getPointsBetween(props.contractId, min, max) + const points = await getPointsBetween(contractId, min, max) setData( buildArray( - props.points.filter((p) => p.x <= min), + points.filter((p) => p.x <= min), points, - props.points.filter((p) => p.x >= max) + points.filter((p) => p.x >= max) ).sort((a, b) => a.x - b.x) ) setLoading(false) } else { - setData(props.points) + setData(points) } }, 100), - [props.contractId] + [contractId] ) useLayoutEffect(() => { - setData(props.points) - }, [props.contractId]) + setData(points) + }, [contractId]) useEffect(() => { - if (props.viewXScale) { - const [minX, maxX] = props.viewXScale.range() + if (viewXScale) { + const [minX, maxX] = viewXScale.range() + if (Math.abs(minX - maxX) <= 1) return // 20px buffer - const min = props.viewXScale.invert(minX - 20).valueOf() - const max = props.viewXScale.invert(maxX + 20).valueOf() + const min = viewXScale.invert(minX - 20).valueOf() + const max = viewXScale.invert(maxX + 20).valueOf() + const fixedMin = Math.max(min, createdTime) + const fixedMax = Math.min(max, lastBetTime) - onZoomData(min, max) + onZoomData(fixedMin, fixedMax) } else { - onZoomData() + onZoomData(createdTime, lastBetTime) } - }, [props.viewXScale]) + }, [viewXScale]) return { points: data, loading } } export async function getMultichoicePointsBetween( contract: MultiContract, - min?: number, - max?: number + min: number, + max: number ) { - const allBetPoints = await getBetPoints(contract.id, { + const allBetPoints = await getBetPointsBetween({ + contractId: contract.id, filterRedemptions: false, includeZeroShareRedemptions: true, beforeTime: max, @@ -95,8 +103,10 @@ export const useMultiChoiceDataZoomFetcher = (props: { contract: MultiContract viewXScale?: ScaleTime points: MultiPoints + createdTime: number + lastBetTime: number }) => { - const { contract, points } = props + const { contract, points, createdTime, lastBetTime, viewXScale } = props const [data, setData] = useState(points) const [loading, setLoading] = useState(false) @@ -115,13 +125,13 @@ export const useMultiChoiceDataZoomFetcher = (props: { // Get all answer IDs from both objects const allAnswerIds = Array.from( - new Set([...Object.keys(props.points), ...Object.keys(zoomedPoints)]) + new Set([...Object.keys(points), ...Object.keys(zoomedPoints)]) ) // Process each answer ID allAnswerIds.forEach((answerId) => { // Get points for this answer ID - const answerPoints = props.points[answerId] || [] + const answerPoints = points[answerId] || [] const zoomedAnswerPoints = zoomedPoints[answerId] || [] // Convert serialized points to HistoryPoint objects @@ -141,32 +151,35 @@ export const useMultiChoiceDataZoomFetcher = (props: { setData(newData) setLoading(false) } else { - setData(props.points) + setData(points) } }, 100), [contract.id] ) useLayoutEffect(() => { - setData(props.points) - if (Object.keys(props.points).length === 0 && props.viewXScale) { - const [minX, maxX] = props.viewXScale.range() + setData(points) + if (Object.keys(points).length === 0 && viewXScale) { + const [minX, maxX] = viewXScale.range() onZoomData(minX, maxX) } }, [contract.id]) useEffect(() => { - if (props.viewXScale) { - const [minX, maxX] = props.viewXScale.range() + if (viewXScale) { + const [minX, maxX] = viewXScale.range() + if (Math.abs(minX - maxX) <= 1) return // 20px buffer - const min = props.viewXScale.invert(minX - 20).valueOf() - const max = props.viewXScale.invert(maxX + 20).valueOf() + const min = viewXScale.invert(minX - 20).valueOf() + const max = viewXScale.invert(maxX + 20).valueOf() + const fixedMin = Math.max(min, createdTime) + const fixedMax = Math.min(max, lastBetTime) - onZoomData(min, max) + onZoomData(fixedMin, fixedMax) } else { onZoomData() } - }, [props.viewXScale]) + }, [viewXScale]) return { points: data, loading } } diff --git a/web/components/contract/contract-overview.tsx b/web/components/contract/contract-overview.tsx index 23fff980e0..65d850d2c8 100644 --- a/web/components/contract/contract-overview.tsx +++ b/web/components/contract/contract-overview.tsx @@ -207,6 +207,8 @@ export const BinaryOverview = (props: { const { points, loading } = useDataZoomFetcher({ contractId: contract.id, + createdTime: contract.createdTime, + lastBetTime: contract.lastBetTime ?? contract.createdTime, viewXScale: zoomParams?.viewXScale, points: props.betPoints, }) @@ -356,6 +358,8 @@ const ChoiceOverview = (props: { contract, viewXScale: zoomParams?.viewXScale, points: props.points, + createdTime: contract.createdTime, + lastBetTime: contract.lastBetTime ?? contract.createdTime, }) return ( <>