+
+ {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 };
+}