Skip to content
Merged
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
1 change: 1 addition & 0 deletions front_end/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,7 @@
"totalComments": "Total Comments",
"mostPredictions": "Most Predictions",
"closingSoon": "Closing Soon",
"closingTime": "Closing Time",
"bestScores": "Best Scores",
"worstScores": "Worst Scores",
"resolvingSoon": "Resolving Soon",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useTranslations } from "next-intl";
import { FC } from "react";

import CalibrationChart from "@/app/(main)/charts/calibration_chart";
import ScatterPlot from "@/app/(main)/charts/scatter_plot";
import UserHistogram from "@/app/(main)/charts/user_histogram";
import { UserProfile } from "@/types/users";

Expand All @@ -15,6 +16,22 @@ const TrackRecord: FC<{ profile: UserProfile }> = ({ profile }) => {
return (
<div className="flex flex-col gap-4 md:gap-6">
<div className="flex flex-col rounded bg-white p-6 dark:bg-blue-900">
<h3 className="my-0 py-0 text-gray-700 dark:text-gray-300">
{t("scoreScatterPlot")}
</h3>
{profile.score_scatter_plot && (
<ScatterPlot score_scatter_plot={profile.score_scatter_plot} />
)}
<h3 className="my-0 py-0 text-gray-700 dark:text-gray-300">
{t("scoreHistogram")}
</h3>
{profile.score_histogram && (
<UserHistogram
rawHistogramData={profile.score_histogram}
color="gray"
/>
)}

<h3 className="my-0 py-0 text-gray-700 dark:text-gray-300">
{t("scoreHistogram")}
</h3>
Expand Down
175 changes: 175 additions & 0 deletions front_end/src/app/(main)/charts/scatter_plot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
"use client";

import { useTranslations } from "next-intl";
import React from "react";
import {
VictoryAxis,
VictoryChart,
VictoryScatter,
VictoryLine,
} from "victory";

import ChartContainer from "@/components/charts/primitives/chart_container";
import XTickLabel from "@/components/charts/primitives/x_tick_label";
import { darkTheme, lightTheme } from "@/constants/chart_theme";
import { METAC_COLORS } from "@/constants/colors";
import useAppTheme from "@/hooks/use_app_theme";
import useContainerSize from "@/hooks/use_container_size";
import { TimelineChartZoomOption } from "@/types/charts";
import {
generateNumericDomain,
generateTicksY,
generateTimestampXScale,
} from "@/utils/charts";

type HistogramProps = {
score_scatter_plot: { score: number; score_timestamp: number }[];
};

const ScatterPlot: React.FC<HistogramProps> = ({ score_scatter_plot }) => {
const t = useTranslations();
const { theme, getThemeColor } = useAppTheme();
const chartTheme = theme === "dark" ? darkTheme : lightTheme;

const { ref: chartContainerRef, width: chartWidth } =
useContainerSize<HTMLDivElement>();

const {
overallAverage,
movingAverage,
ticksY,
ticksYFormat,
xDomain,
xScale,
} = buildChartData({ score_scatter_plot, chartWidth });

return (
<ChartContainer ref={chartContainerRef} height={350}>
<VictoryChart
theme={chartTheme}
domain={{
x: xDomain,
y: [-100, 100],
}}
domainPadding={{
x: 10,
}}
padding={{ top: 20, bottom: 65, left: 40, right: 20 }}
height={350}
width={chartWidth}
>
<VictoryScatter
data={score_scatter_plot.map((point) => {
return {
x: point.score_timestamp,
y: point.score,
size: 5,
};
})}
style={{
data: {
stroke: getThemeColor(METAC_COLORS.blue["600"]),
fill: "none",
strokeWidth: 1,
},
}}
/>
<VictoryLine
y={overallAverage}
style={{
data: {
stroke: getThemeColor(METAC_COLORS.gray["400"]),
strokeDasharray: "5, 2",
},
}}
/>
<VictoryLine
data={movingAverage}
style={{
data: {
stroke: getThemeColor(METAC_COLORS.gray["800"]),
},
}}
/>
<VictoryAxis
dependentAxis
offsetX={40}
tickValues={ticksY}
tickFormat={ticksYFormat}
style={{
tickLabels: {
fontSize: 10,
},
axisLabel: {
fontSize: 13,
},
axis: { stroke: chartTheme.axis?.style?.axis?.stroke },
}}
label={`Metaculus ${t("brierScore")}`}
/>
<VictoryAxis
tickValues={xScale.ticks}
tickFormat={xScale.tickFormat}
offsetY={65}
label={t("closingTime")}
style={{
tickLabels: {
fontSize: 10,
},
axisLabel: {
fontSize: 13,
},
axis: { stroke: chartTheme.axis?.style?.axis?.stroke },
grid: { stroke: "none" },
}}
tickLabelComponent={<XTickLabel chartWidth={chartWidth} />}
/>
</VictoryChart>
</ChartContainer>
);
};

function buildChartData({
score_scatter_plot,
chartWidth,
}: {
score_scatter_plot: { score: number; score_timestamp: number }[];
chartWidth: number;
}) {
const overallAverage =
score_scatter_plot.length === 0
? 0
: score_scatter_plot.reduce((reducer, data) => {
return reducer + data.score;
}, 0) / score_scatter_plot.length;

let scoreLocalSum = 0;
const movingAverage = score_scatter_plot.map((data, index) => {
scoreLocalSum += data.score;
return {
x: data.score_timestamp,
y: scoreLocalSum / (index + 1),
};
});

const { ticks: ticksY, tickFormat: ticksYFormat } = generateTicksY(
270,
[-100, -50, 0, 50, 100]
);
const xDomain = generateNumericDomain(
score_scatter_plot.map((data) => data.score_timestamp),
"all" as TimelineChartZoomOption
);
const xScale = generateTimestampXScale(xDomain, chartWidth);

return {
overallAverage,
movingAverage,
ticksY,
ticksYFormat,
xDomain,
xScale,
};
}

export default ScatterPlot;
155 changes: 64 additions & 91 deletions front_end/src/app/(main)/charts/user_histogram.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {

import { darkTheme, lightTheme } from "@/constants/chart_theme";
import useAppTheme from "@/hooks/use_app_theme";
import { generateTicksY } from "@/utils/charts";

type HistogramProps = {
rawHistogramData: {
Expand All @@ -31,76 +32,77 @@ const UserHistogram: React.FC<HistogramProps> = ({
const { theme } = useAppTheme();
const chartTheme = theme === "dark" ? darkTheme : lightTheme;

const { ticks: ticksY, tickFormat: ticksYFormat } = generateTicksY(180);
const { ticks: ticksY, tickFormat: ticksYFormat } = generateTicksY(
180,
[0.0, 0.2, 0.4, 0.6, 0.8, 1.0]
);
return (
<div className="mb-5 size-full">
<VictoryChart
theme={chartTheme}
<VictoryChart
theme={chartTheme}
domain={{
x: [
rawHistogramData[0].bin_start ?? 1,
rawHistogramData[rawHistogramData.length - 1].bin_end ?? 0,
],
y: [0, 1],
}}
containerComponent={<VictoryContainer responsive={true} />}
padding={{ top: 20, bottom: 65, left: 40, right: 20 }}
height={180}
>
<VictoryAxis
dependentAxis
domain={{
x: [
rawHistogramData[0].bin_start ?? 1,
rawHistogramData[rawHistogramData.length - 1].bin_end ?? 0,
],
y: [0, 1],
}}
containerComponent={<VictoryContainer responsive={true} />}
padding={{ top: 20, bottom: 65, left: 40, right: 20 }}
height={180}
>
<VictoryAxis
dependentAxis
domain={{
x: [
rawHistogramData[0].bin_start ?? 1,
rawHistogramData[rawHistogramData.length - 1].bin_end ?? 0,
],
y: [0, 1],
}}
offsetX={40}
tickValues={ticksY}
tickFormat={ticksYFormat}
style={{
tickLabels: {
fontSize: 5,
},
axisLabel: {
fontSize: 6.5,
},
axis: { stroke: chartTheme.axis?.style?.axis?.stroke },
}}
label={t("frequency")}
/>
<VictoryAxis
tickValues={range(
rawHistogramData[0].bin_start,
rawHistogramData[rawHistogramData.length - 1].bin_end + 70,
70
)}
style={{
tickLabels: {
fontSize: 5,
},
axisLabel: {
fontSize: 6.5,
},
axis: { stroke: chartTheme.axis?.style?.axis?.stroke },
grid: { stroke: "none" },
}}
label={t("brierScoreForPlayer")}
/>
<VictoryArea
data={histogramData}
style={{
data: {
strokeWidth: 1.5,
fill: "light" + color,
stroke: "dark" + color,
},
}}
interpolation="stepAfter"
/>
</VictoryChart>
</div>
offsetX={40}
tickValues={ticksY}
tickFormat={ticksYFormat}
style={{
tickLabels: {
fontSize: 5,
},
axisLabel: {
fontSize: 6.5,
},
axis: { stroke: chartTheme.axis?.style?.axis?.stroke },
}}
label={t("frequency")}
/>
<VictoryAxis
tickValues={range(
rawHistogramData[0].bin_start,
rawHistogramData[rawHistogramData.length - 1].bin_end + 70,
70
)}
style={{
tickLabels: {
fontSize: 5,
},
axisLabel: {
fontSize: 6.5,
},
axis: { stroke: chartTheme.axis?.style?.axis?.stroke },
grid: { stroke: "none" },
}}
label={t("brierScoreForPlayer")}
/>
<VictoryArea
data={histogramData}
style={{
data: {
strokeWidth: 1.5,
fill: "light" + color,
stroke: "dark" + color,
},
}}
interpolation="stepAfter"
/>
</VictoryChart>
);
};

Expand All @@ -123,33 +125,4 @@ const mapHistogramData = (
return mappedArray;
};

function generateTicksY(height: number) {
const desiredMajorTicks = [0.0, 0.2, 0.4, 0.6, 0.8, 1.0];
const minorTicksPerMajor = 9;
const desiredMajorTickDistance = 20;
let majorTicks = desiredMajorTicks;
const maxMajorTicks = Math.floor(height / desiredMajorTickDistance);

if (maxMajorTicks < desiredMajorTicks.length) {
const step = 1 / (maxMajorTicks - 1);
majorTicks = Array.from({ length: maxMajorTicks }, (_, i) => i * step);
}
const ticks = [];
for (let i = 0; i < majorTicks.length - 1; i++) {
ticks.push(majorTicks[i]);
const step = (majorTicks[i + 1] - majorTicks[i]) / (minorTicksPerMajor + 1);
for (let j = 1; j <= minorTicksPerMajor; j++) {
ticks.push(majorTicks[i] + step * j);
}
}
ticks.push(majorTicks[majorTicks.length - 1]);
const tickFormat = (value: number): string => {
if (!majorTicks.includes(value)) {
return "";
}
return value.toString();
};
return { ticks, tickFormat };
}

export default UserHistogram;
Loading