From 02684fd532c82d3babddb35f70e4355ee356c387 Mon Sep 17 00:00:00 2001
From: Nikita <93587872+ncarazon@users.noreply.github.com>
Date: Thu, 7 May 2026 11:32:42 +0300
Subject: [PATCH 1/6] Mount consumer binary + continuous side-by-side with the
detailed chart card & Timeline / Histogram toggle on binary bodies (#4692)
* feat: add QuestionHeaderCPStatus next to timeline for consumer binary questions
* fix: remove extra mt-8 top margin from consumer continuous timeline
* feat: add Timeline/Histogram toggle to binary detailed chart cards and fix consumer binary radial padding
* fix: resolve layout jumping between chart views, fix histogram full-width responsive scaling, and center binary radial charts on mobile
* fix: prevent chart layout snapping with synchronous measurements, enable side-by-side continuous charts on sm breakpoints, and standardize spacing utility classes
* fix: show fallback message when histogram tab has no data
---
front_end/messages/cs.json | 1 +
front_end/messages/en.json | 1 +
front_end/messages/es.json | 1 +
front_end/messages/pt.json | 1 +
front_end/messages/zh-TW.json | 1 +
front_end/messages/zh.json | 1 +
.../components/question_page_shell/index.tsx | 43 ++++---
.../question_header_cp_status.tsx | 7 +-
front_end/src/components/charts/histogram.tsx | 2 +-
.../src/components/charts/numeric_chart.tsx | 2 +-
.../components/charts/numeric_timeline.tsx | 2 +-
.../charts/primitives/chart_container.tsx | 10 +-
.../continuous_chart_card.tsx | 107 +++++++++++++++++-
front_end/src/hooks/use_container_size.ts | 2 +
14 files changed, 158 insertions(+), 23 deletions(-)
diff --git a/front_end/messages/cs.json b/front_end/messages/cs.json
index 0a1cd6fd24..b5d76cd9dd 100644
--- a/front_end/messages/cs.json
+++ b/front_end/messages/cs.json
@@ -2169,5 +2169,6 @@
"whyTrustMetaculusLessNoise": "Předpovědi Metaculus, poháněné davem, prořezávají šum tím, že zakládají každou předpověď na transparentních důkazech, odpovědném skóre a desetiletí doložené přesnosti. Metaculus vybavuje politiky, výzkumníky, novináře a korporátní organizace předpověďmi založenými na důkazech, které poskytují jasný, kvantifikovatelný vhled do nejkritičtějších nejistot světa. Prozkoumejte naši nabídku Business řešení a zjistěte, jak může Metaculus zlepšit rozhodování vaší organizace.",
"publishTimeLockedDescription": "Čas publikace nelze po vytvoření změnit.",
"nMore": "{count} více...",
+ "noHistogramData": "Žádná data histogramu nejsou k dispozici",
"thousandsOfOpenQuestions": "20 000+ otevřených otázek"
}
diff --git a/front_end/messages/en.json b/front_end/messages/en.json
index d4b729c939..b69fb4691f 100644
--- a/front_end/messages/en.json
+++ b/front_end/messages/en.json
@@ -1185,6 +1185,7 @@
"deselectAll": "deselect all",
"forecastDataIsEmpty": "Forecast data is empty",
"noForecastsYet": "No forecasts yet",
+ "noHistogramData": "No histogram data available",
"RevealTemporarily": "Reveal Temporarily",
"CPIsHidden": "Community Prediction is hidden",
"createdByUserOnDate": "Created by {user} on {date}",
diff --git a/front_end/messages/es.json b/front_end/messages/es.json
index dd79b0d44b..a549d019ba 100644
--- a/front_end/messages/es.json
+++ b/front_end/messages/es.json
@@ -2169,5 +2169,6 @@
"whyTrustMetaculusLessNoise": "Los pronósticos impulsados por la multitud de Metaculus cortan el ruido al basar cada predicción en evidencia transparente, puntuaciones responsables y una década de precisión demostrada. Metaculus equipa a los responsables de políticas, investigadores, periodistas y organizaciones corporativas con pronósticos basados en evidencia que ofrecen una visión clara y cuantificable de las incertidumbres más críticas del mundo. Explora nuestra suite de Soluciones Empresariales para aprender cómo Metaculus puede mejorar la toma de decisiones de tu organización.",
"publishTimeLockedDescription": "La hora de publicación no puede cambiarse después de la creación.",
"nMore": "{count} más...",
+ "noHistogramData": "No hay datos de histograma disponibles",
"thousandsOfOpenQuestions": "20,000+ preguntas abiertas"
}
diff --git a/front_end/messages/pt.json b/front_end/messages/pt.json
index b6f289b566..dd627147cf 100644
--- a/front_end/messages/pt.json
+++ b/front_end/messages/pt.json
@@ -2167,5 +2167,6 @@
"whyTrustMetaculusLessNoise": "As previsões baseadas na multidão do Metaculus cortam o ruído ao basear cada previsão em evidências transparentes, pontuações responsáveis e uma década de precisão demonstrada. O Metaculus equipa formuladores de políticas, pesquisadores, jornalistas e organizações corporativas com previsões baseadas em evidências que oferecem insights claros e quantificáveis sobre as incertezas mais críticas do mundo. Explore nosso conjunto de Soluções Empresariais para saber mais sobre como o Metaculus pode melhorar a tomada de decisões da sua organização.",
"publishTimeLockedDescription": "O horário de publicação não pode ser alterado após a criação.",
"nMore": "{count} a mais...",
+ "noHistogramData": "Não há dados de histograma disponíveis",
"thousandsOfOpenQuestions": "20.000+ perguntas abertas"
}
diff --git a/front_end/messages/zh-TW.json b/front_end/messages/zh-TW.json
index b16ee57d94..48d1d4a1bc 100644
--- a/front_end/messages/zh-TW.json
+++ b/front_end/messages/zh-TW.json
@@ -2166,5 +2166,6 @@
"whyTrustMetaculusLessNoise": "Metaculus 的群眾驅動預測通過將每一個預測植根於透明的證據、負責的計分和十年的已證準確性,抑制了噪音。Metaculus 為政策制定者、研究人員、記者和企業組織提供基於證據的預測,為全球最重要的不確定性提供清晰、可量化的洞察。探索我們的 企業解決方案 ,了解 Metaculus 如何改善貴組織的決策。",
"publishTimeLockedDescription": "建立後將無法更改發佈時間。",
"nMore": "還有 {count} 個...",
+ "noHistogramData": "沒有可用的直方圖數據",
"thousandsOfOpenQuestions": "20,000+ 開放問題"
}
diff --git a/front_end/messages/zh.json b/front_end/messages/zh.json
index 6862ba5ce3..916dd4f143 100644
--- a/front_end/messages/zh.json
+++ b/front_end/messages/zh.json
@@ -2171,5 +2171,6 @@
"whyTrustMetaculusLessNoise": "Metaculus 的众包预测通过以透明证据、可追溯的评分以及十年证明的准确性为基础来削减预测中的噪音。Metaculus 为政策制定者、研究人员、记者和企业组织提供基于证据的预测,为世界上最重要的不确定性提供清晰、可量化的洞察。了解我们的企业解决方案 ,了解 Metaculus 如何改善贵组织的决策。",
"publishTimeLockedDescription": "创建后将无法更改发布时间。",
"nMore": "{count} 更多...",
+ "noHistogramData": "没有可用的直方图数据",
"thousandsOfOpenQuestions": "20,000+ 开放问题"
}
diff --git a/front_end/src/app/(main)/questions/[id]/components/question_page_shell/index.tsx b/front_end/src/app/(main)/questions/[id]/components/question_page_shell/index.tsx
index 0c67495458..7da588d633 100644
--- a/front_end/src/app/(main)/questions/[id]/components/question_page_shell/index.tsx
+++ b/front_end/src/app/(main)/questions/[id]/components/question_page_shell/index.tsx
@@ -38,6 +38,7 @@ import { QuestionVariantComposer } from "../question_variant_composer";
import ActionRow from "../question_view/action_row";
import ConsumerQuestionPrediction from "../question_view/consumer_question_view/prediction";
import QuestionTimeline from "../question_view/consumer_question_view/timeline";
+import QuestionHeaderCPStatus from "../question_view/forecaster_question_view/question_header/question_header_cp_status";
const baseSectionClassName =
"relative z-10 flex w-[59rem] max-w-full flex-col gap-6 overflow-x-clip rounded border border-blue-400 p-4 text-gray-900 dark:border-blue-200-dark dark:text-gray-900-dark lg:p-8";
@@ -152,8 +153,10 @@ export const ConsumerShell: FC<{
!isMultipleChoice;
const showSideBySide =
- (isMultipleChoice || isNonFanGroup || isBinarySingleQuestion) &&
- !isContinuousSingleQuestion;
+ isMultipleChoice ||
+ isNonFanGroup ||
+ isBinarySingleQuestion ||
+ isContinuousSingleQuestion;
const showClosedMessageMultipleChoice =
isMultipleChoicePost(postData) &&
@@ -191,7 +194,7 @@ export const ConsumerShell: FC<{
-
+
{showClosedMessageMultipleChoice && (
{t("predictionClosedMessage")}
@@ -204,17 +207,30 @@ export const ConsumerShell: FC<{
!isMultipleChoice &&
!isNonFanGroup &&
"flex-col-reverse",
- showSideBySide && "sm:flex-row sm:items-center sm:gap-8"
+ showSideBySide &&
+ cn("sm:flex-row sm:items-center", {
+ "sm:gap-0 md:gap-8": isBinarySingleQuestion,
+ "sm:gap-8": !isBinarySingleQuestion,
+ })
)}
>
-
-
-
+ {isBinarySingleQuestion && isQuestionPost(postData) ? (
+
+
+
+ ) : (
+
+
+
+ )}
{!isFanGraph && !isDateGroup && (
)}
diff --git a/front_end/src/app/(main)/questions/[id]/components/question_view/forecaster_question_view/question_header/question_header_cp_status.tsx b/front_end/src/app/(main)/questions/[id]/components/question_view/forecaster_question_view/question_header/question_header_cp_status.tsx
index 814150e4e7..b0f70352c2 100644
--- a/front_end/src/app/(main)/questions/[id]/components/question_view/forecaster_question_view/question_header/question_header_cp_status.tsx
+++ b/front_end/src/app/(main)/questions/[id]/components/question_view/forecaster_question_view/question_header/question_header_cp_status.tsx
@@ -208,8 +208,8 @@ const QuestionHeaderCPStatus: FC
= ({
className={cn(
"flex flex-col",
{
- "gap-4": size === "lg", // Desktop: 16px gap
- "gap-1.5": size === "md", // Mobile: 6px gap
+ "w-36 items-center": size === "lg",
+ "gap-1.5": size === "md",
},
isEmbed && "[@container(max-width:375px)]:scale-[130%]"
)}
@@ -225,7 +225,8 @@ const QuestionHeaderCPStatus: FC = ({
= ({
}
padding={{ top: 0, bottom: 20, left: 12, right: 12 }}
height={75}
- width={!!width ? width : undefined}
+ width={width || chartWidth || undefined}
>
ChartData;
- chartTitle?: string;
+ chartTitle?: ReactNode;
height?: number;
hideCP?: boolean;
defaultZoom?: TimelineChartZoomOption;
diff --git a/front_end/src/components/charts/numeric_timeline.tsx b/front_end/src/components/charts/numeric_timeline.tsx
index a6ec474b9a..54867f2919 100644
--- a/front_end/src/components/charts/numeric_timeline.tsx
+++ b/front_end/src/components/charts/numeric_timeline.tsx
@@ -51,7 +51,7 @@ type Props = {
inboundOutcomeCount?: number | null;
isEmbedded?: boolean;
simplifiedCursor?: boolean;
- title?: string;
+ title?: ReactNode;
forecastAvailability?: ForecastAvailability;
questionStatus?: QuestionStatus;
cursorTooltip?: ReactNode;
diff --git a/front_end/src/components/charts/primitives/chart_container.tsx b/front_end/src/components/charts/primitives/chart_container.tsx
index ced56dea0f..d4ee57948c 100644
--- a/front_end/src/components/charts/primitives/chart_container.tsx
+++ b/front_end/src/components/charts/primitives/chart_container.tsx
@@ -1,7 +1,13 @@
"use client";
import { Tab, TabGroup, TabList } from "@headlessui/react";
import { isNil } from "lodash";
-import { forwardRef, Fragment, PropsWithChildren, useState } from "react";
+import {
+ forwardRef,
+ Fragment,
+ PropsWithChildren,
+ ReactNode,
+ useState,
+} from "react";
import { useIsEmbedMode } from "@/app/(embed)/questions/components/question_view_mode_context";
import { TimelineChartZoomOption } from "@/types/charts";
@@ -12,7 +18,7 @@ type Props = {
height: number;
zoom?: TimelineChartZoomOption;
onZoomChange?: (zoom: TimelineChartZoomOption) => void;
- chartTitle?: string;
+ chartTitle?: ReactNode;
leftLegend?: React.ReactNode;
headerExtra?: React.ReactNode;
};
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 5707b95416..739c3061ee 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
@@ -1,14 +1,18 @@
"use client";
import { isNil } from "lodash";
+import dynamic from "next/dynamic";
import { useTranslations } from "next-intl";
import { FC, ReactNode, useCallback, useMemo, useState } from "react";
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";
+import Button from "@/components/ui/button";
+import { GroupButton } from "@/components/ui/button_group";
import { useAuth } from "@/contexts/auth_context";
import { EmbedChartType, TimelineChartZoomOption } from "@/types/charts";
import { KeyFactor } from "@/types/comment";
@@ -31,6 +35,10 @@ import {
isContinuousQuestion,
} from "@/utils/questions/helpers";
+const Histogram = dynamic(() => import("@/components/charts/histogram"), {
+ ssr: false,
+});
+
type Props = {
question: QuestionWithNumericForecasts;
hideCP?: boolean;
@@ -47,6 +55,8 @@ type Props = {
keyFactors?: KeyFactor[];
};
+type ChartView = "timeline" | "histogram";
+
const DetailedContinuousChartCard: FC = ({
question,
hideCP,
@@ -71,6 +81,7 @@ const DetailedContinuousChartCard: FC = ({
const effectiveWithZoomPicker = withZoomPicker ?? true;
const isConsumerView = isConsumerViewProp ?? !user;
const [isChartReady, setIsChartReady] = useState(false);
+ const [activeView, setActiveView] = useState("timeline");
const aggregation =
question.aggregations[question.default_aggregation_method];
@@ -212,8 +223,35 @@ const DetailedContinuousChartCard: FC = ({
const isBinary = question.type === QuestionType.Binary;
- const timelineTitle =
- !isEmbed && !hideTitle ? t("forecastTimelineHeading") : undefined;
+ const chartViewButtons: GroupButton[] = [
+ { value: "timeline", label: t("timeline") },
+ { value: "histogram", label: t("histogram") },
+ ];
+
+ const viewToggle = isBinary ? (
+
+ {chartViewButtons.map(({ value, label }) => (
+ setActiveView(value)}
+ className={cn(
+ "h-6 rounded border-0 px-1 py-0.5 text-sm font-normal leading-4",
+ activeView === value
+ ? "bg-blue-200 text-blue-800 hover:text-blue-800 active:text-blue-800 dark:bg-blue-200-dark dark:text-blue-800-dark"
+ : "text-gray-500 hover:text-gray-500 active:text-gray-500 dark:text-gray-500-dark"
+ )}
+ >
+ {label}
+
+ ))}
+
+ ) : null;
+
+ // Binary gets the toggle as the title node; continuous keeps the text heading.
+ let timelineTitle: ReactNode;
+ if (!isEmbed && !hideTitle) {
+ timelineTitle = isBinary ? viewToggle : t("forecastTimelineHeading");
+ }
const chartHeight = embedChartHeight ?? 150;
@@ -276,6 +314,67 @@ const DetailedContinuousChartCard: FC = ({
!hideCP &&
!forecastAvailability?.cpRevealsOn;
+ const renderHistogram = () => {
+ const aggregationLatest =
+ question.aggregations[question.default_aggregation_method].latest;
+ const histogram = aggregationLatest?.histogram?.at(0);
+ if (!histogram?.length) {
+ return (
+
+ {!isEmbed && !hideTitle && (
+
+ )}
+
+
+ {t("noHistogramData")}
+
+
+
+ );
+ }
+
+ const histogramData = histogram.map((value, index) => ({
+ x: index,
+ y: value,
+ }));
+ const median = aggregationLatest?.centers?.[0];
+ const mean = aggregationLatest?.means?.[0];
+
+ return (
+
+ {!isEmbed && !hideTitle && (
+
+ )}
+
+ {hideCP || isCpHidden ? (
+
+ ) : (
+
+ )}
+
+
+ );
+ };
+
if (canRenderCurrentEmbed) {
return (
@@ -294,6 +393,10 @@ const DetailedContinuousChartCard: FC
= ({
);
}
+ if (isBinary && activeView === "histogram") {
+ return <>{renderHistogram()}>;
+ }
+
return (
() => {
return;
}
+ onResize();
+
const resizeObserver = new ResizeObserver(onResize);
resizeObserver.observe(ref.current);
From 624b3500fb260cd97c785fbac0822b3178696b29 Mon Sep 17 00:00:00 2001
From: Nikita <93587872+ncarazon@users.noreply.github.com>
Date: Mon, 11 May 2026 17:32:36 +0300
Subject: [PATCH 2/6] Live cursor updates the side-by-side left card (#4694)
* feat: live cursor updates left-panel value, range, and distribution for continuous consumer view, with dot indicator on timeline and no tooltip/dashed line in consumer mode
* feat: animate consumer mobile mini chart on timeline cursor and fix mobile tooltip position tracking
* fix: always serialize full forecast_values in aggregation history for cursor-time distribution animation
* fix: memoize cursorChartData to skip cdfToPmf recalculation when forecast_values reference is unchanged
* fix: resolve cursor dot color override through resolveToCssColor
* refactor: lazily fetch full aggregation history from aggregation explorer on pointer enter for continuous consumer cursor animation, keeping posts endpoint lightweight
* fix: pass include_bots_in_aggregates to aggregation explorer to match stored aggregation bot semantics, format aggregate_forecasts.py
* fix: use stable setActiveForecast reference in effect deps to prevent infinite re-render loop on hover
---
.../components/question_page_shell/index.tsx | 16 ++--
.../continuous_question_prediction.tsx | 31 ++++++-
.../question_header_cp_status.tsx | 45 +++++++++-
.../src/components/charts/numeric_chart.tsx | 82 ++++++++++++++++---
.../components/charts/numeric_timeline.tsx | 3 +
.../charts/primitives/line_cursor_points.tsx | 10 +--
.../consumer_continuous_tile.tsx | 24 ++++--
.../continuous_chart_card.tsx | 67 +++++++++++++--
.../hooks/use_full_aggregation.ts | 38 +++++++++
.../question_tile/continuous_cp_bar.tsx | 20 +++--
.../continuous_chart_cursor_context.tsx | 45 ++++++++++
front_end/src/hooks/use_chart_tooltip.ts | 6 +-
12 files changed, 336 insertions(+), 51 deletions(-)
create mode 100644 front_end/src/components/detailed_question_card/detailed_question_card/hooks/use_full_aggregation.ts
create mode 100644 front_end/src/contexts/continuous_chart_cursor_context.tsx
diff --git a/front_end/src/app/(main)/questions/[id]/components/question_page_shell/index.tsx b/front_end/src/app/(main)/questions/[id]/components/question_page_shell/index.tsx
index 7da588d633..c8ac6ed462 100644
--- a/front_end/src/app/(main)/questions/[id]/components/question_page_shell/index.tsx
+++ b/front_end/src/app/(main)/questions/[id]/components/question_page_shell/index.tsx
@@ -9,6 +9,7 @@ import DetailedGroupCard from "@/components/detailed_question_card/detailed_grou
import DetailedQuestionCard from "@/components/detailed_question_card/detailed_question_card";
import ForecastMaker from "@/components/forecast_maker";
import CommunityDisclaimer from "@/components/post_card/community_disclaimer";
+import { ContinuousChartCursorProvider } from "@/contexts/continuous_chart_cursor_context";
import { useContentTranslatedBannerContext } from "@/contexts/translations_banner_context";
import {
GroupOfQuestionsGraphType,
@@ -235,7 +236,7 @@ export const ConsumerShell: FC<{
= ({
+
+ {/* Bridges timeline cursor to the mobile mini chart in consumer view */}
+
+
}
forecaster={
= ({
chartHeight,
}) => {
const forecastAvailability = getQuestionForecastAvailability(question);
+ const cursorCtx = useContinuousChartCursor();
+ const cursorForecast = cursorCtx?.activeForecast ?? null;
// Hide chart if no forecasts or CP not yet revealed
const shouldHideChart =
@@ -27,12 +37,29 @@ const ContinuousQuestionPrediction: React.FC = ({
isClosed: question.status === QuestionStatus.CLOSED,
});
+ const cursorForecastValues = cursorForecast?.forecast_values ?? null;
+
+ // Null when cursor is inactive — chart falls back to the latest aggregate.
+ const cursorChartData = useMemo(() => {
+ if (!cursorForecastValues) return null;
+ return [
+ {
+ pmf: cdfToPmf(cursorForecastValues),
+ cdf: cursorForecastValues,
+ type: (question.status === QuestionStatus.CLOSED
+ ? "community_closed"
+ : "community") as ContinuousAreaType,
+ },
+ ];
+ }, [cursorForecastValues, question.status]);
+
return (
{!shouldHideChart && (
<>
@@ -40,7 +67,7 @@ const ContinuousQuestionPrediction: React.FC
= ({
= ({
@@ -36,6 +46,7 @@ const QuestionHeaderCPStatus: FC = ({
hideLabel = false,
colorOverride,
chartTheme,
+ cursorForecast,
}) => {
const locale = useLocale();
const t = useTranslations();
@@ -58,6 +69,22 @@ const QuestionHeaderCPStatus: FC = ({
const isEmbedBelow376 = isEmbed && (w ?? 0) > 0 && (w ?? 0) < 376;
const isEmbedWide = isEmbed && (w ?? 0) >= 500;
+ const cursorForecastValues = cursorForecast?.forecast_values ?? null;
+ const cursorAreaChartData = useMemo(() => {
+ if (!cursorForecastValues) return null;
+ return [
+ {
+ pmf: cdfToPmf(cursorForecastValues),
+ cdf: cursorForecastValues,
+ type: (question.status === QuestionStatus.RESOLVED
+ ? "community_resolved"
+ : question.status === QuestionStatus.CLOSED
+ ? "community_closed"
+ : "community") as ContinuousAreaType,
+ },
+ ];
+ }, [cursorForecastValues, question.status]);
+
if (question.status === QuestionStatus.RESOLVED && question.resolution) {
// Resolved/Annulled/Ambiguous
const formatedResolution = formatResolution({
@@ -104,6 +131,10 @@ const QuestionHeaderCPStatus: FC = ({
? "border-[0.5px] border-gray-500 p-3 dark:border-gray-500-dark md:p-3"
: "border-[0.5px] border-olive-500 p-3 dark:border-olive-500-dark md:p-3";
+ const cursorCenter = cursorForecast?.centers?.[0] ?? null;
+ const cursorLower = cursorForecast?.interval_lower_bounds?.[0] ?? null;
+ const cursorUpper = cursorForecast?.interval_upper_bounds?.[0] ?? null;
+
if (isContinuous) {
return (
!forecastAvailability.isEmpty && (
@@ -148,6 +179,12 @@ const QuestionHeaderCPStatus: FC = ({
size={size}
variant="question"
colorOverride={colorOverride}
+ overrideCenter={cursorCenter}
+ overrideBounds={
+ cursorLower !== null && cursorUpper !== null
+ ? [cursorLower, cursorUpper]
+ : null
+ }
/>
)}
@@ -161,7 +198,7 @@ const QuestionHeaderCPStatus: FC = ({
>
= ({
nonInteractive = false,
isEmbedded = false,
simplifiedCursor = false,
+ hideCursorValueLabel = false,
forecastAvailability,
questionStatus,
resolution,
cursorTooltip,
+ isConsumerView,
questionType,
newsAnnotations,
showNewsAnnotations,
@@ -142,6 +145,11 @@ const NumericChart: FC = ({
const [zoom, setZoom] = useState(defaultZoom);
const [isCursorActive, setIsCursorActive] = useState(false);
+ const isContinuousConsumerView =
+ isConsumerView &&
+ (questionType === QuestionType.Numeric ||
+ questionType === QuestionType.Date ||
+ questionType === QuestionType.Discrete);
const { ref: chartContainerRef, width: chartWidth } =
useContainerSize();
const { line, area, points, yDomain, xDomain, yScale, xScale } = useMemo(
@@ -252,13 +260,17 @@ const NumericChart: FC = ({
}
}}
cursorComponent={
-
+ isContinuousConsumerView ? (
+
+ ) : (
+
+ )
}
cursorLabelComponent={
@@ -304,6 +316,10 @@ const NumericChart: FC = ({
onTouchStart: () => {
setIsCursorActive(true);
},
+ onTouchEnd: () => {
+ setIsCursorActive(false);
+ handleCursorChange(null);
+ },
onMouseEnter: () => {
setIsCursorActive(true);
},
@@ -385,8 +401,26 @@ const NumericChart: FC = ({
return xDomain;
}, [xDomain, isEmbedded]);
+ const [touchPoint, setTouchPoint] = useState<{
+ x: number;
+ y: number;
+ } | null>(null);
const { getReferenceProps, getFloatingProps, refs, floatingStyles } =
- useChartTooltip({ placement: "top", tooltipOffset: 15 });
+ useChartTooltip({
+ placement: "top",
+ tooltipOffset: touchPoint ? 50 : 15,
+ x: touchPoint?.x ?? null,
+ y: touchPoint?.y ?? null,
+ });
+
+ const handleTouchMove = useCallback((e: React.TouchEvent) => {
+ const touch = e.touches[0];
+ if (!touch) return;
+ setTouchPoint({ x: touch.clientX, y: touch.clientY });
+ }, []);
+ const handleTouchEnd = useCallback(() => {
+ setTouchPoint(null);
+ }, []);
const {
isActive: isDiamondActive,
getReferenceProps: getDiamondRefProps,
@@ -462,6 +496,13 @@ const NumericChart: FC = ({
return typeof fromTheme === "number" ? fromTheme : 0.3;
}, [hasExternalTheme, themeAreaData?.opacity]);
+ const cursorDotFill = useMemo(
+ () =>
+ resolveToCssColor(getThemeColor, colorOverride) ??
+ getThemeColor(colorPalette.chip),
+ [getThemeColor, colorOverride, colorPalette.chip]
+ );
+
const rightPad = Math.max(rightPadding, MIN_RIGHT_PADDING);
const yAxisLabel = !isNil(yLabel) ? `(${yLabel})` : undefined;
const leftPad = 10;
@@ -549,6 +590,8 @@ const NumericChart: FC = ({
)}
ref={refs.setReference}
{...getReferenceProps()}
+ onTouchMove={handleTouchMove}
+ onTouchEnd={handleTouchEnd}
>
= ({
) : null}
- {!isDiamondActive && !isNil(highlightedPoint) && !hideCP ? (
+ {isCursorActive &&
+ !isNil(highlightedPoint) &&
+ !hideCP &&
+ isContinuousConsumerView ? (
+
+ ) : null}
+
+ {!isDiamondActive &&
+ !isNil(highlightedPoint) &&
+ !hideCP &&
+ !(hideCursorValueLabel && isCursorActive) ? (
= ({
{/* Forecaster view tooltip */}
- {isCursorActive && !!cursorTooltip ? (
+ {isCursorActive && !!cursorTooltip && !isContinuousConsumerView ? (
void;
+ hideCursorValueLabel?: boolean;
};
const NumericTimeline: FC
= ({
@@ -98,6 +99,7 @@ const NumericTimeline: FC = ({
keyFactors,
showNewsAnnotations,
onToggleNewsAnnotations,
+ hideCursorValueLabel,
}) => {
const locale = useLocale();
const resolutionPoint = useMemo(() => {
@@ -235,6 +237,7 @@ const NumericTimeline: FC = ({
newsAnnotations={newsAnnotations}
showNewsAnnotations={showNewsAnnotations}
onToggleNewsAnnotations={onToggleNewsAnnotations}
+ hideCursorValueLabel={hideCursorValueLabel}
/>
);
};
diff --git a/front_end/src/components/charts/primitives/line_cursor_points.tsx b/front_end/src/components/charts/primitives/line_cursor_points.tsx
index 15f21f6f1d..0f36610ce5 100644
--- a/front_end/src/components/charts/primitives/line_cursor_points.tsx
+++ b/front_end/src/components/charts/primitives/line_cursor_points.tsx
@@ -10,8 +10,6 @@ import {
} from "@/utils/charts/helpers";
const SIZE = 4;
-// https://commerce.nearform.com/open-source/victory/docs/api/victory-cursor-container#cursorlabeloffset
-const DEFAULT_X_OFFSET = 5;
type Props = {
chartData: Array<{
@@ -87,13 +85,13 @@ const LineCursorPoints = ({
) : (
);
diff --git a/front_end/src/components/consumer_post_card/consumer_question_tile/consumer_continuous_tile.tsx b/front_end/src/components/consumer_post_card/consumer_question_tile/consumer_continuous_tile.tsx
index a7eeeafbda..4aa72c1537 100644
--- a/front_end/src/components/consumer_post_card/consumer_question_tile/consumer_continuous_tile.tsx
+++ b/front_end/src/components/consumer_post_card/consumer_question_tile/consumer_continuous_tile.tsx
@@ -12,25 +12,33 @@ type Props = {
question: QuestionWithForecasts;
forecastAvailability: ForecastAvailability;
variant?: "feed" | "question";
+ overrideCenter?: number | null;
};
const ConsumerContinuousTile: FC = ({
question,
forecastAvailability,
variant = "feed",
+ overrideCenter,
}) => {
const locale = useLocale();
const latest =
question.aggregations[question.default_aggregation_method]?.latest;
- const communityPredictionDisplayValue = latest
- ? getPredictionDisplayValue(latest.centers?.[0], {
- questionType: question.type,
- scaling: question.scaling,
- actual_resolve_time: question.actual_resolve_time ?? null,
- unit: question.unit,
- })
- : null;
+ // Cursor position overrides the latest CP center when hovering the timeline.
+ const effectiveCenter =
+ overrideCenter !== null && overrideCenter !== undefined
+ ? overrideCenter
+ : latest?.centers?.[0];
+ const communityPredictionDisplayValue =
+ effectiveCenter !== undefined
+ ? getPredictionDisplayValue(effectiveCenter, {
+ questionType: question.type,
+ scaling: question.scaling,
+ actual_resolve_time: question.actual_resolve_time ?? null,
+ unit: question.unit,
+ })
+ : null;
// Resolved/Annulled/Ambiguous
if (question.resolution) {
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..0b836a2d81 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
@@ -2,7 +2,14 @@
import { isNil } from "lodash";
import dynamic from "next/dynamic";
import { useTranslations } from "next-intl";
-import { FC, ReactNode, useCallback, useMemo, useState } from "react";
+import {
+ FC,
+ ReactNode,
+ useCallback,
+ useEffect,
+ useMemo,
+ useState,
+} from "react";
import { VictoryThemeDefinition } from "victory";
import { useIsEmbedMode } from "@/app/(embed)/questions/components/question_view_mode_context";
@@ -14,10 +21,12 @@ import ContinuousPredictionChart from "@/components/forecast_maker/continuous_in
import Button from "@/components/ui/button";
import { GroupButton } from "@/components/ui/button_group";
import { useAuth } from "@/contexts/auth_context";
+import { useContinuousChartCursor } from "@/contexts/continuous_chart_cursor_context";
import { EmbedChartType, TimelineChartZoomOption } from "@/types/charts";
import { KeyFactor } from "@/types/comment";
import {
ForecastAvailability,
+ NumericAggregateForecast,
QuestionType,
QuestionWithNumericForecasts,
} from "@/types/question";
@@ -35,6 +44,8 @@ import {
isContinuousQuestion,
} from "@/utils/questions/helpers";
+import { useFullAggregation } from "./hooks/use_full_aggregation";
+
const Histogram = dynamic(() => import("@/components/charts/histogram"), {
ssr: false,
});
@@ -80,11 +91,26 @@ const DetailedContinuousChartCard: FC = ({
const effectiveWithZoomPicker = withZoomPicker ?? true;
const isConsumerView = isConsumerViewProp ?? !user;
+ const isContinuousConsumer = isConsumerView && isContinuousQuestion(question);
const [isChartReady, setIsChartReady] = useState(false);
const [activeView, setActiveView] = useState("timeline");
+ const [shouldFetchFull, setShouldFetchFull] = useState(false);
+ const { data: enrichedAggregation = null } = useFullAggregation(
+ question.id,
+ question.default_aggregation_method,
+ question.include_bots_in_aggregates,
+ isContinuousConsumer && shouldFetchFull
+ );
+
+ const handlePointerEnter = useCallback(() => {
+ if (isContinuousConsumer) setShouldFetchFull(true);
+ }, [isContinuousConsumer]);
+
const aggregation =
question.aggregations[question.default_aggregation_method];
+ const effectiveAggregation = (enrichedAggregation ??
+ aggregation) as typeof aggregation;
const isCpHidden = !!forecastAvailability?.cpRevealsOn;
const [cursorTimestamp, setCursorTimestamp] = useState(null);
@@ -103,7 +129,7 @@ const DetailedContinuousChartCard: FC = ({
};
}
- const forecast = getCursorForecast(cursorTimestamp, aggregation);
+ const forecast = getCursorForecast(cursorTimestamp, effectiveAggregation);
let timestamp = cursorTimestamp;
if (
timestamp === null &&
@@ -127,7 +153,7 @@ const DetailedContinuousChartCard: FC = ({
}, [
isCpHidden,
cursorTimestamp,
- aggregation,
+ effectiveAggregation,
question.my_forecasts,
nrForecasters,
]);
@@ -188,6 +214,29 @@ const DetailedContinuousChartCard: FC = ({
discreteValueOptions,
]);
+ const isBinary = question.type === QuestionType.Binary;
+
+ const activeForecast = useMemo(() => {
+ if (
+ isCpHidden ||
+ cursorTimestamp === null ||
+ !isContinuousQuestion(question)
+ )
+ return null;
+ return getCursorForecast(
+ cursorTimestamp,
+ effectiveAggregation
+ ) as NumericAggregateForecast | null;
+ }, [isCpHidden, cursorTimestamp, effectiveAggregation, question]);
+
+ const cursorCtx = useContinuousChartCursor();
+ const setCursorForecast = cursorCtx?.setActiveForecast;
+ useEffect(() => {
+ if (!isContinuousQuestion(question) || !setCursorForecast) return;
+ setCursorForecast(activeForecast);
+ return () => setCursorForecast(null);
+ }, [activeForecast, setCursorForecast, question]);
+
const handleCursorChange = useCallback((value: number | null) => {
setCursorTimestamp(value);
}, []);
@@ -221,8 +270,6 @@ const DetailedContinuousChartCard: FC = ({
!forecastAvailability?.cpRevealsOn &&
(question.type === QuestionType.Binary || isContinuousQuestion(question));
- const isBinary = question.type === QuestionType.Binary;
-
const chartViewButtons: GroupButton[] = [
{ value: "timeline", label: t("timeline") },
{ value: "histogram", label: t("histogram") },
@@ -278,10 +325,11 @@ const DetailedContinuousChartCard: FC = ({
unit={question.unit}
inboundOutcomeCount={question.inbound_outcome_count}
simplifiedCursor={false}
+ hideCursorValueLabel={isContinuousConsumer && !isEmbed}
title={timelineTitle}
forecastAvailability={forecastAvailability}
cursorTooltip={cursorTooltip}
- isConsumerView={isConsumerView}
+ isConsumerView={isContinuousConsumer}
isEmbedded={isEmbed}
height={chartHeight}
extraTheme={extraTheme}
@@ -306,6 +354,7 @@ const DetailedContinuousChartCard: FC = ({
hideLabel={isContinuousQuestion(question)}
colorOverride={cpColorOverride}
chartTheme={extraTheme}
+ cursorForecast={activeForecast}
/>
);
@@ -403,8 +452,9 @@ const DetailedContinuousChartCard: FC = ({
"flex w-full flex-col",
isChartReady ? "opacity-100" : "opacity-0"
)}
+ onPointerEnter={isContinuousConsumer ? handlePointerEnter : undefined}
>
- {!isConsumerView ? (
+ {isContinuousQuestion(question) && !isEmbed ? (
<>
{/* Desktop */}
@@ -413,6 +463,9 @@ const DetailedContinuousChartCard: FC
= ({
question={question}
size="lg"
hideLabel={true}
+ cursorForecast={
+ isContinuousConsumer ? activeForecast : undefined
+ }
/>
)}
diff --git a/front_end/src/components/detailed_question_card/detailed_question_card/hooks/use_full_aggregation.ts b/front_end/src/components/detailed_question_card/detailed_question_card/hooks/use_full_aggregation.ts
new file mode 100644
index 0000000000..95a981d2ca
--- /dev/null
+++ b/front_end/src/components/detailed_question_card/detailed_question_card/hooks/use_full_aggregation.ts
@@ -0,0 +1,38 @@
+"use client";
+
+import { useQuery } from "@tanstack/react-query";
+
+import ClientAggregationExplorerApi from "@/services/api/aggregation_explorer/aggregation_explorer.client";
+import { NumericAggregateForecastHistory } from "@/types/question";
+
+export function useFullAggregation(
+ questionId: number,
+ defaultAggregationMethod: string,
+ includeBots: boolean,
+ enabled: boolean
+) {
+ return useQuery({
+ queryKey: [
+ "full-aggregation",
+ questionId,
+ defaultAggregationMethod,
+ includeBots,
+ ],
+ enabled,
+ queryFn: async () => {
+ const result = await ClientAggregationExplorerApi.getAggregations({
+ questionId,
+ aggregationMethods: defaultAggregationMethod,
+ includeBots,
+ });
+ return (
+ (
+ result.aggregations as Record<
+ string,
+ NumericAggregateForecastHistory | undefined
+ >
+ )[defaultAggregationMethod] ?? null
+ );
+ },
+ });
+}
diff --git a/front_end/src/components/post_card/question_tile/continuous_cp_bar.tsx b/front_end/src/components/post_card/question_tile/continuous_cp_bar.tsx
index fa521cdb62..8cea6383f9 100644
--- a/front_end/src/components/post_card/question_tile/continuous_cp_bar.tsx
+++ b/front_end/src/components/post_card/question_tile/continuous_cp_bar.tsx
@@ -19,6 +19,8 @@ type Props = {
size?: "md" | "lg";
variant?: "feed" | "question";
colorOverride?: string;
+ overrideCenter?: number | null;
+ overrideBounds?: [number, number] | null;
};
const ContinuousCPBar: FC = ({
@@ -26,6 +28,8 @@ const ContinuousCPBar: FC = ({
size = "md",
variant = "feed",
colorOverride,
+ overrideCenter,
+ overrideBounds,
}) => {
const latest =
question.aggregations[question.default_aggregation_method]?.latest;
@@ -41,18 +45,20 @@ const ContinuousCPBar: FC = ({
}
const discreteValueOptions = getDiscreteValueOptions(question);
+ const effectiveCenter = overrideCenter ?? latest.centers?.[0];
+ const effectiveLower =
+ overrideBounds?.[0] ?? latest.interval_lower_bounds?.[0];
+ const effectiveUpper =
+ overrideBounds?.[1] ?? latest.interval_upper_bounds?.[0];
+
const displayValue = getPredictionDisplayValue(
- latest.centers?.[0],
+ effectiveCenter,
{
questionType: question.type,
scaling: question.scaling,
range:
- !isNil(latest?.interval_lower_bounds?.[0]) &&
- !isNil(latest?.interval_upper_bounds?.[0])
- ? [
- latest?.interval_lower_bounds?.[0] as number,
- latest?.interval_upper_bounds?.[0] as number,
- ]
+ !isNil(effectiveLower) && !isNil(effectiveUpper)
+ ? [effectiveLower as number, effectiveUpper as number]
: [],
unit: question.unit,
actual_resolve_time: question.actual_resolve_time ?? null,
diff --git a/front_end/src/contexts/continuous_chart_cursor_context.tsx b/front_end/src/contexts/continuous_chart_cursor_context.tsx
new file mode 100644
index 0000000000..8be2963a74
--- /dev/null
+++ b/front_end/src/contexts/continuous_chart_cursor_context.tsx
@@ -0,0 +1,45 @@
+"use client";
+
+import {
+ createContext,
+ FC,
+ ReactNode,
+ useCallback,
+ useContext,
+ useMemo,
+ useState,
+} from "react";
+
+import { NumericAggregateForecast } from "@/types/question";
+
+type CursorContextValue = {
+ activeForecast: NumericAggregateForecast | null;
+ setActiveForecast: (f: NumericAggregateForecast | null) => void;
+};
+
+const ContinuousChartCursorContext = createContext(
+ null
+);
+
+export const ContinuousChartCursorProvider: FC<{ children: ReactNode }> = ({
+ children,
+}) => {
+ const [activeForecast, setActiveForecast] =
+ useState(null);
+ const stableSet = useCallback(
+ (f: NumericAggregateForecast | null) => setActiveForecast(f),
+ []
+ );
+ const value = useMemo(
+ () => ({ activeForecast, setActiveForecast: stableSet }),
+ [activeForecast, stableSet]
+ );
+ return (
+
+ {children}
+
+ );
+};
+
+export const useContinuousChartCursor = () =>
+ useContext(ContinuousChartCursorContext);
diff --git a/front_end/src/hooks/use_chart_tooltip.ts b/front_end/src/hooks/use_chart_tooltip.ts
index da53f40498..25f3c7fa55 100644
--- a/front_end/src/hooks/use_chart_tooltip.ts
+++ b/front_end/src/hooks/use_chart_tooltip.ts
@@ -15,11 +15,15 @@ import { useState } from "react";
type Props = {
placement?: Placement;
tooltipOffset?: number;
+ x?: number | null;
+ y?: number | null;
};
const useChartTooltip = ({
placement = "left",
tooltipOffset = 24,
+ x,
+ y,
}: Props = {}) => {
const [isActive, setIsActive] = useState(false);
const { refs, floatingStyles, context } = useFloating({
@@ -34,7 +38,7 @@ const useChartTooltip = ({
placement,
whileElementsMounted: autoUpdate,
});
- const clientPoint = useClientPoint(context);
+ const clientPoint = useClientPoint(context, { x, y });
const dismiss = useDismiss(context);
const hover = useHover(context);
const { getReferenceProps, getFloatingProps } = useInteractions([
From c32cab0c48e8667d4e4fbf837d3a9976432de123 Mon Sep 17 00:00:00 2001
From: Nikita <93587872+ncarazon@users.noreply.github.com>
Date: Mon, 11 May 2026 21:28:56 +0300
Subject: [PATCH 3/6] Empty / pre-CP-reveal audit for the new side-by-side row
(#4700)
* fix: handle hideCP, cpRevealsOn, and isEmpty in the side-by-side left card for binary and continuous questions
* fix: handle isEmpty, cpRevealsOn, and hideCP states in the side-by-side layout left panel and fix title overflow onto the CP block
* fix: refine mobile hidden-CP states for binary and continuous in the forecaster title row and left panel
* fix: prevent CP status from being squeezed off-screen in the mobile title row
* fix: restore "No forecasts yet" overlay in feed cards and suppress it on the question detail page
* fix: wire hideCP, cpRevealsOn, and isEmpty states into all question-type left panels for the side-by-side layout
* fix: replace hardcoded isEmpty:false with suppressEmptyOverlay prop, fix missing useMemo deps, and align binary cpRevealsOn state with continuous
* fix: correct import order in continuous_question_prediction.tsx
---
.../key_factors_question_consumer_section.tsx | 49 +++-
.../multiple_choices_chart_view/index.tsx | 3 +-
.../components/question_page_shell/index.tsx | 51 +++-
.../question_page_shell/title_row.tsx | 83 +++++-
.../continuous_question_prediction.tsx | 12 +-
.../question_header_cp_status.tsx | 253 +++++++++++-------
front_end/src/components/charts/fan_chart.tsx | 2 +-
.../src/components/charts/group_chart.tsx | 4 +-
.../charts/multiple_choice_chart.tsx | 25 +-
.../src/components/charts/numeric_chart.tsx | 19 +-
.../components/charts/numeric_timeline.tsx | 3 +
.../consumer_post_card/binary_cp_bar.tsx | 2 +-
.../detailed_group_card/index.tsx | 96 +++----
.../continuous_chart_card.tsx | 12 +-
.../detailed_question_card/index.tsx | 3 -
15 files changed, 410 insertions(+), 207 deletions(-)
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")}
-
{
- openKeyFactorsElement("[id='key-factors']");
- sendAnalyticsEvent("KeyFactorViewAllClick");
- }}
- className="text-center text-sm font-normal leading-5 text-blue-600 hover:text-blue-700 dark:text-blue-600-dark dark:hover:text-blue-700-dark"
- >
- {t("viewAll", { count: totalCount })}
-
+ {!forecastIsEmpty && (
+
{
+ openKeyFactorsElement("[id='key-factors']");
+ sendAnalyticsEvent("KeyFactorViewAllClick");
+ }}
+ className="text-center text-sm font-normal leading-5 text-blue-600 hover:text-blue-700 dark:text-blue-600-dark dark:hover:text-blue-700-dark"
+ >
+ {t("viewAll", { count: totalCount })}
+
+ )}
-
+ {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 ===
@@ -153,6 +158,11 @@ export const ConsumerShell: FC<{
!isContinuousSingleQuestion &&
!isMultipleChoice;
+ const binaryForecastAvailability =
+ isBinarySingleQuestion && isQuestionPost(postData)
+ ? getQuestionForecastAvailability(postData.question)
+ : null;
+
const showSideBySide =
isMultipleChoice ||
isNonFanGroup ||
@@ -170,7 +180,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 (
@@ -217,19 +234,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) && (
-
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 9d8c06e4a1..1f69a87ad0 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
@@ -2,6 +2,7 @@
import { useMemo } from "react";
+import RevealCPButton from "@/app/(main)/questions/[id]/components/reveal_cp_button";
import {
ContinuousAreaGraphInput,
getContinuousAreaChartData,
@@ -9,6 +10,7 @@ import {
import MinifiedContinuousAreaChart from "@/components/charts/minified_continuous_area_chart";
import ConsumerContinuousTile from "@/components/consumer_post_card/consumer_question_tile/consumer_continuous_tile";
import { useContinuousChartCursor } from "@/contexts/continuous_chart_cursor_context";
+import { useHideCP } from "@/contexts/cp_context";
import { ContinuousAreaType } from "@/types/charts";
import { QuestionStatus } from "@/types/post";
import { QuestionWithNumericForecasts } from "@/types/question";
@@ -27,8 +29,8 @@ const ContinuousQuestionPrediction: React.FC
= ({
const forecastAvailability = getQuestionForecastAvailability(question);
const cursorCtx = useContinuousChartCursor();
const cursorForecast = cursorCtx?.activeForecast ?? null;
+ const { hideCP } = useHideCP();
- // Hide chart if no forecasts or CP not yet revealed
const shouldHideChart =
forecastAvailability.isEmpty || !!forecastAvailability.cpRevealsOn;
@@ -53,6 +55,14 @@ const ContinuousQuestionPrediction: React.FC = ({
];
}, [cursorForecastValues, question.status]);
+ if (hideCP) {
+ return (
+
+
+
+ );
+ }
+
return (
= ({
const cursorUpper = cursorForecast?.interval_upper_bounds?.[0] ?? null;
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 && (
-
- {question.status === QuestionStatus.CLOSED
- ? t("closed")
- : t("communityPredictionLabel")}
-
- )}
- {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 && (
-
+ {question.status === QuestionStatus.CLOSED
+ ? t("closed")
+ : t("communityPredictionLabel")}
)}
- {!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 (
+
+
+
+ );
+ }
+
+ if (forecastAvailability.cpRevealsOn && !isEmbed) {
+ return (
+
+
+
+ );
+ }
+
return (
= ({
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 c494e5c0e1..c987d37ca2 100644
--- a/front_end/src/components/charts/numeric_chart.tsx
+++ b/front_end/src/components/charts/numeric_chart.tsx
@@ -102,6 +102,7 @@ type Props = {
showNewsAnnotations?: boolean;
onToggleNewsAnnotations?: () => void;
animate?: object;
+ suppressEmptyOverlay?: boolean;
};
const BOTTOM_PADDING = 20;
@@ -139,6 +140,7 @@ const NumericChart: FC = ({
showNewsAnnotations,
onToggleNewsAnnotations,
animate,
+ suppressEmptyOverlay = false,
}) => {
const { theme, getThemeColor } = useAppTheme();
const [isChartReady, setIsChartReady] = useState(false);
@@ -229,7 +231,12 @@ const NumericChart: FC = ({
}, [rightPadding, MIN_RIGHT_PADDING]);
const containerComponent = useMemo(() => {
- if (nonInteractive) {
+ if (
+ nonInteractive ||
+ forecastAvailability?.isEmpty ||
+ hideCP ||
+ !!forecastAvailability?.cpRevealsOn
+ ) {
return (
= ({
handleCursorChange,
nonInteractive,
isCursorActive,
+ forecastAvailability,
+ hideCP,
+ isContinuousConsumerView,
]);
const chartEvents = useMemo(() => {
@@ -594,7 +604,12 @@ const NumericChart: FC = ({
onTouchEnd={handleTouchEnd}
>
void;
hideCursorValueLabel?: boolean;
+ suppressEmptyOverlay?: boolean;
};
const NumericTimeline: FC = ({
@@ -100,6 +101,7 @@ const NumericTimeline: FC = ({
showNewsAnnotations,
onToggleNewsAnnotations,
hideCursorValueLabel,
+ suppressEmptyOverlay,
}) => {
const locale = useLocale();
const resolutionPoint = useMemo(() => {
@@ -238,6 +240,7 @@ const NumericTimeline: FC = ({
showNewsAnnotations={showNewsAnnotations}
onToggleNewsAnnotations={onToggleNewsAnnotations}
hideCursorValueLabel={hideCursorValueLabel}
+ suppressEmptyOverlay={suppressEmptyOverlay}
/>
);
};
diff --git a/front_end/src/components/consumer_post_card/binary_cp_bar.tsx b/front_end/src/components/consumer_post_card/binary_cp_bar.tsx
index 4a84ea67d7..2633c6a840 100644
--- a/front_end/src/components/consumer_post_card/binary_cp_bar.tsx
+++ b/front_end/src/components/consumer_post_card/binary_cp_bar.tsx
@@ -33,7 +33,7 @@ const BinaryCPBar: FC = ({
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 0b836a2d81..f61c086fa3 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
@@ -14,7 +14,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";
@@ -161,10 +160,6 @@ const DetailedContinuousChartCard: FC = ({
const discreteValueOptions = getDiscreteValueOptions(question);
const cpCursorElement = useMemo(() => {
- if (forecastAvailability?.isEmpty) {
- return t("noForecastsYet");
- }
-
if (hideCP) {
return "...";
}
@@ -328,7 +323,8 @@ const DetailedContinuousChartCard: FC = ({
hideCursorValueLabel={isContinuousConsumer && !isEmbed}
title={timelineTitle}
forecastAvailability={forecastAvailability}
- cursorTooltip={cursorTooltip}
+ suppressEmptyOverlay
+ cursorTooltip={forecastAvailability?.isEmpty ? undefined : cursorTooltip}
isConsumerView={isContinuousConsumer}
isEmbedded={isEmbed}
height={chartHeight}
@@ -409,9 +405,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:
From 7771a5f8184e77bacadcbfec19e26aedf643a91d Mon Sep 17 00:00:00 2001
From: Nikita
Date: Tue, 12 May 2026 12:54:10 +0300
Subject: [PATCH 4/6] feat: enable live cursor sync on continuous questions for
forecaster view
---
.../continuous_chart_card.tsx | 13 ++++++-------
1 file changed, 6 insertions(+), 7 deletions(-)
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 f61c086fa3..0017b5d807 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
@@ -94,17 +94,18 @@ const DetailedContinuousChartCard: FC = ({
const [isChartReady, setIsChartReady] = useState(false);
const [activeView, setActiveView] = useState("timeline");
+ const isContinuous = isContinuousQuestion(question);
const [shouldFetchFull, setShouldFetchFull] = useState(false);
const { data: enrichedAggregation = null } = useFullAggregation(
question.id,
question.default_aggregation_method,
question.include_bots_in_aggregates,
- isContinuousConsumer && shouldFetchFull
+ isContinuous && shouldFetchFull
);
const handlePointerEnter = useCallback(() => {
- if (isContinuousConsumer) setShouldFetchFull(true);
- }, [isContinuousConsumer]);
+ if (isContinuous) setShouldFetchFull(true);
+ }, [isContinuous]);
const aggregation =
question.aggregations[question.default_aggregation_method];
@@ -446,7 +447,7 @@ const DetailedContinuousChartCard: FC = ({
"flex w-full flex-col",
isChartReady ? "opacity-100" : "opacity-0"
)}
- onPointerEnter={isContinuousConsumer ? handlePointerEnter : undefined}
+ onPointerEnter={isContinuous ? handlePointerEnter : undefined}
>
{isContinuousQuestion(question) && !isEmbed ? (
<>
@@ -457,9 +458,7 @@ const DetailedContinuousChartCard: FC = ({
question={question}
size="lg"
hideLabel={true}
- cursorForecast={
- isContinuousConsumer ? activeForecast : undefined
- }
+ cursorForecast={activeForecast}
/>
)}
From d97ed5f77efb1617ab9567e451302dc112d384d7 Mon Sep 17 00:00:00 2001
From: Nikita
Date: Tue, 12 May 2026 12:54:15 +0300
Subject: [PATCH 5/6] feat: bridge timeline cursor to forecaster mobile CP
status chip
---
.../[id]/components/question_page_shell/index.tsx | 12 +++++++-----
.../components/question_page_shell/title_row.tsx | 4 ++++
2 files changed, 11 insertions(+), 5 deletions(-)
diff --git a/front_end/src/app/(main)/questions/[id]/components/question_page_shell/index.tsx b/front_end/src/app/(main)/questions/[id]/components/question_page_shell/index.tsx
index 87a2cd1977..7df8d1f510 100644
--- a/front_end/src/app/(main)/questions/[id]/components/question_page_shell/index.tsx
+++ b/front_end/src/app/(main)/questions/[id]/components/question_page_shell/index.tsx
@@ -328,11 +328,13 @@ const QuestionPageShell: FC = ({
}
forecaster={
-
+
+
+
}
/>
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 0433151a5d..f60629117a 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
@@ -6,6 +6,7 @@ import QuestionHeaderCPStatus from "@/app/(main)/questions/[id]/components/quest
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 { useContinuousChartCursor } from "@/contexts/continuous_chart_cursor_context";
import { useHideCP } from "@/contexts/cp_context";
import { PostWithForecasts } from "@/types/post";
import { QuestionType, QuestionWithForecasts } from "@/types/question";
@@ -27,6 +28,8 @@ type Props = {
const TitleRow: FC = ({ post, variant, className }) => {
const { hideCP } = useHideCP();
+ const cursorCtx = useContinuousChartCursor();
+ const cursorForecast = cursorCtx?.activeForecast ?? null;
if (isConditionalPost(post)) {
return (
@@ -65,6 +68,7 @@ const TitleRow: FC = ({ post, variant, className }) => {
question={post.question as QuestionWithForecasts}
size="md"
hideLabel={isContinuous}
+ cursorForecast={isContinuous ? cursorForecast : undefined}
/>
)}
From df30316ab7a66f982401adcb4c7449d613b007d8 Mon Sep 17 00:00:00 2001
From: Nikita
Date: Tue, 12 May 2026 12:54:19 +0300
Subject: [PATCH 6/6] fix: hide cursor tooltip for continuous questions in
forecaster view
---
.../detailed_question_card/continuous_chart_card.tsx | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
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 0017b5d807..5fec30471e 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
@@ -325,7 +325,11 @@ const DetailedContinuousChartCard: FC = ({
title={timelineTitle}
forecastAvailability={forecastAvailability}
suppressEmptyOverlay
- cursorTooltip={forecastAvailability?.isEmpty ? undefined : cursorTooltip}
+ cursorTooltip={
+ forecastAvailability?.isEmpty || isContinuous
+ ? undefined
+ : cursorTooltip
+ }
isConsumerView={isContinuousConsumer}
isEmbedded={isEmbed}
height={chartHeight}