React+TypeScript下将日期间隔转为百分比精准展示时间线图表
Hey there! Let's tackle this date-to-percentage conversion for your horizontal timeline—this is a common challenge but totally solvable with date-fns and some straightforward math. Here's a step-by-step approach tailored to your React/TypeScript setup:
Core Concept
To get precise positioning and width for each milestone bar, you need three key values:
- Global timeline bounds: The start and end dates that cover your entire calendar view (in your case, it looks like 2 full years of months).
- Milestone-specific dates: The start and end dates of each individual milestone.
- Percentage calculations:
- The offset percentage of the milestone's start relative to the global timeline start.
- The width percentage of the milestone's duration relative to the total timeline length.
Step 1: Set Up Date-Fns Helpers
First, we'll use date-fns to parse dates and calculate time differences. We'll focus on millisecond-level precision to avoid rounding errors.
Add these imports to your component:
import { parse, differenceInMilliseconds, startOfMonth, endOfMonth } from 'date-fns';
Step 2: Define Your Global Timeline Bounds
Based on your months array (2 full years), let's define the exact start and end dates of your timeline. We'll use the start of the first month and end of the last month for accuracy:
// Adjust the year values to match your actual timeline const timelineStart = startOfMonth(parse('January 2023', 'MMMM yyyy', new Date())); const timelineEnd = endOfMonth(parse('December 2024', 'MMMM yyyy', new Date())); const totalTimelineDurationMs = differenceInMilliseconds(timelineEnd, timelineStart);
Step 3: Create a Helper Function for Percentages
Write a reusable function to calculate the start offset and width percentage for any milestone. This will take your milestone's start/end dates (from JSON) and return the values you need:
interface MilestonePercentages { startPercentage: number; widthPercentage: number; } const getMilestonePercentages = ( milestoneStartDate: string, // Use ISO format like "2023-02-07" from your JSON milestoneEndDate: string ): MilestonePercentages => { // Parse the milestone dates (ISO format is most reliable) const start = new Date(milestoneStartDate); const end = new Date(milestoneEndDate); // Calculate offsets and duration in milliseconds const startOffsetMs = differenceInMilliseconds(start, timelineStart); const milestoneDurationMs = differenceInMilliseconds(end, start); // Convert to percentages const startPercentage = (startOffsetMs / totalTimelineDurationMs) * 100; const widthPercentage = (milestoneDurationMs / totalTimelineDurationMs) * 100; return { startPercentage, widthPercentage }; };
Pro tip: Have your JSON return full ISO dates (e.g., "2023-02-07") instead of short formats like "2/07"—this eliminates ambiguity and makes parsing way easier.
Step 4: Update Your Bar Component
Modify your Bar component to accept the percentage values instead of hardcoded pixel widths. We'll use absolute positioning relative to the timeline container:
// Bar.tsx import React from 'react'; interface BarProps { title: string; date: string; startPercentage: number; widthPercentage: number; color: string; } export const Bar: React.FC<BarProps> = ({ title, date, startPercentage, widthPercentage, color }) => { return ( <div style={{ position: 'absolute', left: `${startPercentage}%`, width: `${widthPercentage}%`, backgroundColor: color, height: '2.5rem', display: 'flex', alignItems: 'center', padding: '0 0.75rem', color: 'white', borderRadius: '4px', fontWeight: 500 }} > <span>{title} ({date})</span> </div> ); };
Step 5: Integrate with Your Parent Component
Update your TheSchedule component to use the helper function and pass the calculated percentages to each Bar. Also, make sure your timeline container has position: relative so the bars align correctly:
// Inside TheSchedule component // Example JSON milestone data (replace with your actual data) const milestones = [ { title: "BOP", startDate: "2023-02-07", endDate: "2023-02-28", color: "#F78C20" }, { title: "ASR", startDate: "2023-03-22", endDate: "2023-04-15", color: "#5F308F" }, { title: "EMV", startDate: "2023-12-22", endDate: "2024-03-10", color: "#5F308F" }, ]; // Then, in your JSX: <div style={{ width: "6000px", display: "flex", position: "relative" }}> {/* Add position: relative */} {months.map((month, index) => ( <div key={index} style={{ background: index % 2 === 0 ? "#EDEBE9" : "#E5E5E5", height: "26.8125rem", width: "15.4375rem", border: "solid .05rem grey" }} > {/* Month header */} <p style={{ borderBottom: "1px solid grey", width: "75%", fontFamily: "Segoe UI", fontSize: ".8rem", lineHeight: "14px", fontWeight: "bold", fontVariantCaps: "all-small-caps", textAlign: "center", margin: "0 auto" }}> {month} </p> </div> ))} {/* Render all milestones here (outside the month loop to avoid duplication) */} {milestones.map((milestone, idx) => { const percentages = getMilestonePercentages(milestone.startDate, milestone.endDate); return ( <Bar key={idx} title={milestone.title} date={milestone.startDate.split('T')[0]} // Format date for display startPercentage={percentages.startPercentage} widthPercentage={percentages.widthPercentage} color={milestone.color} /> ); })} </div>
Key Notes for Precision
- Avoid hardcoded positions: By using percentages, your bars will automatically adjust if you ever change the timeline's total width or date range.
- Handle edge cases: Add checks to ensure milestones don't fall outside the global timeline bounds (e.g., cap percentages at 0% and 100% if needed).
- Smooth scrolling: Adjust your
getScrollTargetfunction to scroll by the exact width of a month (instead of 305px) for more intuitive navigation:const monthWidthPx = 15.4375 * 16; // 1rem = 16px by default // Update getScrollTarget to use monthWidthPx instead of 305
内容的提问来源于stack exchange,提问作者Shankstee

