diff --git a/front_end/messages/en.json b/front_end/messages/en.json index 56d3d3a338..97e6ad8e9b 100644 --- a/front_end/messages/en.json +++ b/front_end/messages/en.json @@ -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", diff --git a/front_end/src/app/(main)/accounts/profile/components/track_record.tsx b/front_end/src/app/(main)/accounts/profile/components/track_record.tsx index f4e463bf70..f07a492fb2 100644 --- a/front_end/src/app/(main)/accounts/profile/components/track_record.tsx +++ b/front_end/src/app/(main)/accounts/profile/components/track_record.tsx @@ -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"; @@ -15,6 +16,22 @@ const TrackRecord: FC<{ profile: UserProfile }> = ({ profile }) => { return (
+

+ {t("scoreScatterPlot")} +

+ {profile.score_scatter_plot && ( + + )} +

+ {t("scoreHistogram")} +

+ {profile.score_histogram && ( + + )} +

{t("scoreHistogram")}

diff --git a/front_end/src/app/(main)/charts/scatter_plot.tsx b/front_end/src/app/(main)/charts/scatter_plot.tsx index e69de29bb2..fbe36b5356 100644 --- a/front_end/src/app/(main)/charts/scatter_plot.tsx +++ b/front_end/src/app/(main)/charts/scatter_plot.tsx @@ -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 = ({ score_scatter_plot }) => { + const t = useTranslations(); + const { theme, getThemeColor } = useAppTheme(); + const chartTheme = theme === "dark" ? darkTheme : lightTheme; + + const { ref: chartContainerRef, width: chartWidth } = + useContainerSize(); + + const { + overallAverage, + movingAverage, + ticksY, + ticksYFormat, + xDomain, + xScale, + } = buildChartData({ score_scatter_plot, chartWidth }); + + return ( + + + { + return { + x: point.score_timestamp, + y: point.score, + size: 5, + }; + })} + style={{ + data: { + stroke: getThemeColor(METAC_COLORS.blue["600"]), + fill: "none", + strokeWidth: 1, + }, + }} + /> + + + + } + /> + + + ); +}; + +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; diff --git a/front_end/src/app/(main)/charts/user_histogram.tsx b/front_end/src/app/(main)/charts/user_histogram.tsx index efd94c4677..dd2e53f9cc 100644 --- a/front_end/src/app/(main)/charts/user_histogram.tsx +++ b/front_end/src/app/(main)/charts/user_histogram.tsx @@ -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: { @@ -31,11 +32,26 @@ const UserHistogram: React.FC = ({ 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 ( -
- } + padding={{ top: 20, bottom: 65, left: 40, right: 20 }} + height={180} + > + = ({ ], y: [0, 1], }} - containerComponent={} - padding={{ top: 20, bottom: 65, left: 40, right: 20 }} - height={180} - > - - - - -
+ offsetX={40} + tickValues={ticksY} + tickFormat={ticksYFormat} + style={{ + tickLabels: { + fontSize: 5, + }, + axisLabel: { + fontSize: 6.5, + }, + axis: { stroke: chartTheme.axis?.style?.axis?.stroke }, + }} + label={t("frequency")} + /> + + + ); }; @@ -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; diff --git a/front_end/src/utils/charts.ts b/front_end/src/utils/charts.ts index 5d85535c2b..fb262b9243 100644 --- a/front_end/src/utils/charts.ts +++ b/front_end/src/utils/charts.ts @@ -95,6 +95,15 @@ export function generateTimestampXScale( ticks = d3.timeMinute.range(start, end); format = d3.timeFormat("%_I:%M %p"); cursorFormat = d3.timeFormat("%_I:%M %p, %b %d"); + } else if (timeRange < oneHour * 6) { + const every5Minutes = d3.timeMinute.every(5); + if (every5Minutes) { + ticks = every5Minutes.range(start, end); + } else { + ticks = d3.timeHour.range(start, end); + } + format = d3.timeFormat("%_I:%M %p"); + cursorFormat = d3.timeFormat("%_I:%M %p, %b %d"); } else if (timeRange < oneDay) { const every30Minutes = d3.timeMinute.every(30); if (every30Minutes) { @@ -477,3 +486,31 @@ export const interpolateYValue = (xValue: number, line: Line) => { const t = (xValue - p1.x) / (p2.x - p1.x); return p1.y + t * (p2.y - p1.y); }; + +export function generateTicksY(height: number, desiredMajorTicks: number[]) { + const minorTicksPerMajor = 9; + const desiredMajorTickDistance = 50; + 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 }; +}