-
Notifications
You must be signed in to change notification settings - Fork 32
issue/3887/feat/max-attainable-coverage #4537
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
abd56d3
589efc8
13c5a6e
23c1195
a6b3163
3bb2c17
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,11 @@ | ||
| import { faCircleInfo } from "@fortawesome/free-solid-svg-icons"; | ||
| import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; | ||
| import { isNil } from "lodash"; | ||
| import { useTranslations } from "next-intl"; | ||
| import React, { FC } from "react"; | ||
| import React, { FC, ReactNode } from "react"; | ||
|
|
||
| import SectionToggle from "@/components/ui/section_toggle"; | ||
| import Tooltip from "@/components/ui/tooltip"; | ||
| import { QuestionWithForecasts, ScoreData } from "@/types/question"; | ||
| import { TranslationKey } from "@/types/translations"; | ||
| import cn from "@/utils/core/cn"; | ||
|
|
@@ -14,8 +17,10 @@ type Props = { | |
| variant?: Variant; | ||
| }; | ||
|
|
||
| type ScoreRow = { label: string; value: string; valueSuffix?: ReactNode }; | ||
|
|
||
| const ScoreTable: FC<{ | ||
| rows: { label: string; value: string }[]; | ||
| rows: ScoreRow[]; | ||
| className?: string; | ||
| variant?: Variant; | ||
| }> = ({ rows, className, variant = "auto" }) => ( | ||
|
|
@@ -42,11 +47,12 @@ const ScoreTable: FC<{ | |
| </span> | ||
| <span | ||
| className={cn( | ||
| "w-[34%] pl-4 text-center text-base text-gray-800 dark:text-gray-800-dark", | ||
| "flex w-[34%] items-center justify-center gap-0.5 pl-4 text-center text-base text-gray-800 dark:text-gray-800-dark", | ||
| { "sm:w-1/2": variant === "auto" } | ||
| )} | ||
| > | ||
| {row.value} | ||
| {row.valueSuffix} | ||
| </span> | ||
| </div> | ||
| ))} | ||
|
|
@@ -77,6 +83,21 @@ const buildScoreLabelKey = ( | |
| return (prefix + toCamel(key) + suffix) as TranslationKey; | ||
| }; | ||
|
|
||
| /** | ||
| * Returns the max attainable peer coverage (0–1) for a question that resolved | ||
| * before its scheduled close time, or null if not applicable. | ||
| */ | ||
| const getMaxCoverage = (question: QuestionWithForecasts): number | null => { | ||
| const { open_time, actual_close_time, scheduled_close_time } = question; | ||
| if (!open_time || !actual_close_time || !scheduled_close_time) return null; | ||
| const open = new Date(open_time).getTime(); | ||
| const actualClose = new Date(actual_close_time).getTime(); | ||
| const scheduledClose = new Date(scheduled_close_time).getTime(); | ||
| const totalDuration = scheduledClose - open; | ||
| if (totalDuration <= 0) return null; | ||
| return (actualClose - open) / totalDuration; | ||
| }; | ||
|
lsabor marked this conversation as resolved.
Comment on lines
+78
to
+87
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| export const AdditionalScoresTable: FC<Props> = ({ | ||
| question, | ||
| separateCoverage, | ||
|
|
@@ -107,8 +128,41 @@ export const AdditionalScoresTable: FC<Props> = ({ | |
| "weighted_coverage", | ||
| ]; | ||
|
|
||
| const coverageRows: { label: string; value: string }[] = []; | ||
| const otherRows: { label: string; value: string }[] = []; | ||
| // Peer coverage uses scheduled_close_time as total duration, so early | ||
| // resolution reduces the max attainable coverage. | ||
| const maxCoverage = getMaxCoverage(question); | ||
| let maxCoverageValueSuffix: ReactNode; | ||
| if (maxCoverage !== null) { | ||
| maxCoverageValueSuffix = ( | ||
| <span className="text-sm text-gray-600 dark:text-gray-600-dark"> | ||
| {" (max. "} | ||
| {(maxCoverage * 100).toFixed(1)}% | ||
| <Tooltip | ||
| tooltipContent={t.rich("maxAttainableCoverageExplanation", { | ||
| link: (chunks) => ( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid hardcoded English in max coverage label.
Based on learnings: Do not hardcode English strings in TSX components; prefer 🤖 Prompt for AI Agents |
||
| <a | ||
| href="https://www.metaculus.com/help/scores-faq/#score-truncation" | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| className="underline" | ||
| > | ||
| {chunks} | ||
| </a> | ||
| ), | ||
| })} | ||
| > | ||
| <FontAwesomeIcon | ||
| icon={faCircleInfo} | ||
| className="ml-0.5 cursor-help text-blue-500 dark:text-blue-500-dark" | ||
| /> | ||
| </Tooltip> | ||
| {")"} | ||
| </span> | ||
| ); | ||
| } | ||
|
|
||
| const coverageRows: ScoreRow[] = []; | ||
| const otherRows: ScoreRow[] = []; | ||
|
|
||
| for (const key of scoreKeys) { | ||
| if (key === peerKey || key === baselineKey) continue; | ||
|
|
@@ -126,6 +180,8 @@ export const AdditionalScoresTable: FC<Props> = ({ | |
| targetRows.push({ | ||
| label: t(buildScoreLabelKey(key, "user")), | ||
| value: formattedValue, | ||
| // Only peer coverage (key === "coverage") is affected by early resolution | ||
| ...(key === "coverage" ? { valueSuffix: maxCoverageValueSuffix } : {}), | ||
| }); | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,15 @@ | ||
| import { faClock } from "@fortawesome/free-regular-svg-icons"; | ||
| import { faFire, faRepeat } from "@fortawesome/free-solid-svg-icons"; | ||
| import { | ||
| faCircleInfo, | ||
| faFire, | ||
| faRepeat, | ||
| } from "@fortawesome/free-solid-svg-icons"; | ||
| import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; | ||
| import { isNil } from "lodash"; | ||
| import { useTranslations } from "next-intl"; | ||
| import React, { PropsWithChildren, ReactNode } from "react"; | ||
|
|
||
| import Tooltip from "@/components/ui/tooltip"; | ||
| import { QuestionWithForecasts } from "@/types/question"; | ||
| import cn from "@/utils/core/cn"; | ||
|
|
||
|
|
@@ -34,6 +39,21 @@ type Props = { | |
| itemClassName?: string; | ||
| }; | ||
|
|
||
| /** | ||
| * Returns the max attainable peer coverage (0–1) for a question that resolved | ||
| * before its scheduled close time, or null if not applicable. | ||
| */ | ||
| const getMaxCoverage = (question: QuestionWithForecasts): number | null => { | ||
| const { open_time, actual_close_time, scheduled_close_time } = question; | ||
| if (!open_time || !actual_close_time || !scheduled_close_time) return null; | ||
| const open = new Date(open_time).getTime(); | ||
| const actualClose = new Date(actual_close_time).getTime(); | ||
| const scheduledClose = new Date(scheduled_close_time).getTime(); | ||
| const totalDuration = scheduledClose - open; | ||
| if (totalDuration <= 0) return null; | ||
| return (actualClose - open) / totalDuration; | ||
| }; | ||
|
lsabor marked this conversation as resolved.
|
||
|
|
||
| export const ParticipationSummary: React.FC<Props> = ({ | ||
| question, | ||
| forecastsCount, | ||
|
|
@@ -91,6 +111,38 @@ export const ParticipationSummary: React.FC<Props> = ({ | |
| </span> | ||
| ); | ||
|
|
||
| // Only peer coverage (non-spot) is affected by early resolution. | ||
| const maxCoverage = isSpot ? null : getMaxCoverage(question); | ||
| const richMaxCoverageDisplay = (_chunks: ReactNode) => { | ||
| if (maxCoverage === null) return null; | ||
| return ( | ||
| <span> | ||
| {" (max. "} | ||
| {Math.round(maxCoverage * 100)}% | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This prevents full localization of the participation summary string. Based on learnings: Do not hardcode English strings in TSX components; prefer 🤖 Prompt for AI Agents |
||
| <Tooltip | ||
| tooltipContent={t.rich("maxAttainableCoverageExplanation", { | ||
| link: (chunks) => ( | ||
| <a | ||
| href="https://www.metaculus.com/help/scores-faq/#score-truncation" | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| className="underline" | ||
| > | ||
| {chunks} | ||
| </a> | ||
| ), | ||
| })} | ||
| > | ||
| <FontAwesomeIcon | ||
| icon={faCircleInfo} | ||
| className="ml-0.5 cursor-help text-blue-500 dark:text-blue-500-dark" | ||
| /> | ||
| </Tooltip> | ||
| {")"} | ||
| </span> | ||
| ); | ||
| }; | ||
|
|
||
| return ( | ||
| <div className={cn("flex flex-col gap-2", className)}> | ||
| <ParticipationItem | ||
|
|
@@ -115,6 +167,7 @@ export const ParticipationSummary: React.FC<Props> = ({ | |
| strong: richStrong, | ||
| userCoverage: Math.round(userCoverage * 100), | ||
| averageCoverage: Math.round(averageCoverage * 100), | ||
| maxCoverageDisplay: richMaxCoverageDisplay, | ||
| } | ||
| )} | ||
| </ParticipationItem> | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.