Skip to content

Commit 2c8e28e

Browse files
committed
wip: continuous
1 parent d490af8 commit 2c8e28e

File tree

7 files changed

+127
-21
lines changed

7 files changed

+127
-21
lines changed

front_end/src/components/forecast_maker/continuous_group_accordion/accordion_resolution_cell.tsx

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons";
2+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
13
import { isNil } from "lodash";
24
import { FC } from "react";
35

46
import ResolutionIcon from "@/components/icons/resolution";
7+
import Tooltip from "@/components/ui/tooltip";
58
import { QuestionStatus, Resolution } from "@/types/post";
69
import cn from "@/utils/core/cn";
710
import { isUnsuccessfullyResolved } from "@/utils/questions/resolution";
@@ -13,6 +16,8 @@ type Props = {
1316
resolution: Resolution | null;
1417
type: QuestionStatus.OPEN | QuestionStatus.CLOSED | QuestionStatus.RESOLVED;
1518
unit?: string;
19+
withdrawnLabel?: string;
20+
showWithdrawn?: boolean;
1621
};
1722

1823
const AccordionResolutionCell: FC<Props> = ({
@@ -21,6 +26,8 @@ const AccordionResolutionCell: FC<Props> = ({
2126
userMedian,
2227
resolution,
2328
type,
29+
withdrawnLabel = false,
30+
showWithdrawn = false,
2431
}) => {
2532
const isResolved = !isNil(resolution);
2633
if (isResolved) {
@@ -54,9 +61,25 @@ const AccordionResolutionCell: FC<Props> = ({
5461
{median}
5562
</p>
5663
{!!userMedian && (
57-
<p className="m-0 text-sm leading-4 text-orange-700 dark:text-orange-700-dark">
58-
{userMedian}
59-
</p>
64+
<div className="m-0 flex items-center gap-1 leading-4">
65+
{showWithdrawn ? (
66+
<Tooltip
67+
tooltipContent={withdrawnLabel}
68+
showDelayMs={120}
69+
placement="right-start"
70+
tooltipClassName="z-[999] rounded-sm px-1.5 py-0.5 text-[10px] leading-tight border-0 bg-salmon-800 dark:text-gray-0-dark text-gray-0 shadow-none dark:bg-salmon-800-dark"
71+
>
72+
<div className="inline-flex items-center gap-1 text-orange-700 dark:text-orange-700-dark">
73+
<span className="text-sm font-normal">{userMedian}</span>
74+
<FontAwesomeIcon size="xs" icon={faTriangleExclamation} />
75+
</div>
76+
</Tooltip>
77+
) : (
78+
<p className="m-0 text-sm text-orange-700 dark:text-orange-700-dark">
79+
{userMedian}
80+
</p>
81+
)}
82+
</div>
6083
)}
6184
</div>
6285
);

front_end/src/components/forecast_maker/continuous_group_accordion/group_forecast_accordion.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export type ContinuousGroupOption = {
3636
resolution: Resolution | null;
3737
menu?: ReactNode;
3838
forecastExpiration?: ForecastExpirationValue;
39+
wasWithdrawn?: boolean;
40+
withdrawnEndTimeSec?: number | null;
3941
};
4042

4143
type Props = {

front_end/src/components/forecast_maker/continuous_group_accordion/group_forecast_accordion_item.tsx

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ import {
1818
getQuantileNumericForecastDataset,
1919
getSliderNumericForecastDataset,
2020
} from "@/utils/forecasts/dataset";
21+
import { formatRelativeDate } from "@/utils/formatters/date";
2122
import { getPredictionDisplayValue } from "@/utils/formatters/prediction";
2223
import { formatResolution } from "@/utils/formatters/resolution";
24+
import { computeQuartilesFromCDF } from "@/utils/math";
2325

2426
import { AccordionOpenButton } from "./accordion_open_button";
2527
import { AccordionResolutionCell } from "./accordion_resolution_cell";
@@ -94,7 +96,22 @@ const AccordionItem: FC<PropsWithChildren<AccordionItemProps>> = memo(
9496
actual_resolve_time: option.question.actual_resolve_time ?? null,
9597
}
9698
);
97-
const userMedian = showUserPrediction
99+
const wasWithdrawn = !!option.withdrawnEndTimeSec;
100+
const withdrawnMedian =
101+
wasWithdrawn && question.my_forecasts?.latest?.forecast_values
102+
? computeQuartilesFromCDF(question.my_forecasts.latest.forecast_values)
103+
.median
104+
: undefined;
105+
106+
const withdrawnLabel = wasWithdrawn
107+
? `Withdrawn ${formatRelativeDate(
108+
locale,
109+
new Date(option.withdrawnEndTimeSec * 1000),
110+
{ short: true }
111+
)}`
112+
: undefined;
113+
114+
let userMedian = showUserPrediction
98115
? forecastInputMode === ContinuousForecastInputType.Quantile
99116
? getPredictionDisplayValue(
100117
option.userQuantileForecast?.find((q) => q.quantile === Quantile.q2)
@@ -113,6 +130,15 @@ const AccordionItem: FC<PropsWithChildren<AccordionItemProps>> = memo(
113130
})
114131
: undefined;
115132

133+
if (wasWithdrawn && !isDirty && withdrawnMedian != null) {
134+
userMedian = getPredictionDisplayValue(withdrawnMedian, {
135+
questionType: option.question.type,
136+
scaling: option.question.scaling,
137+
unit,
138+
actual_resolve_time: option.question.actual_resolve_time ?? null,
139+
});
140+
}
141+
116142
const handleClick = () => {
117143
setIsModalOpen((prev) => !prev);
118144
};
@@ -171,6 +197,10 @@ const AccordionItem: FC<PropsWithChildren<AccordionItemProps>> = memo(
171197
: undefined
172198
}
173199
type={type}
200+
withdrawnLabel={
201+
wasWithdrawn && !isDirty ? withdrawnLabel : undefined
202+
}
203+
showWithdrawn={wasWithdrawn && !isDirty}
174204
/>
175205
<div className="hidden h-full shrink-0 grow-0 items-center justify-center sm:block sm:w-[325px]">
176206
<ContinuousAreaChart

front_end/src/components/forecast_maker/continuous_input/index.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ type Props = {
5454
predictionMessage?: ReactNode;
5555
menu?: ReactNode;
5656
copyMenu?: ReactNode;
57+
userPreviousLabel?: string;
58+
userPreviousRowClassName?: string;
59+
hideCurrentUserRow?: boolean;
5760
};
5861

5962
const ContinuousInput: FC<Props> = ({
@@ -78,6 +81,9 @@ const ContinuousInput: FC<Props> = ({
7881
predictionMessage,
7982
menu,
8083
copyMenu,
84+
userPreviousLabel,
85+
userPreviousRowClassName,
86+
hideCurrentUserRow,
8187
}) => {
8288
const { user } = useAuth();
8389
const { hideCP } = useHideCP();
@@ -120,6 +126,10 @@ const ContinuousInput: FC<Props> = ({
120126

121127
const discrete = question.type === QuestionType.Discrete;
122128

129+
const derivedHideCurrentUserRow =
130+
hideCurrentUserRow ??
131+
(!isDirty && (!hasUserForecast || !userCdf || userCdf.length === 0));
132+
123133
return (
124134
<ContinuousInputContainer
125135
forecastInputMode={forecastInputMode}
@@ -187,6 +197,9 @@ const ContinuousInput: FC<Props> = ({
187197
disableQuantileInput={disabled}
188198
hasUserForecast={hasUserForecast}
189199
forecastInputMode={forecastInputMode}
200+
userPreviousLabel={userPreviousLabel}
201+
userPreviousRowClassName={userPreviousRowClassName}
202+
hideCurrentUserRow={derivedHideCurrentUserRow}
190203
/>
191204

192205
{forecastInputMode === ContinuousForecastInputType.Quantile && (

front_end/src/components/forecast_maker/continuous_table/index.tsx

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ type Props = {
5050
quantileComponents?: DistributionQuantileComponent;
5151
onQuantileChange?: (quantileComponents: QuantileValue[]) => void;
5252
disableQuantileInput?: boolean;
53+
userPreviousLabel?: string;
54+
userPreviousRowClassName?: string;
55+
hideCurrentUserRow?: boolean;
5356
};
5457

5558
const ContinuousTable: FC<Props> = ({
@@ -68,6 +71,9 @@ const ContinuousTable: FC<Props> = ({
6871
quantileComponents,
6972
onQuantileChange,
7073
disableQuantileInput = false,
74+
userPreviousLabel,
75+
userPreviousRowClassName,
76+
hideCurrentUserRow = false,
7177
}) => {
7278
const t = useTranslations();
7379
// initial state is a safety measure to avoid errors when we already have slider forecast
@@ -238,7 +244,7 @@ const ContinuousTable: FC<Props> = ({
238244
forecastInputMode === ContinuousForecastInputType.Slider && (
239245
<tr className="text-orange-800 dark:text-orange-800-dark">
240246
<Td>{t("myPrediction")}</Td>
241-
{isDirty || hasUserForecast ? (
247+
{!hideCurrentUserRow && (isDirty || hasUserForecast) ? (
242248
<>
243249
{question.open_lower_bound && (
244250
<Td>
@@ -418,8 +424,13 @@ const ContinuousTable: FC<Props> = ({
418424
)}
419425

420426
{withUserQuartiles && userPreviousQuartiles && (
421-
<tr className="text-orange-800 dark:text-orange-800-dark">
422-
<Td>{t("myPredictionPrevious")}</Td>
427+
<tr
428+
className={cn(
429+
"text-orange-800 dark:text-orange-800-dark",
430+
userPreviousRowClassName
431+
)}
432+
>
433+
<Td>{userPreviousLabel ?? t("myPredictionPrevious")}</Td>
423434
<>
424435
{question.open_lower_bound && (
425436
<Td>
@@ -463,8 +474,13 @@ const ContinuousTable: FC<Props> = ({
463474
</Td>
464475
)}
465476
{withUserQuartiles && userPreviousQuartiles && (
466-
<Td className="text-orange-800 dark:text-orange-800-dark">
467-
{t("myPredictionPrevious")}
477+
<Td
478+
className={cn(
479+
"text-orange-800 dark:text-orange-800-dark",
480+
userPreviousRowClassName
481+
)}
482+
>
483+
{userPreviousLabel ?? t("myPredictionPrevious")}
468484
</Td>
469485
)}
470486
</tr>
@@ -503,7 +519,9 @@ const ContinuousTable: FC<Props> = ({
503519
</Td>
504520
) : (
505521
<Td className="text-orange-800 dark:text-orange-800-dark">
506-
{(isDirty || hasUserForecast) && userBounds
522+
{!hideCurrentUserRow &&
523+
(isDirty || hasUserForecast) &&
524+
userBounds
507525
? `${(userBounds.belowLower * 100).toFixed(1)}%`
508526
: "—"}
509527
</Td>
@@ -557,7 +575,7 @@ const ContinuousTable: FC<Props> = ({
557575
</Td>
558576
) : (
559577
<Td className="text-orange-800 dark:text-orange-800-dark">
560-
{isDirty || hasUserForecast ? (
578+
{!hideCurrentUserRow && (isDirty || hasUserForecast) ? (
561579
<>{getDisplayValue(userQuartiles?.lower25)}</>
562580
) : (
563581
"—"
@@ -601,7 +619,7 @@ const ContinuousTable: FC<Props> = ({
601619
</Td>
602620
) : (
603621
<Td className="text-orange-800 dark:text-orange-800-dark">
604-
{isDirty || hasUserForecast ? (
622+
{!hideCurrentUserRow && (isDirty || hasUserForecast) ? (
605623
<>{getDisplayValue(userQuartiles?.median)}</>
606624
) : (
607625
"—"
@@ -645,7 +663,7 @@ const ContinuousTable: FC<Props> = ({
645663
</Td>
646664
) : (
647665
<Td className="text-orange-800 dark:text-orange-800-dark">
648-
{isDirty || hasUserForecast ? (
666+
{!hideCurrentUserRow && (isDirty || hasUserForecast) ? (
649667
<>{getDisplayValue(userQuartiles?.upper75)}</>
650668
) : (
651669
"—"
@@ -699,7 +717,7 @@ const ContinuousTable: FC<Props> = ({
699717
</Td>
700718
) : (
701719
<Td className="text-orange-800 dark:text-orange-800-dark">
702-
{(isDirty || hasUserForecast) && userBounds
720+
{!hideCurrentUserRow && userBounds
703721
? `${(userBounds.aboveUpper * 100).toFixed(1)}%`
704722
: "—"}
705723
</Td>

front_end/src/components/forecast_maker/forecast_maker_group/continuous_input_wrapper.tsx

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,18 @@ const ContinuousInputWrapper: FC<PropsWithChildren<Props>> = ({
104104
const locale = useLocale();
105105

106106
const previousForecast = option.question.my_forecasts?.latest;
107+
const showWithdrawnRow = option.wasWithdrawn && !option.isDirty;
108+
109+
const userPreviousCdf: number[] | undefined =
110+
showWithdrawnRow && previousForecast
111+
? previousForecast.forecast_values
112+
: undefined;
107113
const [overlayPreviousForecast, setOverlayPreviousForecast] =
108114
useState<boolean>(
109115
!!previousForecast?.forecast_values &&
110-
!previousForecast.distribution_input
116+
(!previousForecast.distribution_input ||
117+
(!!previousForecast.end_time &&
118+
previousForecast.end_time * 1000 < Date.now()))
111119
);
112120
const [submitError, setSubmitError] = useState<ErrorResponse>();
113121
const [isWithdrawModalOpen, setIsWithdrawModalOpen] = useState(false);
@@ -139,6 +147,11 @@ const ContinuousInputWrapper: FC<PropsWithChildren<Props>> = ({
139147

140148
const dataset = useMemo(() => {
141149
setSubmitError(undefined);
150+
151+
if (option.wasWithdrawn && !option.isDirty) {
152+
return { cdf: [], pmf: [] };
153+
}
154+
142155
if (forecastInputMode === ContinuousForecastInputType.Slider) {
143156
return getSliderNumericForecastDataset(
144157
forecast as DistributionSliderComponent[],
@@ -214,10 +227,11 @@ const ContinuousInputWrapper: FC<PropsWithChildren<Props>> = ({
214227
getNormalizedContinuousForecast(option.userSliderForecast),
215228
option.question
216229
).cdf;
217-
const userPreviousCdf: number[] | undefined =
218-
overlayPreviousForecast && previousForecast
219-
? previousForecast.forecast_values
220-
: undefined;
230+
// const userPreviousCdf: number[] | undefined =
231+
// (overlayPreviousForecast || (option.wasWithdrawn && !option.isDirty)) &&
232+
// previousForecast
233+
// ? previousForecast.forecast_values
234+
// : undefined;
221235
const communityCdf: number[] | undefined =
222236
option.question.aggregations[option.question.default_aggregation_method]
223237
.latest?.forecast_values;
@@ -403,6 +417,9 @@ const ContinuousInputWrapper: FC<PropsWithChildren<Props>> = ({
403417
}
404418
menu={option.menu}
405419
copyMenu={copyMenu}
420+
userPreviousLabel={showWithdrawnRow ? "(Withdrawn)" : undefined}
421+
userPreviousRowClassName={showWithdrawnRow ? "text-xs" : undefined}
422+
hideCurrentUserRow={showWithdrawnRow}
406423
/>
407424
</div>
408425

front_end/src/components/forecast_maker/forecast_maker_group/forecast_maker_group_continuous.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,7 @@ const ForecastMakerGroupContinuous: FC<Props> = ({
174174
() =>
175175
groupOptions.filter(
176176
(option) =>
177-
option.question.status === QuestionStatus.OPEN &&
178-
(option.isDirty || option.hasUserForecast)
177+
option.question.status === QuestionStatus.OPEN && option.isDirty
179178
),
180179
[groupOptions]
181180
);
@@ -682,6 +681,8 @@ function generateGroupOptions({
682681
q
683682
);
684683
const latest = q.aggregations[q.default_aggregation_method].latest;
684+
const last = q.my_forecasts?.latest;
685+
const wasWithdrawn = !!last?.end_time && last.end_time * 1000 < Date.now();
685686
return {
686687
id: q.id,
687688
name: q.label,
@@ -701,6 +702,8 @@ function generateGroupOptions({
701702
resolution: q.resolution,
702703
isDirty: false,
703704
hasUserForecast: !isNil(prevForecast),
705+
wasWithdrawn,
706+
withdrawnEndTimeSec: last?.end_time ?? null,
704707
menu: (
705708
<ForecastMakerGroupControls
706709
question={q}

0 commit comments

Comments
 (0)