You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React+TypeScript下将日期间隔转为百分比精准展示时间线图表

How to Convert Date Durations to Precise Percentages for Your Horizontal Timeline

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:

  1. 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).
  2. Milestone-specific dates: The start and end dates of each individual milestone.
  3. 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 getScrollTarget function 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.06 22:42:32