Skip to content
138 changes: 87 additions & 51 deletions front_end/src/components/charts/continuous_area_chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ type Props = {
withTodayLine?: boolean;
globalScaling?: Scaling;
outlineUser?: boolean;
centerOOBResolution?: boolean;
};

const ContinuousAreaChart: FC<Props> = ({
Expand All @@ -113,6 +114,7 @@ const ContinuousAreaChart: FC<Props> = ({
withTodayLine = true,
globalScaling,
outlineUser = false,
centerOOBResolution = false,
}) => {
const locale = useLocale();
const { ref: chartContainerRef, width: containerWidth } =
Expand Down Expand Up @@ -825,6 +827,47 @@ const ContinuousAreaChart: FC<Props> = ({
/>
))
)}

{/* Today's date dot for date questions */}
{question.type === QuestionType.Date && withTodayLine && (
<VictoryScatter
data={[
{
x: unscaleNominalLocation(
Math.floor(Date.now() / 1000),
question.scaling
),
y: yDomain[0], // Bottom of the chart
symbol: "circle",
size: 3,
},
]}
style={{
data: {
fill: getThemeColor(METAC_COLORS.blue["700"]),
stroke: "none",
},
}}
/>
)}

{question.type === QuestionType.Date &&
todayLabelPosition &&
withTodayLine && (
<VictoryPortal>
<VictoryLabel
x={todayLabelPosition.x}
y={height - BOTTOM_PADDING - 12} // Position above the dot
text="Today"
style={{
fill: getThemeColor(METAC_COLORS.blue["700"]),
fontSize: 12,
}}
textAnchor="middle"
/>
</VictoryPortal>
)}

{/* Resolution point */}
{resX != null && resPlacement === "in" && (
<VictoryScatter
Expand All @@ -849,8 +892,11 @@ const ContinuousAreaChart: FC<Props> = ({
{resX != null &&
resPlacement === "in" &&
withResolutionChip &&
(question.type === QuestionType.Discrete ||
question.type === QuestionType.Numeric) && (
[
QuestionType.Numeric,
QuestionType.Discrete,
QuestionType.Date,
].includes(question.type) && (
<VictoryScatter
data={[
{
Expand All @@ -875,6 +921,42 @@ const ContinuousAreaChart: FC<Props> = ({
/>
)}

{/* Resolution chip for out of bounds resolution */}
{resX != null &&
resPlacement !== "in" &&
withResolutionChip &&
[
QuestionType.Numeric,
QuestionType.Discrete,
QuestionType.Date,
].includes(question.type) && (
<VictoryScatter
data={[
{
x:
resPlacement === "left"
? Math.min(...xDomain)
: Math.max(...xDomain),
y: centerOOBResolution ? Math.max(...yDomain) / 2 : 0,
placement: resPlacement,
},
]}
dataComponent={
<VictoryPortal>
<ChartValueBox
rightPadding={0}
chartWidth={chartWidth}
isCursorActive={false}
isDistributionChip
colorOverride={METAC_COLORS.purple["800"]}
resolution={formattedResolution}
textAlignToSide={centerOOBResolution}
/>
</VictoryPortal>
}
/>
)}

{resX != null && resPlacement && resPlacement !== "in" && (
<VictoryPortal>
<VictoryScatter
Expand All @@ -884,62 +966,16 @@ const ContinuousAreaChart: FC<Props> = ({
resPlacement === "left"
? Math.min(...xDomain)
: Math.max(...xDomain),
y: yDomain[1] - (yDomain[1] - yDomain[0]) * 0.04,
placement: resPlacement === "left" ? "above" : "below",
y: centerOOBResolution ? Math.max(...yDomain) / 2 : 0,
placement: resPlacement,
primary: METAC_COLORS.purple["800"],
secondary: METAC_COLORS.purple["500"],
},
]}
dataComponent={
<ResolutionDiamond
hoverable={false}
axisPadPx={3}
rotateDeg={resPlacement === "left" ? 90 : -90}
refProps={{}}
/>
}
dataComponent={<ResolutionDiamond hoverable={false} />}
/>
</VictoryPortal>
)}
{/* Today's date dot for date questions */}
{question.type === QuestionType.Date && withTodayLine && (
<VictoryScatter
data={[
{
x: unscaleNominalLocation(
Math.floor(Date.now() / 1000),
question.scaling
),
y: yDomain[0], // Bottom of the chart
symbol: "circle",
size: 3,
},
]}
style={{
data: {
fill: getThemeColor(METAC_COLORS.blue["700"]),
stroke: "none",
},
}}
/>
)}

{question.type === QuestionType.Date &&
todayLabelPosition &&
withTodayLine && (
<VictoryPortal>
<VictoryLabel
x={todayLabelPosition.x}
y={height - BOTTOM_PADDING - 12} // Position above the dot
text="Today"
style={{
fill: getThemeColor(METAC_COLORS.blue["700"]),
fontSize: 12,
}}
textAnchor="middle"
/>
</VictoryPortal>
)}

{/* Manually render cursor component when cursor is on edge */}
{!isNil(cursorEdge) && (
Expand Down
75 changes: 54 additions & 21 deletions front_end/src/components/charts/fan_chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import ChartFanTooltip from "@/components/charts/primitives/chart_fan_tooltip";
import FanPoint from "@/components/charts/primitives/fan_point";
import PredictionWithRange from "@/components/charts/primitives/prediction_with_range";
import ResolutionDiamond from "@/components/charts/primitives/resolution_diamond";
import ForecastAvailabilityChartOverflow from "@/components/post_card/chart_overflow";
import { darkTheme, lightTheme } from "@/constants/chart_theme";
import { METAC_COLORS } from "@/constants/colors";
Expand Down Expand Up @@ -457,27 +458,48 @@ const FanChart: FC<Props> = ({
/>
)}

{resolutionPoints.map((point) => (
<VictoryScatter
key={`res-${point.x}`}
data={[{ ...point, symbol: "diamond" }]}
style={{
data: {
fill: v.resolutionPoint.fill({ getThemeColor }),
stroke: () => palette.resolutionStroke,
strokeWidth: 2,
strokeOpacity: 1,
},
}}
dataComponent={
<FanPoint
activePoint={null}
pointSize={v.resolutionPoint.size}
strokeWidth={v.resolutionPoint.strokeWidth}
/>
}
/>
))}
{resolutionPoints.map((point) => {
if (
point.placement &&
["below", "above"].includes(point.placement)
) {
return (
<VictoryPortal key={`res-portal-${point.x}`}>
<VictoryScatter
key={`res-${point.x}`}
data={[
{
...point,
y: point.placement === "below" ? 0 : 1,
},
]}
dataComponent={<ResolutionDiamond hoverable={false} />}
/>
</VictoryPortal>
);
}
return (
<VictoryScatter
key={`res-${point.x}`}
data={[{ ...point, symbol: "diamond" }]}
style={{
data: {
fill: v.resolutionPoint.fill({ getThemeColor }),
stroke: () => palette.resolutionStroke,
strokeWidth: 2,
strokeOpacity: 1,
},
}}
dataComponent={
<FanPoint
activePoint={null}
pointSize={v.resolutionPoint.size}
strokeWidth={v.resolutionPoint.strokeWidth}
/>
}
/>
);
})}
{emptyPoints.map((point) => (
<VictoryScatter
key={`empty-${point.x}`}
Expand Down Expand Up @@ -514,6 +536,7 @@ type FanGraphPoint = {
y: number;
resolved?: boolean;
unsuccessfullyResolved?: boolean;
placement?: "in" | "below" | "above";
};

function buildChartData({
Expand Down Expand Up @@ -593,11 +616,21 @@ function buildChartData({
? getResolutionPosition({ question: option.question, scaling })
: NaN;

const isAboveUpperBound =
option.question?.resolution === "above_upper_bound" || yVal > 1;
const isBelowLowerBound =
option.question?.resolution === "below_lower_bound" || yVal < 0;

resolutionPoints.push({
x: option.name,
y: yVal,
unsuccessfullyResolved: false,
resolved: true,
placement: isAboveUpperBound
? "above"
: isBelowLowerBound
? "below"
: "in",
});
}

Expand Down
4 changes: 2 additions & 2 deletions front_end/src/components/charts/fan_chart_variants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ export const fanVariants: Record<FanChartVariant, VariantConfig> = {
communityPoint: getThemeColor(METAC_COLORS.olive["800"]),
}),
resolutionPoint: {
size: 8,
strokeWidth: 2,
size: 10,
strokeWidth: 2.5,
fill: () => "none",
},
},
Expand Down
46 changes: 42 additions & 4 deletions front_end/src/components/charts/group_chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import ForecastAvailabilityChartOverflow from "../post_card/chart_overflow";
import ChartContainer from "./primitives/chart_container";
import ChartCursorLabel from "./primitives/chart_cursor_label";
import GroupResolutionPoint from "./primitives/group_resolution_point";
import ResolutionDiamond from "./primitives/resolution_diamond";
import XTickLabel from "./primitives/x_tick_label";

type Props = {
Expand Down Expand Up @@ -515,6 +516,31 @@ const GroupChart: FC<Props> = ({
? METAC_COLORS["mc-option-text"][1]
: color;

if (
resolutionPoint.placement &&
["below", "above"].includes(resolutionPoint.placement)
) {
return (
<VictoryPortal key={`group-resolution-portal-${index}`}>
<VictoryScatter
key={`group-resolution-${index}`}
data={[
{
x: resolutionPoint?.x,
y: resolutionPoint?.placement === "below" ? 0 : 1,
x1: resolutionPoint?.x1,
y1: resolutionPoint?.y1,
text: resolutionPoint?.text,
placement: resolutionPoint?.placement,
primary: color,
},
]}
dataComponent={<ResolutionDiamond hoverable={false} />}
/>
</VictoryPortal>
);
}

return (
<VictoryScatter
key={`group-resolution-${index}`}
Expand Down Expand Up @@ -593,6 +619,7 @@ export type ChoiceGraph = {
text?: string;
x1?: number;
y1?: number;
placement?: "in" | "below" | "above";
};
choice: string;
color: ThemeColor;
Expand Down Expand Up @@ -826,18 +853,26 @@ function buildChartData({
text,
x1: lastLineItem?.x,
y1: lastLineItem?.y ?? undefined,
placement:
resolution === "below_lower_bound"
? "below"
: resolution === "above_upper_bound"
? "above"
: "in",
};
}

if (isFinite(Number(resolution))) {
const yPos = scaling
? unscaleNominalLocation(Number(resolution), scaling)
: Number(resolution) ?? 0;
// continuous group case
item.resolutionPoint = {
x: resolveTime,
y: scaling
? unscaleNominalLocation(Number(resolution), scaling)
: Number(resolution) ?? 0,
y: yPos,
x1: lastLineItem?.x,
y1: lastLineItem?.y ?? undefined,
placement: yPos < 0 ? "below" : yPos > 1 ? "above" : "in",
};
} else if (
typeof resolution === "string" &&
Expand All @@ -851,10 +886,13 @@ function buildChartData({
resolveTime,
scaling,
});

if (dateResolution) {
const yPos = dateResolution.y ?? 0;
item.resolutionPoint = {
x: dateResolution.x,
y: dateResolution.y ?? 0,
y: yPos,
placement: yPos < 0 ? "below" : yPos > 1 ? "above" : "in",
x1: lastLineItem?.x,
y1: lastLineItem?.y ?? undefined,
};
Expand Down
Loading