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
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"@urql/core": "^4.1.4",
"@urql/exchange-graphcache": "^6.3.3",
"algoliasearch": "4.17.1",
"chart.js": "^4.5.0",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Chart.js/react-chartjs-2 integration: ensure registration and SSR strategy; consider tree-shaking

  • With react-chartjs-2 v5 + chart.js v4, you must register controllers or import 'chart.js/auto' before rendering charts. Otherwise you'll see “element type is invalid” or blank charts.
  • For Next.js, prefer client-side rendering for Chart.js (or dynamic import with ssr: false) to avoid window undefined/rehydration issues.
  • For bundle size, avoid 'chart.js/auto' in production and register only the used components (LineElement, PointElement, LinearScale, CategoryScale, Tooltip, Legend, etc.).

[suggest_optional_refactor]

Run this to confirm registration and SSR strategy are present:

Also applies to: 62-62


🏁 Script executed:

#!/bin/bash
# Find Chart.js auto import or manual registration
rg -n -C3 -S "chart.js/auto|ChartJS\.register|Chart\.register" --type=ts --type=tsx

# Check if VoteCurves rendering is client-only or dynamically imported with ssr: false
rg -n -C3 -S "use client|next/dynamic|ssr:\s*false" src/components/Post/GovernanceSidebar/VoteCurvesData

Length of output: 220


To ensure we’re catching every import/registration and the SSR setup, let’s broaden the search across all JS/TS extensions and locate the VoteCurvesData component dynamically:

This will confirm:

  • Whether chart.js/auto or manual ChartJS.register calls exist.
  • That VoteCurvesData is either marked "use client" or loaded via next/dynamic with ssr: false.

If nothing shows up, please verify manually that you’re registering Chart.js components before use and that your chart component is client-only.


🏁 Script executed:

#!/bin/bash
# 1. Search for chart.js auto-import and manual registration across all JS/TS files
rg -n -C3 "import ['\"]chart\.js/auto['\"]"
rg -n -C3 "ChartJS\.register|Chart\.register"

# 2. Locate the VoteCurvesData component in any JS/TS file
FILES=$(fd VoteCurvesData -e tsx -e ts -e jsx -e js)
if [ -z "$FILES" ]; then
  echo "⚠️ VoteCurvesData file not found."
else
  for f in $FILES; do
    echo "🔍 Checking SSR setup in $f"
    rg -n -C3 "use client|next/dynamic|ssr:\s*false" "$f" || echo "  – No client-only or dynamic import found in $f"
  done
fi

Length of output: 2432


Ensure client-only rendering for Chart.js in Next.js

We’ve confirmed that you’re manually registering only the needed Chart.js components in VoteCurves.tsx (no chart.js/auto import—good for tree-shaking). However, both VoteCurvesData files are still rendering server-side, which will break under SSR.

Please apply one of the following fixes:

• In each file under src/components/Post/GovernanceSidebar/VoteCurvesData/ (both VoteCurvesData.tsx and VoteCurvesDataWrapper.tsx), add at the very top:

+ "use client";

• Or, wherever you import VoteCurvesData, switch to dynamic loading:

import dynamic from "next/dynamic";

const VoteCurvesData = dynamic(
  () => import("@/components/Post/GovernanceSidebar/VoteCurvesData/VoteCurvesData"),
  { ssr: false }
);

This ensures Chart.js only runs in the browser and avoids “window is undefined” or blank-chart issues.

🤖 Prompt for AI Agents
In package.json around line 51, the review flags that Chart.js-related
components are being rendered server-side: update both
src/components/Post/GovernanceSidebar/VoteCurvesData/VoteCurvesData.tsx and
src/components/Post/GovernanceSidebar/VoteCurvesData/VoteCurvesDataWrapper.tsx
to be client-only by adding the Next.js client directive as the very first line
of each file ("use client"), or alternatively change every import site to load
VoteCurvesData via next/dynamic with ssr: false; ensure the directive/import
change is applied to both files so Chart.js runs only in the browser.

"classnames": "^2.3.2",
"dayjs": "^1.11.10",
"fetch-ponyfill": "^7.1.0",
Expand All @@ -58,6 +59,7 @@
"next-themes": "^0.2.1",
"next-usequerystate": "^1.10.2",
"react": "^18",
"react-chartjs-2": "^5.3.0",
"react-dom": "^18",
"react-hook-form": "^7.47.0",
"react-json-view": "^1.21.3",
Expand Down
38 changes: 38 additions & 0 deletions src/app/api/v1/curves/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright 2019-2025 @polkassembly/fellowship authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

import { NextRequest, NextResponse } from 'next/server';
import { GET_CURVE_DATA_BY_INDEX } from '@/app/api/v1/subsquidQueries';
import { urqlClient } from '@/services/urqlClient';
import getNetworkFromHeaders from '@/app/api/api-utils/getNetworkFromHeaders';
import getReqBody from '@/app/api/api-utils/getReqBody';
import withErrorHandling from '@/app/api/api-utils/withErrorHandling';
import { headers } from 'next/headers';
import MESSAGES from '@/global/messages';
import { APIError } from '@/global/exceptions';
import { API_ERROR_CODE } from '@/global/constants/errorCodes';

export const POST = withErrorHandling(async (req: NextRequest) => {
const { blockGte, postId } = await getReqBody(req);

if (!blockGte || !postId || isNaN(postId) || isNaN(blockGte)) throw new APIError(`${MESSAGES.INVALID_PARAMS_ERROR}`, 500, API_ERROR_CODE.INVALID_PARAMS_ERROR);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Bug: valid zero values are incorrectly rejected; use robust numeric validation and return 400 for bad params.

The falsy checks treat 0 as invalid, breaking cases where blockGte can legitimately be 0. Switch to Number coercion with finite checks and respond with 400 for invalid params.

Apply:

- if (!blockGte || !postId || isNaN(postId) || isNaN(blockGte)) throw new APIError(`${MESSAGES.INVALID_PARAMS_ERROR}`, 500, API_ERROR_CODE.INVALID_PARAMS_ERROR);
+ const blockGteNum = Number(blockGte);
+ const postIdNum = Number(postId);
+ if (!Number.isFinite(blockGteNum) || blockGteNum < 0 || !Number.isFinite(postIdNum) || postIdNum < 0) {
+   throw new APIError(MESSAGES.INVALID_PARAMS_ERROR, 400, API_ERROR_CODE.INVALID_PARAMS_ERROR);
+ }
🤖 Prompt for AI Agents
In src/app/api/v1/curves/route.ts around line 19, the current falsy checks
reject valid zero values and incorrectly return a 500; instead coerce postId and
blockGte to numbers (e.g. const postIdNum = Number(postId); const blockGteNum =
Number(blockGte)), validate with Number.isFinite or isFinite(postIdNum) /
isFinite(blockGteNum) to allow 0, and when validation fails throw an APIError
with status 400 and API_ERROR_CODE.INVALID_PARAMS_ERROR; ensure you use the
numeric variables downstream after successful validation.


const headersList = headers();
const network = getNetworkFromHeaders(headersList);

const gqlClient = urqlClient(network);

const variables = {
block_gte: Number(blockGte),
index_eq: Number(postId)
};

const { data, error } = await gqlClient.query(GET_CURVE_DATA_BY_INDEX, variables).toPromise();

if (error) throw new APIError(`${error || MESSAGES.SUBSQUID_FETCH_ERROR}`, 500, API_ERROR_CODE.SUBSQUID_FETCH_ERROR);

const curveData = data?.curveData || [];

return NextResponse.json(curveData);
});
13 changes: 13 additions & 0 deletions src/app/api/v1/subsquidQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,3 +473,16 @@ export const GET_RANK_ACTIVITY = gql`
}
}
`;

export const GET_CURVE_DATA_BY_INDEX = gql`
query CurveDataByIndex($index_eq: Int, $block_gte: Int, $limit: Int = 1000) {
curveData(limit: $limit, where: { index_eq: $index_eq, block_gte: $block_gte }, orderBy: block_ASC) {
approvalPercent
block
id
index
supportPercent
timestamp
}
}
`;
297 changes: 297 additions & 0 deletions src/components/Post/GovernanceSidebar/VoteCurvesData/VoteCurves.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
// Copyright 2019-2025 @polkassembly/fellowship authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

import { useEffect, useMemo } from 'react';
import { Line } from 'react-chartjs-2';
import { Chart as ChartJS, CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend, ChartOptions, ChartData, Point } from 'chart.js';
import { ProposalType, IStatusHistoryItem, IVoteCurve } from '@/global/types';
import dayjs from '@/services/dayjs-init';
import { Network } from '@/global/types';
import { getTrackFunctions } from '@/utils/trackCurvesUtils';

ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);

interface Props {
voteCurveData: IVoteCurve[];
trackName: ProposalType;
timeline?: IStatusHistoryItem[];
createdAt?: Date;
setThresholdValues?: (values: { approvalThreshold: number; supportThreshold: number }) => void;
}

function formatHoursAndDays(num: number, unit: 'day' | 'hr') {
if (num === 1) {
return `${num}${unit}`;
}
return `${num}${unit}s`;
}

function convertGraphPoint(value?: number) {
if (!value) {
return '--';
}

return `${Number(value).toFixed(2)}%`;
}
Comment on lines +30 to +36
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix percent formatter to correctly handle zero values

Using a falsy check treats 0 as “no value,” rendering 0% as “--”. Return “0.00%” for zero and only use “--” for null/undefined/NaN.

Apply this diff:

 function convertGraphPoint(value?: number) {
-	if (!value) {
-		return '--';
-	}
-
-	return `${Number(value).toFixed(2)}%`;
+	if (value === null || value === undefined || Number.isNaN(value)) {
+		return '--';
+	}
+	return `${Number(value).toFixed(2)}%`;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function convertGraphPoint(value?: number) {
if (!value) {
return '--';
}
return `${Number(value).toFixed(2)}%`;
}
function convertGraphPoint(value?: number) {
if (value === null || value === undefined || Number.isNaN(value)) {
return '--';
}
return `${Number(value).toFixed(2)}%`;
}
🤖 Prompt for AI Agents
In src/components/Post/GovernanceSidebar/VoteCurvesData/VoteCurves.tsx around
lines 30 to 36, the percent formatter uses a falsy check so 0 is treated as
missing and returns "--"; update the guard to only treat null, undefined, or NaN
as missing (e.g., value === null || value === undefined || Number.isNaN(value))
and otherwise return the formatted string `${Number(value).toFixed(2)}%` so that
0 becomes "0.00%".


function VoteCurves({ voteCurveData, trackName, timeline, createdAt, setThresholdValues }: Props) {
const network: Network = 'collectives' as Network;

const { approvalCalc, supportCalc } = getTrackFunctions({ network, trackName });

const chartData: ChartData<'line', (number | Point | null)[]> = useMemo(() => {
if (!voteCurveData || voteCurveData.length === 0) {
return {
datasets: [],
labels: []
};
}

// Simplified track info for fellowship referendums
const trackInfo = {
decisionPeriod: 100800 // 7 days in blocks
};

const labels: number[] = [];
const supportData: { x: number; y: number }[] = [];
const approvalData: { x: number; y: number }[] = [];

const approvalThresholdData: { x: number; y: number }[] = [];
const supportThresholdData: { x: number; y: number }[] = [];

const statusBlock = timeline?.find((s) => s?.status === 'Deciding');

const lastGraphPoint = voteCurveData[voteCurveData.length - 1];
const proposalCreatedAt = dayjs(statusBlock?.timestamp || createdAt || voteCurveData[0].timestamp);

const { decisionPeriod } = trackInfo;

const decisionPeriodInHrs = Math.floor(dayjs.duration(decisionPeriod * 6, 'seconds').asHours()); // Assuming 6s block time
const decisionPeriodFromTimelineInHrs = dayjs(lastGraphPoint.timestamp).diff(proposalCreatedAt, 'hour');

if (decisionPeriodFromTimelineInHrs < decisionPeriodInHrs) {
for (let i = 0; i < decisionPeriodInHrs; i += 1) {
labels.push(i);

if (approvalCalc) {
approvalThresholdData.push({
x: i,
y: approvalCalc(i / decisionPeriodInHrs) * 100
});
}

if (supportCalc) {
supportThresholdData.push({
x: i,
y: supportCalc(i / decisionPeriodInHrs) * 100
});
}
}
}

// Process each data point
voteCurveData.forEach((point) => {
const hour = dayjs(point.timestamp).diff(proposalCreatedAt, 'hour');
labels.push(hour);

if (decisionPeriodFromTimelineInHrs > decisionPeriodInHrs) {
if (approvalCalc) {
approvalThresholdData.push({
x: hour,
y: approvalCalc(hour / decisionPeriodFromTimelineInHrs) * 100
});
}
if (supportCalc) {
supportThresholdData.push({
x: hour,
y: supportCalc(hour / decisionPeriodFromTimelineInHrs) * 100
});
}
}
Comment on lines +98 to +111
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Normalize thresholds to the decision period and handle the “equal” case

Two issues:

  • When the timeline equals the decision period, thresholds aren’t generated (strict “>” check skips both branches).
  • When the timeline exceeds the decision period, thresholds are normalized by the timeline length, not the fixed decision period, shifting the curve.

Use “>=” and always normalize by the decision period (clamped to 1).

Apply this diff:

-			if (decisionPeriodFromTimelineInHrs > decisionPeriodInHrs) {
+			if (decisionPeriodFromTimelineInHrs >= decisionPeriodInHrs) {
 				if (approvalCalc) {
 					approvalThresholdData.push({
 						x: hour,
-						y: approvalCalc(hour / decisionPeriodFromTimelineInHrs) * 100
+						y: approvalCalc(Math.min(hour / decisionPeriodInHrs, 1)) * 100
 					});
 				}
 				if (supportCalc) {
 					supportThresholdData.push({
 						x: hour,
-						y: supportCalc(hour / decisionPeriodFromTimelineInHrs) * 100
+						y: supportCalc(Math.min(hour / decisionPeriodInHrs, 1)) * 100
 					});
 				}
 			}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (decisionPeriodFromTimelineInHrs > decisionPeriodInHrs) {
if (approvalCalc) {
approvalThresholdData.push({
x: hour,
y: approvalCalc(hour / decisionPeriodFromTimelineInHrs) * 100
});
}
if (supportCalc) {
supportThresholdData.push({
x: hour,
y: supportCalc(hour / decisionPeriodFromTimelineInHrs) * 100
});
}
}
if (decisionPeriodFromTimelineInHrs >= decisionPeriodInHrs) {
if (approvalCalc) {
approvalThresholdData.push({
x: hour,
y: approvalCalc(Math.min(hour / decisionPeriodInHrs, 1)) * 100
});
}
if (supportCalc) {
supportThresholdData.push({
x: hour,
y: supportCalc(Math.min(hour / decisionPeriodInHrs, 1)) * 100
});
}
}
🤖 Prompt for AI Agents
In src/components/Post/GovernanceSidebar/VoteCurvesData/VoteCurves.tsx around
lines 98 to 111, the code skips generating thresholds when
decisionPeriodFromTimelineInHrs equals decisionPeriodInHrs and normalizes by the
timeline length instead of the fixed decision period; change the conditional to
use ">=" so the equal case is handled, and when computing normalized input for
approvalCalc/supportCalc divide by decisionPeriodInHrs (not
decisionPeriodFromTimelineInHrs), clamping the divisor to at least 1 or the
normalized value to a maximum of 1 so inputs never exceed the decision period.


// Add actual data points
if (point.supportPercent !== undefined) {
supportData.push({
x: hour,
y: point.supportPercent
});
}

if (point.approvalPercent !== undefined) {
approvalData.push({
x: hour,
y: point.approvalPercent
});
}
});

return {
datasets: [
{
backgroundColor: 'transparent',
borderColor: '#2ED47A', // voteAye color
borderWidth: 2,
borderDash: [4, 4],
data: approvalData,
label: 'Approval',
pointHitRadius: 10,
pointHoverRadius: 5,
pointRadius: 0,
tension: 0.1
},
{
backgroundColor: 'transparent',
borderColor: '#FF3C5F', // voteNay color
borderWidth: 2,
borderDash: [4, 4],
data: supportData,
label: 'Support',
pointHitRadius: 10,
pointHoverRadius: 5,
pointRadius: 0,
tension: 0.1
},
{
backgroundColor: 'transparent',
borderColor: '#2ED47A', // voteAye color
borderWidth: 2,
data: approvalThresholdData,
label: 'Approval Threshold',
pointHitRadius: 10,
pointHoverRadius: 5,
pointRadius: 0,
tension: 0.1
},
{
backgroundColor: 'transparent',
borderColor: '#FF3C5F', // voteNay color
borderWidth: 2,
data: supportThresholdData,
label: 'Support Threshold',
pointHitRadius: 10,
pointHoverRadius: 5,
pointRadius: 0,
tension: 0.1
}
],
labels
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [voteCurveData]);

useEffect(() => {
if (!chartData || !chartData.datasets || chartData.datasets.length < 4) {
return;
}

const approvalThresholdData = chartData.datasets[2].data as Point[];
const supportThresholdData = chartData.datasets[3].data as Point[];
const currentApproval = chartData.datasets[0].data[chartData.datasets[0].data.length - 1] as Point;
const currentSupport = chartData.datasets[1].data[chartData.datasets[1].data.length - 1] as Point;

setThresholdValues?.({
approvalThreshold: approvalThresholdData.find((data) => data && data?.x >= currentApproval?.x)?.y || 0,
supportThreshold: supportThresholdData.find((data) => data && data?.x >= currentSupport?.x)?.y || 0
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [chartData]);

const chartOptions: ChartOptions<'line'> = {
animation: {
duration: 0
},
clip: false,
plugins: {
legend: {
display: false,
position: 'bottom'
},
tooltip: {
callbacks: {
label(tooltipItem) {
const { dataIndex, parsed, dataset } = tooltipItem;
if (dataset.label === 'Support Threshold') {
const threshold = Number(parsed.y).toFixed(2);
const data = chartData.datasets.find((d) => d.label === 'Support');

const currSupport = data?.data.find((d) => (d as Point).x > dataIndex) as Point;
return `Support: ${convertGraphPoint(currSupport?.y)} / ${threshold}%`;
}
if (dataset.label === 'Approval Threshold') {
const threshold = Number(parsed.y).toFixed(2);
const data = chartData.datasets.find((d) => d.label === 'Approval');

const currApproval = data?.data.find((d) => (d as Point).x > dataIndex) as Point;
return `Approval: ${convertGraphPoint(currApproval?.y)} / ${threshold}%`;
}
Comment on lines +211 to +227
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Tooltip “current” values are computed using dataset index, not x; use parsed.x

The tooltip label compares current vs threshold. It currently uses dataIndex as a proxy for time, which is not the x value and leads to incorrect readings. Use parsed.x to find the nearest point at or before the hovered x.

Apply this diff:

 					label(tooltipItem) {
-						const { dataIndex, parsed, dataset } = tooltipItem;
+						const { parsed, dataset } = tooltipItem;
+						const hoveredX = Number((parsed as any)?.x ?? 0);
 						if (dataset.label === 'Support Threshold') {
 							const threshold = Number(parsed.y).toFixed(2);
-							const data = chartData.datasets.find((d) => d.label === 'Support');
-
-							const currSupport = data?.data.find((d) => (d as Point).x > dataIndex) as Point;
-							return `Support: ${convertGraphPoint(currSupport?.y)} / ${threshold}%`;
+							const data = chartData.datasets.find((d) => d.label === 'Support');
+							const points = ((data?.data as unknown as Point[]) || []) as Point[];
+							const currSupport = points.reduce<Point | undefined>((acc, p) => {
+								const px = (p as any)?.x as number;
+								return px <= hoveredX && (!acc || px > (acc as any).x) ? p : acc;
+							}, undefined);
+							return `Support: ${convertGraphPoint((currSupport as any)?.y)} / ${threshold}%`;
 						}
 						if (dataset.label === 'Approval Threshold') {
 							const threshold = Number(parsed.y).toFixed(2);
-							const data = chartData.datasets.find((d) => d.label === 'Approval');
-
-							const currApproval = data?.data.find((d) => (d as Point).x > dataIndex) as Point;
-							return `Approval: ${convertGraphPoint(currApproval?.y)} / ${threshold}%`;
+							const data = chartData.datasets.find((d) => d.label === 'Approval');
+							const points = ((data?.data as unknown as Point[]) || []) as Point[];
+							const currApproval = points.reduce<Point | undefined>((acc, p) => {
+								const px = (p as any)?.x as number;
+								return px <= hoveredX && (!acc || px > (acc as any).x) ? p : acc;
+							}, undefined);
+							return `Approval: ${convertGraphPoint((currApproval as any)?.y)} / ${threshold}%`;
 						}
 
 						return '';
 					},
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
callbacks: {
label(tooltipItem) {
const { dataIndex, parsed, dataset } = tooltipItem;
if (dataset.label === 'Support Threshold') {
const threshold = Number(parsed.y).toFixed(2);
const data = chartData.datasets.find((d) => d.label === 'Support');
const currSupport = data?.data.find((d) => (d as Point).x > dataIndex) as Point;
return `Support: ${convertGraphPoint(currSupport?.y)} / ${threshold}%`;
}
if (dataset.label === 'Approval Threshold') {
const threshold = Number(parsed.y).toFixed(2);
const data = chartData.datasets.find((d) => d.label === 'Approval');
const currApproval = data?.data.find((d) => (d as Point).x > dataIndex) as Point;
return `Approval: ${convertGraphPoint(currApproval?.y)} / ${threshold}%`;
}
callbacks: {
label(tooltipItem) {
const { parsed, dataset } = tooltipItem;
const hoveredX = Number((parsed as any)?.x ?? 0);
if (dataset.label === 'Support Threshold') {
const threshold = Number(parsed.y).toFixed(2);
const data = chartData.datasets.find((d) => d.label === 'Support');
const points = (data?.data as Point[]) || [];
const currSupport = points.reduce<Point | undefined>((acc, p) => {
const px = (p as any).x as number;
return px <= hoveredX && (!acc || px > (acc as any).x) ? p : acc;
}, undefined);
return `Support: ${convertGraphPoint((currSupport as any)?.y)} / ${threshold}%`;
}
if (dataset.label === 'Approval Threshold') {
const threshold = Number(parsed.y).toFixed(2);
const data = chartData.datasets.find((d) => d.label === 'Approval');
const points = (data?.data as Point[]) || [];
const currApproval = points.reduce<Point | undefined>((acc, p) => {
const px = (p as any).x as number;
return px <= hoveredX && (!acc || px > (acc as any).x) ? p : acc;
}, undefined);
return `Approval: ${convertGraphPoint((currApproval as any)?.y)} / ${threshold}%`;
}
return '';
},
},
🤖 Prompt for AI Agents
In src/components/Post/GovernanceSidebar/VoteCurvesData/VoteCurves.tsx around
lines 211 to 227, the tooltip label logic uses dataIndex to locate the "current"
Support/Approval point which is incorrect; replace uses of dataIndex with
parsed.x and search the dataset for the point whose x is the greatest value <=
parsed.x (i.e., nearest point at or before the hovered x). Update the find logic
to compare (d as Point).x to parsed.x (cast parsed.x to the correct numeric type
if necessary), handle possible undefined results safely, and keep the existing
formatting using convertGraphPoint and threshold formatting.


return '';
},
title(values) {
const { label } = values[0];
const hours = Number(label);
const days = Math.floor(hours / 24);
const resultHours = hours - days * 24;
let result = `Time: ${formatHoursAndDays(hours, 'hr')}`;
if (days > 0) {
result += ` (${formatHoursAndDays(days, 'day')} ${resultHours > 0 ? formatHoursAndDays(resultHours, 'hr') : ''})`;
}
return result;
}
},
displayColors: false,
intersect: false,
mode: 'index'
}
},
scales: {
x: {
min: 0,
title: {
display: true,
text: 'Days'
},
grid: {
display: false
},
ticks: {
callback(v) {
return (Number(v) / 24).toFixed(0);
},
stepSize: 24
},
type: 'linear'
},
y: {
title: {
display: false,
text: 'Passing Percentage'
},
max: 100,
min: 0,
ticks: {
stepSize: 10,
callback(val) {
return `${val}%`;
}
},
grid: {
display: false
}
}
}
};

return (
<div className='mt-1 w-full'>
<Line
className='h-full w-full'
data={chartData}
options={chartOptions}
/>
</div>
);
}

export default VoteCurves;
Loading