diff --git a/front_end/src/app/(main)/questions/[id]/components/key_factors/key_factors_question_consumer_section.tsx b/front_end/src/app/(main)/questions/[id]/components/key_factors/key_factors_question_consumer_section.tsx index 0c29532cd1..8140cc7829 100644 --- a/front_end/src/app/(main)/questions/[id]/components/key_factors/key_factors_question_consumer_section.tsx +++ b/front_end/src/app/(main)/questions/[id]/components/key_factors/key_factors_question_consumer_section.tsx @@ -7,12 +7,15 @@ import { useCommentsFeed } from "@/app/(main)/components/comments_feed_provider" import { openKeyFactorsSectionAndScrollTo } from "@/app/(main)/questions/[id]/components/key_factors/utils"; import { PostStatus, PostWithForecasts } from "@/types/post"; import { sendAnalyticsEvent } from "@/utils/analytics"; +import { getQuestionForecastAvailability } from "@/utils/questions/forecastAvailability"; +import { isQuestionPost } from "@/utils/questions/helpers"; import { MAX_TOP_KEY_FACTORS, useTopKeyFactorsCarouselItems, } from "./hooks/use_top_key_factors_carousel_items"; import KeyFactorDetailOverlay from "./key_factor_detail_overlay"; +import KeyFactorsCarousel from "./key_factors_carousel"; import KeyFactorsConsumerCarousel from "./key_factors_consumer_carousel"; import { useShouldHideKeyFactors } from "./use_should_hide_key_factors"; import { useQuestionLayout } from "../question_layout/question_layout_context"; @@ -41,14 +44,21 @@ const KeyFactorsQuestionConsumerSection: FC = ({ post }) => { if (post.status === PostStatus.RESOLVED) return null; + const postForecastAvailability = isQuestionPost(post) + ? getQuestionForecastAvailability(post.question) + : null; + const forecastIsEmpty = + !!postForecastAvailability?.isEmpty && + !postForecastAvailability?.cpRevealsOn; + + if (topItems.length === 0 && !forecastIsEmpty) return null; + const openKeyFactorsElement = (selector: string) => { requestKeyFactorsExpand?.(); openKeyFactorsSectionAndScrollTo({ selector, mobileOnly: false }); sendAnalyticsEvent("KeyFactorClick", { event_label: "fromTopList" }); }; - if (topItems.length === 0) return null; - return (
= ({ post }) => {
{t("topKeyFactors")}
- + {!forecastIsEmpty && ( + + )}
- + {forecastIsEmpty ? ( + ( +
+ )} + /> + ) : ( + + )} {keyFactorOverlay?.kind === "keyFactor" && ( = ({ )} {isTooltipActive && + !hideCP && + !forecastAvailability?.cpRevealsOn && !activeTimelineMarkerId && (tooltipChoices.length > 0 || !!tooltipUserChoices?.length || - !!forecastAvailability?.cpRevealsOn || !!forecastAvailability?.isEmpty) && (
= ({ postData, preselectedGroupQuestionId, mobileSidebar }) => { const t = useTranslations(); const { aggregateCoherenceLinks } = useCoherenceLinksContext(); + const { hideCP } = useHideCP(); const isFanGraph = postData.group_of_questions?.graph_type === @@ -152,6 +157,11 @@ export const ConsumerShell: FC<{ !isContinuousSingleQuestion && !isMultipleChoice; + const binaryForecastAvailability = + isBinarySingleQuestion && isQuestionPost(postData) + ? getQuestionForecastAvailability(postData.question) + : null; + const showSideBySide = isMultipleChoice || isNonFanGroup || @@ -169,7 +179,14 @@ export const ConsumerShell: FC<{ aggregateCoherenceLinks?.data.filter(isDisplayableQuestionLink) ?? []; const hasKeyFactors = (postData.key_factors?.length ?? 0) > 0; const hasQuestionLinks = questionLinkAggregates.length > 0; - const shouldShowKeyFactorsSection = hasKeyFactors || hasQuestionLinks; + const questionForecastAvailability = isQuestionPost(postData) + ? getQuestionForecastAvailability(postData.question) + : null; + const isForecastEmpty = + !!questionForecastAvailability?.isEmpty && + !questionForecastAvailability?.cpRevealsOn; + const shouldShowKeyFactorsSection = + hasKeyFactors || hasQuestionLinks || isForecastEmpty; return (
@@ -216,19 +233,39 @@ export const ConsumerShell: FC<{ > {isBinarySingleQuestion && isQuestionPost(postData) ? (
- + {hideCP ? ( + + ) : binaryForecastAvailability?.cpRevealsOn ? ( + + ) : ( + + )}
) : (
- + {hideCP && !isContinuousSingleQuestion ? ( + + ) : ( + + )}
)} {!isFanGraph && !isDateGroup && ( diff --git a/front_end/src/app/(main)/questions/[id]/components/question_page_shell/title_row.tsx b/front_end/src/app/(main)/questions/[id]/components/question_page_shell/title_row.tsx index 69b40e9597..0433151a5d 100644 --- a/front_end/src/app/(main)/questions/[id]/components/question_page_shell/title_row.tsx +++ b/front_end/src/app/(main)/questions/[id]/components/question_page_shell/title_row.tsx @@ -4,13 +4,16 @@ import { FC } from "react"; import QuestionHeaderCPStatus from "@/app/(main)/questions/[id]/components/question_view/forecaster_question_view/question_header/question_header_cp_status"; import QuestionTitle from "@/app/(main)/questions/[id]/components/question_view/shared/question_title"; +import RevealCPButton from "@/app/(main)/questions/[id]/components/reveal_cp_button"; import ConditionalTile from "@/components/conditional_tile"; +import { useHideCP } from "@/contexts/cp_context"; import { PostWithForecasts } from "@/types/post"; -import { QuestionWithForecasts } from "@/types/question"; +import { QuestionType, QuestionWithForecasts } from "@/types/question"; import cn from "@/utils/core/cn"; import { isConditionalPost, isContinuousQuestion, + isGroupOfQuestionsPost, isQuestionPost, } from "@/utils/questions/helpers"; @@ -23,6 +26,8 @@ type Props = { }; const TitleRow: FC = ({ post, variant, className }) => { + const { hideCP } = useHideCP(); + if (isConditionalPost(post)) { return (
@@ -32,6 +37,9 @@ const TitleRow: FC = ({ post, variant, className }) => { } if (variant === "forecaster" && isQuestionPost(post)) { + const isMultipleChoice = post.question.type === QuestionType.MultipleChoice; + const isContinuous = isContinuousQuestion(post.question); + return (
= ({ post, variant, className }) => { className )} > -
-
- +
+
+ {post.title} -
+
+ {isMultipleChoice ? ( + hideCP && + ) : ( + + )} +
+
+
+ {!isContinuous && ( +
+ {isMultipleChoice && hideCP ? ( + + ) : ( -
+ )} +
+ )} +
+ ); + } + + if (variant === "forecaster" && isGroupOfQuestionsPost(post)) { + return ( +
+
+
+ + {post.title} + + {hideCP && ( +
+ +
+ )}
- {!isContinuousQuestion(post.question) && ( -
- + {hideCP && ( +
+
)}
diff --git a/front_end/src/app/(main)/questions/[id]/components/question_view/consumer_question_view/prediction/single_question_prediction/continuous_question_prediction.tsx b/front_end/src/app/(main)/questions/[id]/components/question_view/consumer_question_view/prediction/single_question_prediction/continuous_question_prediction.tsx index c6fbcf3084..c0777871a6 100644 --- a/front_end/src/app/(main)/questions/[id]/components/question_view/consumer_question_view/prediction/single_question_prediction/continuous_question_prediction.tsx +++ b/front_end/src/app/(main)/questions/[id]/components/question_view/consumer_question_view/prediction/single_question_prediction/continuous_question_prediction.tsx @@ -1,8 +1,10 @@ "use client"; +import RevealCPButton from "@/app/(main)/questions/[id]/components/reveal_cp_button"; import { getContinuousAreaChartData } from "@/components/charts/continuous_area_chart"; import MinifiedContinuousAreaChart from "@/components/charts/minified_continuous_area_chart"; import ConsumerContinuousTile from "@/components/consumer_post_card/consumer_question_tile/consumer_continuous_tile"; +import { useHideCP } from "@/contexts/cp_context"; import { QuestionStatus } from "@/types/post"; import { QuestionWithNumericForecasts } from "@/types/question"; import { getQuestionForecastAvailability } from "@/utils/questions/forecastAvailability"; @@ -17,8 +19,8 @@ const ContinuousQuestionPrediction: React.FC = ({ chartHeight, }) => { const forecastAvailability = getQuestionForecastAvailability(question); + const { hideCP } = useHideCP(); - // Hide chart if no forecasts or CP not yet revealed const shouldHideChart = forecastAvailability.isEmpty || !!forecastAvailability.cpRevealsOn; @@ -27,6 +29,14 @@ const ContinuousQuestionPrediction: React.FC = ({ isClosed: question.status === QuestionStatus.CLOSED, }); + if (hideCP) { + return ( +
+ +
+ ); + } + return (
= ({ : "border-[0.5px] border-olive-500 p-3 dark:border-olive-500-dark md:p-3"; if (isContinuous) { - return ( - !forecastAvailability.isEmpty && ( + const containerClassName = cn( + "flex min-w-[110px] flex-col rounded-md border border-olive-800/20 p-2 dark:border-olive-800 md:px-3 md:py-2.5", + { + "min-h-full w-[200px]": size === "lg" && hideLabel, + "w-max max-w-[200px]": size === "lg" && !hideLabel, + "max-w-[130px]": + !forecastAvailability.cpRevealsOn && + (size === "md" || (isEmbed && !isEmbedBelow376 && !isEmbedWide)), + "gap-1": !hideLabel && size === "lg", + "gap-0": size === "md", + "-gap-2": size === "md" && hideLabel, + [embedBorderClass]: isEmbed, + "max-w-[195px]": isEmbedWide, + "min-w-[200px] border-none p-0": isEmbedBelow376, + } + ); + + // No forecasts and no upcoming reveal — render empty outline box to preserve column width + if (forecastAvailability.isEmpty && !forecastAvailability.cpRevealsOn) { + return (
-
- {!hideLabel && ( - - )} - {isEmbedBelow376 && ( -

- {t("currentEstimate")} -

- )} - {!hideCP && ( - - )} +

+ {t("currentEstimate")} +

+
+ ); + } + + // CP reveals in the future — center the countdown, skip the mini chart + if (forecastAvailability.cpRevealsOn) { + return ( +
+ +
+ ); + } + + // CP hidden by user preference — center the reveal button, skip the mini chart + if (hideCP && !isEmbed) { + if (size === "md") { + return ( +
+
- {!!continuousAreaChartData && ( + ); + } + return ( +
+ +
+ ); + } + + return ( +
+
+ {!hideLabel && ( )} - {!hideCP && ( - span]:whitespace-normal", - { - "-mt-2 text-center": size === "md" && hideLabel, - "text-center": size === "md" && !hideLabel, - "mt-0 md:[&>span]:whitespace-nowrap": isEmbed, - } - )} - size={"sm"} - unit={size === "md" ? "" : undefined} - boldValueUnit={true} + {isEmbedBelow376 && ( +

+ {t("currentEstimate")} +

+ )} + {!hideCP && !forecastAvailability.cpRevealsOn && ( + )}
- ) + {!!continuousAreaChartData && !forecastAvailability.cpRevealsOn && ( +
+ +
+ )} + {!hideCP && !forecastAvailability.cpRevealsOn && ( + span]:whitespace-normal", + { + "-mt-2 text-center": size === "md" && hideLabel, + "text-center": size === "md" && !hideLabel, + "mt-0 md:[&>span]:whitespace-nowrap": isEmbed, + } + )} + size={"sm"} + unit={size === "md" ? "" : undefined} + boldValueUnit={true} + /> + )} +
); } else if (question.type === QuestionType.Binary) { + if (hideCP && !isEmbed) { + return ( +
+ +
+ ); + } + return (
= ({ colorOverride={colorOverride} /> )} - {!hideCP && ( + {!hideCP && !forecastAvailability.cpRevealsOn && ( = ({ boldValueUnit={true} /> )} + {!!forecastAvailability.cpRevealsOn && !isEmbed && ( + + )}
); } diff --git a/front_end/src/components/charts/fan_chart.tsx b/front_end/src/components/charts/fan_chart.tsx index 633af04d31..3c6cb661f7 100644 --- a/front_end/src/components/charts/fan_chart.tsx +++ b/front_end/src/components/charts/fan_chart.tsx @@ -452,7 +452,7 @@ const FanChart: FC = ({ domainPadding={v.domainPadding(variantArgs)} padding={chartPadding} containerComponent={ - withTooltip ? ( + withTooltip && !hideCP && !forecastAvailability?.cpRevealsOn ? ( containerWithTooltip ) : ( = ({ }, ]} containerComponent={ - onCursorChange ? ( + onCursorChange && + !hideCP && + !forecastAvailability?.cpRevealsOn ? ( CursorContainer ) : ( = ({ }, ]} containerComponent={ - onCursorChange ? ( + onCursorChange && + !hideCP && + !forecastAvailability?.cpRevealsOn ? ( CursorContainer ) : ( = ({ axis: { stroke: "transparent", }, - grid: isEmptyDomain - ? { - stroke: getThemeColor(METAC_COLORS.gray["300"]), - strokeWidth: 1, - strokeDasharray: "2, 5", - } - : { - stroke: "transparent", - }, + grid: + isEmptyDomain || hideCP + ? { + stroke: getThemeColor(METAC_COLORS.gray["300"]), + strokeWidth: 1, + strokeDasharray: "2, 5", + } + : { + stroke: "transparent", + }, }} label={yLabel} offsetX={ diff --git a/front_end/src/components/charts/numeric_chart.tsx b/front_end/src/components/charts/numeric_chart.tsx index 0268d32d5f..a8da8bf077 100644 --- a/front_end/src/components/charts/numeric_chart.tsx +++ b/front_end/src/components/charts/numeric_chart.tsx @@ -221,7 +221,12 @@ const NumericChart: FC = ({ }, [rightPadding, MIN_RIGHT_PADDING]); const containerComponent = useMemo(() => { - if (nonInteractive) { + if ( + nonInteractive || + forecastAvailability?.isEmpty || + hideCP || + !!forecastAvailability?.cpRevealsOn + ) { return ( = ({ {...getReferenceProps()} > = ({ question.aggregations[question.default_aggregation_method]?.latest ?.centers?.[0]; - if (question.type !== QuestionType.Binary || !questionCP) { + if (question.type !== QuestionType.Binary) { return null; } diff --git a/front_end/src/components/detailed_question_card/detailed_group_card/index.tsx b/front_end/src/components/detailed_question_card/detailed_group_card/index.tsx index 9be6aed4b5..d64adce9f1 100644 --- a/front_end/src/components/detailed_question_card/detailed_group_card/index.tsx +++ b/front_end/src/components/detailed_question_card/detailed_group_card/index.tsx @@ -5,7 +5,6 @@ import { VictoryThemeDefinition } from "victory"; import { useIsEmbedMode } from "@/app/(embed)/questions/components/question_view_mode_context"; import GroupTimeline from "@/app/(main)/questions/[id]/components/group_timeline"; -import RevealCPButton from "@/app/(main)/questions/[id]/components/reveal_cp_button"; import FanChart from "@/components/charts/fan_chart"; import { MultipleChoiceTile } from "@/components/post_card/multiple_choice_tile"; import { @@ -147,68 +146,59 @@ const DetailedGroupCard: FC = ({ ); return ( - <> - - {hideCP && } - - ); - } - - return ( - <> - - {hideCP && } - + ); + } + + return ( + ); } case GroupOfQuestionsGraphType.FanGraph: return ( - <> - - {hideCP && } - + ); default: return null; diff --git a/front_end/src/components/detailed_question_card/detailed_question_card/continuous_chart_card.tsx b/front_end/src/components/detailed_question_card/detailed_question_card/continuous_chart_card.tsx index 739c3061ee..bff014a26d 100644 --- a/front_end/src/components/detailed_question_card/detailed_question_card/continuous_chart_card.tsx +++ b/front_end/src/components/detailed_question_card/detailed_question_card/continuous_chart_card.tsx @@ -7,7 +7,6 @@ import { VictoryThemeDefinition } from "victory"; import { useIsEmbedMode } from "@/app/(embed)/questions/components/question_view_mode_context"; import QuestionHeaderCPStatus from "@/app/(main)/questions/[id]/components/question_view/forecaster_question_view/question_header/question_header_cp_status"; -import RevealCPButton from "@/app/(main)/questions/[id]/components/reveal_cp_button"; import NumericTimeline from "@/components/charts/numeric_timeline"; import QuestionPredictionTooltip from "@/components/charts/primitives/question_prediction_tooltip"; import ContinuousPredictionChart from "@/components/forecast_maker/continuous_input/continuous_prediction_chart"; @@ -135,10 +134,6 @@ const DetailedContinuousChartCard: FC = ({ const discreteValueOptions = getDiscreteValueOptions(question); const cpCursorElement = useMemo(() => { - if (forecastAvailability?.isEmpty) { - return t("noForecastsYet"); - } - if (hideCP) { return "..."; } @@ -280,7 +275,7 @@ const DetailedContinuousChartCard: FC = ({ simplifiedCursor={false} title={timelineTitle} forecastAvailability={forecastAvailability} - cursorTooltip={cursorTooltip} + cursorTooltip={forecastAvailability?.isEmpty ? undefined : cursorTooltip} isConsumerView={isConsumerView} isEmbedded={isEmbed} height={chartHeight} @@ -360,9 +355,7 @@ const DetailedContinuousChartCard: FC = ({ className="flex w-full flex-col justify-center" style={{ height: chartHeight }} > - {hideCP || isCpHidden ? ( - - ) : ( + {!hideCP && !isCpHidden && ( = ({ embedChartType={embedChartType} keyFactors={keyFactors} /> - {hideCP && } ); case QuestionType.MultipleChoice: @@ -102,7 +100,6 @@ const DetailedQuestionCard: FC = ({ chartTheme={chartTheme} defaultZoom={defaultZoom} /> - {hideCP && } ); default: