From 69fe190c7087dc808e233eef3bf9a09e55f44bcd Mon Sep 17 00:00:00 2001 From: Nikita Date: Thu, 7 May 2026 16:13:36 +0300 Subject: [PATCH 1/5] 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 --- .../components/question_page_shell/index.tsx | 2 +- .../question_header_cp_status.tsx | 41 +++++++++++++-- .../src/components/charts/numeric_chart.tsx | 51 +++++++++++++++---- .../components/charts/numeric_timeline.tsx | 3 ++ .../charts/primitives/line_cursor_points.tsx | 10 ++-- .../continuous_chart_card.tsx | 26 ++++++++-- .../question_tile/continuous_cp_bar.tsx | 20 +++++--- 7 files changed, 123 insertions(+), 30 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 7da588d633..509479ce7c 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 @@ -235,7 +235,7 @@ export const ConsumerShell: FC<{ = ({ @@ -36,6 +46,7 @@ const QuestionHeaderCPStatus: FC = ({ hideLabel = false, colorOverride, chartTheme, + cursorForecast, }) => { const locale = useLocale(); const t = useTranslations(); @@ -104,6 +115,24 @@ 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; + const cursorAreaChartData: ContinuousAreaGraphInput | null = + cursorForecast?.forecast_values + ? [ + { + pmf: cdfToPmf(cursorForecast.forecast_values), + cdf: cursorForecast.forecast_values, + type: (question.status === QuestionStatus.RESOLVED + ? "community_resolved" + : question.status === QuestionStatus.CLOSED + ? "community_closed" + : "community") as ContinuousAreaType, + }, + ] + : null; + if (isContinuous) { return ( !forecastAvailability.isEmpty && ( @@ -148,6 +177,12 @@ const QuestionHeaderCPStatus: FC = ({ size={size} variant="question" colorOverride={colorOverride} + overrideCenter={cursorCenter} + overrideBounds={ + cursorLower !== null && cursorUpper !== null + ? [cursorLower, cursorUpper] + : null + } /> )} @@ -161,7 +196,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={ @@ -779,7 +791,28 @@ const NumericChart: FC = ({ ) : 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/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..56b8790a2c 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 @@ -18,6 +18,7 @@ import { EmbedChartType, TimelineChartZoomOption } from "@/types/charts"; import { KeyFactor } from "@/types/comment"; import { ForecastAvailability, + NumericAggregateForecast, QuestionType, QuestionWithNumericForecasts, } from "@/types/question"; @@ -80,6 +81,7 @@ 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"); @@ -188,6 +190,21 @@ const DetailedContinuousChartCard: FC = ({ discreteValueOptions, ]); + const isBinary = question.type === QuestionType.Binary; + + const activeForecast = useMemo(() => { + if ( + isCpHidden || + cursorTimestamp === null || + !isContinuousQuestion(question) + ) + return null; + return getCursorForecast( + cursorTimestamp, + aggregation + ) as NumericAggregateForecast | null; + }, [isCpHidden, cursorTimestamp, aggregation, question]); + const handleCursorChange = useCallback((value: number | null) => { setCursorTimestamp(value); }, []); @@ -221,8 +238,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 +293,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 +322,7 @@ const DetailedContinuousChartCard: FC = ({ hideLabel={isContinuousQuestion(question)} colorOverride={cpColorOverride} chartTheme={extraTheme} + cursorForecast={activeForecast} /> ); @@ -404,7 +421,7 @@ const DetailedContinuousChartCard: FC = ({ isChartReady ? "opacity-100" : "opacity-0" )} > - {!isConsumerView ? ( + {isContinuousQuestion(question) && !isEmbed ? ( <> {/* Desktop */}
@@ -413,6 +430,7 @@ const DetailedContinuousChartCard: FC = ({ question={question} size="lg" hideLabel={true} + cursorForecast={activeForecast} /> )} 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, From c484a054b9828e01ff3314db033cb4a9cf355406 Mon Sep 17 00:00:00 2001 From: Nikita Date: Thu, 7 May 2026 16:13:42 +0300 Subject: [PATCH 2/5] feat: animate consumer mobile mini chart on timeline cursor and fix mobile tooltip position tracking --- .../components/question_page_shell/index.tsx | 14 +++--- .../continuous_question_prediction.tsx | 27 ++++++++++- .../src/components/charts/numeric_chart.tsx | 26 ++++++++++- .../consumer_continuous_tile.tsx | 24 ++++++---- .../continuous_chart_card.tsx | 21 ++++++++- .../continuous_chart_cursor_context.tsx | 45 +++++++++++++++++++ front_end/src/hooks/use_chart_tooltip.ts | 6 ++- 7 files changed, 144 insertions(+), 19 deletions(-) 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 509479ce7c..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, @@ -280,11 +281,14 @@ const QuestionPageShell: 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 +35,27 @@ const ContinuousQuestionPrediction: React.FC = ({ isClosed: question.status === QuestionStatus.CLOSED, }); + // Null when cursor is inactive — chart falls back to the latest aggregate. + const cursorChartData: ContinuousAreaGraphInput | null = + cursorForecast?.forecast_values + ? [ + { + pmf: cdfToPmf(cursorForecast.forecast_values), + cdf: cursorForecast.forecast_values, + type: (question.status === QuestionStatus.CLOSED + ? "community_closed" + : "community") as ContinuousAreaType, + }, + ] + : null; + return (
{!shouldHideChart && ( <> @@ -40,7 +63,7 @@ const ContinuousQuestionPrediction: React.FC = ({
= ({ onTouchStart: () => { setIsCursorActive(true); }, + onTouchEnd: () => { + setIsCursorActive(false); + handleCursorChange(null); + }, onMouseEnter: () => { setIsCursorActive(true); }, @@ -397,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, @@ -561,6 +583,8 @@ const NumericChart: FC = ({ )} ref={refs.setReference} {...getReferenceProps()} + onTouchMove={handleTouchMove} + onTouchEnd={handleTouchEnd} > = ({ 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 56b8790a2c..3bb54698f0 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,6 +21,7 @@ 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 { @@ -205,6 +213,13 @@ const DetailedContinuousChartCard: FC = ({ ) as NumericAggregateForecast | null; }, [isCpHidden, cursorTimestamp, aggregation, question]); + const cursorCtx = useContinuousChartCursor(); + useEffect(() => { + if (!isContinuousQuestion(question) || !cursorCtx) return; + cursorCtx.setActiveForecast(activeForecast); + return () => cursorCtx.setActiveForecast(null); + }, [activeForecast, cursorCtx, question]); + const handleCursorChange = useCallback((value: number | null) => { setCursorTimestamp(value); }, []); @@ -430,7 +445,9 @@ const DetailedContinuousChartCard: FC = ({ question={question} size="lg" hideLabel={true} - cursorForecast={activeForecast} + cursorForecast={ + isContinuousConsumer ? activeForecast : undefined + } /> )} 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 0cda58e3361a9b3ba7f186a126eb0a8515a1881d Mon Sep 17 00:00:00 2001 From: Nikita Date: Thu, 7 May 2026 16:13:47 +0300 Subject: [PATCH 3/5] fix: always serialize full forecast_values in aggregation history for cursor-time distribution animation --- questions/serializers/aggregate_forecasts.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/questions/serializers/aggregate_forecasts.py b/questions/serializers/aggregate_forecasts.py index 14b950f8e5..e4e12d625b 100644 --- a/questions/serializers/aggregate_forecasts.py +++ b/questions/serializers/aggregate_forecasts.py @@ -128,9 +128,7 @@ def serialize_question_aggregations( for method, forecasts in aggregate_forecasts_by_method.items(): serialized_data[method]["history"] = [ - serialize_aggregate_forecast( - forecast, question.type, full=full_forecast_values - ) + serialize_aggregate_forecast(forecast, question.type, full=True) for forecast in forecasts ] serialized_data[method]["latest"] = ( From fbf720bb496013c3fd88fc86447dec49f9d11323 Mon Sep 17 00:00:00 2001 From: Nikita Date: Thu, 7 May 2026 16:13:52 +0300 Subject: [PATCH 4/5] fix: memoize cursorChartData to skip cdfToPmf recalculation when forecast_values reference is unchanged --- .../continuous_question_prediction.tsx | 28 +++++++++------- .../question_header_cp_status.tsx | 32 ++++++++++--------- 2 files changed, 33 insertions(+), 27 deletions(-) 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 d406377871..9d8c06e4a1 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,5 +1,7 @@ "use client"; +import { useMemo } from "react"; + import { ContinuousAreaGraphInput, getContinuousAreaChartData, @@ -35,19 +37,21 @@ 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: ContinuousAreaGraphInput | null = - cursorForecast?.forecast_values - ? [ - { - pmf: cdfToPmf(cursorForecast.forecast_values), - cdf: cursorForecast.forecast_values, - type: (question.status === QuestionStatus.CLOSED - ? "community_closed" - : "community") as ContinuousAreaType, - }, - ] - : null; + 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 (
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 c8310232d4..5620237f50 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 @@ -1,6 +1,6 @@ "use client"; import { useLocale, useTranslations } from "next-intl"; -import React, { FC } from "react"; +import React, { FC, useMemo } from "react"; import { VictoryThemeDefinition } from "victory"; import { @@ -69,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({ @@ -118,20 +134,6 @@ const QuestionHeaderCPStatus: FC = ({ const cursorCenter = cursorForecast?.centers?.[0] ?? null; const cursorLower = cursorForecast?.interval_lower_bounds?.[0] ?? null; const cursorUpper = cursorForecast?.interval_upper_bounds?.[0] ?? null; - const cursorAreaChartData: ContinuousAreaGraphInput | null = - cursorForecast?.forecast_values - ? [ - { - pmf: cdfToPmf(cursorForecast.forecast_values), - cdf: cursorForecast.forecast_values, - type: (question.status === QuestionStatus.RESOLVED - ? "community_resolved" - : question.status === QuestionStatus.CLOSED - ? "community_closed" - : "community") as ContinuousAreaType, - }, - ] - : null; if (isContinuous) { return ( From 7b594a3e6d839bfc18ff8fbe1c7256af6cd91ecc Mon Sep 17 00:00:00 2001 From: Nikita Date: Thu, 7 May 2026 19:05:38 +0300 Subject: [PATCH 5/5] fix: resolve cursor dot color override through resolveToCssColor --- front_end/src/components/charts/numeric_chart.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/front_end/src/components/charts/numeric_chart.tsx b/front_end/src/components/charts/numeric_chart.tsx index 47119b9faf..c494e5c0e1 100644 --- a/front_end/src/components/charts/numeric_chart.tsx +++ b/front_end/src/components/charts/numeric_chart.tsx @@ -496,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; @@ -824,9 +831,7 @@ const NumericChart: FC = ({ size={4} style={{ data: { - fill: colorOverride - ? String(colorOverride) - : getThemeColor(colorPalette.chip), + fill: cursorDotFill, stroke: "none", }, }}