Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -235,7 +236,7 @@ export const ConsumerShell: FC<{
<QuestionTimeline
postData={postData}
keyFactors={postData.key_factors}
isConsumerView={!isContinuousSingleQuestion}
isConsumerView={true}
preselectedGroupQuestionId={preselectedGroupQuestionId}
className={cn(
"hidden sm:block",
Expand Down Expand Up @@ -280,11 +281,14 @@ const QuestionPageShell: FC<Props> = ({
<QuestionLayoutProvider>
<QuestionVariantComposer
consumer={
<ConsumerShell
postData={postData}
preselectedGroupQuestionId={preselectedGroupQuestionId}
mobileSidebar={mobileSidebar}
/>
<ContinuousChartCursorProvider>
{/* Bridges timeline cursor to the mobile mini chart in consumer view */}
<ConsumerShell
postData={postData}
preselectedGroupQuestionId={preselectedGroupQuestionId}
mobileSidebar={mobileSidebar}
/>
</ContinuousChartCursorProvider>
}
forecaster={
<ForecasterShell
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
"use client";

import { getContinuousAreaChartData } from "@/components/charts/continuous_area_chart";
import { useMemo } from "react";

import {
ContinuousAreaGraphInput,
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 { useContinuousChartCursor } from "@/contexts/continuous_chart_cursor_context";
import { ContinuousAreaType } from "@/types/charts";
import { QuestionStatus } from "@/types/post";
import { QuestionWithNumericForecasts } from "@/types/question";
import { cdfToPmf } from "@/utils/math";
import { getQuestionForecastAvailability } from "@/utils/questions/forecastAvailability";

type Props = {
Expand All @@ -17,6 +25,8 @@ const ContinuousQuestionPrediction: React.FC<Props> = ({
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 =
Expand All @@ -27,20 +37,37 @@ const ContinuousQuestionPrediction: React.FC<Props> = ({
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<ContinuousAreaGraphInput | null>(() => {
if (!cursorForecastValues) return null;
return [
{
pmf: cdfToPmf(cursorForecastValues),
cdf: cursorForecastValues,
type: (question.status === QuestionStatus.CLOSED
? "community_closed"
: "community") as ContinuousAreaType,
},
];
}, [cursorForecastValues, question.status]);

return (
<div className="mx-auto mb-7 flex max-w-[340px] flex-col items-center gap-2.5">
<ConsumerContinuousTile
question={question}
forecastAvailability={forecastAvailability}
variant="question"
overrideCenter={cursorForecast?.centers?.[0] ?? null}
/>
{!shouldHideChart && (
<>
<div className="w-full overflow-visible pb-1 md:pb-4 md:pt-4">
<div className="origin-center transform-gpu md:scale-150">
<MinifiedContinuousAreaChart
question={question}
data={continuousAreaChartData}
data={cursorChartData ?? continuousAreaChartData}
height={chartHeight ?? 50}
forceTickCount={2}
variant="question"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,33 @@
"use client";
import { useLocale, useTranslations } from "next-intl";
import React, { FC } from "react";
import React, { FC, useMemo } from "react";
import { VictoryThemeDefinition } from "victory";

import {
useEmbedContainerWidth,
useIsEmbedMode,
} from "@/app/(embed)/questions/components/question_view_mode_context";
import QuestionHeaderContinuousResolutionChip from "@/app/(main)/questions/[id]/components/question_view/forecaster_question_view/question_header/question_header_continuous_resolution_chip";
import { getContinuousAreaChartData } from "@/components/charts/continuous_area_chart";
import {
ContinuousAreaGraphInput,
getContinuousAreaChartData,
} from "@/components/charts/continuous_area_chart";
import MinifiedContinuousAreaChart from "@/components/charts/minified_continuous_area_chart";
import BinaryCPBar from "@/components/consumer_post_card/binary_cp_bar";
import QuestionResolutionChip from "@/components/consumer_post_card/question_resolution_chip";
import QuestionCPMovement from "@/components/cp_movement";
import ContinuousCPBar from "@/components/post_card/question_tile/continuous_cp_bar";
import { useHideCP } from "@/contexts/cp_context";
import { ContinuousAreaType } from "@/types/charts";
import { QuestionStatus } from "@/types/post";
import { QuestionType, QuestionWithForecasts } from "@/types/question";
import {
NumericAggregateForecast,
QuestionType,
QuestionWithForecasts,
} from "@/types/question";
import cn from "@/utils/core/cn";
import { formatResolution } from "@/utils/formatters/resolution";
import { cdfToPmf } from "@/utils/math";
import { getQuestionForecastAvailability } from "@/utils/questions/forecastAvailability";
import { isSuccessfullyResolved } from "@/utils/questions/resolution";

Expand All @@ -28,6 +37,7 @@ type Props = {
hideLabel?: boolean;
colorOverride?: string;
chartTheme?: VictoryThemeDefinition;
cursorForecast?: NumericAggregateForecast | null;
};

const QuestionHeaderCPStatus: FC<Props> = ({
Expand All @@ -36,6 +46,7 @@ const QuestionHeaderCPStatus: FC<Props> = ({
hideLabel = false,
colorOverride,
chartTheme,
cursorForecast,
}) => {
const locale = useLocale();
const t = useTranslations();
Expand All @@ -58,6 +69,22 @@ const QuestionHeaderCPStatus: FC<Props> = ({
const isEmbedBelow376 = isEmbed && (w ?? 0) > 0 && (w ?? 0) < 376;
const isEmbedWide = isEmbed && (w ?? 0) >= 500;

const cursorForecastValues = cursorForecast?.forecast_values ?? null;
const cursorAreaChartData = useMemo<ContinuousAreaGraphInput | null>(() => {
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({
Expand Down Expand Up @@ -104,6 +131,10 @@ const QuestionHeaderCPStatus: FC<Props> = ({
? "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 && (
Expand Down Expand Up @@ -148,6 +179,12 @@ const QuestionHeaderCPStatus: FC<Props> = ({
size={size}
variant="question"
colorOverride={colorOverride}
overrideCenter={cursorCenter}
overrideBounds={
cursorLower !== null && cursorUpper !== null
? [cursorLower, cursorUpper]
: null
}
/>
)}
</div>
Expand All @@ -161,7 +198,7 @@ const QuestionHeaderCPStatus: FC<Props> = ({
>
<MinifiedContinuousAreaChart
question={question}
data={continuousAreaChartData}
data={cursorAreaChartData ?? continuousAreaChartData}
height={
hideLabel && size === "lg"
? 120
Expand Down
82 changes: 72 additions & 10 deletions front_end/src/components/charts/numeric_chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ type Props = {
chartTitle?: ReactNode;
height?: number;
hideCP?: boolean;
hideCursorValueLabel?: boolean;
defaultZoom?: TimelineChartZoomOption;
withZoomPicker?: boolean;
resolutionPoint?: LinePoint[];
Expand Down Expand Up @@ -127,10 +128,12 @@ const NumericChart: FC<Props> = ({
nonInteractive = false,
isEmbedded = false,
simplifiedCursor = false,
hideCursorValueLabel = false,
forecastAvailability,
questionStatus,
resolution,
cursorTooltip,
isConsumerView,
questionType,
newsAnnotations,
showNewsAnnotations,
Expand All @@ -142,6 +145,11 @@ const NumericChart: FC<Props> = ({
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<HTMLDivElement>();
const { line, area, points, yDomain, xDomain, yScale, xScale } = useMemo(
Expand Down Expand Up @@ -252,13 +260,17 @@ const NumericChart: FC<Props> = ({
}
}}
cursorComponent={
<LineSegment
style={{
stroke: getThemeColor(METAC_COLORS.blue["700"]),
opacity: 0.5,
strokeDasharray: "5,2",
}}
/>
isContinuousConsumerView ? (
<LineSegment style={{ stroke: "none" }} />
) : (
<LineSegment
style={{
stroke: getThemeColor(METAC_COLORS.blue["700"]),
opacity: 0.5,
strokeDasharray: "5,2",
}}
/>
)
}
cursorLabelComponent={
<VictoryPortal>
Expand Down Expand Up @@ -304,6 +316,10 @@ const NumericChart: FC<Props> = ({
onTouchStart: () => {
setIsCursorActive(true);
},
onTouchEnd: () => {
setIsCursorActive(false);
handleCursorChange(null);
},
onMouseEnter: () => {
setIsCursorActive(true);
},
Expand Down Expand Up @@ -385,8 +401,26 @@ const NumericChart: FC<Props> = ({
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<HTMLDivElement>) => {
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,
Expand Down Expand Up @@ -462,6 +496,13 @@ const NumericChart: FC<Props> = ({
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;
Expand Down Expand Up @@ -549,6 +590,8 @@ const NumericChart: FC<Props> = ({
)}
ref={refs.setReference}
{...getReferenceProps()}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
>
<ForecastAvailabilityChartOverflow
forecastAvailability={forecastAvailability}
Expand Down Expand Up @@ -779,7 +822,26 @@ const NumericChart: FC<Props> = ({
</VictoryPortal>
) : null}

{!isDiamondActive && !isNil(highlightedPoint) && !hideCP ? (
{isCursorActive &&
!isNil(highlightedPoint) &&
!hideCP &&
isContinuousConsumerView ? (
<VictoryScatter
data={[highlightedPoint]}
size={4}
style={{
data: {
fill: cursorDotFill,
stroke: "none",
},
}}
/>
Comment thread
ncarazon marked this conversation as resolved.
) : null}

{!isDiamondActive &&
!isNil(highlightedPoint) &&
!hideCP &&
!(hideCursorValueLabel && isCursorActive) ? (
<VictoryScatter
data={[highlightedPoint]}
dataComponent={
Expand Down Expand Up @@ -825,7 +887,7 @@ const NumericChart: FC<Props> = ({
</div>

{/* Forecaster view tooltip */}
{isCursorActive && !!cursorTooltip ? (
{isCursorActive && !!cursorTooltip && !isContinuousConsumerView ? (
<FloatingPortal>
<div
className="pointer-events-none z-100 rounded bg-gray-0 leading-4 shadow-lg dark:bg-gray-0-dark"
Expand Down
3 changes: 3 additions & 0 deletions front_end/src/components/charts/numeric_timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ type Props = {
keyFactors?: KeyFactor[];
showNewsAnnotations?: boolean;
onToggleNewsAnnotations?: () => void;
hideCursorValueLabel?: boolean;
};

const NumericTimeline: FC<Props> = ({
Expand Down Expand Up @@ -98,6 +99,7 @@ const NumericTimeline: FC<Props> = ({
keyFactors,
showNewsAnnotations,
onToggleNewsAnnotations,
hideCursorValueLabel,
}) => {
const locale = useLocale();
const resolutionPoint = useMemo(() => {
Expand Down Expand Up @@ -235,6 +237,7 @@ const NumericTimeline: FC<Props> = ({
newsAnnotations={newsAnnotations}
showNewsAnnotations={showNewsAnnotations}
onToggleNewsAnnotations={onToggleNewsAnnotations}
hideCursorValueLabel={hideCursorValueLabel}
/>
);
};
Expand Down
Loading
Loading