Skip to content

updates for labor hub v4#4635

Merged
cemreinanc merged 8 commits into
mainfrom
fix/labor-hub-v4
Apr 21, 2026
Merged

updates for labor hub v4#4635
cemreinanc merged 8 commits into
mainfrom
fix/labor-hub-v4

Conversation

@cemreinanc
Copy link
Copy Markdown
Contributor

@cemreinanc cemreinanc commented Apr 21, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Added metadata display in the hero section showing "Forecasts: Live" status and "Commentary last updated" timestamp.
  • Improvements

    • Refined sort column indicators in the research table for clearer sorting status visualization.
    • Updated external links in the methodology section to new Metaculus-hosted URLs.
    • Enhanced contrast calculation for improved chart label and text visibility.
    • Improved interactivity and hit-area detection for timeline markers.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 21, 2026

Warning

Rate limit exceeded

@cemreinanc has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 41 minutes and 47 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 41 minutes and 47 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 273123f9-70c2-4567-9838-7b1228f5f25b

📥 Commits

Reviewing files that changed from the base of the PR and between 251dfab and 10422d7.

📒 Files selected for processing (2)
  • front_end/src/app/(main)/components/headers/hooks/useNavbarLinks.tsx
  • front_end/src/components/charts/primitives/timeline_markers/group_timeline_markers_overlay.tsx
📝 Walkthrough

Walkthrough

This PR enhances the labor-hub UI with improved chart interactions, updated visual indicators, new utility color functions, and metadata display improvements. Changes include adding a "Forecasts: Live" indicator to the hero section, refactoring sort indicators with new visual states, implementing APCA-based contrast color selection, improving marker hit-area behavior, updating external links, and minor formatting adjustments.

Changes

Cohort / File(s) Summary
Color Utilities
front_end/src/utils/core/colors.ts
Added parseHexColor and pickHighestContrastTextColor functions implementing APCA-based contrast calculations to select optimal text colors against backgrounds, with fallback to gray defaults.
Chart Label Contrast
front_end/src/app/(main)/labor-hub/components/question_cards/multi_line_chart.tsx
Replaced local getContrastTextColor with imported pickHighestContrastTextColor for badge label color computation, removing related constants.
Marker Interactions
front_end/src/components/charts/primitives/timeline_markers/group_timeline_markers_overlay.tsx
Enhanced marker hit-area behavior by introducing VictoryPortal wrapping, near-transparent fill, and pointer event management to improve interaction targeting without affecting visual appearance.
Sort Indicators
front_end/src/app/(main)/labor-hub/sections/sortable_research_table.tsx
Replaced SortArrow component with SortColumnIndicator supporting three visual states: neutral faSort, ascending faCaretUp, and descending faCaretDown icons based on active/inactive state.
Hero Section
front_end/src/app/(main)/labor-hub/sections/hero.tsx
Added relative positioning to enable absolute child placement and introduced responsive "Forecasts: Live" indicator with "Commentary last updated" timestamp metadata in large breakpoint.
External Links
front_end/src/app/(main)/labor-hub/sections/methodology.tsx
Updated three external tool URLs from vercel.app domains to labor-hub-tools.metaculus.com subdomain paths.
Context Formatting
front_end/src/app/(main)/labor-hub/components/labor_hub_chart_hover_context.tsx
Minor formatting addition of blank line in memoized context value block.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~28 minutes

Possibly related PRs

  • Labor Hub page #4109: Modifies the same labor-hub chart components and color utilities used in this PR's contrast color refactoring.
  • updates for labor hub v3 #4626: Updates overlapping chart rendering files (multi_line_chart.tsx, timeline markers) with related visual and interaction logic changes.

Suggested reviewers

  • ncarazon
  • elisescu
  • lsabor

Poem

🐰 Contrast hops and markers leap,
Sort icons dance, colors deep—
Forecasts live on screens so wide,
With APCA logic as our guide!

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'updates for labor hub v4' is vague and generic, using non-descriptive language that doesn't convey meaningful information about the specific changes made. Replace with a more specific title that highlights a key change, such as 'Add contrast-aware text color selection and UI improvements for labor hub' or 'Refactor labor hub with APCA contrast calculations and chart enhancements'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/labor-hub-v4

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@front_end/src/components/charts/primitives/timeline_markers/group_timeline_markers_overlay.tsx`:
- Line 5: The VictoryPortal should not be a direct child of VictoryChart;
instead keep VictoryScatter (from group_timeline_markers_overlay.tsx) as a
direct child and move the portal into the scatter's dataComponent so the cloned
props (domain/scale) are preserved; replace the current
VictoryPortal-wrapping-child approach by configuring VictoryScatter's
dataComponent to be a VictoryPortal that renders TimelineMarkerPoint (so
TimelineMarkerPoint is portaled but VictoryScatter remains a chart child and
receives the injected props).

In `@front_end/src/utils/core/colors.ts`:
- Around line 67-90: The parseHexColor function accepts malformed hex channels
(e.g., "#0g0000") because Number.parseInt can silently parse partial values;
update parseHexColor to validate the normalized/expanded hex string with a
strict regex (only 6 hex digits 0-9a-fA-F) before calling Number.parseInt and
return null if it fails, ensuring parseHexColor (used by
pickHighestContrastTextColor) never returns incorrect RGB values from malformed
input.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d552a72b-817a-4b5e-a794-d2d667528f09

📥 Commits

Reviewing files that changed from the base of the PR and between a6b4532 and 251dfab.

📒 Files selected for processing (7)
  • front_end/src/app/(main)/labor-hub/components/labor_hub_chart_hover_context.tsx
  • front_end/src/app/(main)/labor-hub/components/question_cards/multi_line_chart.tsx
  • front_end/src/app/(main)/labor-hub/sections/hero.tsx
  • front_end/src/app/(main)/labor-hub/sections/methodology.tsx
  • front_end/src/app/(main)/labor-hub/sections/sortable_research_table.tsx
  • front_end/src/components/charts/primitives/timeline_markers/group_timeline_markers_overlay.tsx
  • front_end/src/utils/core/colors.ts

Comment on lines +67 to +90
/** Parses `#RGB` / `#RRGGBB` (with optional leading `#`). Returns null if not a 6-digit hex. */
export function parseHexColor(color: string): RGBColor | null {
const normalizedColor = color.replace("#", "");
const hex =
normalizedColor.length === 3
? normalizedColor
.split("")
.map((value) => `${value}${value}`)
.join("")
: normalizedColor;

if (hex.length !== 6) {
return null;
}

const red = Number.parseInt(hex.slice(0, 2), 16);
const green = Number.parseInt(hex.slice(2, 4), 16);
const blue = Number.parseInt(hex.slice(4, 6), 16);

if ([red, green, blue].some(Number.isNaN)) {
return null;
}

return [red, green, blue];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify JavaScript hex parsing behavior for malformed channels.
node - <<'NODE'
console.log('parseInt("0g", 16) =', Number.parseInt("0g", 16));
console.log('Expected: a regex gate is needed because this prints 0, not NaN.');
NODE

Repository: Metaculus/metaculus

Length of output: 150


🏁 Script executed:

# Check if the file exists and matches the review snippet
cat -n front_end/src/utils/core/colors.ts | sed -n '60,95p'

Repository: Metaculus/metaculus

Length of output: 1330


🏁 Script executed:

# Search for usage of parseHexColor in the codebase
rg "parseHexColor" --type typescript --type tsx -n

Repository: Metaculus/metaculus

Length of output: 97


🏁 Script executed:

# Verify the specific behavior with test cases
node - <<'NODE'
// Test the current behavior with malformed hex
function parseHexColorOld(color) {
  const normalizedColor = color.replace("#", "");
  const hex =
    normalizedColor.length === 3
      ? normalizedColor.split("").map((value) => `${value}${value}`).join("")
      : normalizedColor;

  if (hex.length !== 6) {
    return null;
  }

  const red = Number.parseInt(hex.slice(0, 2), 16);
  const green = Number.parseInt(hex.slice(2, 4), 16);
  const blue = Number.parseInt(hex.slice(4, 6), 16);

  if ([red, green, blue].some(Number.isNaN)) {
    return null;
  }

  return [red, green, blue];
}

console.log("Test 1 - Valid `#RGB`: `#F0A`");
console.log(parseHexColorOld("#F0A"));

console.log("\nTest 2 - Valid `#RRGGBB`: `#FF00AA`");
console.log(parseHexColorOld("#FF00AA"));

console.log("\nTest 3 - Malformed (invalid hex char): `#0g0000`");
console.log(parseHexColorOld("#0g0000"));

console.log("\nTest 4 - Malformed (# in middle): F#F0AA");
console.log(parseHexColorOld("F#F0AA"));

console.log("\nTest 5 - Malformed (all invalid): `#gggggg`");
console.log(parseHexColorOld("#gggggg"));
NODE

Repository: Metaculus/metaculus

Length of output: 308


🏁 Script executed:

# Search for usage of parseHexColor (file type detection failed, try simple grep)
rg "parseHexColor" -n

Repository: Metaculus/metaculus

Length of output: 348


Validate hex format before parsing to reject malformed channels.

Number.parseInt(..., 16) accepts partial hex like "0g" as 0, not NaN. The current NaN check only catches complete invalidity. For example, parseHexColor("#0g0000") returns [0, 0, 0] instead of null, silently producing incorrect RGB values. This can skew contrast calculations in pickHighestContrastTextColor (lines 108–109).

Use regex to validate the full hex string before parsing:

Proposed fix
 /** Parses `#RGB` / `#RRGGBB` (with optional leading `#`). Returns null if not a 6-digit hex. */
 export function parseHexColor(color: string): RGBColor | null {
-  const normalizedColor = color.replace("#", "");
+  const match = color.match(/^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/);
+  if (!match) {
+    return null;
+  }
+
+  const normalizedColor = match[1];
   const hex =
     normalizedColor.length === 3
       ? normalizedColor
           .split("")
📝 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
/** Parses `#RGB` / `#RRGGBB` (with optional leading `#`). Returns null if not a 6-digit hex. */
export function parseHexColor(color: string): RGBColor | null {
const normalizedColor = color.replace("#", "");
const hex =
normalizedColor.length === 3
? normalizedColor
.split("")
.map((value) => `${value}${value}`)
.join("")
: normalizedColor;
if (hex.length !== 6) {
return null;
}
const red = Number.parseInt(hex.slice(0, 2), 16);
const green = Number.parseInt(hex.slice(2, 4), 16);
const blue = Number.parseInt(hex.slice(4, 6), 16);
if ([red, green, blue].some(Number.isNaN)) {
return null;
}
return [red, green, blue];
/** Parses `#RGB` / `#RRGGBB` (with optional leading `#`). Returns null if not a 6-digit hex. */
export function parseHexColor(color: string): RGBColor | null {
const match = color.match(/^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/);
if (!match) {
return null;
}
const normalizedColor = match[1];
const hex =
normalizedColor.length === 3
? normalizedColor
.split("")
.map((value) => `${value}${value}`)
.join("")
: normalizedColor;
if (hex.length !== 6) {
return null;
}
const red = Number.parseInt(hex.slice(0, 2), 16);
const green = Number.parseInt(hex.slice(2, 4), 16);
const blue = Number.parseInt(hex.slice(4, 6), 16);
if ([red, green, blue].some(Number.isNaN)) {
return null;
}
return [red, green, blue];
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@front_end/src/utils/core/colors.ts` around lines 67 - 90, The parseHexColor
function accepts malformed hex channels (e.g., "#0g0000") because
Number.parseInt can silently parse partial values; update parseHexColor to
validate the normalized/expanded hex string with a strict regex (only 6 hex
digits 0-9a-fA-F) before calling Number.parseInt and return null if it fails,
ensuring parseHexColor (used by pickHighestContrastTextColor) never returns
incorrect RGB values from malformed input.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 21, 2026

Cleanup: Preview Environment Removed

The preview environment for this PR has been destroyed.

Resource Status
🌐 Preview App Deleted
🗄️ PostgreSQL Branch Deleted
⚡ Redis Database Deleted
🔧 GitHub Deployments Removed
📦 Docker Image Retained (auto-cleanup via GHCR policies)

Cleanup triggered by PR close at 2026-04-21T08:29:35Z

@cemreinanc cemreinanc merged commit aa76f4f into main Apr 21, 2026
14 checks passed
@cemreinanc cemreinanc deleted the fix/labor-hub-v4 branch April 21, 2026 08:29
@coderabbitai coderabbitai Bot mentioned this pull request Apr 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant