diff --git a/front_end/messages/en.json b/front_end/messages/en.json index 353b40dda0..44840a24f3 100644 --- a/front_end/messages/en.json +++ b/front_end/messages/en.json @@ -72,6 +72,7 @@ "metaculusPredictionLabel": "Metaculus Prediction", "userPredictionLabel": "User Prediction", "myPrediction": "My Prediction", + "myPredictionPrevious": "My Prediction (previous)", "myPredictionValue": "My Prediction: {forecastValue}", "myCoverageLabel": "My Coverage", "predictionLabel": "Prediction", @@ -85,6 +86,8 @@ "secondQuartile": "median", "thirdQuartile": "upper 75%", "you": "you", + "youPrevious": "you (previous)", + "overlayPreviousForecast": "Overlay Previous Forecast", "resolutionDescriptionBinary": "Did this actually happen?", "resolutionDescriptionMultipleChoice": "Which of these actually happened?", "resolutionDescriptionContinuous": "What was the final result?", diff --git a/front_end/src/app/(main)/questions/[id]/components/forecast_maker/continuous_prediction_chart.tsx b/front_end/src/app/(main)/questions/[id]/components/forecast_maker/continuous_prediction_chart.tsx index f2eecf781c..7171478217 100644 --- a/front_end/src/app/(main)/questions/[id]/components/forecast_maker/continuous_prediction_chart.tsx +++ b/front_end/src/app/(main)/questions/[id]/components/forecast_maker/continuous_prediction_chart.tsx @@ -15,6 +15,7 @@ import { cdfToPmf } from "@/utils/math"; type Props = { question: QuestionWithNumericForecasts; + overlayPreviousForecast?: boolean; dataset: { cdf: number[]; pmf: number[]; @@ -28,6 +29,7 @@ type Props = { const ContinuousPredictionChart: FC = ({ question, + overlayPreviousForecast, dataset, graphType, readOnly = false, @@ -59,6 +61,13 @@ const ContinuousPredictionChart: FC = ({ : graphType === "pmf" ? (hoverState.yData.user * 200).toFixed(3) : getForecastPctDisplayValue(hoverState.yData.user), + yUserPreviousLabel: readOnly + ? null + : !hoverState.yData.user_previous + ? null + : graphType === "pmf" + ? (hoverState.yData.user_previous * 200).toFixed(3) + : getForecastPctDisplayValue(hoverState.yData.user_previous), yCommunityLabel: !hoverState.yData.community ? null : graphType === "pmf" @@ -86,6 +95,14 @@ const ContinuousPredictionChart: FC = ({ }); } + if (overlayPreviousForecast && question.my_forecasts?.latest) { + charts.push({ + pmf: cdfToPmf(question.my_forecasts.latest.forecast_values), + cdf: question.my_forecasts.latest.forecast_values, + type: "user_previous", + }); + } + if (!readOnly || !!question.my_forecasts?.latest) { charts.push({ pmf: dataset.pmf, @@ -102,6 +119,7 @@ const ContinuousPredictionChart: FC = ({ readOnly, showCP, question.my_forecasts?.latest, + overlayPreviousForecast, ]); return ( @@ -136,6 +154,16 @@ const ContinuousPredictionChart: FC = ({ {")"} )} + {cursorDisplayData.yUserPreviousLabel !== null && ( + + + {cursorDisplayData.yUserPreviousLabel} + + {" ("} + {t("youPrevious")} + {")"} + + )} {showCP && cursorDisplayData.yCommunityLabel && ( diff --git a/front_end/src/app/(main)/questions/[id]/components/forecast_maker/continuous_slider.tsx b/front_end/src/app/(main)/questions/[id]/components/forecast_maker/continuous_slider.tsx index 5c187cbd9a..8e58dffc28 100644 --- a/front_end/src/app/(main)/questions/[id]/components/forecast_maker/continuous_slider.tsx +++ b/front_end/src/app/(main)/questions/[id]/components/forecast_maker/continuous_slider.tsx @@ -15,6 +15,7 @@ import { QuestionWithNumericForecasts } from "@/types/question"; import ContinuousPredictionChart from "./continuous_prediction_chart"; import { useHideCP } from "../cp_provider"; +import Checkbox from "@/components/ui/checkbox"; type Props = { forecast: MultiSliderValue[]; @@ -24,6 +25,8 @@ type Props = { pmf: number[]; }; onChange: (forecast: MultiSliderValue[], weights: number[]) => void; + overlayPreviousForecast: boolean; + setOverlayPreviousForecast: (value: boolean) => void; question: QuestionWithNumericForecasts; disabled?: boolean; }; @@ -33,6 +36,8 @@ const ContinuousSlider: FC = ({ weights, dataset, onChange, + overlayPreviousForecast, + setOverlayPreviousForecast, question, disabled = false, }) => { @@ -40,23 +45,38 @@ const ContinuousSlider: FC = ({ const { hideCP } = useHideCP(); const t = useTranslations(); const [graphType, setGraphType] = useState("pmf"); + const previousForecast = question.my_forecasts?.latest; return (
- - options={[ - { label: t("pdfLabel"), value: "pmf" }, - { label: t("cdfLabel"), value: "cdf" }, - ]} - defaultValue={graphType} - className="appearance-none border-none !p-0 text-sm" - onChange={(e) => - setGraphType(e.target.value as ContinuousAreaGraphType) - } - /> +
+ + options={[ + { label: t("pdfLabel"), value: "pmf" }, + { label: t("cdfLabel"), value: "cdf" }, + ]} + defaultValue={graphType} + className="appearance-none border-none !p-0 text-sm" + onChange={(e) => + setGraphType(e.target.value as ContinuousAreaGraphType) + } + /> + {previousForecast && ( +
+ setOverlayPreviousForecast(checked)} + className={"text-sm"} + label={t("overlayPreviousForecast")} + > +
+ )} +
+ = ({ } }; + const previousForecast = activeOptionData?.question.my_forecasts?.latest; + const [overlayPreviousForecast, setOverlayPreviousForecast] = + useState( + !!previousForecast?.forecast_values && !previousForecast.slider_values + ); + const userCdf: number[] | undefined = activeOptionData && getNumericForecastDataset( @@ -323,6 +329,10 @@ const ForecastMakerConditionalContinuous: FC = ({ activeOptionData.question.open_lower_bound!, activeOptionData.question.open_upper_bound! ).cdf; + const userPreviousCdf: number[] | undefined = + overlayPreviousForecast && previousForecast + ? previousForecast.forecast_values + : undefined; const communityCdf: number[] | undefined = activeOptionData?.question.aggregations.recency_weighted.latest ?.forecast_values; @@ -361,6 +371,8 @@ const ForecastMakerConditionalContinuous: FC = ({ question={option.question} forecast={option.sliderForecast} weights={option.weights} + overlayPreviousForecast={overlayPreviousForecast} + setOverlayPreviousForecast={setOverlayPreviousForecast} dataset={getNumericForecastDataset( option.sliderForecast, option.weights, @@ -450,6 +462,19 @@ const ForecastMakerConditionalContinuous: FC = ({ aboveUpper: 1 - communityCdf![communityCdf!.length - 1], } } + userPreviousBounds={ + userPreviousCdf + ? { + belowLower: userPreviousCdf[0], + aboveUpper: 1 - userPreviousCdf[userPreviousCdf.length - 1], + } + : undefined + } + userPreviousQuartiles={ + userPreviousCdf + ? computeQuartilesFromCDF(userPreviousCdf) + : undefined + } communityQuartiles={ communityCdf && computeQuartilesFromCDF(communityCdf) } diff --git a/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_group/forecast_maker_group_continuous.tsx b/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_group/forecast_maker_group_continuous.tsx index bd98ac54d1..4ed6b75fe4 100644 --- a/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_group/forecast_maker_group_continuous.tsx +++ b/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_group/forecast_maker_group_continuous.tsx @@ -267,6 +267,12 @@ const ForecastMakerGroupContinuous: FC = ({ } }, [postId, questionsToSubmit]); + const previousForecast = activeGroupOption?.question.my_forecasts?.latest; + const [overlayPreviousForecast, setOverlayPreviousForecast] = + useState( + !!previousForecast?.forecast_values && !previousForecast.slider_values + ); + const userCdf: number[] | undefined = activeGroupOption && getNumericForecastDataset( @@ -275,6 +281,10 @@ const ForecastMakerGroupContinuous: FC = ({ activeGroupOption?.question.open_lower_bound!, activeGroupOption?.question.open_upper_bound! ).cdf; + const userPreviousCdf: number[] | undefined = + overlayPreviousForecast && previousForecast + ? previousForecast.forecast_values + : undefined; const communityCdf: number[] | undefined = activeGroupOption?.question.aggregations.recency_weighted.latest ?.forecast_values; @@ -312,6 +322,8 @@ const ForecastMakerGroupContinuous: FC = ({ @@ -387,6 +399,19 @@ const ForecastMakerGroupContinuous: FC = ({ } } userQuartiles={activeGroupOption.userQuartiles ?? undefined} + userPreviousBounds={ + userPreviousCdf + ? { + belowLower: userPreviousCdf[0], + aboveUpper: 1 - userPreviousCdf[userPreviousCdf.length - 1], + } + : undefined + } + userPreviousQuartiles={ + userPreviousCdf + ? computeQuartilesFromCDF(userPreviousCdf) + : undefined + } communityBounds={ communityCdf && { belowLower: communityCdf[0], diff --git a/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_question/forecast_maker_continuous.tsx b/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_question/forecast_maker_continuous.tsx index 09657b4e73..1411baa6d4 100644 --- a/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_question/forecast_maker_continuous.tsx +++ b/front_end/src/app/(main)/questions/[id]/components/forecast_maker/forecast_maker_question/forecast_maker_continuous.tsx @@ -68,6 +68,11 @@ const ForecastMakerContinuous: FC = ({ const [weights, setWeights] = useState( prevForecastValue?.weights ?? [1] ); + const previousForecast = question.my_forecasts?.latest; + const [overlayPreviousForecast, setOverlayPreviousForecast] = + useState( + !!previousForecast?.forecast_values && !previousForecast.slider_values + ); const dataset = useMemo( () => @@ -81,6 +86,10 @@ const ForecastMakerContinuous: FC = ({ ); const userCdf: number[] = dataset.cdf; + const userPreviousCdf: number[] | undefined = + overlayPreviousForecast && previousForecast + ? previousForecast.forecast_values + : undefined; const communityCdf: number[] | undefined = question.aggregations.recency_weighted.latest?.forecast_values; @@ -131,6 +140,8 @@ const ForecastMakerContinuous: FC = ({ setWeights(weight); setIsDirty(true); }} + overlayPreviousForecast={overlayPreviousForecast} + setOverlayPreviousForecast={setOverlayPreviousForecast} question={question} disabled={!canPredict} /> @@ -176,6 +187,17 @@ const ForecastMakerContinuous: FC = ({ aboveUpper: 1 - userCdf[userCdf.length - 1], }} userQuartiles={userCdf ? computeQuartilesFromCDF(userCdf) : undefined} + userPreviousBounds={ + userPreviousCdf + ? { + belowLower: userPreviousCdf[0], + aboveUpper: 1 - userPreviousCdf[userPreviousCdf.length - 1], + } + : undefined + } + userPreviousQuartiles={ + userPreviousCdf ? computeQuartilesFromCDF(userPreviousCdf) : undefined + } communityBounds={ communityCdf ? { diff --git a/front_end/src/app/(main)/questions/[id]/components/forecast_maker/numeric_table.tsx b/front_end/src/app/(main)/questions/[id]/components/forecast_maker/numeric_table.tsx index fd41679e7c..b6b1ada23e 100644 --- a/front_end/src/app/(main)/questions/[id]/components/forecast_maker/numeric_table.tsx +++ b/front_end/src/app/(main)/questions/[id]/components/forecast_maker/numeric_table.tsx @@ -12,6 +12,8 @@ type Props = { question: QuestionWithNumericForecasts; userBounds?: Bounds; userQuartiles?: Quartiles; + userPreviousBounds?: Bounds; + userPreviousQuartiles?: Quartiles; communityBounds?: Bounds; communityQuartiles?: Quartiles; withUserQuartiles?: boolean; @@ -24,6 +26,8 @@ const NumericForecastTable: FC = ({ question, userBounds, userQuartiles, + userPreviousBounds, + userPreviousQuartiles, withUserQuartiles = true, communityBounds, communityQuartiles, @@ -44,6 +48,13 @@ const NumericForecastTable: FC = ({
)} + {withUserQuartiles && userPreviousQuartiles && ( + <> +
+ {t("myPredictionPrevious")} +
+ + )} {withCommunityQuartiles && ( <> @@ -123,6 +134,49 @@ const NumericForecastTable: FC = ({ )} )} + {withUserQuartiles && userPreviousQuartiles && ( +
+ <> + {question.open_lower_bound && ( +
+ {userPreviousBounds && + (userPreviousBounds.belowLower * 100).toFixed(1)} + % +
+ )} +
+ {checkQuartilesOutOfBorders(userPreviousQuartiles?.lower25)} + {getDisplayValue( + userPreviousQuartiles?.lower25, + question.type, + question.scaling, + 4 + )} +
+
+ {checkQuartilesOutOfBorders(userPreviousQuartiles?.median)} + {getDisplayValue( + userPreviousQuartiles?.median, + question.type, + question.scaling, + 4 + )} +
+
+ {checkQuartilesOutOfBorders(userPreviousQuartiles?.upper75)} + {getDisplayValue( + userPreviousQuartiles?.upper75, + question.type, + question.scaling, + 4 + )} +
+ {question.open_upper_bound && ( +
{(userPreviousBounds!.aboveUpper * 100).toFixed(1)}%
+ )} + +
+ )} {withCommunityQuartiles && (
diff --git a/front_end/src/components/charts/continuous_area_chart.tsx b/front_end/src/components/charts/continuous_area_chart.tsx index 99710d654e..b503ead834 100644 --- a/front_end/src/components/charts/continuous_area_chart.tsx +++ b/front_end/src/components/charts/continuous_area_chart.tsx @@ -39,6 +39,7 @@ type ContinuousAreaColor = "orange" | "green"; const CHART_COLOR_MAP: Record = { community: "green", user: "orange", + user_previous: "orange", }; export type ContinuousAreaGraphInput = Array<{ @@ -158,7 +159,7 @@ const ContinuousAreaChart: FC = ({ line: chart.graphLine, color: getThemeColor( chart.color === "orange" - ? METAC_COLORS.orange["800"] + ? METAC_COLORS.orange[chart.type === "user" ? "800" : "500"] : METAC_COLORS.olive["700"] ), type: chart.type, @@ -189,6 +190,7 @@ const ContinuousAreaChart: FC = ({ yData: { community: 0, user: 0, + user_previous: 0, }, } ); @@ -233,9 +235,15 @@ const ContinuousAreaChart: FC = ({ data: { fill: chart.color === "orange" - ? getThemeColor(METAC_COLORS.orange["700"]) - : undefined, - opacity: 0.3, + ? getThemeColor( + METAC_COLORS.orange[ + chart.type === "user" ? "700" : "400" + ] + ) + : chart.color === "green" + ? getThemeColor(METAC_COLORS.olive["500"]) + : undefined, + opacity: chart.type === "user_previous" ? 0.1 : 0.3, }, }} /> @@ -248,8 +256,14 @@ const ContinuousAreaChart: FC = ({ data: { stroke: chart.color === "orange" - ? getThemeColor(METAC_COLORS.orange["800"]) - : undefined, + ? getThemeColor( + METAC_COLORS.orange[ + chart.type === "user" ? "800" : "500" + ] + ) + : chart.color === "green" + ? getThemeColor(METAC_COLORS.olive["500"]) + : undefined, strokeDasharray: chart.color === "orange" ? "2,2" : undefined, }, }} diff --git a/front_end/src/types/charts.ts b/front_end/src/types/charts.ts index c62bb59050..f91e4b3df5 100644 --- a/front_end/src/types/charts.ts +++ b/front_end/src/types/charts.ts @@ -41,7 +41,7 @@ export enum TimelineChartZoomOption { All = "all", } -export type ContinuousAreaType = "community" | "user"; +export type ContinuousAreaType = "community" | "user" | "user_previous"; export type ContinuousAreaHoverState = { x: number;