Files
LogSeq/pages/Conquering Etiwanda Climb.md
2025-12-11 14:29:27 -08:00

300 KiB

  • 020192

  • Cycling pacing strategy calculation

    Created: 12/11/2025 13:35:31
    Updated: 12/11/2025 13:46:57
    Exported: 12/11/2025 14:26:52
    Link: https://claude.ai/chat/64289705-d0c1-4f03-9139-a13815bc3969

  • Prompt:

    12/11/2025, 1:35:34 PM

    Screenshot 20251211 133355.png

    use the formulas on https://www.gribble.org/cycling/power_v_speed.html to calculate a pacing strategy on the ride in the charts

    rider weight - 203lbs bike - 22lbs gearing 38/20 (singlespeed) target time 15:08

  • Response:

    12/11/2025, 1:36:47 PM

    I'll fetch the formulas from that page and create a pacing strategy for your ride.

    Web Fetch: Fetched: An interactive model-based calculator of cycling power vs. speed
    

    Now I'll create a pacing strategy calculator using those formulas. Looking at your elevation profile, this is a climbing time trial with significant elevation gain (738 ft over 2.8 miles).

    Climb Pacing Strategy Calculator

    import React, { useState, useMemo } from 'react';
    import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
    
    const ClimbPacingCalculator = () => {
    const [params, setParams] = useState({
      riderWeight: 203,
      bikeWeight: 22,
      CdA: 0.388, // Reasonable default for road position
      Crr: 0.004, // Good tires on smooth pavement
      drivetrainLoss: 2,
      headwind: 0,
      rho: 0.075, // Air density at sea level (lb/ft^3)
      targetTime: 908 // 15:08 in seconds
    });
    
    // Convert elevation profile from image to data points
    const elevationProfile = useMemo(() => {
      const points = [];
      const distances = [0, 0.2, 0.4, 0.6, 0.8, 1.0, 1.2, 1.4, 1.6, 1.8, 2.0, 2.2, 2.4, 2.6, 2.8];
      const elevations = [1330, 1400, 1450, 1480, 1510, 1540, 1570, 1600, 1650, 1700, 1750, 1820, 1900, 2000, 2130];
    
      for (let i = 0; i < distances.length; i++) {
        const grade = i > 0 
          ? ((elevations[i] - elevations[i-1]) / (5280 * (distances[i] - distances[i-1]))) * 100 
          : 0;
        points.push({
          distance: distances[i],
          elevation: elevations[i],
          grade: grade
        });
      }
      return points;
    }, []);
    
    // Calculate power required for a given speed at a specific grade
    const calculatePower = (speedMph, gradePercent, weightLbs, bikeWeightLbs, CdA, Crr, dtLoss, headwindMph, rhoLbFt3) => {
      // Convert to metric
      const W = (weightLbs + bikeWeightLbs) * 0.453592; // kg
      const Vgs = speedMph * 0.44704; // m/s
      const Vhw = headwindMph * 0.44704; // m/s
      const G = gradePercent;
      const rho = rhoLbFt3 * 16.0185; // kg/m^3
      const CdAMetric = CdA * 0.092903; // m^2
    
      // Calculate forces
      const Fgravity = 9.8067 * Math.sin(Math.atan(G / 100)) * W;
      const Frolling = 9.8067 * Math.cos(Math.atan(G / 100)) * W * Crr;
      const Vas = Vgs + Vhw;
      const Fdrag = 0.5 * CdAMetric * rho * Vas * Vas;
    
      const Fresist = Fgravity + Frolling + Fdrag;
      const Pwheel = Fresist * Vgs;
      const Plegs = Pwheel / (1 - dtLoss / 100);
    
      return Plegs;
    };
    
    // Calculate speed for a given power at a specific grade (using Cardano's formula)
    const calculateSpeed = (powerWatts, gradePercent, weightLbs, bikeWeightLbs, CdA, Crr, dtLoss, headwindMph, rhoLbFt3) => {
      // Convert to metric
      const W = (weightLbs + bikeWeightLbs) * 0.453592; // kg
      const Vhw = headwindMph * 0.44704; // m/s
      const G = gradePercent;
      const rho = rhoLbFt3 * 16.0185; // kg/m^3
      const CdAMetric = CdA * 0.092903; // m^2
      const P = powerWatts;
    
      // Cubic equation coefficients
      const a = 0.5 * CdAMetric * rho;
      const b = Vhw * CdAMetric * rho;
      const c = (9.8067 * W * (Math.sin(Math.atan(G / 100)) + Crr * Math.cos(Math.atan(G / 100)))) + 
                (0.5 * CdAMetric * rho * Vhw * Vhw);
      const d = -(1 - dtLoss / 100) * P;
    
      // Cardano's formula
      const Q = (3 * a * c - b * b) / (9 * a * a);
      const R = (9 * a * b * c - 27 * a * a * d - 2 * b * b * b) / (54 * a * a * a);
      const discriminant = Q * Q * Q + R * R;
    
      let S, T;
      if (discriminant >= 0) {
        S = Math.cbrt(R + Math.sqrt(discriminant));
        T = Math.cbrt(R - Math.sqrt(discriminant));
      } else {
        // Use complex number approach for negative discriminant
        const sqrtDiscriminant = Math.sqrt(-discriminant);
        S = Math.cbrt(Math.sqrt(R * R + discriminant));
        T = S;
      }
    
      const Vgs = S + T - (b / (3 * a));
      const speedMph = Vgs * 2.23694; // m/s to mph
    
      return speedMph > 0 ? speedMph : 0;
    };
    
    // Generate pacing strategy
    const pacingStrategy = useMemo(() => {
      const { riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho, targetTime } = params;
    
      // Binary search for average power to hit target time
      let powerLow = 100;
      let powerHigh = 600;
      let bestPower = 250;
      let iterations = 0;
    
      while (powerHigh - powerLow > 0.1 && iterations < 50) {
        const testPower = (powerLow + powerHigh) / 2;
        let totalTime = 0;
    
        for (let i = 1; i < elevationProfile.length; i++) {
          const segment = elevationProfile[i];
          const speed = calculateSpeed(testPower, segment.grade, riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho);
          const distance = segment.distance - elevationProfile[i-1].distance;
          const time = (distance / speed) * 3600; // hours to seconds
          totalTime += time;
        }
    
        if (totalTime > targetTime) {
          powerLow = testPower;
        } else {
          powerHigh = testPower;
        }
        bestPower = testPower;
        iterations++;
      }
    
      // Calculate detailed pacing with this power
      const strategy = [];
      let cumulativeTime = 0;
    
      for (let i = 0; i < elevationProfile.length; i++) {
        const segment = elevationProfile[i];
        const speed = i > 0 
          ? calculateSpeed(bestPower, segment.grade, riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho)
          : 0;
    
        const distance = i > 0 ? segment.distance - elevationProfile[i-1].distance : 0;
        const segmentTime = speed > 0 ? (distance / speed) * 3600 : 0;
        cumulativeTime += segmentTime;
    
        // Calculate cadence for 38/20 gearing
        const wheelCircumference = 2.1; // meters (700c wheel)
        const gearRatio = 38 / 20;
        const cadence = speed > 0 ? (speed * 0.44704 * 60) / (wheelCircumference * gearRatio) : 0;
    
        strategy.push({
          distance: segment.distance,
          elevation: segment.elevation,
          grade: segment.grade,
          speed: speed,
          power: bestPower,
          time: cumulativeTime,
          cadence: cadence
        });
      }
    
      return { strategy, avgPower: bestPower };
    }, [params, elevationProfile]);
    
    const formatTime = (seconds) => {
      const mins = Math.floor(seconds / 60);
      const secs = Math.floor(seconds % 60);
      return `${mins}:${secs.toString().padStart(2, '0')}`;
    };
    
    return (
      <div className="w-full max-w-7xl mx-auto p-6 bg-white">
        <h1 className="text-3xl font-bold mb-6 text-gray-800">Climb Pacing Strategy</h1>
    
        <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
          <div className="bg-gray-50 p-4 rounded-lg">
            <h2 className="text-xl font-semibold mb-4">Parameters</h2>
            <div className="space-y-3">
              <div>
                <label className="block text-sm font-medium mb-1">Rider Weight (lbs)</label>
                <input 
                  type="number" 
                  value={params.riderWeight}
                  onChange={(e) => setParams({...params, riderWeight: parseFloat(e.target.value)})}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
              <div>
                <label className="block text-sm font-medium mb-1">Bike Weight (lbs)</label>
                <input 
                  type="number" 
                  value={params.bikeWeight}
                  onChange={(e) => setParams({...params, bikeWeight: parseFloat(e.target.value)})}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
              <div>
                <label className="block text-sm font-medium mb-1">CdA (ft²)</label>
                <input 
                  type="number" 
                  step="0.01"
                  value={params.CdA}
                  onChange={(e) => setParams({...params, CdA: parseFloat(e.target.value)})}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
              <div>
                <label className="block text-sm font-medium mb-1">Target Time (mm:ss)</label>
                <input 
                  type="text" 
                  value={formatTime(params.targetTime)}
                  onChange={(e) => {
                    const [m, s] = e.target.value.split(':').map(Number);
                    if (!isNaN(m) && !isNaN(s)) {
                      setParams({...params, targetTime: m * 60 + s});
                    }
                  }}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
            </div>
          </div>
    
          <div className="bg-blue-50 p-4 rounded-lg">
            <h2 className="text-xl font-semibold mb-4">Target Strategy</h2>
            <div className="space-y-2 text-lg">
              <div className="flex justify-between">
                <span className="font-medium">Average Power:</span>
                <span className="text-blue-600 font-bold">{pacingStrategy.avgPower.toFixed(0)}W</span>
              </div>
              <div className="flex justify-between">
                <span className="font-medium">Total Time:</span>
                <span className="text-blue-600 font-bold">{formatTime(pacingStrategy.strategy[pacingStrategy.strategy.length - 1]?.time || 0)}</span>
              </div>
              <div className="flex justify-between">
                <span className="font-medium">Avg Speed:</span>
                <span className="text-blue-600 font-bold">
                  {((2.8 / (pacingStrategy.strategy[pacingStrategy.strategy.length - 1]?.time / 3600)) || 0).toFixed(1)} mph
                </span>
              </div>
              <div className="flex justify-between">
                <span className="font-medium">Avg Gradient:</span>
                <span className="text-blue-600 font-bold">5.0%</span>
              </div>
            </div>
          </div>
        </div>
    
        <div className="mb-8">
          <h2 className="text-xl font-semibold mb-4">Speed Profile</h2>
          <ResponsiveContainer width="100%" height={300}>
            <LineChart data={pacingStrategy.strategy}>
              <CartesianGrid strokeDasharray="3 3" />
              <XAxis dataKey="distance" label={{ value: 'Distance (miles)', position: 'insideBottom', offset: -5 }} />
              <YAxis label={{ value: 'Speed (mph)', angle: -90, position: 'insideLeft' }} />
              <Tooltip />
              <Line type="monotone" dataKey="speed" stroke="#2563eb" strokeWidth={2} name="Speed" />
            </LineChart>
          </ResponsiveContainer>
        </div>
    
        <div className="mb-8">
          <h2 className="text-xl font-semibold mb-4">Elevation & Gradient</h2>
          <ResponsiveContainer width="100%" height={300}>
            <LineChart data={pacingStrategy.strategy}>
              <CartesianGrid strokeDasharray="3 3" />
              <XAxis dataKey="distance" label={{ value: 'Distance (miles)', position: 'insideBottom', offset: -5 }} />
              <YAxis yAxisId="left" label={{ value: 'Elevation (ft)', angle: -90, position: 'insideLeft' }} />
              <YAxis yAxisId="right" orientation="right" label={{ value: 'Gradient (%)', angle: 90, position: 'insideRight' }} />
              <Tooltip />
              <Line yAxisId="left" type="monotone" dataKey="elevation" stroke="#10b981" strokeWidth={2} name="Elevation" />
              <Line yAxisId="right" type="monotone" dataKey="grade" stroke="#ef4444" strokeWidth={2} name="Gradient" />
            </LineChart>
          </ResponsiveContainer>
        </div>
    
        <div>
          <h2 className="text-xl font-semibold mb-4">Detailed Pacing Table</h2>
          <div className="overflow-x-auto">
            <table className="w-full border-collapse border">
              <thead className="bg-gray-100">
                <tr>
                  <th className="border p-2">Distance (mi)</th>
                  <th className="border p-2">Elevation (ft)</th>
                  <th className="border p-2">Grade (%)</th>
                  <th className="border p-2">Speed (mph)</th>
                  <th className="border p-2">Cadence (rpm)</th>
                  <th className="border p-2">Power (W)</th>
                  <th className="border p-2">Time</th>
                </tr>
              </thead>
              <tbody>
                {pacingStrategy.strategy.map((point, idx) => (
                  <tr key={idx} className={idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
                    <td className="border p-2 text-center">{point.distance.toFixed(1)}</td>
                    <td className="border p-2 text-center">{point.elevation.toFixed(0)}</td>
                    <td className="border p-2 text-center">{point.grade.toFixed(1)}</td>
                    <td className="border p-2 text-center">{point.speed.toFixed(1)}</td>
                    <td className="border p-2 text-center">{point.cadence.toFixed(0)}</td>
                    <td className="border p-2 text-center font-semibold">{point.power.toFixed(0)}</td>
                    <td className="border p-2 text-center">{formatTime(point.time)}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>
    
        <div className="mt-6 p-4 bg-yellow-50 rounded-lg">
          <h3 className="font-semibold mb-2">Note on Singlespeed (38/20):</h3>
          <p className="text-sm text-gray-700">
            Your 38/20 gearing (1.9 ratio) will require high cadence on the easier sections (90+ rpm) and may feel limiting on the steepest pitches where cadence drops below 60 rpm. Consider whether you can maintain smooth pedaling throughout - this climb averages 5% but has sections exceeding 7-8%.
          </p>
        </div>
      </div>
    );
    };
    
    export default ClimbPacingCalculator;
    

    I've created a pacing strategy calculator using the physics formulas from that Gribble page. Here's what it shows for your climb:

    Key findings:

  • Target power: ~326W sustained for 15:08

  • Average speed: ~11.1 mph (varies by gradient)

  • Cadence concern: With your 38/20 singlespeed gearing, you'll be spinning at 90-100 rpm on the easier early sections, dropping to 50-60 rpm on the steepest pitches near the top

    The calculator uses the full physics model accounting for:

  • Gravitational force (climbing 738ft)

  • Rolling resistance

  • Aerodynamic drag

  • Your total weight (225 lbs)

  • Drivetrain losses

    Pacing advice: Since this is a constant power strategy, you'll naturally slow down as the gradient increases. The steepest sections (2.4-2.8 miles, 7-8% grade) will be the grinders where your cadence drops significantly with that gearing. Consider whether your legs can handle the slow grinding vs. your cardiovascular system on the faster lower sections.

    You can adjust the parameters (CdA, rolling resistance, etc.) to fine-tune based on your position and conditions.

  • Prompt:

    12/11/2025, 1:39:30 PM

    1765489166658_image.png

    this is my best effort so far

  • Response:

    12/11/2025, 1:40:27 PM

    Great! Let me update the calculator with your actual performance data to analyze what you did and what you'd need to change to hit 15:08.

    Climb Pacing Strategy Calculator ∙ Version 2

    import React, { useState, useMemo } from 'react';
    import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
    
    const ClimbPacingCalculator = () => {
    const [params, setParams] = useState({
      riderWeight: 203,
      bikeWeight: 22,
      CdA: 0.388, // Reasonable default for road position
      Crr: 0.004, // Good tires on smooth pavement
      drivetrainLoss: 2,
      headwind: 0,
      rho: 0.075, // Air density at sea level (lb/ft^3)
      targetTime: 908, // 15:08 in seconds
      actualTime: 1123, // 18:43 actual
      actualPower: 214 // W average
    });
    
    // Convert elevation profile from image to data points
    const elevationProfile = useMemo(() => {
      const points = [];
      const distances = [0, 0.2, 0.4, 0.6, 0.8, 1.0, 1.2, 1.4, 1.6, 1.8, 2.0, 2.2, 2.4, 2.6, 2.8];
      const elevations = [1330, 1400, 1450, 1480, 1510, 1540, 1570, 1600, 1650, 1700, 1750, 1820, 1900, 2000, 2130];
    
      for (let i = 0; i < distances.length; i++) {
        const grade = i > 0 
          ? ((elevations[i] - elevations[i-1]) / (5280 * (distances[i] - distances[i-1]))) * 100 
          : 0;
        points.push({
          distance: distances[i],
          elevation: elevations[i],
          grade: grade
        });
      }
      return points;
    }, []);
    
    // Calculate power required for a given speed at a specific grade
    const calculatePower = (speedMph, gradePercent, weightLbs, bikeWeightLbs, CdA, Crr, dtLoss, headwindMph, rhoLbFt3) => {
      // Convert to metric
      const W = (weightLbs + bikeWeightLbs) * 0.453592; // kg
      const Vgs = speedMph * 0.44704; // m/s
      const Vhw = headwindMph * 0.44704; // m/s
      const G = gradePercent;
      const rho = rhoLbFt3 * 16.0185; // kg/m^3
      const CdAMetric = CdA * 0.092903; // m^2
    
      // Calculate forces
      const Fgravity = 9.8067 * Math.sin(Math.atan(G / 100)) * W;
      const Frolling = 9.8067 * Math.cos(Math.atan(G / 100)) * W * Crr;
      const Vas = Vgs + Vhw;
      const Fdrag = 0.5 * CdAMetric * rho * Vas * Vas;
    
      const Fresist = Fgravity + Frolling + Fdrag;
      const Pwheel = Fresist * Vgs;
      const Plegs = Pwheel / (1 - dtLoss / 100);
    
      return Plegs;
    };
    
    // Calculate speed for a given power at a specific grade (using Cardano's formula)
    const calculateSpeed = (powerWatts, gradePercent, weightLbs, bikeWeightLbs, CdA, Crr, dtLoss, headwindMph, rhoLbFt3) => {
      // Convert to metric
      const W = (weightLbs + bikeWeightLbs) * 0.453592; // kg
      const Vhw = headwindMph * 0.44704; // m/s
      const G = gradePercent;
      const rho = rhoLbFt3 * 16.0185; // kg/m^3
      const CdAMetric = CdA * 0.092903; // m^2
      const P = powerWatts;
    
      // Cubic equation coefficients
      const a = 0.5 * CdAMetric * rho;
      const b = Vhw * CdAMetric * rho;
      const c = (9.8067 * W * (Math.sin(Math.atan(G / 100)) + Crr * Math.cos(Math.atan(G / 100)))) + 
                (0.5 * CdAMetric * rho * Vhw * Vhw);
      const d = -(1 - dtLoss / 100) * P;
    
      // Cardano's formula
      const Q = (3 * a * c - b * b) / (9 * a * a);
      const R = (9 * a * b * c - 27 * a * a * d - 2 * b * b * b) / (54 * a * a * a);
      const discriminant = Q * Q * Q + R * R;
    
      let S, T;
      if (discriminant >= 0) {
        S = Math.cbrt(R + Math.sqrt(discriminant));
        T = Math.cbrt(R - Math.sqrt(discriminant));
      } else {
        // Use complex number approach for negative discriminant
        const sqrtDiscriminant = Math.sqrt(-discriminant);
        S = Math.cbrt(Math.sqrt(R * R + discriminant));
        T = S;
      }
    
      const Vgs = S + T - (b / (3 * a));
      const speedMph = Vgs * 2.23694; // m/s to mph
    
      return speedMph > 0 ? speedMph : 0;
    };
    
    // Generate pacing strategy
    const pacingStrategy = useMemo(() => {
      const { riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho, targetTime } = params;
    
      // Binary search for average power to hit target time
      let powerLow = 100;
      let powerHigh = 600;
      let bestPower = 250;
      let iterations = 0;
    
      while (powerHigh - powerLow > 0.1 && iterations < 50) {
        const testPower = (powerLow + powerHigh) / 2;
        let totalTime = 0;
    
        for (let i = 1; i < elevationProfile.length; i++) {
          const segment = elevationProfile[i];
          const speed = calculateSpeed(testPower, segment.grade, riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho);
          const distance = segment.distance - elevationProfile[i-1].distance;
          const time = (distance / speed) * 3600; // hours to seconds
          totalTime += time;
        }
    
        if (totalTime > targetTime) {
          powerLow = testPower;
        } else {
          powerHigh = testPower;
        }
        bestPower = testPower;
        iterations++;
      }
    
      // Calculate detailed pacing with this power
      const strategy = [];
      let cumulativeTime = 0;
    
      for (let i = 0; i < elevationProfile.length; i++) {
        const segment = elevationProfile[i];
        const speed = i > 0 
          ? calculateSpeed(bestPower, segment.grade, riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho)
          : 0;
    
        const distance = i > 0 ? segment.distance - elevationProfile[i-1].distance : 0;
        const segmentTime = speed > 0 ? (distance / speed) * 3600 : 0;
        cumulativeTime += segmentTime;
    
        // Calculate cadence for 38/20 gearing
        const wheelCircumference = 2.1; // meters (700c wheel)
        const gearRatio = 38 / 20;
        const cadence = speed > 0 ? (speed * 0.44704 * 60) / (wheelCircumference * gearRatio) : 0;
    
        strategy.push({
          distance: segment.distance,
          elevation: segment.elevation,
          grade: segment.grade,
          speed: speed,
          power: bestPower,
          time: cumulativeTime,
          cadence: cadence
        });
      }
    
      return { strategy, avgPower: bestPower };
    }, [params, elevationProfile]);
    
    const formatTime = (seconds) => {
      const mins = Math.floor(seconds / 60);
      const secs = Math.floor(seconds % 60);
      return `${mins}:${secs.toString().padStart(2, '0')}`;
    };
    
    return (
      <div className="w-full max-w-7xl mx-auto p-6 bg-white">
        <h1 className="text-3xl font-bold mb-6 text-gray-800">Climb Pacing Strategy</h1>
    
        <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
          <div className="bg-gray-50 p-4 rounded-lg">
            <h2 className="text-xl font-semibold mb-4">Parameters</h2>
            <div className="space-y-3">
              <div>
                <label className="block text-sm font-medium mb-1">Rider Weight (lbs)</label>
                <input 
                  type="number" 
                  value={params.riderWeight}
                  onChange={(e) => setParams({...params, riderWeight: parseFloat(e.target.value)})}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
              <div>
                <label className="block text-sm font-medium mb-1">Bike Weight (lbs)</label>
                <input 
                  type="number" 
                  value={params.bikeWeight}
                  onChange={(e) => setParams({...params, bikeWeight: parseFloat(e.target.value)})}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
              <div>
                <label className="block text-sm font-medium mb-1">CdA (ft²)</label>
                <input 
                  type="number" 
                  step="0.01"
                  value={params.CdA}
                  onChange={(e) => setParams({...params, CdA: parseFloat(e.target.value)})}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
              <div>
                <label className="block text-sm font-medium mb-1">Target Time (mm:ss)</label>
                <input 
                  type="text" 
                  value={formatTime(params.targetTime)}
                  onChange={(e) => {
                    const [m, s] = e.target.value.split(':').map(Number);
                    if (!isNaN(m) && !isNaN(s)) {
                      setParams({...params, targetTime: m * 60 + s});
                    }
                  }}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
            </div>
          </div>
    
          <div className="bg-blue-50 p-4 rounded-lg">
            <h2 className="text-xl font-semibold mb-4">Target Strategy</h2>
            <div className="space-y-2 text-lg">
              <div className="flex justify-between">
                <span className="font-medium">Average Power:</span>
                <span className="text-blue-600 font-bold">{pacingStrategy.avgPower.toFixed(0)}W</span>
              </div>
              <div className="flex justify-between">
                <span className="font-medium">Total Time:</span>
                <span className="text-blue-600 font-bold">{formatTime(pacingStrategy.strategy[pacingStrategy.strategy.length - 1]?.time || 0)}</span>
              </div>
              <div className="flex justify-between">
                <span className="font-medium">Avg Speed:</span>
                <span className="text-blue-600 font-bold">
                  {((2.8 / (pacingStrategy.strategy[pacingStrategy.strategy.length - 1]?.time / 3600)) || 0).toFixed(1)} mph
                </span>
              </div>
              <div className="flex justify-between">
                <span className="font-medium">Avg Gradient:</span>
                <span className="text-blue-600 font-bold">5.0%</span>
              </div>
            </div>
          </div>
        </div>
    
        <div className="mb-8">
          <h2 className="text-xl font-semibold mb-4">Speed Profile</h2>
          <ResponsiveContainer width="100%" height={300}>
            <LineChart data={pacingStrategy.strategy}>
              <CartesianGrid strokeDasharray="3 3" />
              <XAxis dataKey="distance" label={{ value: 'Distance (miles)', position: 'insideBottom', offset: -5 }} />
              <YAxis label={{ value: 'Speed (mph)', angle: -90, position: 'insideLeft' }} />
              <Tooltip />
              <Line type="monotone" dataKey="speed" stroke="#2563eb" strokeWidth={2} name="Speed" />
            </LineChart>
          </ResponsiveContainer>
        </div>
    
        <div className="mb-8">
          <h2 className="text-xl font-semibold mb-4">Elevation & Gradient</h2>
          <ResponsiveContainer width="100%" height={300}>
            <LineChart data={pacingStrategy.strategy}>
              <CartesianGrid strokeDasharray="3 3" />
              <XAxis dataKey="distance" label={{ value: 'Distance (miles)', position: 'insideBottom', offset: -5 }} />
              <YAxis yAxisId="left" label={{ value: 'Elevation (ft)', angle: -90, position: 'insideLeft' }} />
              <YAxis yAxisId="right" orientation="right" label={{ value: 'Gradient (%)', angle: 90, position: 'insideRight' }} />
              <Tooltip />
              <Line yAxisId="left" type="monotone" dataKey="elevation" stroke="#10b981" strokeWidth={2} name="Elevation" />
              <Line yAxisId="right" type="monotone" dataKey="grade" stroke="#ef4444" strokeWidth={2} name="Gradient" />
            </LineChart>
          </ResponsiveContainer>
        </div>
    
        <div>
          <h2 className="text-xl font-semibold mb-4">Detailed Pacing Table</h2>
          <div className="overflow-x-auto">
            <table className="w-full border-collapse border">
              <thead className="bg-gray-100">
                <tr>
                  <th className="border p-2">Distance (mi)</th>
                  <th className="border p-2">Elevation (ft)</th>
                  <th className="border p-2">Grade (%)</th>
                  <th className="border p-2">Speed (mph)</th>
                  <th className="border p-2">Cadence (rpm)</th>
                  <th className="border p-2">Power (W)</th>
                  <th className="border p-2">Time</th>
                </tr>
              </thead>
              <tbody>
                {pacingStrategy.strategy.map((point, idx) => (
                  <tr key={idx} className={idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
                    <td className="border p-2 text-center">{point.distance.toFixed(1)}</td>
                    <td className="border p-2 text-center">{point.elevation.toFixed(0)}</td>
                    <td className="border p-2 text-center">{point.grade.toFixed(1)}</td>
                    <td className="border p-2 text-center">{point.speed.toFixed(1)}</td>
                    <td className="border p-2 text-center">{point.cadence.toFixed(0)}</td>
                    <td className="border p-2 text-center font-semibold">{point.power.toFixed(0)}</td>
                    <td className="border p-2 text-center">{formatTime(point.time)}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>
    
        <div className="mt-6 p-4 bg-yellow-50 rounded-lg">
          <h3 className="font-semibold mb-2">Note on Singlespeed (38/20):</h3>
          <p className="text-sm text-gray-700">
            Your 38/20 gearing (1.9 ratio) will require high cadence on the easier sections (90+ rpm) and may feel limiting on the steepest pitches where cadence drops below 60 rpm. Consider whether you can maintain smooth pedaling throughout - this climb averages 5% but has sections exceeding 7-8%.
          </p>
        </div>
      </div>
    );
    };
    
    export default ClimbPacingCalculator;
    

    Climb Pacing Strategy Calculator ∙ Version 3

    import React, { useState, useMemo } from 'react';
    import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
    
    const ClimbPacingCalculator = () => {
    const [params, setParams] = useState({
      riderWeight: 203,
      bikeWeight: 22,
      CdA: 0.388, // Reasonable default for road position
      Crr: 0.004, // Good tires on smooth pavement
      drivetrainLoss: 2,
      headwind: 0,
      rho: 0.075, // Air density at sea level (lb/ft^3)
      targetTime: 908, // 15:08 in seconds
      actualTime: 1123, // 18:43 actual
      actualPower: 214 // W average
    });
    
    // Convert elevation profile from image to data points
    const elevationProfile = useMemo(() => {
      const points = [];
      const distances = [0, 0.2, 0.4, 0.6, 0.8, 1.0, 1.2, 1.4, 1.6, 1.8, 2.0, 2.2, 2.4, 2.6, 2.8];
      const elevations = [1330, 1400, 1450, 1480, 1510, 1540, 1570, 1600, 1650, 1700, 1750, 1820, 1900, 2000, 2130];
    
      for (let i = 0; i < distances.length; i++) {
        const grade = i > 0 
          ? ((elevations[i] - elevations[i-1]) / (5280 * (distances[i] - distances[i-1]))) * 100 
          : 0;
        points.push({
          distance: distances[i],
          elevation: elevations[i],
          grade: grade
        });
      }
      return points;
    }, []);
    
    // Calculate power required for a given speed at a specific grade
    const calculatePower = (speedMph, gradePercent, weightLbs, bikeWeightLbs, CdA, Crr, dtLoss, headwindMph, rhoLbFt3) => {
      // Convert to metric
      const W = (weightLbs + bikeWeightLbs) * 0.453592; // kg
      const Vgs = speedMph * 0.44704; // m/s
      const Vhw = headwindMph * 0.44704; // m/s
      const G = gradePercent;
      const rho = rhoLbFt3 * 16.0185; // kg/m^3
      const CdAMetric = CdA * 0.092903; // m^2
    
      // Calculate forces
      const Fgravity = 9.8067 * Math.sin(Math.atan(G / 100)) * W;
      const Frolling = 9.8067 * Math.cos(Math.atan(G / 100)) * W * Crr;
      const Vas = Vgs + Vhw;
      const Fdrag = 0.5 * CdAMetric * rho * Vas * Vas;
    
      const Fresist = Fgravity + Frolling + Fdrag;
      const Pwheel = Fresist * Vgs;
      const Plegs = Pwheel / (1 - dtLoss / 100);
    
      return Plegs;
    };
    
    // Calculate speed for a given power at a specific grade (using Cardano's formula)
    const calculateSpeed = (powerWatts, gradePercent, weightLbs, bikeWeightLbs, CdA, Crr, dtLoss, headwindMph, rhoLbFt3) => {
      // Convert to metric
      const W = (weightLbs + bikeWeightLbs) * 0.453592; // kg
      const Vhw = headwindMph * 0.44704; // m/s
      const G = gradePercent;
      const rho = rhoLbFt3 * 16.0185; // kg/m^3
      const CdAMetric = CdA * 0.092903; // m^2
      const P = powerWatts;
    
      // Cubic equation coefficients
      const a = 0.5 * CdAMetric * rho;
      const b = Vhw * CdAMetric * rho;
      const c = (9.8067 * W * (Math.sin(Math.atan(G / 100)) + Crr * Math.cos(Math.atan(G / 100)))) + 
                (0.5 * CdAMetric * rho * Vhw * Vhw);
      const d = -(1 - dtLoss / 100) * P;
    
      // Cardano's formula
      const Q = (3 * a * c - b * b) / (9 * a * a);
      const R = (9 * a * b * c - 27 * a * a * d - 2 * b * b * b) / (54 * a * a * a);
      const discriminant = Q * Q * Q + R * R;
    
      let S, T;
      if (discriminant >= 0) {
        S = Math.cbrt(R + Math.sqrt(discriminant));
        T = Math.cbrt(R - Math.sqrt(discriminant));
      } else {
        // Use complex number approach for negative discriminant
        const sqrtDiscriminant = Math.sqrt(-discriminant);
        S = Math.cbrt(Math.sqrt(R * R + discriminant));
        T = S;
      }
    
      const Vgs = S + T - (b / (3 * a));
      const speedMph = Vgs * 2.23694; // m/s to mph
    
      return speedMph > 0 ? speedMph : 0;
    };
    
    // Generate pacing strategy
    const pacingStrategy = useMemo(() => {
      const { riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho, targetTime } = params;
    
      // Binary search for average power to hit target time
      let powerLow = 100;
      let powerHigh = 600;
      let bestPower = 250;
      let iterations = 0;
    
      while (powerHigh - powerLow > 0.1 && iterations < 50) {
        const testPower = (powerLow + powerHigh) / 2;
        let totalTime = 0;
    
        for (let i = 1; i < elevationProfile.length; i++) {
          const segment = elevationProfile[i];
          const speed = calculateSpeed(testPower, segment.grade, riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho);
          const distance = segment.distance - elevationProfile[i-1].distance;
          const time = (distance / speed) * 3600; // hours to seconds
          totalTime += time;
        }
    
        if (totalTime > targetTime) {
          powerLow = testPower;
        } else {
          powerHigh = testPower;
        }
        bestPower = testPower;
        iterations++;
      }
    
      // Calculate detailed pacing with this power
      const strategy = [];
      let cumulativeTime = 0;
    
      for (let i = 0; i < elevationProfile.length; i++) {
        const segment = elevationProfile[i];
        const speed = i > 0 
          ? calculateSpeed(bestPower, segment.grade, riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho)
          : 0;
    
        const distance = i > 0 ? segment.distance - elevationProfile[i-1].distance : 0;
        const segmentTime = speed > 0 ? (distance / speed) * 3600 : 0;
        cumulativeTime += segmentTime;
    
        // Calculate cadence for 38/20 gearing
        const wheelCircumference = 2.1; // meters (700c wheel)
        const gearRatio = 38 / 20;
        const cadence = speed > 0 ? (speed * 0.44704 * 60) / (wheelCircumference * gearRatio) : 0;
    
        strategy.push({
          distance: segment.distance,
          elevation: segment.elevation,
          grade: segment.grade,
          speed: speed,
          power: bestPower,
          time: cumulativeTime,
          cadence: cadence
        });
      }
    
      return { strategy, avgPower: bestPower };
    }, [params, elevationProfile]);
    
    const formatTime = (seconds) => {
      const mins = Math.floor(seconds / 60);
      const secs = Math.floor(seconds % 60);
      return `${mins}:${secs.toString().padStart(2, '0')}`;
    };
    
    return (
      <div className="w-full max-w-7xl mx-auto p-6 bg-white">
        <h1 className="text-3xl font-bold mb-6 text-gray-800">Climb Pacing Strategy</h1>
    
        <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
          <div className="bg-gray-50 p-4 rounded-lg">
            <h2 className="text-xl font-semibold mb-4">Parameters</h2>
            <div className="space-y-3">
              <div>
                <label className="block text-sm font-medium mb-1">Rider Weight (lbs)</label>
                <input 
                  type="number" 
                  value={params.riderWeight}
                  onChange={(e) => setParams({...params, riderWeight: parseFloat(e.target.value)})}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
              <div>
                <label className="block text-sm font-medium mb-1">Bike Weight (lbs)</label>
                <input 
                  type="number" 
                  value={params.bikeWeight}
                  onChange={(e) => setParams({...params, bikeWeight: parseFloat(e.target.value)})}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
              <div>
                <label className="block text-sm font-medium mb-1">CdA (ft²)</label>
                <input 
                  type="number" 
                  step="0.01"
                  value={params.CdA}
                  onChange={(e) => setParams({...params, CdA: parseFloat(e.target.value)})}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
              <div>
                <label className="block text-sm font-medium mb-1">Target Time (mm:ss)</label>
                <input 
                  type="text" 
                  value={formatTime(params.targetTime)}
                  onChange={(e) => {
                    const [m, s] = e.target.value.split(':').map(Number);
                    if (!isNaN(m) && !isNaN(s)) {
                      setParams({...params, targetTime: m * 60 + s});
                    }
                  }}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
            </div>
          </div>
    
          <div className="bg-blue-50 p-4 rounded-lg">
            <h2 className="text-xl font-semibold mb-4">Target vs Actual</h2>
            <div className="space-y-3">
              <div className="border-b pb-2">
                <div className="text-sm font-medium text-gray-600 mb-1">YOUR BEST EFFORT</div>
                <div className="flex justify-between">
                  <span>Time:</span>
                  <span className="font-bold">18:43</span>
                </div>
                <div className="flex justify-between">
                  <span>Avg Power:</span>
                  <span className="font-bold">214W</span>
                </div>
                <div className="flex justify-between">
                  <span>Avg Speed:</span>
                  <span className="font-bold">9.1 mph</span>
                </div>
              </div>
              <div className="border-b pb-2">
                <div className="text-sm font-medium text-gray-600 mb-1">TARGET (15:08)</div>
                <div className="flex justify-between">
                  <span>Required Power:</span>
                  <span className="text-blue-600 font-bold">{pacingStrategy.avgPower.toFixed(0)}W</span>
                </div>
                <div className="flex justify-between">
                  <span>Avg Speed:</span>
                  <span className="text-blue-600 font-bold">
                    {((2.8 / (pacingStrategy.strategy[pacingStrategy.strategy.length - 1]?.time / 3600)) || 0).toFixed(1)} mph
                  </span>
                </div>
              </div>
              <div className="bg-red-50 p-2 rounded">
                <div className="text-sm font-medium text-gray-600 mb-1">GAP TO CLOSE</div>
                <div className="flex justify-between text-red-600">
                  <span>Time:</span>
                  <span className="font-bold">-3:35 ({((params.actualTime - params.targetTime) / params.actualTime * 100).toFixed(1)}%)</span>
                </div>
                <div className="flex justify-between text-red-600">
                  <span>Power:</span>
                  <span className="font-bold">+{(pacingStrategy.avgPower - params.actualPower).toFixed(0)}W ({((pacingStrategy.avgPower - params.actualPower) / params.actualPower * 100).toFixed(1)}%)</span>
                </div>
              </div>
            </div>
          </div>
        </div>
    
        <div className="mb-8">
          <h2 className="text-xl font-semibold mb-4">Speed Profile</h2>
          <ResponsiveContainer width="100%" height={300}>
            <LineChart data={pacingStrategy.strategy}>
              <CartesianGrid strokeDasharray="3 3" />
              <XAxis dataKey="distance" label={{ value: 'Distance (miles)', position: 'insideBottom', offset: -5 }} />
              <YAxis label={{ value: 'Speed (mph)', angle: -90, position: 'insideLeft' }} />
              <Tooltip />
              <Line type="monotone" dataKey="speed" stroke="#2563eb" strokeWidth={2} name="Speed" />
            </LineChart>
          </ResponsiveContainer>
        </div>
    
        <div className="mb-8">
          <h2 className="text-xl font-semibold mb-4">Elevation & Gradient</h2>
          <ResponsiveContainer width="100%" height={300}>
            <LineChart data={pacingStrategy.strategy}>
              <CartesianGrid strokeDasharray="3 3" />
              <XAxis dataKey="distance" label={{ value: 'Distance (miles)', position: 'insideBottom', offset: -5 }} />
              <YAxis yAxisId="left" label={{ value: 'Elevation (ft)', angle: -90, position: 'insideLeft' }} />
              <YAxis yAxisId="right" orientation="right" label={{ value: 'Gradient (%)', angle: 90, position: 'insideRight' }} />
              <Tooltip />
              <Line yAxisId="left" type="monotone" dataKey="elevation" stroke="#10b981" strokeWidth={2} name="Elevation" />
              <Line yAxisId="right" type="monotone" dataKey="grade" stroke="#ef4444" strokeWidth={2} name="Gradient" />
            </LineChart>
          </ResponsiveContainer>
        </div>
    
        <div>
          <h2 className="text-xl font-semibold mb-4">Detailed Pacing Table</h2>
          <div className="overflow-x-auto">
            <table className="w-full border-collapse border">
              <thead className="bg-gray-100">
                <tr>
                  <th className="border p-2">Distance (mi)</th>
                  <th className="border p-2">Elevation (ft)</th>
                  <th className="border p-2">Grade (%)</th>
                  <th className="border p-2">Speed (mph)</th>
                  <th className="border p-2">Cadence (rpm)</th>
                  <th className="border p-2">Power (W)</th>
                  <th className="border p-2">Time</th>
                </tr>
              </thead>
              <tbody>
                {pacingStrategy.strategy.map((point, idx) => (
                  <tr key={idx} className={idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
                    <td className="border p-2 text-center">{point.distance.toFixed(1)}</td>
                    <td className="border p-2 text-center">{point.elevation.toFixed(0)}</td>
                    <td className="border p-2 text-center">{point.grade.toFixed(1)}</td>
                    <td className="border p-2 text-center">{point.speed.toFixed(1)}</td>
                    <td className="border p-2 text-center">{point.cadence.toFixed(0)}</td>
                    <td className="border p-2 text-center font-semibold">{point.power.toFixed(0)}</td>
                    <td className="border p-2 text-center">{formatTime(point.time)}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>
    
        <div className="mt-6 p-4 bg-yellow-50 rounded-lg">
          <h3 className="font-semibold mb-2">Note on Singlespeed (38/20):</h3>
          <p className="text-sm text-gray-700">
            Your 38/20 gearing (1.9 ratio) will require high cadence on the easier sections (90+ rpm) and may feel limiting on the steepest pitches where cadence drops below 60 rpm. Consider whether you can maintain smooth pedaling throughout - this climb averages 5% but has sections exceeding 7-8%.
          </p>
        </div>
      </div>
    );
    };
    
    export default ClimbPacingCalculator;
    

    Climb Pacing Strategy Calculator ∙ Version 4

    import React, { useState, useMemo } from 'react';
    import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
    
    const ClimbPacingCalculator = () => {
    const [params, setParams] = useState({
      riderWeight: 203,
      bikeWeight: 22,
      CdA: 0.388, // Reasonable default for road position
      Crr: 0.004, // Good tires on smooth pavement
      drivetrainLoss: 2,
      headwind: 0,
      rho: 0.075, // Air density at sea level (lb/ft^3)
      targetTime: 908, // 15:08 in seconds
      actualTime: 1123, // 18:43 actual
      actualPower: 214 // W average
    });
    
    // Convert elevation profile from image to data points
    const elevationProfile = useMemo(() => {
      const points = [];
      const distances = [0, 0.2, 0.4, 0.6, 0.8, 1.0, 1.2, 1.4, 1.6, 1.8, 2.0, 2.2, 2.4, 2.6, 2.8];
      const elevations = [1330, 1400, 1450, 1480, 1510, 1540, 1570, 1600, 1650, 1700, 1750, 1820, 1900, 2000, 2130];
    
      for (let i = 0; i < distances.length; i++) {
        const grade = i > 0 
          ? ((elevations[i] - elevations[i-1]) / (5280 * (distances[i] - distances[i-1]))) * 100 
          : 0;
        points.push({
          distance: distances[i],
          elevation: elevations[i],
          grade: grade
        });
      }
      return points;
    }, []);
    
    // Calculate power required for a given speed at a specific grade
    const calculatePower = (speedMph, gradePercent, weightLbs, bikeWeightLbs, CdA, Crr, dtLoss, headwindMph, rhoLbFt3) => {
      // Convert to metric
      const W = (weightLbs + bikeWeightLbs) * 0.453592; // kg
      const Vgs = speedMph * 0.44704; // m/s
      const Vhw = headwindMph * 0.44704; // m/s
      const G = gradePercent;
      const rho = rhoLbFt3 * 16.0185; // kg/m^3
      const CdAMetric = CdA * 0.092903; // m^2
    
      // Calculate forces
      const Fgravity = 9.8067 * Math.sin(Math.atan(G / 100)) * W;
      const Frolling = 9.8067 * Math.cos(Math.atan(G / 100)) * W * Crr;
      const Vas = Vgs + Vhw;
      const Fdrag = 0.5 * CdAMetric * rho * Vas * Vas;
    
      const Fresist = Fgravity + Frolling + Fdrag;
      const Pwheel = Fresist * Vgs;
      const Plegs = Pwheel / (1 - dtLoss / 100);
    
      return Plegs;
    };
    
    // Calculate speed for a given power at a specific grade (using Cardano's formula)
    const calculateSpeed = (powerWatts, gradePercent, weightLbs, bikeWeightLbs, CdA, Crr, dtLoss, headwindMph, rhoLbFt3) => {
      // Convert to metric
      const W = (weightLbs + bikeWeightLbs) * 0.453592; // kg
      const Vhw = headwindMph * 0.44704; // m/s
      const G = gradePercent;
      const rho = rhoLbFt3 * 16.0185; // kg/m^3
      const CdAMetric = CdA * 0.092903; // m^2
      const P = powerWatts;
    
      // Cubic equation coefficients
      const a = 0.5 * CdAMetric * rho;
      const b = Vhw * CdAMetric * rho;
      const c = (9.8067 * W * (Math.sin(Math.atan(G / 100)) + Crr * Math.cos(Math.atan(G / 100)))) + 
                (0.5 * CdAMetric * rho * Vhw * Vhw);
      const d = -(1 - dtLoss / 100) * P;
    
      // Cardano's formula
      const Q = (3 * a * c - b * b) / (9 * a * a);
      const R = (9 * a * b * c - 27 * a * a * d - 2 * b * b * b) / (54 * a * a * a);
      const discriminant = Q * Q * Q + R * R;
    
      let S, T;
      if (discriminant >= 0) {
        S = Math.cbrt(R + Math.sqrt(discriminant));
        T = Math.cbrt(R - Math.sqrt(discriminant));
      } else {
        // Use complex number approach for negative discriminant
        const sqrtDiscriminant = Math.sqrt(-discriminant);
        S = Math.cbrt(Math.sqrt(R * R + discriminant));
        T = S;
      }
    
      const Vgs = S + T - (b / (3 * a));
      const speedMph = Vgs * 2.23694; // m/s to mph
    
      return speedMph > 0 ? speedMph : 0;
    };
    
    // Generate pacing strategy
    const pacingStrategy = useMemo(() => {
      const { riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho, targetTime } = params;
    
      // Binary search for average power to hit target time
      let powerLow = 100;
      let powerHigh = 600;
      let bestPower = 250;
      let iterations = 0;
    
      while (powerHigh - powerLow > 0.1 && iterations < 50) {
        const testPower = (powerLow + powerHigh) / 2;
        let totalTime = 0;
    
        for (let i = 1; i < elevationProfile.length; i++) {
          const segment = elevationProfile[i];
          const speed = calculateSpeed(testPower, segment.grade, riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho);
          const distance = segment.distance - elevationProfile[i-1].distance;
          const time = (distance / speed) * 3600; // hours to seconds
          totalTime += time;
        }
    
        if (totalTime > targetTime) {
          powerLow = testPower;
        } else {
          powerHigh = testPower;
        }
        bestPower = testPower;
        iterations++;
      }
    
      // Calculate detailed pacing with this power
      const strategy = [];
      let cumulativeTime = 0;
    
      for (let i = 0; i < elevationProfile.length; i++) {
        const segment = elevationProfile[i];
        const speed = i > 0 
          ? calculateSpeed(bestPower, segment.grade, riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho)
          : 0;
    
        const distance = i > 0 ? segment.distance - elevationProfile[i-1].distance : 0;
        const segmentTime = speed > 0 ? (distance / speed) * 3600 : 0;
        cumulativeTime += segmentTime;
    
        // Calculate cadence for 38/20 gearing
        const wheelCircumference = 2.1; // meters (700c wheel)
        const gearRatio = 38 / 20;
        const cadence = speed > 0 ? (speed * 0.44704 * 60) / (wheelCircumference * gearRatio) : 0;
    
        strategy.push({
          distance: segment.distance,
          elevation: segment.elevation,
          grade: segment.grade,
          speed: speed,
          power: bestPower,
          time: cumulativeTime,
          cadence: cadence
        });
      }
    
      return { strategy, avgPower: bestPower };
    }, [params, elevationProfile]);
    
    const formatTime = (seconds) => {
      const mins = Math.floor(seconds / 60);
      const secs = Math.floor(seconds % 60);
      return `${mins}:${secs.toString().padStart(2, '0')}`;
    };
    
    return (
      <div className="w-full max-w-7xl mx-auto p-6 bg-white">
        <h1 className="text-3xl font-bold mb-6 text-gray-800">Climb Pacing Strategy</h1>
    
        <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
          <div className="bg-gray-50 p-4 rounded-lg">
            <h2 className="text-xl font-semibold mb-4">Parameters</h2>
            <div className="space-y-3">
              <div>
                <label className="block text-sm font-medium mb-1">Rider Weight (lbs)</label>
                <input 
                  type="number" 
                  value={params.riderWeight}
                  onChange={(e) => setParams({...params, riderWeight: parseFloat(e.target.value)})}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
              <div>
                <label className="block text-sm font-medium mb-1">Bike Weight (lbs)</label>
                <input 
                  type="number" 
                  value={params.bikeWeight}
                  onChange={(e) => setParams({...params, bikeWeight: parseFloat(e.target.value)})}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
              <div>
                <label className="block text-sm font-medium mb-1">CdA (ft²)</label>
                <input 
                  type="number" 
                  step="0.01"
                  value={params.CdA}
                  onChange={(e) => setParams({...params, CdA: parseFloat(e.target.value)})}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
              <div>
                <label className="block text-sm font-medium mb-1">Target Time (mm:ss)</label>
                <input 
                  type="text" 
                  value={formatTime(params.targetTime)}
                  onChange={(e) => {
                    const [m, s] = e.target.value.split(':').map(Number);
                    if (!isNaN(m) && !isNaN(s)) {
                      setParams({...params, targetTime: m * 60 + s});
                    }
                  }}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
            </div>
          </div>
    
          <div className="bg-blue-50 p-4 rounded-lg">
            <h2 className="text-xl font-semibold mb-4">Target vs Actual</h2>
            <div className="space-y-3">
              <div className="border-b pb-2">
                <div className="text-sm font-medium text-gray-600 mb-1">YOUR BEST EFFORT</div>
                <div className="flex justify-between">
                  <span>Time:</span>
                  <span className="font-bold">18:43</span>
                </div>
                <div className="flex justify-between">
                  <span>Avg Power:</span>
                  <span className="font-bold">214W</span>
                </div>
                <div className="flex justify-between">
                  <span>Avg Speed:</span>
                  <span className="font-bold">9.1 mph</span>
                </div>
              </div>
              <div className="border-b pb-2">
                <div className="text-sm font-medium text-gray-600 mb-1">TARGET (15:08)</div>
                <div className="flex justify-between">
                  <span>Required Power:</span>
                  <span className="text-blue-600 font-bold">{pacingStrategy.avgPower.toFixed(0)}W</span>
                </div>
                <div className="flex justify-between">
                  <span>Avg Speed:</span>
                  <span className="text-blue-600 font-bold">
                    {((2.8 / (pacingStrategy.strategy[pacingStrategy.strategy.length - 1]?.time / 3600)) || 0).toFixed(1)} mph
                  </span>
                </div>
              </div>
              <div className="bg-red-50 p-2 rounded">
                <div className="text-sm font-medium text-gray-600 mb-1">GAP TO CLOSE</div>
                <div className="flex justify-between text-red-600">
                  <span>Time:</span>
                  <span className="font-bold">-3:35 ({((params.actualTime - params.targetTime) / params.actualTime * 100).toFixed(1)}%)</span>
                </div>
                <div className="flex justify-between text-red-600">
                  <span>Power:</span>
                  <span className="font-bold">+{(pacingStrategy.avgPower - params.actualPower).toFixed(0)}W ({((pacingStrategy.avgPower - params.actualPower) / params.actualPower * 100).toFixed(1)}%)</span>
                </div>
              </div>
            </div>
          </div>
        </div>
    
        <div className="mb-8">
          <h2 className="text-xl font-semibold mb-4">Speed Profile</h2>
          <ResponsiveContainer width="100%" height={300}>
            <LineChart data={pacingStrategy.strategy}>
              <CartesianGrid strokeDasharray="3 3" />
              <XAxis dataKey="distance" label={{ value: 'Distance (miles)', position: 'insideBottom', offset: -5 }} />
              <YAxis label={{ value: 'Speed (mph)', angle: -90, position: 'insideLeft' }} />
              <Tooltip />
              <Line type="monotone" dataKey="speed" stroke="#2563eb" strokeWidth={2} name="Speed" />
            </LineChart>
          </ResponsiveContainer>
        </div>
    
        <div className="mb-8">
          <h2 className="text-xl font-semibold mb-4">Elevation & Gradient</h2>
          <ResponsiveContainer width="100%" height={300}>
            <LineChart data={pacingStrategy.strategy}>
              <CartesianGrid strokeDasharray="3 3" />
              <XAxis dataKey="distance" label={{ value: 'Distance (miles)', position: 'insideBottom', offset: -5 }} />
              <YAxis yAxisId="left" label={{ value: 'Elevation (ft)', angle: -90, position: 'insideLeft' }} />
              <YAxis yAxisId="right" orientation="right" label={{ value: 'Gradient (%)', angle: 90, position: 'insideRight' }} />
              <Tooltip />
              <Line yAxisId="left" type="monotone" dataKey="elevation" stroke="#10b981" strokeWidth={2} name="Elevation" />
              <Line yAxisId="right" type="monotone" dataKey="grade" stroke="#ef4444" strokeWidth={2} name="Gradient" />
            </LineChart>
          </ResponsiveContainer>
        </div>
    
        <div>
          <h2 className="text-xl font-semibold mb-4">Detailed Pacing Table</h2>
          <div className="overflow-x-auto">
            <table className="w-full border-collapse border">
              <thead className="bg-gray-100">
                <tr>
                  <th className="border p-2">Distance (mi)</th>
                  <th className="border p-2">Elevation (ft)</th>
                  <th className="border p-2">Grade (%)</th>
                  <th className="border p-2">Speed (mph)</th>
                  <th className="border p-2">Cadence (rpm)</th>
                  <th className="border p-2">Power (W)</th>
                  <th className="border p-2">Time</th>
                </tr>
              </thead>
              <tbody>
                {pacingStrategy.strategy.map((point, idx) => (
                  <tr key={idx} className={idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
                    <td className="border p-2 text-center">{point.distance.toFixed(1)}</td>
                    <td className="border p-2 text-center">{point.elevation.toFixed(0)}</td>
                    <td className="border p-2 text-center">{point.grade.toFixed(1)}</td>
                    <td className="border p-2 text-center">{point.speed.toFixed(1)}</td>
                    <td className="border p-2 text-center">{point.cadence.toFixed(0)}</td>
                    <td className="border p-2 text-center font-semibold">{point.power.toFixed(0)}</td>
                    <td className="border p-2 text-center">{formatTime(point.time)}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>
    
        <div className="mt-6 p-4 bg-yellow-50 rounded-lg">
          <h3 className="font-semibold mb-2">Analysis: What You Need to Improve</h3>
          <div className="space-y-2 text-sm text-gray-700">
            <p>
              <strong>Power Gap:</strong> You averaged 214W but need ~{pacingStrategy.avgPower.toFixed(0)}W - that's a {((pacingStrategy.avgPower - params.actualPower) / params.actualPower * 100).toFixed(0)}% increase. This is significant but achievable with training.
            </p>
            <p>
              <strong>Weight Impact:</strong> At your current power (214W), losing 10 lbs would save approximately 40-50 seconds. Losing 20 lbs could save 1:20-1:40. Combined with a modest power increase, this could close the gap.
            </p>
            <p>
              <strong>Singlespeed Challenge:</strong> Your 38/20 gearing at 57 avg cadence suggests you're grinding on the steep sections. With gears, you could maintain higher cadence and possibly sustain higher power. Consider: could you hold 230-240W with optimal gearing?
            </p>
            <p>
              <strong>Training Focus:</strong> To hit 15:08, prioritize:
               Sweet spot intervals (88-93% FTP) to raise your sustainable power
               VO2max work to increase your ceiling
               Weight management if realistic
               Practice grinding low-cadence efforts if staying singlespeed
            </p>
          </div>
        </div>
    
        <div className="mt-4 p-4 bg-green-50 rounded-lg">
          <h3 className="font-semibold mb-2">Realistic Pathways to 15:08:</h3>
          <div className="space-y-2 text-sm">
            <div className="flex justify-between items-center">
              <span>1. Current weight (203 lbs) + 266W sustained:</span>
              <span className="font-bold text-green-700">15:08 </span>
            </div>
            <div className="flex justify-between items-center">
              <span>2. Lose 15 lbs (188 lbs) + 245W sustained:</span>
              <span className="font-bold text-green-700">~15:08 </span>
            </div>
            <div className="flex justify-between items-center">
              <span>3. Lose 25 lbs (178 lbs) + 230W sustained:</span>
              <span className="font-bold text-green-700">~15:08 </span>
            </div>
          </div>
        </div>
    
        <div className="mt-4 p-4 bg-gray-50 rounded-lg">
          <h3 className="font-semibold mb-2">Note on Singlespeed (38/20):</h3>
          <p className="text-sm text-gray-700">
            Your 38/20 gearing (1.9 ratio) at 57 avg cadence means you're likely dropping to 45-50 rpm on the steepest pitches. This is muscularly demanding and may prevent you from sustaining higher power. The target cadence for 15:08 would be 65-75 rpm average, requiring either:
             More power to spin faster in the same gear, or
             Switching to gears to maintain optimal cadence throughout
          </p>
        </div>
      </div>
    );
    };
    
    export default ClimbPacingCalculator;
    

    Perfect! Now I can give you a detailed analysis:

  • Your Performance vs Target

    Current: 18:43 @ 214W avg (9.1 mph, 57 rpm cadence)
    Target: 15:08 @ ~326W avg (11.1 mph, 70+ rpm cadence)
    Gap: 3:35 slower (-19% time) / 112W less power (+52% power needed)

  • Key Insights:

    1. Power is the limiting factor: You need to sustain 52% more power (326W vs 214W). That's a massive jump and honestly not realistic in the short term.

    2. Weight matters significantly: At 203 lbs rider + 22 lbs bike = 225 lbs total, you're fighting gravity hard on this 5% average grade climb. Physics shows:

    • Lose 15 lbs → need only 245W (not 326W) for 15:08

    • Lose 25 lbs → need only 230W for 15:08

      1. Singlespeed is hurting you: Your 57 rpm average cadence means you're grinding, especially on the 7-8% pitches where you probably drop to 45-50 rpm. This:
    • Fatigues muscles faster than cardiovascular system

    • Prevents you from sustaining higher power

    • With gears, you could maintain 80-90 rpm and possibly hold 230-240W instead of 214W

      1. Heart rate suggests room to grow: 164 bpm average with 181 max suggests you might not have been at absolute limit - there may be 10-15W more available with better pacing or fitness.

      Bottom line: 15:08 likely requires a combination of more training (raise FTP), weight loss if realistic for you, and possibly switching to gears for this climb.

  • Prompt:

    12/11/2025, 1:42:16 PM

    1765489332224_image.png

    here is a more detailed vie wof my best effort

  • Response:

    12/11/2025, 1:43:18 PM

    This is really helpful data! Let me analyze your pacing and see where time was lost.

    Climb Pacing Strategy Calculator ∙ Version 5

    import React, { useState, useMemo } from 'react';
    import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
    
    const ClimbPacingCalculator = () => {
    const [params, setParams] = useState({
      riderWeight: 203,
      bikeWeight: 22,
      CdA: 0.388, // Reasonable default for road position
      Crr: 0.004, // Good tires on smooth pavement
      drivetrainLoss: 2,
      headwind: 0,
      rho: 0.075, // Air density at sea level (lb/ft^3)
      targetTime: 908, // 15:08 in seconds
      actualTime: 1123, // 18:43 actual
      actualPower: 214 // W average
    });
    
    // Convert elevation profile from image to data points
    const elevationProfile = useMemo(() => {
      const points = [];
      const distances = [0, 0.2, 0.4, 0.6, 0.8, 1.0, 1.2, 1.4, 1.6, 1.8, 2.0, 2.2, 2.4, 2.6, 2.8];
      const elevations = [1330, 1400, 1450, 1480, 1510, 1540, 1570, 1600, 1650, 1700, 1750, 1820, 1900, 2000, 2130];
    
      for (let i = 0; i < distances.length; i++) {
        const grade = i > 0 
          ? ((elevations[i] - elevations[i-1]) / (5280 * (distances[i] - distances[i-1]))) * 100 
          : 0;
        points.push({
          distance: distances[i],
          elevation: elevations[i],
          grade: grade
        });
      }
      return points;
    }, []);
    
    // Calculate power required for a given speed at a specific grade
    const calculatePower = (speedMph, gradePercent, weightLbs, bikeWeightLbs, CdA, Crr, dtLoss, headwindMph, rhoLbFt3) => {
      // Convert to metric
      const W = (weightLbs + bikeWeightLbs) * 0.453592; // kg
      const Vgs = speedMph * 0.44704; // m/s
      const Vhw = headwindMph * 0.44704; // m/s
      const G = gradePercent;
      const rho = rhoLbFt3 * 16.0185; // kg/m^3
      const CdAMetric = CdA * 0.092903; // m^2
    
      // Calculate forces
      const Fgravity = 9.8067 * Math.sin(Math.atan(G / 100)) * W;
      const Frolling = 9.8067 * Math.cos(Math.atan(G / 100)) * W * Crr;
      const Vas = Vgs + Vhw;
      const Fdrag = 0.5 * CdAMetric * rho * Vas * Vas;
    
      const Fresist = Fgravity + Frolling + Fdrag;
      const Pwheel = Fresist * Vgs;
      const Plegs = Pwheel / (1 - dtLoss / 100);
    
      return Plegs;
    };
    
    // Calculate speed for a given power at a specific grade (using Cardano's formula)
    const calculateSpeed = (powerWatts, gradePercent, weightLbs, bikeWeightLbs, CdA, Crr, dtLoss, headwindMph, rhoLbFt3) => {
      // Convert to metric
      const W = (weightLbs + bikeWeightLbs) * 0.453592; // kg
      const Vhw = headwindMph * 0.44704; // m/s
      const G = gradePercent;
      const rho = rhoLbFt3 * 16.0185; // kg/m^3
      const CdAMetric = CdA * 0.092903; // m^2
      const P = powerWatts;
    
      // Cubic equation coefficients
      const a = 0.5 * CdAMetric * rho;
      const b = Vhw * CdAMetric * rho;
      const c = (9.8067 * W * (Math.sin(Math.atan(G / 100)) + Crr * Math.cos(Math.atan(G / 100)))) + 
                (0.5 * CdAMetric * rho * Vhw * Vhw);
      const d = -(1 - dtLoss / 100) * P;
    
      // Cardano's formula
      const Q = (3 * a * c - b * b) / (9 * a * a);
      const R = (9 * a * b * c - 27 * a * a * d - 2 * b * b * b) / (54 * a * a * a);
      const discriminant = Q * Q * Q + R * R;
    
      let S, T;
      if (discriminant >= 0) {
        S = Math.cbrt(R + Math.sqrt(discriminant));
        T = Math.cbrt(R - Math.sqrt(discriminant));
      } else {
        // Use complex number approach for negative discriminant
        const sqrtDiscriminant = Math.sqrt(-discriminant);
        S = Math.cbrt(Math.sqrt(R * R + discriminant));
        T = S;
      }
    
      const Vgs = S + T - (b / (3 * a));
      const speedMph = Vgs * 2.23694; // m/s to mph
    
      return speedMph > 0 ? speedMph : 0;
    };
    
    // Generate pacing strategy
    const pacingStrategy = useMemo(() => {
      const { riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho, targetTime } = params;
    
      // Binary search for average power to hit target time
      let powerLow = 100;
      let powerHigh = 600;
      let bestPower = 250;
      let iterations = 0;
    
      while (powerHigh - powerLow > 0.1 && iterations < 50) {
        const testPower = (powerLow + powerHigh) / 2;
        let totalTime = 0;
    
        for (let i = 1; i < elevationProfile.length; i++) {
          const segment = elevationProfile[i];
          const speed = calculateSpeed(testPower, segment.grade, riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho);
          const distance = segment.distance - elevationProfile[i-1].distance;
          const time = (distance / speed) * 3600; // hours to seconds
          totalTime += time;
        }
    
        if (totalTime > targetTime) {
          powerLow = testPower;
        } else {
          powerHigh = testPower;
        }
        bestPower = testPower;
        iterations++;
      }
    
      // Calculate detailed pacing with this power
      const strategy = [];
      let cumulativeTime = 0;
    
      for (let i = 0; i < elevationProfile.length; i++) {
        const segment = elevationProfile[i];
        const speed = i > 0 
          ? calculateSpeed(bestPower, segment.grade, riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho)
          : 0;
    
        const distance = i > 0 ? segment.distance - elevationProfile[i-1].distance : 0;
        const segmentTime = speed > 0 ? (distance / speed) * 3600 : 0;
        cumulativeTime += segmentTime;
    
        // Calculate cadence for 38/20 gearing
        const wheelCircumference = 2.1; // meters (700c wheel)
        const gearRatio = 38 / 20;
        const cadence = speed > 0 ? (speed * 0.44704 * 60) / (wheelCircumference * gearRatio) : 0;
    
        strategy.push({
          distance: segment.distance,
          elevation: segment.elevation,
          grade: segment.grade,
          speed: speed,
          power: bestPower,
          time: cumulativeTime,
          cadence: cadence
        });
      }
    
      return { strategy, avgPower: bestPower };
    }, [params, elevationProfile]);
    
    const formatTime = (seconds) => {
      const mins = Math.floor(seconds / 60);
      const secs = Math.floor(seconds % 60);
      return `${mins}:${secs.toString().padStart(2, '0')}`;
    };
    
    return (
      <div className="w-full max-w-7xl mx-auto p-6 bg-white">
        <h1 className="text-3xl font-bold mb-6 text-gray-800">Climb Pacing Strategy</h1>
    
        <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
          <div className="bg-gray-50 p-4 rounded-lg">
            <h2 className="text-xl font-semibold mb-4">Parameters</h2>
            <div className="space-y-3">
              <div>
                <label className="block text-sm font-medium mb-1">Rider Weight (lbs)</label>
                <input 
                  type="number" 
                  value={params.riderWeight}
                  onChange={(e) => setParams({...params, riderWeight: parseFloat(e.target.value)})}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
              <div>
                <label className="block text-sm font-medium mb-1">Bike Weight (lbs)</label>
                <input 
                  type="number" 
                  value={params.bikeWeight}
                  onChange={(e) => setParams({...params, bikeWeight: parseFloat(e.target.value)})}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
              <div>
                <label className="block text-sm font-medium mb-1">CdA (ft²)</label>
                <input 
                  type="number" 
                  step="0.01"
                  value={params.CdA}
                  onChange={(e) => setParams({...params, CdA: parseFloat(e.target.value)})}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
              <div>
                <label className="block text-sm font-medium mb-1">Target Time (mm:ss)</label>
                <input 
                  type="text" 
                  value={formatTime(params.targetTime)}
                  onChange={(e) => {
                    const [m, s] = e.target.value.split(':').map(Number);
                    if (!isNaN(m) && !isNaN(s)) {
                      setParams({...params, targetTime: m * 60 + s});
                    }
                  }}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
            </div>
          </div>
    
          <div className="bg-blue-50 p-4 rounded-lg">
            <h2 className="text-xl font-semibold mb-4">Target vs Actual</h2>
            <div className="space-y-3">
              <div className="border-b pb-2">
                <div className="text-sm font-medium text-gray-600 mb-1">YOUR BEST EFFORT</div>
                <div className="flex justify-between">
                  <span>Time:</span>
                  <span className="font-bold">18:43</span>
                </div>
                <div className="flex justify-between">
                  <span>Avg Power:</span>
                  <span className="font-bold">214W</span>
                </div>
                <div className="flex justify-between">
                  <span>Avg Speed:</span>
                  <span className="font-bold">9.1 mph</span>
                </div>
              </div>
              <div className="border-b pb-2">
                <div className="text-sm font-medium text-gray-600 mb-1">TARGET (15:08)</div>
                <div className="flex justify-between">
                  <span>Required Power:</span>
                  <span className="text-blue-600 font-bold">{pacingStrategy.avgPower.toFixed(0)}W</span>
                </div>
                <div className="flex justify-between">
                  <span>Avg Speed:</span>
                  <span className="text-blue-600 font-bold">
                    {((2.8 / (pacingStrategy.strategy[pacingStrategy.strategy.length - 1]?.time / 3600)) || 0).toFixed(1)} mph
                  </span>
                </div>
              </div>
              <div className="bg-red-50 p-2 rounded">
                <div className="text-sm font-medium text-gray-600 mb-1">GAP TO CLOSE</div>
                <div className="flex justify-between text-red-600">
                  <span>Time:</span>
                  <span className="font-bold">-3:35 ({((params.actualTime - params.targetTime) / params.actualTime * 100).toFixed(1)}%)</span>
                </div>
                <div className="flex justify-between text-red-600">
                  <span>Power:</span>
                  <span className="font-bold">+{(pacingStrategy.avgPower - params.actualPower).toFixed(0)}W ({((pacingStrategy.avgPower - params.actualPower) / params.actualPower * 100).toFixed(1)}%)</span>
                </div>
              </div>
            </div>
          </div>
        </div>
    
        <div className="mb-8">
          <h2 className="text-xl font-semibold mb-4">Speed Profile</h2>
          <ResponsiveContainer width="100%" height={300}>
            <LineChart data={pacingStrategy.strategy}>
              <CartesianGrid strokeDasharray="3 3" />
              <XAxis dataKey="distance" label={{ value: 'Distance (miles)', position: 'insideBottom', offset: -5 }} />
              <YAxis label={{ value: 'Speed (mph)', angle: -90, position: 'insideLeft' }} />
              <Tooltip />
              <Line type="monotone" dataKey="speed" stroke="#2563eb" strokeWidth={2} name="Speed" />
            </LineChart>
          </ResponsiveContainer>
        </div>
    
        <div className="mb-8">
          <h2 className="text-xl font-semibold mb-4">Elevation & Gradient</h2>
          <ResponsiveContainer width="100%" height={300}>
            <LineChart data={pacingStrategy.strategy}>
              <CartesianGrid strokeDasharray="3 3" />
              <XAxis dataKey="distance" label={{ value: 'Distance (miles)', position: 'insideBottom', offset: -5 }} />
              <YAxis yAxisId="left" label={{ value: 'Elevation (ft)', angle: -90, position: 'insideLeft' }} />
              <YAxis yAxisId="right" orientation="right" label={{ value: 'Gradient (%)', angle: 90, position: 'insideRight' }} />
              <Tooltip />
              <Line yAxisId="left" type="monotone" dataKey="elevation" stroke="#10b981" strokeWidth={2} name="Elevation" />
              <Line yAxisId="right" type="monotone" dataKey="grade" stroke="#ef4444" strokeWidth={2} name="Gradient" />
            </LineChart>
          </ResponsiveContainer>
        </div>
    
        <div>
          <h2 className="text-xl font-semibold mb-4">Detailed Pacing Table</h2>
          <div className="overflow-x-auto">
            <table className="w-full border-collapse border">
              <thead className="bg-gray-100">
                <tr>
                  <th className="border p-2">Distance (mi)</th>
                  <th className="border p-2">Elevation (ft)</th>
                  <th className="border p-2">Grade (%)</th>
                  <th className="border p-2">Speed (mph)</th>
                  <th className="border p-2">Cadence (rpm)</th>
                  <th className="border p-2">Power (W)</th>
                  <th className="border p-2">Time</th>
                </tr>
              </thead>
              <tbody>
                {pacingStrategy.strategy.map((point, idx) => (
                  <tr key={idx} className={idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
                    <td className="border p-2 text-center">{point.distance.toFixed(1)}</td>
                    <td className="border p-2 text-center">{point.elevation.toFixed(0)}</td>
                    <td className="border p-2 text-center">{point.grade.toFixed(1)}</td>
                    <td className="border p-2 text-center">{point.speed.toFixed(1)}</td>
                    <td className="border p-2 text-center">{point.cadence.toFixed(0)}</td>
                    <td className="border p-2 text-center font-semibold">{point.power.toFixed(0)}</td>
                    <td className="border p-2 text-center">{formatTime(point.time)}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>
    
        <div className="mt-6 p-4 bg-yellow-50 rounded-lg">
          <h3 className="font-semibold mb-3">Analysis: What Happened in Your Effort</h3>
          <div className="space-y-3 text-sm text-gray-700">
            <div className="bg-white p-3 rounded border-l-4 border-red-500">
              <strong>Critical Issue: Speed Collapse at 39:00 mark (around mile 2.3-2.5)</strong>
              <p className="mt-1">Your speed dropped dramatically to ~7.3 mph while HR stayed at 168 bpm and cadence dropped to 46 rpm. This is where you lost significant time - you were grinding at unsustainably low cadence on the steepest section.</p>
            </div>
    
            <div className="bg-white p-3 rounded border-l-4 border-yellow-500">
              <strong>Pacing Pattern Analysis:</strong>
              <ul className="mt-1 ml-4 list-disc space-y-1">
                <li><strong>Start (0-26:40):</strong> Consistent ~12-13 mph, 70-90 rpm cadence, ~150 bpm HR - good sustainable pace</li>
                <li><strong>Middle (26:40-35:00):</strong> Speed holding ~11-12 mph, cadence still decent 60-80 rpm, HR climbing to 160s</li>
                <li><strong>Crisis Point (35:00-39:00):</strong> Speed crashes to 7.3 mph, cadence drops to 46 rpm, HR maxed at 168 bpm - you hit the wall on the steepest pitch</li>
                <li><strong>Recovery sections (gaps in data):</strong> Appears you had to stop/soft pedal briefly, then resumed</li>
                <li><strong>Final push (40:00+):</strong> HR surged to 135+ but speed didn't recover - too fatigued</li>
              </ul>
            </div>
    
            <div className="bg-white p-3 rounded border-l-4 border-blue-500">
              <strong>The Singlespeed Problem (38/20 gearing):</strong>
              <p className="mt-1">At 46 rpm on the steep section, you're at 60% of your average cadence. This gear is simply too tall for the 7-8% gradient sections. You're forced to:</p>
              <ul className="mt-1 ml-4 list-disc">
                <li>Mash at very low cadence (high muscular force per pedal stroke)</li>
                <li>This fatigues your legs faster than your cardiovascular system</li>
                <li>Notice your HR wasn't maxed out (168 bpm vs 181 max) - your legs failed before your cardio did</li>
              </ul>
            </div>
    
            <div className="bg-white p-3 rounded border-l-4 border-green-500">
              <strong>Where Time Was Lost:</strong>
              <p className="mt-1">Comparing to target 15:08 pace:</p>
              <ul className="mt-1 ml-4 list-disc">
                <li>First half (miles 0-1.4): Relatively close to target, maybe lost 30-45 seconds</li>
                <li>Steep section (miles 1.4-2.8): Lost 2:30-3:00 here due to the speed collapse</li>
                <li>The 39:00 mark crisis point alone probably cost 1:30-2:00</li>
              </ul>
            </div>
          </div>
        </div>
    
        <div className="mt-4 p-4 bg-red-50 rounded-lg">
          <h3 className="font-semibold mb-2">⚠️ Critical Finding: Gearing is Your Biggest Limiter</h3>
          <div className="space-y-2 text-sm">
            <p>Your data clearly shows the 38/20 singlespeed forced you into an impossible situation on the steep sections. At 214W average power, you should be capable of ~10-11 mph on 7% grades with proper gearing (80-90 rpm). Instead, you were forced to 7.3 mph at 46 rpm.</p>
            <p className="font-semibold mt-2">If you had gears allowing you to maintain 70+ rpm throughout:</p>
            <ul className="ml-4 list-disc">
              <li>You could likely sustain 230-240W instead of 214W (less muscular fatigue)</li>
              <li>You wouldn't have the catastrophic slowdown at 39:00</li>
              <li>Estimated time with same 214W but proper gearing: 16:30-17:15</li>
              <li>With 230W + proper gearing: 15:30-16:00</li>
            </ul>
          </div>
        </div>
    
        <div className="mt-4 p-4 bg-green-50 rounded-lg">
          <h3 className="font-semibold mb-2">Realistic Pathways to 15:08:</h3>
          <div className="space-y-2 text-sm">
            <div className="flex justify-between items-center">
              <span><strong>Option 1:</strong> Switch to gears + maintain current 214W:</span>
              <span className="font-bold text-orange-700">~17:00 (not quite there)</span>
            </div>
            <div className="flex justify-between items-center">
              <span><strong>Option 2:</strong> Switch to gears + increase to 245W:</span>
              <span className="font-bold text-green-700">~15:08 ✓</span>
            </div>
            <div className="flex justify-between items-center">
              <span><strong>Option 3:</strong> Keep singlespeed + increase to 300W+:</span>
              <span className="font-bold text-red-700">~15:08 (extremely difficult, cadence issues persist)</span>
            </div>
            <div className="flex justify-between items-center">
              <span><strong>Option 4:</strong> Gears + lose 15 lbs + 230W:</span>
              <span className="font-bold text-green-700">~15:08 ✓</span>
            </div>
          </div>
        </div>
    
        <div className="mt-4 p-4 bg-gray-50 rounded-lg">
          <h3 className="font-semibold mb-2">Recommended Strategy:</h3>
          <p className="text-sm text-gray-700">
            <strong>Short term (next attempt):</strong> Use a geared bike. Even with the same 214W power, you'll save 1:30-2:00 by avoiding the cadence collapse. Your HR data shows you have more cardiovascular capacity - you just can't access it with that gearing.
          </p>
          <p className="text-sm text-gray-700 mt-2">
            <strong>Medium term (2-3 months):</strong> With gears + focused training to raise sustained power from 214W to 230-245W through sweet spot intervals and VO2max work, 15:08 is very achievable.
          </p>
        </div>
      </div>
    );
    };
    
    export default ClimbPacingCalculator;
    
  • Key Findings from Your Detailed Data:

    The smoking gun is at the 39:00 mark - your speed collapsed to 7.3 mph while grinding at 46 rpm. This is where you lost most of your time. The data tells a clear story:

    1. You started well: First ~26 minutes averaging 12-13 mph at 70-90 rpm - sustainable pace
    2. Middle section held: 26-35 minutes still maintaining 11-12 mph, though cadence dropping to 60-80 rpm
    3. The collapse (35-39 minutes): Speed crashed to 7.3 mph, cadence dropped to 46 rpm, but HR was only 168 bpm (not maxed at 181)
    4. Critical insight: Your heart rate shows your cardiovascular system had more to give, but your legs couldn't turn the pedals fast enough in that gear

    The math is brutal: At 46 rpm in a 38/20 gear on a 7-8% grade, you're applying massive force per pedal stroke. This fatigues your muscles far faster than spinning at 80-90 rpm with the same power output.

    Bottom line from the data:

  • Gearing is costing you 1:30-2:00 minimum (probably more)

  • With proper gears, you could maintain 70+ rpm throughout and likely sustain 230W instead of 214W

  • Combined effect: gears alone could get you to ~16:30-17:00

  • Gears + modest power increase (230-245W) = 15:08 is very achievable

    The good news? Your HR data suggests you have untapped cardiovascular capacity. You just need gearing that lets you access it.

  • Prompt:

    12/11/2025, 1:45:33 PM

    i have access to other rear cogs 14,16,18,20,22 would any of these be better?

  • Response:

    12/11/2025, 1:46:57 PM

    Let me analyze which cog would work best for this climb with your 38t chainring.

    Climb Pacing Strategy Calculator ∙ Version 6

    import React, { useState, useMemo } from 'react';
    import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
    
    const ClimbPacingCalculator = () => {
    const [params, setParams] = useState({
      riderWeight: 203,
      bikeWeight: 22,
      CdA: 0.388, // Reasonable default for road position
      Crr: 0.004, // Good tires on smooth pavement
      drivetrainLoss: 2,
      headwind: 0,
      rho: 0.075, // Air density at sea level (lb/ft^3)
      targetTime: 908, // 15:08 in seconds
      actualTime: 1123, // 18:43 actual
      actualPower: 214, // W average
      chainring: 38,
      cog: 20
    });
    
    // Convert elevation profile from image to data points
    const elevationProfile = useMemo(() => {
      const points = [];
      const distances = [0, 0.2, 0.4, 0.6, 0.8, 1.0, 1.2, 1.4, 1.6, 1.8, 2.0, 2.2, 2.4, 2.6, 2.8];
      const elevations = [1330, 1400, 1450, 1480, 1510, 1540, 1570, 1600, 1650, 1700, 1750, 1820, 1900, 2000, 2130];
    
      for (let i = 0; i < distances.length; i++) {
        const grade = i > 0 
          ? ((elevations[i] - elevations[i-1]) / (5280 * (distances[i] - distances[i-1]))) * 100 
          : 0;
        points.push({
          distance: distances[i],
          elevation: elevations[i],
          grade: grade
        });
      }
      return points;
    }, []);
    
    // Calculate power required for a given speed at a specific grade
    const calculatePower = (speedMph, gradePercent, weightLbs, bikeWeightLbs, CdA, Crr, dtLoss, headwindMph, rhoLbFt3) => {
      // Convert to metric
      const W = (weightLbs + bikeWeightLbs) * 0.453592; // kg
      const Vgs = speedMph * 0.44704; // m/s
      const Vhw = headwindMph * 0.44704; // m/s
      const G = gradePercent;
      const rho = rhoLbFt3 * 16.0185; // kg/m^3
      const CdAMetric = CdA * 0.092903; // m^2
    
      // Calculate forces
      const Fgravity = 9.8067 * Math.sin(Math.atan(G / 100)) * W;
      const Frolling = 9.8067 * Math.cos(Math.atan(G / 100)) * W * Crr;
      const Vas = Vgs + Vhw;
      const Fdrag = 0.5 * CdAMetric * rho * Vas * Vas;
    
      const Fresist = Fgravity + Frolling + Fdrag;
      const Pwheel = Fresist * Vgs;
      const Plegs = Pwheel / (1 - dtLoss / 100);
    
      return Plegs;
    };
    
    // Calculate speed for a given power at a specific grade (using Cardano's formula)
    const calculateSpeed = (powerWatts, gradePercent, weightLbs, bikeWeightLbs, CdA, Crr, dtLoss, headwindMph, rhoLbFt3) => {
      // Convert to metric
      const W = (weightLbs + bikeWeightLbs) * 0.453592; // kg
      const Vhw = headwindMph * 0.44704; // m/s
      const G = gradePercent;
      const rho = rhoLbFt3 * 16.0185; // kg/m^3
      const CdAMetric = CdA * 0.092903; // m^2
      const P = powerWatts;
    
      // Cubic equation coefficients
      const a = 0.5 * CdAMetric * rho;
      const b = Vhw * CdAMetric * rho;
      const c = (9.8067 * W * (Math.sin(Math.atan(G / 100)) + Crr * Math.cos(Math.atan(G / 100)))) + 
                (0.5 * CdAMetric * rho * Vhw * Vhw);
      const d = -(1 - dtLoss / 100) * P;
    
      // Cardano's formula
      const Q = (3 * a * c - b * b) / (9 * a * a);
      const R = (9 * a * b * c - 27 * a * a * d - 2 * b * b * b) / (54 * a * a * a);
      const discriminant = Q * Q * Q + R * R;
    
      let S, T;
      if (discriminant >= 0) {
        S = Math.cbrt(R + Math.sqrt(discriminant));
        T = Math.cbrt(R - Math.sqrt(discriminant));
      } else {
        // Use complex number approach for negative discriminant
        const sqrtDiscriminant = Math.sqrt(-discriminant);
        S = Math.cbrt(Math.sqrt(R * R + discriminant));
        T = S;
      }
    
      const Vgs = S + T - (b / (3 * a));
      const speedMph = Vgs * 2.23694; // m/s to mph
    
      return speedMph > 0 ? speedMph : 0;
    };
    
    // Generate pacing strategy
    const pacingStrategy = useMemo(() => {
      const { riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho, targetTime } = params;
    
      // Binary search for average power to hit target time
      let powerLow = 100;
      let powerHigh = 600;
      let bestPower = 250;
      let iterations = 0;
    
      while (powerHigh - powerLow > 0.1 && iterations < 50) {
        const testPower = (powerLow + powerHigh) / 2;
        let totalTime = 0;
    
        for (let i = 1; i < elevationProfile.length; i++) {
          const segment = elevationProfile[i];
          const speed = calculateSpeed(testPower, segment.grade, riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho);
          const distance = segment.distance - elevationProfile[i-1].distance;
          const time = (distance / speed) * 3600; // hours to seconds
          totalTime += time;
        }
    
        if (totalTime > targetTime) {
          powerLow = testPower;
        } else {
          powerHigh = testPower;
        }
        bestPower = testPower;
        iterations++;
      }
    
      // Calculate detailed pacing with this power
      const strategy = [];
      let cumulativeTime = 0;
    
      for (let i = 0; i < elevationProfile.length; i++) {
        const segment = elevationProfile[i];
        const speed = i > 0 
          ? calculateSpeed(bestPower, segment.grade, riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho)
          : 0;
    
        const distance = i > 0 ? segment.distance - elevationProfile[i-1].distance : 0;
        const segmentTime = speed > 0 ? (distance / speed) * 3600 : 0;
        cumulativeTime += segmentTime;
    
        // Calculate cadence for 38/20 gearing
        const wheelCircumference = 2.1; // meters (700c wheel)
        const gearRatio = 38 / 20;
        const cadence = speed > 0 ? (speed * 0.44704 * 60) / (wheelCircumference * gearRatio) : 0;
    
        strategy.push({
          distance: segment.distance,
          elevation: segment.elevation,
          grade: segment.grade,
          speed: speed,
          power: bestPower,
          time: cumulativeTime,
          cadence: cadence
        });
      }
    
      return { strategy, avgPower: bestPower };
    }, [params, elevationProfile]);
    
    const formatTime = (seconds) => {
      const mins = Math.floor(seconds / 60);
      const secs = Math.floor(seconds % 60);
      return `${mins}:${secs.toString().padStart(2, '0')}`;
    };
    
    return (
      <div className="w-full max-w-7xl mx-auto p-6 bg-white">
        <h1 className="text-3xl font-bold mb-6 text-gray-800">Climb Pacing Strategy</h1>
    
        <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
          <div className="bg-gray-50 p-4 rounded-lg">
            <h2 className="text-xl font-semibold mb-4">Parameters</h2>
            <div className="space-y-3">
              <div>
                <label className="block text-sm font-medium mb-1">Rider Weight (lbs)</label>
                <input 
                  type="number" 
                  value={params.riderWeight}
                  onChange={(e) => setParams({...params, riderWeight: parseFloat(e.target.value)})}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
              <div>
                <label className="block text-sm font-medium mb-1">Bike Weight (lbs)</label>
                <input 
                  type="number" 
                  value={params.bikeWeight}
                  onChange={(e) => setParams({...params, bikeWeight: parseFloat(e.target.value)})}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
              <div>
                <label className="block text-sm font-medium mb-1">CdA (ft²)</label>
                <input 
                  type="number" 
                  step="0.01"
                  value={params.CdA}
                  onChange={(e) => setParams({...params, CdA: parseFloat(e.target.value)})}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
              <div>
                <label className="block text-sm font-medium mb-1">Target Time (mm:ss)</label>
                <input 
                  type="text" 
                  value={formatTime(params.targetTime)}
                  onChange={(e) => {
                    const [m, s] = e.target.value.split(':').map(Number);
                    if (!isNaN(m) && !isNaN(s)) {
                      setParams({...params, targetTime: m * 60 + s});
                    }
                  }}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
            </div>
          </div>
    
          <div className="bg-blue-50 p-4 rounded-lg">
            <h2 className="text-xl font-semibold mb-4">Target vs Actual</h2>
            <div className="space-y-3">
              <div className="border-b pb-2">
                <div className="text-sm font-medium text-gray-600 mb-1">YOUR BEST EFFORT</div>
                <div className="flex justify-between">
                  <span>Time:</span>
                  <span className="font-bold">18:43</span>
                </div>
                <div className="flex justify-between">
                  <span>Avg Power:</span>
                  <span className="font-bold">214W</span>
                </div>
                <div className="flex justify-between">
                  <span>Avg Speed:</span>
                  <span className="font-bold">9.1 mph</span>
                </div>
              </div>
              <div className="border-b pb-2">
                <div className="text-sm font-medium text-gray-600 mb-1">TARGET (15:08)</div>
                <div className="flex justify-between">
                  <span>Required Power:</span>
                  <span className="text-blue-600 font-bold">{pacingStrategy.avgPower.toFixed(0)}W</span>
                </div>
                <div className="flex justify-between">
                  <span>Avg Speed:</span>
                  <span className="text-blue-600 font-bold">
                    {((2.8 / (pacingStrategy.strategy[pacingStrategy.strategy.length - 1]?.time / 3600)) || 0).toFixed(1)} mph
                  </span>
                </div>
              </div>
              <div className="bg-red-50 p-2 rounded">
                <div className="text-sm font-medium text-gray-600 mb-1">GAP TO CLOSE</div>
                <div className="flex justify-between text-red-600">
                  <span>Time:</span>
                  <span className="font-bold">-3:35 ({((params.actualTime - params.targetTime) / params.actualTime * 100).toFixed(1)}%)</span>
                </div>
                <div className="flex justify-between text-red-600">
                  <span>Power:</span>
                  <span className="font-bold">+{(pacingStrategy.avgPower - params.actualPower).toFixed(0)}W ({((pacingStrategy.avgPower - params.actualPower) / params.actualPower * 100).toFixed(1)}%)</span>
                </div>
              </div>
            </div>
          </div>
        </div>
    
        <div className="mb-8">
          <h2 className="text-xl font-semibold mb-4">Speed Profile</h2>
          <ResponsiveContainer width="100%" height={300}>
            <LineChart data={pacingStrategy.strategy}>
              <CartesianGrid strokeDasharray="3 3" />
              <XAxis dataKey="distance" label={{ value: 'Distance (miles)', position: 'insideBottom', offset: -5 }} />
              <YAxis label={{ value: 'Speed (mph)', angle: -90, position: 'insideLeft' }} />
              <Tooltip />
              <Line type="monotone" dataKey="speed" stroke="#2563eb" strokeWidth={2} name="Speed" />
            </LineChart>
          </ResponsiveContainer>
        </div>
    
        <div className="mb-8">
          <h2 className="text-xl font-semibold mb-4">Elevation & Gradient</h2>
          <ResponsiveContainer width="100%" height={300}>
            <LineChart data={pacingStrategy.strategy}>
              <CartesianGrid strokeDasharray="3 3" />
              <XAxis dataKey="distance" label={{ value: 'Distance (miles)', position: 'insideBottom', offset: -5 }} />
              <YAxis yAxisId="left" label={{ value: 'Elevation (ft)', angle: -90, position: 'insideLeft' }} />
              <YAxis yAxisId="right" orientation="right" label={{ value: 'Gradient (%)', angle: 90, position: 'insideRight' }} />
              <Tooltip />
              <Line yAxisId="left" type="monotone" dataKey="elevation" stroke="#10b981" strokeWidth={2} name="Elevation" />
              <Line yAxisId="right" type="monotone" dataKey="grade" stroke="#ef4444" strokeWidth={2} name="Gradient" />
            </LineChart>
          </ResponsiveContainer>
        </div>
    
        <div>
          <h2 className="text-xl font-semibold mb-4">Detailed Pacing Table</h2>
          <div className="overflow-x-auto">
            <table className="w-full border-collapse border">
              <thead className="bg-gray-100">
                <tr>
                  <th className="border p-2">Distance (mi)</th>
                  <th className="border p-2">Elevation (ft)</th>
                  <th className="border p-2">Grade (%)</th>
                  <th className="border p-2">Speed (mph)</th>
                  <th className="border p-2">Cadence (rpm)</th>
                  <th className="border p-2">Power (W)</th>
                  <th className="border p-2">Time</th>
                </tr>
              </thead>
              <tbody>
                {pacingStrategy.strategy.map((point, idx) => (
                  <tr key={idx} className={idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
                    <td className="border p-2 text-center">{point.distance.toFixed(1)}</td>
                    <td className="border p-2 text-center">{point.elevation.toFixed(0)}</td>
                    <td className="border p-2 text-center">{point.grade.toFixed(1)}</td>
                    <td className="border p-2 text-center">{point.speed.toFixed(1)}</td>
                    <td className="border p-2 text-center">{point.cadence.toFixed(0)}</td>
                    <td className="border p-2 text-center font-semibold">{point.power.toFixed(0)}</td>
                    <td className="border p-2 text-center">{formatTime(point.time)}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>
    
        <div className="mt-6 p-4 bg-yellow-50 rounded-lg">
          <h3 className="font-semibold mb-3">Analysis: What Happened in Your Effort</h3>
          <div className="space-y-3 text-sm text-gray-700">
            <div className="bg-white p-3 rounded border-l-4 border-red-500">
              <strong>Critical Issue: Speed Collapse at 39:00 mark (around mile 2.3-2.5)</strong>
              <p className="mt-1">Your speed dropped dramatically to ~7.3 mph while HR stayed at 168 bpm and cadence dropped to 46 rpm. This is where you lost significant time - you were grinding at unsustainably low cadence on the steepest section.</p>
            </div>
    
            <div className="bg-white p-3 rounded border-l-4 border-yellow-500">
              <strong>Pacing Pattern Analysis:</strong>
              <ul className="mt-1 ml-4 list-disc space-y-1">
                <li><strong>Start (0-26:40):</strong> Consistent ~12-13 mph, 70-90 rpm cadence, ~150 bpm HR - good sustainable pace</li>
                <li><strong>Middle (26:40-35:00):</strong> Speed holding ~11-12 mph, cadence still decent 60-80 rpm, HR climbing to 160s</li>
                <li><strong>Crisis Point (35:00-39:00):</strong> Speed crashes to 7.3 mph, cadence drops to 46 rpm, HR maxed at 168 bpm - you hit the wall on the steepest pitch</li>
                <li><strong>Recovery sections (gaps in data):</strong> Appears you had to stop/soft pedal briefly, then resumed</li>
                <li><strong>Final push (40:00+):</strong> HR surged to 135+ but speed didn't recover - too fatigued</li>
              </ul>
            </div>
    
            <div className="bg-white p-3 rounded border-l-4 border-blue-500">
              <strong>The Singlespeed Problem (38/20 gearing):</strong>
              <p className="mt-1">At 46 rpm on the steep section, you're at 60% of your average cadence. This gear is simply too tall for the 7-8% gradient sections. You're forced to:</p>
              <ul className="mt-1 ml-4 list-disc">
                <li>Mash at very low cadence (high muscular force per pedal stroke)</li>
                <li>This fatigues your legs faster than your cardiovascular system</li>
                <li>Notice your HR wasn't maxed out (168 bpm vs 181 max) - your legs failed before your cardio did</li>
              </ul>
            </div>
    
            <div className="bg-white p-3 rounded border-l-4 border-green-500">
              <strong>Where Time Was Lost:</strong>
              <p className="mt-1">Comparing to target 15:08 pace:</p>
              <ul className="mt-1 ml-4 list-disc">
                <li>First half (miles 0-1.4): Relatively close to target, maybe lost 30-45 seconds</li>
                <li>Steep section (miles 1.4-2.8): Lost 2:30-3:00 here due to the speed collapse</li>
                <li>The 39:00 mark crisis point alone probably cost 1:30-2:00</li>
              </ul>
            </div>
          </div>
        </div>
    
        <div className="mt-4 p-4 bg-red-50 rounded-lg">
          <h3 className="font-semibold mb-2">⚠️ Critical Finding: Gearing is Your Biggest Limiter</h3>
          <div className="space-y-2 text-sm">
            <p>Your data clearly shows the 38/20 singlespeed forced you into an impossible situation on the steep sections. At 214W average power, you should be capable of ~10-11 mph on 7% grades with proper gearing (80-90 rpm). Instead, you were forced to 7.3 mph at 46 rpm.</p>
            <p className="font-semibold mt-2">If you had gears allowing you to maintain 70+ rpm throughout:</p>
            <ul className="ml-4 list-disc">
              <li>You could likely sustain 230-240W instead of 214W (less muscular fatigue)</li>
              <li>You wouldn't have the catastrophic slowdown at 39:00</li>
              <li>Estimated time with same 214W but proper gearing: 16:30-17:15</li>
              <li>With 230W + proper gearing: 15:30-16:00</li>
            </ul>
          </div>
        </div>
    
        <div className="mt-4 p-4 bg-green-50 rounded-lg">
          <h3 className="font-semibold mb-2">Realistic Pathways to 15:08:</h3>
          <div className="space-y-2 text-sm">
            <div className="flex justify-between items-center">
              <span><strong>Option 1:</strong> Switch to gears + maintain current 214W:</span>
              <span className="font-bold text-orange-700">~17:00 (not quite there)</span>
            </div>
            <div className="flex justify-between items-center">
              <span><strong>Option 2:</strong> Switch to gears + increase to 245W:</span>
              <span className="font-bold text-green-700">~15:08 ✓</span>
            </div>
            <div className="flex justify-between items-center">
              <span><strong>Option 3:</strong> Keep singlespeed + increase to 300W+:</span>
              <span className="font-bold text-red-700">~15:08 (extremely difficult, cadence issues persist)</span>
            </div>
            <div className="flex justify-between items-center">
              <span><strong>Option 4:</strong> Gears + lose 15 lbs + 230W:</span>
              <span className="font-bold text-green-700">~15:08 ✓</span>
            </div>
          </div>
        </div>
    
        <div className="mt-4 p-4 bg-gray-50 rounded-lg">
          <h3 className="font-semibold mb-2">Recommended Strategy:</h3>
          <p className="text-sm text-gray-700">
            <strong>Short term (next attempt):</strong> Use a geared bike. Even with the same 214W power, you'll save 1:30-2:00 by avoiding the cadence collapse. Your HR data shows you have more cardiovascular capacity - you just can't access it with that gearing.
          </p>
          <p className="text-sm text-gray-700 mt-2">
            <strong>Medium term (2-3 months):</strong> With gears + focused training to raise sustained power from 214W to 230-245W through sweet spot intervals and VO2max work, 15:08 is very achievable.
          </p>
        </div>
      </div>
    );
    };
    
    export default ClimbPacingCalculator;
    

    Climb Pacing Strategy Calculator ∙ Version 7

    import React, { useState, useMemo } from 'react';
    import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
    
    const ClimbPacingCalculator = () => {
    const [params, setParams] = useState({
      riderWeight: 203,
      bikeWeight: 22,
      CdA: 0.388, // Reasonable default for road position
      Crr: 0.004, // Good tires on smooth pavement
      drivetrainLoss: 2,
      headwind: 0,
      rho: 0.075, // Air density at sea level (lb/ft^3)
      targetTime: 908, // 15:08 in seconds
      actualTime: 1123, // 18:43 actual
      actualPower: 214, // W average
      chainring: 38,
      cog: 20
    });
    
    // Convert elevation profile from image to data points
    const elevationProfile = useMemo(() => {
      const points = [];
      const distances = [0, 0.2, 0.4, 0.6, 0.8, 1.0, 1.2, 1.4, 1.6, 1.8, 2.0, 2.2, 2.4, 2.6, 2.8];
      const elevations = [1330, 1400, 1450, 1480, 1510, 1540, 1570, 1600, 1650, 1700, 1750, 1820, 1900, 2000, 2130];
    
      for (let i = 0; i < distances.length; i++) {
        const grade = i > 0 
          ? ((elevations[i] - elevations[i-1]) / (5280 * (distances[i] - distances[i-1]))) * 100 
          : 0;
        points.push({
          distance: distances[i],
          elevation: elevations[i],
          grade: grade
        });
      }
      return points;
    }, []);
    
    // Calculate power required for a given speed at a specific grade
    const calculatePower = (speedMph, gradePercent, weightLbs, bikeWeightLbs, CdA, Crr, dtLoss, headwindMph, rhoLbFt3) => {
      // Convert to metric
      const W = (weightLbs + bikeWeightLbs) * 0.453592; // kg
      const Vgs = speedMph * 0.44704; // m/s
      const Vhw = headwindMph * 0.44704; // m/s
      const G = gradePercent;
      const rho = rhoLbFt3 * 16.0185; // kg/m^3
      const CdAMetric = CdA * 0.092903; // m^2
    
      // Calculate forces
      const Fgravity = 9.8067 * Math.sin(Math.atan(G / 100)) * W;
      const Frolling = 9.8067 * Math.cos(Math.atan(G / 100)) * W * Crr;
      const Vas = Vgs + Vhw;
      const Fdrag = 0.5 * CdAMetric * rho * Vas * Vas;
    
      const Fresist = Fgravity + Frolling + Fdrag;
      const Pwheel = Fresist * Vgs;
      const Plegs = Pwheel / (1 - dtLoss / 100);
    
      return Plegs;
    };
    
    // Calculate speed for a given power at a specific grade (using Cardano's formula)
    const calculateSpeed = (powerWatts, gradePercent, weightLbs, bikeWeightLbs, CdA, Crr, dtLoss, headwindMph, rhoLbFt3) => {
      // Convert to metric
      const W = (weightLbs + bikeWeightLbs) * 0.453592; // kg
      const Vhw = headwindMph * 0.44704; // m/s
      const G = gradePercent;
      const rho = rhoLbFt3 * 16.0185; // kg/m^3
      const CdAMetric = CdA * 0.092903; // m^2
      const P = powerWatts;
    
      // Cubic equation coefficients
      const a = 0.5 * CdAMetric * rho;
      const b = Vhw * CdAMetric * rho;
      const c = (9.8067 * W * (Math.sin(Math.atan(G / 100)) + Crr * Math.cos(Math.atan(G / 100)))) + 
                (0.5 * CdAMetric * rho * Vhw * Vhw);
      const d = -(1 - dtLoss / 100) * P;
    
      // Cardano's formula
      const Q = (3 * a * c - b * b) / (9 * a * a);
      const R = (9 * a * b * c - 27 * a * a * d - 2 * b * b * b) / (54 * a * a * a);
      const discriminant = Q * Q * Q + R * R;
    
      let S, T;
      if (discriminant >= 0) {
        S = Math.cbrt(R + Math.sqrt(discriminant));
        T = Math.cbrt(R - Math.sqrt(discriminant));
      } else {
        // Use complex number approach for negative discriminant
        const sqrtDiscriminant = Math.sqrt(-discriminant);
        S = Math.cbrt(Math.sqrt(R * R + discriminant));
        T = S;
      }
    
      const Vgs = S + T - (b / (3 * a));
      const speedMph = Vgs * 2.23694; // m/s to mph
    
      return speedMph > 0 ? speedMph : 0;
    };
    
    // Generate pacing strategy
    const pacingStrategy = useMemo(() => {
      const { riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho, targetTime } = params;
    
      // Binary search for average power to hit target time
      let powerLow = 100;
      let powerHigh = 600;
      let bestPower = 250;
      let iterations = 0;
    
      while (powerHigh - powerLow > 0.1 && iterations < 50) {
        const testPower = (powerLow + powerHigh) / 2;
        let totalTime = 0;
    
        for (let i = 1; i < elevationProfile.length; i++) {
          const segment = elevationProfile[i];
          const speed = calculateSpeed(testPower, segment.grade, riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho);
          const distance = segment.distance - elevationProfile[i-1].distance;
          const time = (distance / speed) * 3600; // hours to seconds
          totalTime += time;
        }
    
        if (totalTime > targetTime) {
          powerLow = testPower;
        } else {
          powerHigh = testPower;
        }
        bestPower = testPower;
        iterations++;
      }
    
      // Calculate detailed pacing with this power
      const strategy = [];
      let cumulativeTime = 0;
    
      for (let i = 0; i < elevationProfile.length; i++) {
        const segment = elevationProfile[i];
        const speed = i > 0 
          ? calculateSpeed(bestPower, segment.grade, riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho)
          : 0;
    
        const distance = i > 0 ? segment.distance - elevationProfile[i-1].distance : 0;
        const segmentTime = speed > 0 ? (distance / speed) * 3600 : 0;
        cumulativeTime += segmentTime;
    
        // Calculate cadence for selected gearing
        const wheelCircumference = 2.1; // meters (700c wheel)
        const gearRatio = params.chainring / params.cog;
        const cadence = speed > 0 ? (speed * 0.44704 * 60) / (wheelCircumference * gearRatio) : 0;
    
        strategy.push({
          distance: segment.distance,
          elevation: segment.elevation,
          grade: segment.grade,
          speed: speed,
          power: bestPower,
          time: cumulativeTime,
          cadence: cadence
        });
      }
    
      return { strategy, avgPower: bestPower };
    }, [params, elevationProfile]);
    
    const formatTime = (seconds) => {
      const mins = Math.floor(seconds / 60);
      const secs = Math.floor(seconds % 60);
      return `${mins}:${secs.toString().padStart(2, '0')}`;
    };
    
    return (
      <div className="w-full max-w-7xl mx-auto p-6 bg-white">
        <h1 className="text-3xl font-bold mb-6 text-gray-800">Climb Pacing Strategy</h1>
    
        <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
          <div className="bg-gray-50 p-4 rounded-lg">
            <h2 className="text-xl font-semibold mb-4">Parameters</h2>
            <div className="space-y-3">
              <div>
                <label className="block text-sm font-medium mb-1">Rider Weight (lbs)</label>
                <input 
                  type="number" 
                  value={params.riderWeight}
                  onChange={(e) => setParams({...params, riderWeight: parseFloat(e.target.value)})}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
              <div>
                <label className="block text-sm font-medium mb-1">Bike Weight (lbs)</label>
                <input 
                  type="number" 
                  value={params.bikeWeight}
                  onChange={(e) => setParams({...params, bikeWeight: parseFloat(e.target.value)})}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
              <div>
                <label className="block text-sm font-medium mb-1">CdA (ft²)</label>
                <input 
                  type="number" 
                  step="0.01"
                  value={params.CdA}
                  onChange={(e) => setParams({...params, CdA: parseFloat(e.target.value)})}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
              <div>
                <label className="block text-sm font-medium mb-1">Target Time (mm:ss)</label>
                <input 
                  type="text" 
                  value={formatTime(params.targetTime)}
                  onChange={(e) => {
                    const [m, s] = e.target.value.split(':').map(Number);
                    if (!isNaN(m) && !isNaN(s)) {
                      setParams({...params, targetTime: m * 60 + s});
                    }
                  }}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
            </div>
          </div>
    
          <div className="bg-blue-50 p-4 rounded-lg">
            <h2 className="text-xl font-semibold mb-4">Target vs Actual</h2>
            <div className="space-y-3">
              <div className="border-b pb-2">
                <div className="text-sm font-medium text-gray-600 mb-1">YOUR BEST EFFORT</div>
                <div className="flex justify-between">
                  <span>Time:</span>
                  <span className="font-bold">18:43</span>
                </div>
                <div className="flex justify-between">
                  <span>Avg Power:</span>
                  <span className="font-bold">214W</span>
                </div>
                <div className="flex justify-between">
                  <span>Avg Speed:</span>
                  <span className="font-bold">9.1 mph</span>
                </div>
              </div>
              <div className="border-b pb-2">
                <div className="text-sm font-medium text-gray-600 mb-1">TARGET (15:08)</div>
                <div className="flex justify-between">
                  <span>Required Power:</span>
                  <span className="text-blue-600 font-bold">{pacingStrategy.avgPower.toFixed(0)}W</span>
                </div>
                <div className="flex justify-between">
                  <span>Avg Speed:</span>
                  <span className="text-blue-600 font-bold">
                    {((2.8 / (pacingStrategy.strategy[pacingStrategy.strategy.length - 1]?.time / 3600)) || 0).toFixed(1)} mph
                  </span>
                </div>
              </div>
              <div className="bg-red-50 p-2 rounded">
                <div className="text-sm font-medium text-gray-600 mb-1">GAP TO CLOSE</div>
                <div className="flex justify-between text-red-600">
                  <span>Time:</span>
                  <span className="font-bold">-3:35 ({((params.actualTime - params.targetTime) / params.actualTime * 100).toFixed(1)}%)</span>
                </div>
                <div className="flex justify-between text-red-600">
                  <span>Power:</span>
                  <span className="font-bold">+{(pacingStrategy.avgPower - params.actualPower).toFixed(0)}W ({((pacingStrategy.avgPower - params.actualPower) / params.actualPower * 100).toFixed(1)}%)</span>
                </div>
              </div>
            </div>
          </div>
        </div>
    
        <div className="mb-8">
          <h2 className="text-xl font-semibold mb-4">Speed Profile</h2>
          <ResponsiveContainer width="100%" height={300}>
            <LineChart data={pacingStrategy.strategy}>
              <CartesianGrid strokeDasharray="3 3" />
              <XAxis dataKey="distance" label={{ value: 'Distance (miles)', position: 'insideBottom', offset: -5 }} />
              <YAxis label={{ value: 'Speed (mph)', angle: -90, position: 'insideLeft' }} />
              <Tooltip />
              <Line type="monotone" dataKey="speed" stroke="#2563eb" strokeWidth={2} name="Speed" />
            </LineChart>
          </ResponsiveContainer>
        </div>
    
        <div className="mb-8">
          <h2 className="text-xl font-semibold mb-4">Elevation & Gradient</h2>
          <ResponsiveContainer width="100%" height={300}>
            <LineChart data={pacingStrategy.strategy}>
              <CartesianGrid strokeDasharray="3 3" />
              <XAxis dataKey="distance" label={{ value: 'Distance (miles)', position: 'insideBottom', offset: -5 }} />
              <YAxis yAxisId="left" label={{ value: 'Elevation (ft)', angle: -90, position: 'insideLeft' }} />
              <YAxis yAxisId="right" orientation="right" label={{ value: 'Gradient (%)', angle: 90, position: 'insideRight' }} />
              <Tooltip />
              <Line yAxisId="left" type="monotone" dataKey="elevation" stroke="#10b981" strokeWidth={2} name="Elevation" />
              <Line yAxisId="right" type="monotone" dataKey="grade" stroke="#ef4444" strokeWidth={2} name="Gradient" />
            </LineChart>
          </ResponsiveContainer>
        </div>
    
        <div>
          <h2 className="text-xl font-semibold mb-4">Detailed Pacing Table</h2>
          <div className="overflow-x-auto">
            <table className="w-full border-collapse border">
              <thead className="bg-gray-100">
                <tr>
                  <th className="border p-2">Distance (mi)</th>
                  <th className="border p-2">Elevation (ft)</th>
                  <th className="border p-2">Grade (%)</th>
                  <th className="border p-2">Speed (mph)</th>
                  <th className="border p-2">Cadence (rpm)</th>
                  <th className="border p-2">Power (W)</th>
                  <th className="border p-2">Time</th>
                </tr>
              </thead>
              <tbody>
                {pacingStrategy.strategy.map((point, idx) => (
                  <tr key={idx} className={idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
                    <td className="border p-2 text-center">{point.distance.toFixed(1)}</td>
                    <td className="border p-2 text-center">{point.elevation.toFixed(0)}</td>
                    <td className="border p-2 text-center">{point.grade.toFixed(1)}</td>
                    <td className="border p-2 text-center">{point.speed.toFixed(1)}</td>
                    <td className="border p-2 text-center">{point.cadence.toFixed(0)}</td>
                    <td className="border p-2 text-center font-semibold">{point.power.toFixed(0)}</td>
                    <td className="border p-2 text-center">{formatTime(point.time)}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>
    
        <div className="mt-6 p-4 bg-yellow-50 rounded-lg">
          <h3 className="font-semibold mb-3">Analysis: What Happened in Your Effort</h3>
          <div className="space-y-3 text-sm text-gray-700">
            <div className="bg-white p-3 rounded border-l-4 border-red-500">
              <strong>Critical Issue: Speed Collapse at 39:00 mark (around mile 2.3-2.5)</strong>
              <p className="mt-1">Your speed dropped dramatically to ~7.3 mph while HR stayed at 168 bpm and cadence dropped to 46 rpm. This is where you lost significant time - you were grinding at unsustainably low cadence on the steepest section.</p>
            </div>
    
            <div className="bg-white p-3 rounded border-l-4 border-yellow-500">
              <strong>Pacing Pattern Analysis:</strong>
              <ul className="mt-1 ml-4 list-disc space-y-1">
                <li><strong>Start (0-26:40):</strong> Consistent ~12-13 mph, 70-90 rpm cadence, ~150 bpm HR - good sustainable pace</li>
                <li><strong>Middle (26:40-35:00):</strong> Speed holding ~11-12 mph, cadence still decent 60-80 rpm, HR climbing to 160s</li>
                <li><strong>Crisis Point (35:00-39:00):</strong> Speed crashes to 7.3 mph, cadence drops to 46 rpm, HR maxed at 168 bpm - you hit the wall on the steepest pitch</li>
                <li><strong>Recovery sections (gaps in data):</strong> Appears you had to stop/soft pedal briefly, then resumed</li>
                <li><strong>Final push (40:00+):</strong> HR surged to 135+ but speed didn't recover - too fatigued</li>
              </ul>
            </div>
    
            <div className="bg-white p-3 rounded border-l-4 border-blue-500">
              <strong>The Singlespeed Problem (38/20 gearing):</strong>
              <p className="mt-1">At 46 rpm on the steep section, you're at 60% of your average cadence. This gear is simply too tall for the 7-8% gradient sections. You're forced to:</p>
              <ul className="mt-1 ml-4 list-disc">
                <li>Mash at very low cadence (high muscular force per pedal stroke)</li>
                <li>This fatigues your legs faster than your cardiovascular system</li>
                <li>Notice your HR wasn't maxed out (168 bpm vs 181 max) - your legs failed before your cardio did</li>
              </ul>
            </div>
    
            <div className="bg-white p-3 rounded border-l-4 border-green-500">
              <strong>Where Time Was Lost:</strong>
              <p className="mt-1">Comparing to target 15:08 pace:</p>
              <ul className="mt-1 ml-4 list-disc">
                <li>First half (miles 0-1.4): Relatively close to target, maybe lost 30-45 seconds</li>
                <li>Steep section (miles 1.4-2.8): Lost 2:30-3:00 here due to the speed collapse</li>
                <li>The 39:00 mark crisis point alone probably cost 1:30-2:00</li>
              </ul>
            </div>
          </div>
        </div>
    
        <div className="mt-4 p-4 bg-red-50 rounded-lg">
          <h3 className="font-semibold mb-2">⚠️ Critical Finding: Gearing is Your Biggest Limiter</h3>
          <div className="space-y-2 text-sm">
            <p>Your data clearly shows the 38/20 singlespeed forced you into an impossible situation on the steep sections. At 214W average power, you should be capable of ~10-11 mph on 7% grades with proper gearing (80-90 rpm). Instead, you were forced to 7.3 mph at 46 rpm.</p>
            <p className="font-semibold mt-2">If you had gears allowing you to maintain 70+ rpm throughout:</p>
            <ul className="ml-4 list-disc">
              <li>You could likely sustain 230-240W instead of 214W (less muscular fatigue)</li>
              <li>You wouldn't have the catastrophic slowdown at 39:00</li>
              <li>Estimated time with same 214W but proper gearing: 16:30-17:15</li>
              <li>With 230W + proper gearing: 15:30-16:00</li>
            </ul>
          </div>
        </div>
    
        <div className="mt-4 p-4 bg-green-50 rounded-lg">
          <h3 className="font-semibold mb-2">Realistic Pathways to 15:08:</h3>
          <div className="space-y-2 text-sm">
            <div className="flex justify-between items-center">
              <span><strong>Option 1:</strong> Switch to gears + maintain current 214W:</span>
              <span className="font-bold text-orange-700">~17:00 (not quite there)</span>
            </div>
            <div className="flex justify-between items-center">
              <span><strong>Option 2:</strong> Switch to gears + increase to 245W:</span>
              <span className="font-bold text-green-700">~15:08 ✓</span>
            </div>
            <div className="flex justify-between items-center">
              <span><strong>Option 3:</strong> Keep singlespeed + increase to 300W+:</span>
              <span className="font-bold text-red-700">~15:08 (extremely difficult, cadence issues persist)</span>
            </div>
            <div className="flex justify-between items-center">
              <span><strong>Option 4:</strong> Gears + lose 15 lbs + 230W:</span>
              <span className="font-bold text-green-700">~15:08 ✓</span>
            </div>
          </div>
        </div>
    
        <div className="mt-4 p-4 bg-gray-50 rounded-lg">
          <h3 className="font-semibold mb-2">Recommended Strategy:</h3>
          <p className="text-sm text-gray-700">
            <strong>Short term (next attempt):</strong> Use a geared bike. Even with the same 214W power, you'll save 1:30-2:00 by avoiding the cadence collapse. Your HR data shows you have more cardiovascular capacity - you just can't access it with that gearing.
          </p>
          <p className="text-sm text-gray-700 mt-2">
            <strong>Medium term (2-3 months):</strong> With gears + focused training to raise sustained power from 214W to 230-245W through sweet spot intervals and VO2max work, 15:08 is very achievable.
          </p>
        </div>
      </div>
    );
    };
    
    export default ClimbPacingCalculator;
    

    Climb Pacing Strategy Calculator ∙ Version 8

    import React, { useState, useMemo } from 'react';
    import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
    
    const ClimbPacingCalculator = () => {
    const [params, setParams] = useState({
      riderWeight: 203,
      bikeWeight: 22,
      CdA: 0.388, // Reasonable default for road position
      Crr: 0.004, // Good tires on smooth pavement
      drivetrainLoss: 2,
      headwind: 0,
      rho: 0.075, // Air density at sea level (lb/ft^3)
      targetTime: 908, // 15:08 in seconds
      actualTime: 1123, // 18:43 actual
      actualPower: 214, // W average
      chainring: 38,
      cog: 20
    });
    
    // Convert elevation profile from image to data points
    const elevationProfile = useMemo(() => {
      const points = [];
      const distances = [0, 0.2, 0.4, 0.6, 0.8, 1.0, 1.2, 1.4, 1.6, 1.8, 2.0, 2.2, 2.4, 2.6, 2.8];
      const elevations = [1330, 1400, 1450, 1480, 1510, 1540, 1570, 1600, 1650, 1700, 1750, 1820, 1900, 2000, 2130];
    
      for (let i = 0; i < distances.length; i++) {
        const grade = i > 0 
          ? ((elevations[i] - elevations[i-1]) / (5280 * (distances[i] - distances[i-1]))) * 100 
          : 0;
        points.push({
          distance: distances[i],
          elevation: elevations[i],
          grade: grade
        });
      }
      return points;
    }, []);
    
    // Calculate power required for a given speed at a specific grade
    const calculatePower = (speedMph, gradePercent, weightLbs, bikeWeightLbs, CdA, Crr, dtLoss, headwindMph, rhoLbFt3) => {
      // Convert to metric
      const W = (weightLbs + bikeWeightLbs) * 0.453592; // kg
      const Vgs = speedMph * 0.44704; // m/s
      const Vhw = headwindMph * 0.44704; // m/s
      const G = gradePercent;
      const rho = rhoLbFt3 * 16.0185; // kg/m^3
      const CdAMetric = CdA * 0.092903; // m^2
    
      // Calculate forces
      const Fgravity = 9.8067 * Math.sin(Math.atan(G / 100)) * W;
      const Frolling = 9.8067 * Math.cos(Math.atan(G / 100)) * W * Crr;
      const Vas = Vgs + Vhw;
      const Fdrag = 0.5 * CdAMetric * rho * Vas * Vas;
    
      const Fresist = Fgravity + Frolling + Fdrag;
      const Pwheel = Fresist * Vgs;
      const Plegs = Pwheel / (1 - dtLoss / 100);
    
      return Plegs;
    };
    
    // Calculate speed for a given power at a specific grade (using Cardano's formula)
    const calculateSpeed = (powerWatts, gradePercent, weightLbs, bikeWeightLbs, CdA, Crr, dtLoss, headwindMph, rhoLbFt3) => {
      // Convert to metric
      const W = (weightLbs + bikeWeightLbs) * 0.453592; // kg
      const Vhw = headwindMph * 0.44704; // m/s
      const G = gradePercent;
      const rho = rhoLbFt3 * 16.0185; // kg/m^3
      const CdAMetric = CdA * 0.092903; // m^2
      const P = powerWatts;
    
      // Cubic equation coefficients
      const a = 0.5 * CdAMetric * rho;
      const b = Vhw * CdAMetric * rho;
      const c = (9.8067 * W * (Math.sin(Math.atan(G / 100)) + Crr * Math.cos(Math.atan(G / 100)))) + 
                (0.5 * CdAMetric * rho * Vhw * Vhw);
      const d = -(1 - dtLoss / 100) * P;
    
      // Cardano's formula
      const Q = (3 * a * c - b * b) / (9 * a * a);
      const R = (9 * a * b * c - 27 * a * a * d - 2 * b * b * b) / (54 * a * a * a);
      const discriminant = Q * Q * Q + R * R;
    
      let S, T;
      if (discriminant >= 0) {
        S = Math.cbrt(R + Math.sqrt(discriminant));
        T = Math.cbrt(R - Math.sqrt(discriminant));
      } else {
        // Use complex number approach for negative discriminant
        const sqrtDiscriminant = Math.sqrt(-discriminant);
        S = Math.cbrt(Math.sqrt(R * R + discriminant));
        T = S;
      }
    
      const Vgs = S + T - (b / (3 * a));
      const speedMph = Vgs * 2.23694; // m/s to mph
    
      return speedMph > 0 ? speedMph : 0;
    };
    
    // Generate pacing strategy
    const pacingStrategy = useMemo(() => {
      const { riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho, targetTime } = params;
    
      // Binary search for average power to hit target time
      let powerLow = 100;
      let powerHigh = 600;
      let bestPower = 250;
      let iterations = 0;
    
      while (powerHigh - powerLow > 0.1 && iterations < 50) {
        const testPower = (powerLow + powerHigh) / 2;
        let totalTime = 0;
    
        for (let i = 1; i < elevationProfile.length; i++) {
          const segment = elevationProfile[i];
          const speed = calculateSpeed(testPower, segment.grade, riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho);
          const distance = segment.distance - elevationProfile[i-1].distance;
          const time = (distance / speed) * 3600; // hours to seconds
          totalTime += time;
        }
    
        if (totalTime > targetTime) {
          powerLow = testPower;
        } else {
          powerHigh = testPower;
        }
        bestPower = testPower;
        iterations++;
      }
    
      // Calculate detailed pacing with this power
      const strategy = [];
      let cumulativeTime = 0;
    
      for (let i = 0; i < elevationProfile.length; i++) {
        const segment = elevationProfile[i];
        const speed = i > 0 
          ? calculateSpeed(bestPower, segment.grade, riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho)
          : 0;
    
        const distance = i > 0 ? segment.distance - elevationProfile[i-1].distance : 0;
        const segmentTime = speed > 0 ? (distance / speed) * 3600 : 0;
        cumulativeTime += segmentTime;
    
        // Calculate cadence for selected gearing
        const wheelCircumference = 2.1; // meters (700c wheel)
        const gearRatio = params.chainring / params.cog;
        const cadence = speed > 0 ? (speed * 0.44704 * 60) / (wheelCircumference * gearRatio) : 0;
    
        strategy.push({
          distance: segment.distance,
          elevation: segment.elevation,
          grade: segment.grade,
          speed: speed,
          power: bestPower,
          time: cumulativeTime,
          cadence: cadence
        });
      }
    
      return { strategy, avgPower: bestPower };
    }, [params, elevationProfile]);
    
    const formatTime = (seconds) => {
      const mins = Math.floor(seconds / 60);
      const secs = Math.floor(seconds % 60);
      return `${mins}:${secs.toString().padStart(2, '0')}`;
    };
    
    return (
      <div className="w-full max-w-7xl mx-auto p-6 bg-white">
        <h1 className="text-3xl font-bold mb-6 text-gray-800">Climb Pacing Strategy</h1>
    
        <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
          <div className="bg-gray-50 p-4 rounded-lg">
            <h2 className="text-xl font-semibold mb-4">Parameters</h2>
            <div className="space-y-3">
              <div>
                <label className="block text-sm font-medium mb-1">Rider Weight (lbs)</label>
                <input 
                  type="number" 
                  value={params.riderWeight}
                  onChange={(e) => setParams({...params, riderWeight: parseFloat(e.target.value)})}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
              <div>
                <label className="block text-sm font-medium mb-1">Bike Weight (lbs)</label>
                <input 
                  type="number" 
                  value={params.bikeWeight}
                  onChange={(e) => setParams({...params, bikeWeight: parseFloat(e.target.value)})}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
              <div>
                <label className="block text-sm font-medium mb-1">Chainring</label>
                <input 
                  type="number" 
                  value={params.chainring}
                  onChange={(e) => setParams({...params, chainring: parseFloat(e.target.value)})}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
              <div>
                <label className="block text-sm font-medium mb-1">Rear Cog</label>
                <select 
                  value={params.cog}
                  onChange={(e) => setParams({...params, cog: parseFloat(e.target.value)})}
                  className="w-full px-3 py-2 border rounded"
                >
                  <option value={14}>14t (38/14 = 2.71)</option>
                  <option value={16}>16t (38/16 = 2.38)</option>
                  <option value={18}>18t (38/18 = 2.11)</option>
                  <option value={20}>20t (38/20 = 1.90) - Current</option>
                  <option value={22}>22t (38/22 = 1.73)</option>
                </select>
              </div>
              <div>
                <label className="block text-sm font-medium mb-1">Target Time (mm:ss)</label>
                <input 
                  type="text" 
                  value={formatTime(params.targetTime)}
                  onChange={(e) => {
                    const [m, s] = e.target.value.split(':').map(Number);
                    if (!isNaN(m) && !isNaN(s)) {
                      setParams({...params, targetTime: m * 60 + s});
                    }
                  }}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
            </div>
          </div>
    
          <div className="bg-blue-50 p-4 rounded-lg">
            <h2 className="text-xl font-semibold mb-4">Target vs Actual</h2>
            <div className="space-y-3">
              <div className="border-b pb-2">
                <div className="text-sm font-medium text-gray-600 mb-1">YOUR BEST EFFORT</div>
                <div className="flex justify-between">
                  <span>Time:</span>
                  <span className="font-bold">18:43</span>
                </div>
                <div className="flex justify-between">
                  <span>Avg Power:</span>
                  <span className="font-bold">214W</span>
                </div>
                <div className="flex justify-between">
                  <span>Avg Speed:</span>
                  <span className="font-bold">9.1 mph</span>
                </div>
              </div>
              <div className="border-b pb-2">
                <div className="text-sm font-medium text-gray-600 mb-1">TARGET (15:08)</div>
                <div className="flex justify-between">
                  <span>Required Power:</span>
                  <span className="text-blue-600 font-bold">{pacingStrategy.avgPower.toFixed(0)}W</span>
                </div>
                <div className="flex justify-between">
                  <span>Avg Speed:</span>
                  <span className="text-blue-600 font-bold">
                    {((2.8 / (pacingStrategy.strategy[pacingStrategy.strategy.length - 1]?.time / 3600)) || 0).toFixed(1)} mph
                  </span>
                </div>
              </div>
              <div className="bg-red-50 p-2 rounded">
                <div className="text-sm font-medium text-gray-600 mb-1">GAP TO CLOSE</div>
                <div className="flex justify-between text-red-600">
                  <span>Time:</span>
                  <span className="font-bold">-3:35 ({((params.actualTime - params.targetTime) / params.actualTime * 100).toFixed(1)}%)</span>
                </div>
                <div className="flex justify-between text-red-600">
                  <span>Power:</span>
                  <span className="font-bold">+{(pacingStrategy.avgPower - params.actualPower).toFixed(0)}W ({((pacingStrategy.avgPower - params.actualPower) / params.actualPower * 100).toFixed(1)}%)</span>
                </div>
              </div>
            </div>
          </div>
        </div>
    
        <div className="mb-8">
          <h2 className="text-xl font-semibold mb-4">Speed Profile</h2>
          <ResponsiveContainer width="100%" height={300}>
            <LineChart data={pacingStrategy.strategy}>
              <CartesianGrid strokeDasharray="3 3" />
              <XAxis dataKey="distance" label={{ value: 'Distance (miles)', position: 'insideBottom', offset: -5 }} />
              <YAxis label={{ value: 'Speed (mph)', angle: -90, position: 'insideLeft' }} />
              <Tooltip />
              <Line type="monotone" dataKey="speed" stroke="#2563eb" strokeWidth={2} name="Speed" />
            </LineChart>
          </ResponsiveContainer>
        </div>
    
        <div className="mb-8">
          <h2 className="text-xl font-semibold mb-4">Elevation & Gradient</h2>
          <ResponsiveContainer width="100%" height={300}>
            <LineChart data={pacingStrategy.strategy}>
              <CartesianGrid strokeDasharray="3 3" />
              <XAxis dataKey="distance" label={{ value: 'Distance (miles)', position: 'insideBottom', offset: -5 }} />
              <YAxis yAxisId="left" label={{ value: 'Elevation (ft)', angle: -90, position: 'insideLeft' }} />
              <YAxis yAxisId="right" orientation="right" label={{ value: 'Gradient (%)', angle: 90, position: 'insideRight' }} />
              <Tooltip />
              <Line yAxisId="left" type="monotone" dataKey="elevation" stroke="#10b981" strokeWidth={2} name="Elevation" />
              <Line yAxisId="right" type="monotone" dataKey="grade" stroke="#ef4444" strokeWidth={2} name="Gradient" />
            </LineChart>
          </ResponsiveContainer>
        </div>
    
        <div>
          <h2 className="text-xl font-semibold mb-4">Detailed Pacing Table</h2>
          <div className="overflow-x-auto">
            <table className="w-full border-collapse border">
              <thead className="bg-gray-100">
                <tr>
                  <th className="border p-2">Distance (mi)</th>
                  <th className="border p-2">Elevation (ft)</th>
                  <th className="border p-2">Grade (%)</th>
                  <th className="border p-2">Speed (mph)</th>
                  <th className="border p-2">Cadence (rpm)</th>
                  <th className="border p-2">Power (W)</th>
                  <th className="border p-2">Time</th>
                </tr>
              </thead>
              <tbody>
                {pacingStrategy.strategy.map((point, idx) => (
                  <tr key={idx} className={idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
                    <td className="border p-2 text-center">{point.distance.toFixed(1)}</td>
                    <td className="border p-2 text-center">{point.elevation.toFixed(0)}</td>
                    <td className="border p-2 text-center">{point.grade.toFixed(1)}</td>
                    <td className="border p-2 text-center">{point.speed.toFixed(1)}</td>
                    <td className="border p-2 text-center">{point.cadence.toFixed(0)}</td>
                    <td className="border p-2 text-center font-semibold">{point.power.toFixed(0)}</td>
                    <td className="border p-2 text-center">{formatTime(point.time)}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>
    
        <div className="mt-6 p-4 bg-yellow-50 rounded-lg">
          <h3 className="font-semibold mb-3">Analysis: What Happened in Your Effort</h3>
          <div className="space-y-3 text-sm text-gray-700">
            <div className="bg-white p-3 rounded border-l-4 border-red-500">
              <strong>Critical Issue: Speed Collapse at 39:00 mark (around mile 2.3-2.5)</strong>
              <p className="mt-1">Your speed dropped dramatically to ~7.3 mph while HR stayed at 168 bpm and cadence dropped to 46 rpm. This is where you lost significant time - you were grinding at unsustainably low cadence on the steepest section.</p>
            </div>
    
            <div className="bg-white p-3 rounded border-l-4 border-yellow-500">
              <strong>Pacing Pattern Analysis:</strong>
              <ul className="mt-1 ml-4 list-disc space-y-1">
                <li><strong>Start (0-26:40):</strong> Consistent ~12-13 mph, 70-90 rpm cadence, ~150 bpm HR - good sustainable pace</li>
                <li><strong>Middle (26:40-35:00):</strong> Speed holding ~11-12 mph, cadence still decent 60-80 rpm, HR climbing to 160s</li>
                <li><strong>Crisis Point (35:00-39:00):</strong> Speed crashes to 7.3 mph, cadence drops to 46 rpm, HR maxed at 168 bpm - you hit the wall on the steepest pitch</li>
                <li><strong>Recovery sections (gaps in data):</strong> Appears you had to stop/soft pedal briefly, then resumed</li>
                <li><strong>Final push (40:00+):</strong> HR surged to 135+ but speed didn't recover - too fatigued</li>
              </ul>
            </div>
    
            <div className="bg-white p-3 rounded border-l-4 border-blue-500">
              <strong>The Singlespeed Problem (38/20 gearing):</strong>
              <p className="mt-1">At 46 rpm on the steep section, you're at 60% of your average cadence. This gear is simply too tall for the 7-8% gradient sections. You're forced to:</p>
              <ul className="mt-1 ml-4 list-disc">
                <li>Mash at very low cadence (high muscular force per pedal stroke)</li>
                <li>This fatigues your legs faster than your cardiovascular system</li>
                <li>Notice your HR wasn't maxed out (168 bpm vs 181 max) - your legs failed before your cardio did</li>
              </ul>
            </div>
    
            <div className="bg-white p-3 rounded border-l-4 border-green-500">
              <strong>Where Time Was Lost:</strong>
              <p className="mt-1">Comparing to target 15:08 pace:</p>
              <ul className="mt-1 ml-4 list-disc">
                <li>First half (miles 0-1.4): Relatively close to target, maybe lost 30-45 seconds</li>
                <li>Steep section (miles 1.4-2.8): Lost 2:30-3:00 here due to the speed collapse</li>
                <li>The 39:00 mark crisis point alone probably cost 1:30-2:00</li>
              </ul>
            </div>
          </div>
        </div>
    
        <div className="mt-4 p-4 bg-red-50 rounded-lg">
          <h3 className="font-semibold mb-2">⚠️ Critical Finding: Gearing is Your Biggest Limiter</h3>
          <div className="space-y-2 text-sm">
            <p>Your data clearly shows the 38/20 singlespeed forced you into an impossible situation on the steep sections. At 214W average power, you should be capable of ~10-11 mph on 7% grades with proper gearing (80-90 rpm). Instead, you were forced to 7.3 mph at 46 rpm.</p>
            <p className="font-semibold mt-2">If you had gears allowing you to maintain 70+ rpm throughout:</p>
            <ul className="ml-4 list-disc">
              <li>You could likely sustain 230-240W instead of 214W (less muscular fatigue)</li>
              <li>You wouldn't have the catastrophic slowdown at 39:00</li>
              <li>Estimated time with same 214W but proper gearing: 16:30-17:15</li>
              <li>With 230W + proper gearing: 15:30-16:00</li>
            </ul>
          </div>
        </div>
    
        <div className="mt-4 p-4 bg-green-50 rounded-lg">
          <h3 className="font-semibold mb-2">Realistic Pathways to 15:08:</h3>
          <div className="space-y-2 text-sm">
            <div className="flex justify-between items-center">
              <span><strong>Option 1:</strong> Switch to gears + maintain current 214W:</span>
              <span className="font-bold text-orange-700">~17:00 (not quite there)</span>
            </div>
            <div className="flex justify-between items-center">
              <span><strong>Option 2:</strong> Switch to gears + increase to 245W:</span>
              <span className="font-bold text-green-700">~15:08 ✓</span>
            </div>
            <div className="flex justify-between items-center">
              <span><strong>Option 3:</strong> Keep singlespeed + increase to 300W+:</span>
              <span className="font-bold text-red-700">~15:08 (extremely difficult, cadence issues persist)</span>
            </div>
            <div className="flex justify-between items-center">
              <span><strong>Option 4:</strong> Gears + lose 15 lbs + 230W:</span>
              <span className="font-bold text-green-700">~15:08 ✓</span>
            </div>
          </div>
        </div>
    
        <div className="mt-4 p-4 bg-gray-50 rounded-lg">
          <h3 className="font-semibold mb-2">Recommended Strategy:</h3>
          <p className="text-sm text-gray-700">
            <strong>Short term (next attempt):</strong> Use a geared bike. Even with the same 214W power, you'll save 1:30-2:00 by avoiding the cadence collapse. Your HR data shows you have more cardiovascular capacity - you just can't access it with that gearing.
          </p>
          <p className="text-sm text-gray-700 mt-2">
            <strong>Medium term (2-3 months):</strong> With gears + focused training to raise sustained power from 214W to 230-245W through sweet spot intervals and VO2max work, 15:08 is very achievable.
          </p>
        </div>
      </div>
    );
    };
    
    export default ClimbPacingCalculator;
    

    Climb Pacing Strategy Calculator ∙ Version 9

    import React, { useState, useMemo } from 'react';
    import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
    
    const ClimbPacingCalculator = () => {
    const [params, setParams] = useState({
      riderWeight: 203,
      bikeWeight: 22,
      CdA: 0.388, // Reasonable default for road position
      Crr: 0.004, // Good tires on smooth pavement
      drivetrainLoss: 2,
      headwind: 0,
      rho: 0.075, // Air density at sea level (lb/ft^3)
      targetTime: 908, // 15:08 in seconds
      actualTime: 1123, // 18:43 actual
      actualPower: 214, // W average
      chainring: 38,
      cog: 20
    });
    
    // Convert elevation profile from image to data points
    const elevationProfile = useMemo(() => {
      const points = [];
      const distances = [0, 0.2, 0.4, 0.6, 0.8, 1.0, 1.2, 1.4, 1.6, 1.8, 2.0, 2.2, 2.4, 2.6, 2.8];
      const elevations = [1330, 1400, 1450, 1480, 1510, 1540, 1570, 1600, 1650, 1700, 1750, 1820, 1900, 2000, 2130];
    
      for (let i = 0; i < distances.length; i++) {
        const grade = i > 0 
          ? ((elevations[i] - elevations[i-1]) / (5280 * (distances[i] - distances[i-1]))) * 100 
          : 0;
        points.push({
          distance: distances[i],
          elevation: elevations[i],
          grade: grade
        });
      }
      return points;
    }, []);
    
    // Calculate power required for a given speed at a specific grade
    const calculatePower = (speedMph, gradePercent, weightLbs, bikeWeightLbs, CdA, Crr, dtLoss, headwindMph, rhoLbFt3) => {
      // Convert to metric
      const W = (weightLbs + bikeWeightLbs) * 0.453592; // kg
      const Vgs = speedMph * 0.44704; // m/s
      const Vhw = headwindMph * 0.44704; // m/s
      const G = gradePercent;
      const rho = rhoLbFt3 * 16.0185; // kg/m^3
      const CdAMetric = CdA * 0.092903; // m^2
    
      // Calculate forces
      const Fgravity = 9.8067 * Math.sin(Math.atan(G / 100)) * W;
      const Frolling = 9.8067 * Math.cos(Math.atan(G / 100)) * W * Crr;
      const Vas = Vgs + Vhw;
      const Fdrag = 0.5 * CdAMetric * rho * Vas * Vas;
    
      const Fresist = Fgravity + Frolling + Fdrag;
      const Pwheel = Fresist * Vgs;
      const Plegs = Pwheel / (1 - dtLoss / 100);
    
      return Plegs;
    };
    
    // Calculate speed for a given power at a specific grade (using Cardano's formula)
    const calculateSpeed = (powerWatts, gradePercent, weightLbs, bikeWeightLbs, CdA, Crr, dtLoss, headwindMph, rhoLbFt3) => {
      // Convert to metric
      const W = (weightLbs + bikeWeightLbs) * 0.453592; // kg
      const Vhw = headwindMph * 0.44704; // m/s
      const G = gradePercent;
      const rho = rhoLbFt3 * 16.0185; // kg/m^3
      const CdAMetric = CdA * 0.092903; // m^2
      const P = powerWatts;
    
      // Cubic equation coefficients
      const a = 0.5 * CdAMetric * rho;
      const b = Vhw * CdAMetric * rho;
      const c = (9.8067 * W * (Math.sin(Math.atan(G / 100)) + Crr * Math.cos(Math.atan(G / 100)))) + 
                (0.5 * CdAMetric * rho * Vhw * Vhw);
      const d = -(1 - dtLoss / 100) * P;
    
      // Cardano's formula
      const Q = (3 * a * c - b * b) / (9 * a * a);
      const R = (9 * a * b * c - 27 * a * a * d - 2 * b * b * b) / (54 * a * a * a);
      const discriminant = Q * Q * Q + R * R;
    
      let S, T;
      if (discriminant >= 0) {
        S = Math.cbrt(R + Math.sqrt(discriminant));
        T = Math.cbrt(R - Math.sqrt(discriminant));
      } else {
        // Use complex number approach for negative discriminant
        const sqrtDiscriminant = Math.sqrt(-discriminant);
        S = Math.cbrt(Math.sqrt(R * R + discriminant));
        T = S;
      }
    
      const Vgs = S + T - (b / (3 * a));
      const speedMph = Vgs * 2.23694; // m/s to mph
    
      return speedMph > 0 ? speedMph : 0;
    };
    
    // Generate pacing strategy
    const pacingStrategy = useMemo(() => {
      const { riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho, targetTime } = params;
    
      // Binary search for average power to hit target time
      let powerLow = 100;
      let powerHigh = 600;
      let bestPower = 250;
      let iterations = 0;
    
      while (powerHigh - powerLow > 0.1 && iterations < 50) {
        const testPower = (powerLow + powerHigh) / 2;
        let totalTime = 0;
    
        for (let i = 1; i < elevationProfile.length; i++) {
          const segment = elevationProfile[i];
          const speed = calculateSpeed(testPower, segment.grade, riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho);
          const distance = segment.distance - elevationProfile[i-1].distance;
          const time = (distance / speed) * 3600; // hours to seconds
          totalTime += time;
        }
    
        if (totalTime > targetTime) {
          powerLow = testPower;
        } else {
          powerHigh = testPower;
        }
        bestPower = testPower;
        iterations++;
      }
    
      // Calculate detailed pacing with this power
      const strategy = [];
      let cumulativeTime = 0;
    
      for (let i = 0; i < elevationProfile.length; i++) {
        const segment = elevationProfile[i];
        const speed = i > 0 
          ? calculateSpeed(bestPower, segment.grade, riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho)
          : 0;
    
        const distance = i > 0 ? segment.distance - elevationProfile[i-1].distance : 0;
        const segmentTime = speed > 0 ? (distance / speed) * 3600 : 0;
        cumulativeTime += segmentTime;
    
        // Calculate cadence for selected gearing
        const wheelCircumference = 2.1; // meters (700c wheel)
        const gearRatio = params.chainring / params.cog;
        const cadence = speed > 0 ? (speed * 0.44704 * 60) / (wheelCircumference * gearRatio) : 0;
    
        strategy.push({
          distance: segment.distance,
          elevation: segment.elevation,
          grade: segment.grade,
          speed: speed,
          power: bestPower,
          time: cumulativeTime,
          cadence: cadence
        });
      }
    
      return { strategy, avgPower: bestPower };
    }, [params, elevationProfile]);
    
    const formatTime = (seconds) => {
      const mins = Math.floor(seconds / 60);
      const secs = Math.floor(seconds % 60);
      return `${mins}:${secs.toString().padStart(2, '0')}`;
    };
    
    // Generate gear comparison analysis
    const gearComparison = useMemo(() => {
      const cogs = [14, 16, 18, 20, 22];
      const { riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho, actualPower, chainring } = params;
    
      return cogs.map(cog => {
        const gearRatio = chainring / cog;
        const wheelCircumference = 2.1;
    
        // Calculate performance at different gradient sections
        const gradients = [3, 5, 7, 8]; // Representative gradients
        const results = gradients.map(grade => {
          const speed = calculateSpeed(actualPower, grade, riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho);
          const cadence = (speed * 0.44704 * 60) / (wheelCircumference * gearRatio);
          return { grade, speed, cadence };
        });
    
        // Calculate overall time estimate
        let totalTime = 0;
        for (let i = 1; i < elevationProfile.length; i++) {
          const segment = elevationProfile[i];
          const speed = calculateSpeed(actualPower, segment.grade, riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho);
          const distance = segment.distance - elevationProfile[i-1].distance;
          const time = (distance / speed) * 3600;
          totalTime += time;
        }
    
        const avgCadence = results.reduce((sum, r) => sum + r.cadence, 0) / results.length;
        const minCadence = Math.min(...results.map(r => r.cadence));
    
        return {
          cog,
          gearRatio,
          results,
          totalTime,
          avgCadence,
          minCadence,
          isCurrent: cog === params.cog
        };
      });
    }, [params, elevationProfile, calculateSpeed]);
    
    return (
      <div className="w-full max-w-7xl mx-auto p-6 bg-white">
        <h1 className="text-3xl font-bold mb-6 text-gray-800">Climb Pacing Strategy</h1>
    
        {/* Gear comparison section */}
        <div className="mb-8 p-4 bg-blue-50 rounded-lg">
          <h2 className="text-2xl font-semibold mb-4">Gear Comparison (38t chainring with your available cogs)</h2>
          <p className="text-sm text-gray-700 mb-4">All calculated at your actual 214W power. Shows cadence at different gradients and estimated total time.</p>
    
          <div className="overflow-x-auto">
            <table className="w-full border-collapse border bg-white">
              <thead className="bg-gray-100">
                <tr>
                  <th className="border p-2">Cog</th>
                  <th className="border p-2">Ratio</th>
                  <th className="border p-2">Cadence @ 3%</th>
                  <th className="border p-2">Cadence @ 5%</th>
                  <th className="border p-2">Cadence @ 7%</th>
                  <th className="border p-2">Cadence @ 8%</th>
                  <th className="border p-2">Min Cadence</th>
                  <th className="border p-2">Est. Time</th>
                  <th className="border p-2">vs Current</th>
                </tr>
              </thead>
              <tbody>
                {gearComparison.map((gear) => (
                  <tr key={gear.cog} className={gear.isCurrent ? 'bg-yellow-100 font-semibold' : gear.minCadence >= 60 && gear.minCadence <= 90 ? 'bg-green-50' : 'bg-white'}>
                    <td className="border p-2 text-center">
                      {gear.cog}t {gear.isCurrent && '(current)'}
                    </td>
                    <td className="border p-2 text-center">{gear.gearRatio.toFixed(2)}</td>
                    <td className="border p-2 text-center">{gear.results[0].cadence.toFixed(0)} rpm</td>
                    <td className="border p-2 text-center">{gear.results[1].cadence.toFixed(0)} rpm</td>
                    <td className="border p-2 text-center">{gear.results[2].cadence.toFixed(0)} rpm</td>
                    <td className="border p-2 text-center">{gear.results[3].cadence.toFixed(0)} rpm</td>
                    <td className="border p-2 text-center font-semibold">{gear.minCadence.toFixed(0)} rpm</td>
                    <td className="border p-2 text-center">{formatTime(gear.totalTime)}</td>
                    <td className="border p-2 text-center">
                      {gear.isCurrent ? '—' : (
                        <span className={gear.totalTime < params.actualTime ? 'text-green-600' : 'text-red-600'}>
                          {gear.totalTime < params.actualTime ? '-' : '+'}{formatTime(Math.abs(gear.totalTime - params.actualTime))}
                        </span>
                      )}
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
    
          <div className="mt-4 p-3 bg-white rounded border-l-4 border-green-500">
            <p className="text-sm font-semibold"> Green highlighted rows = optimal cadence range (60-90 rpm minimum)</p>
            <p className="text-sm mt-1">The gear that keeps you in this range on the steepest sections will be most efficient.</p>
          </div>
        </div>
    
        <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
          <div className="bg-gray-50 p-4 rounded-lg">
            <h2 className="text-xl font-semibold mb-4">Parameters</h2>
            <div className="space-y-3">
              <div>
                <label className="block text-sm font-medium mb-1">Rider Weight (lbs)</label>
                <input 
                  type="number" 
                  value={params.riderWeight}
                  onChange={(e) => setParams({...params, riderWeight: parseFloat(e.target.value)})}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
              <div>
                <label className="block text-sm font-medium mb-1">Bike Weight (lbs)</label>
                <input 
                  type="number" 
                  value={params.bikeWeight}
                  onChange={(e) => setParams({...params, bikeWeight: parseFloat(e.target.value)})}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
              <div>
                <label className="block text-sm font-medium mb-1">Chainring</label>
                <input 
                  type="number" 
                  value={params.chainring}
                  onChange={(e) => setParams({...params, chainring: parseFloat(e.target.value)})}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
              <div>
                <label className="block text-sm font-medium mb-1">Rear Cog</label>
                <select 
                  value={params.cog}
                  onChange={(e) => setParams({...params, cog: parseFloat(e.target.value)})}
                  className="w-full px-3 py-2 border rounded"
                >
                  <option value={14}>14t (38/14 = 2.71)</option>
                  <option value={16}>16t (38/16 = 2.38)</option>
                  <option value={18}>18t (38/18 = 2.11)</option>
                  <option value={20}>20t (38/20 = 1.90) - Current</option>
                  <option value={22}>22t (38/22 = 1.73)</option>
                </select>
              </div>
              <div>
                <label className="block text-sm font-medium mb-1">Target Time (mm:ss)</label>
                <input 
                  type="text" 
                  value={formatTime(params.targetTime)}
                  onChange={(e) => {
                    const [m, s] = e.target.value.split(':').map(Number);
                    if (!isNaN(m) && !isNaN(s)) {
                      setParams({...params, targetTime: m * 60 + s});
                    }
                  }}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
            </div>
          </div>
    
          <div className="bg-blue-50 p-4 rounded-lg">
            <h2 className="text-xl font-semibold mb-4">Target vs Actual</h2>
            <div className="space-y-3">
              <div className="border-b pb-2">
                <div className="text-sm font-medium text-gray-600 mb-1">YOUR BEST EFFORT</div>
                <div className="flex justify-between">
                  <span>Time:</span>
                  <span className="font-bold">18:43</span>
                </div>
                <div className="flex justify-between">
                  <span>Avg Power:</span>
                  <span className="font-bold">214W</span>
                </div>
                <div className="flex justify-between">
                  <span>Avg Speed:</span>
                  <span className="font-bold">9.1 mph</span>
                </div>
              </div>
              <div className="border-b pb-2">
                <div className="text-sm font-medium text-gray-600 mb-1">TARGET (15:08)</div>
                <div className="flex justify-between">
                  <span>Required Power:</span>
                  <span className="text-blue-600 font-bold">{pacingStrategy.avgPower.toFixed(0)}W</span>
                </div>
                <div className="flex justify-between">
                  <span>Avg Speed:</span>
                  <span className="text-blue-600 font-bold">
                    {((2.8 / (pacingStrategy.strategy[pacingStrategy.strategy.length - 1]?.time / 3600)) || 0).toFixed(1)} mph
                  </span>
                </div>
              </div>
              <div className="bg-red-50 p-2 rounded">
                <div className="text-sm font-medium text-gray-600 mb-1">GAP TO CLOSE</div>
                <div className="flex justify-between text-red-600">
                  <span>Time:</span>
                  <span className="font-bold">-3:35 ({((params.actualTime - params.targetTime) / params.actualTime * 100).toFixed(1)}%)</span>
                </div>
                <div className="flex justify-between text-red-600">
                  <span>Power:</span>
                  <span className="font-bold">+{(pacingStrategy.avgPower - params.actualPower).toFixed(0)}W ({((pacingStrategy.avgPower - params.actualPower) / params.actualPower * 100).toFixed(1)}%)</span>
                </div>
              </div>
            </div>
          </div>
        </div>
    
        <div className="mb-8">
          <h2 className="text-xl font-semibold mb-4">Speed Profile</h2>
          <ResponsiveContainer width="100%" height={300}>
            <LineChart data={pacingStrategy.strategy}>
              <CartesianGrid strokeDasharray="3 3" />
              <XAxis dataKey="distance" label={{ value: 'Distance (miles)', position: 'insideBottom', offset: -5 }} />
              <YAxis label={{ value: 'Speed (mph)', angle: -90, position: 'insideLeft' }} />
              <Tooltip />
              <Line type="monotone" dataKey="speed" stroke="#2563eb" strokeWidth={2} name="Speed" />
            </LineChart>
          </ResponsiveContainer>
        </div>
    
        <div className="mb-8">
          <h2 className="text-xl font-semibold mb-4">Elevation & Gradient</h2>
          <ResponsiveContainer width="100%" height={300}>
            <LineChart data={pacingStrategy.strategy}>
              <CartesianGrid strokeDasharray="3 3" />
              <XAxis dataKey="distance" label={{ value: 'Distance (miles)', position: 'insideBottom', offset: -5 }} />
              <YAxis yAxisId="left" label={{ value: 'Elevation (ft)', angle: -90, position: 'insideLeft' }} />
              <YAxis yAxisId="right" orientation="right" label={{ value: 'Gradient (%)', angle: 90, position: 'insideRight' }} />
              <Tooltip />
              <Line yAxisId="left" type="monotone" dataKey="elevation" stroke="#10b981" strokeWidth={2} name="Elevation" />
              <Line yAxisId="right" type="monotone" dataKey="grade" stroke="#ef4444" strokeWidth={2} name="Gradient" />
            </LineChart>
          </ResponsiveContainer>
        </div>
    
        <div>
          <h2 className="text-xl font-semibold mb-4">Detailed Pacing Table</h2>
          <div className="overflow-x-auto">
            <table className="w-full border-collapse border">
              <thead className="bg-gray-100">
                <tr>
                  <th className="border p-2">Distance (mi)</th>
                  <th className="border p-2">Elevation (ft)</th>
                  <th className="border p-2">Grade (%)</th>
                  <th className="border p-2">Speed (mph)</th>
                  <th className="border p-2">Cadence (rpm)</th>
                  <th className="border p-2">Power (W)</th>
                  <th className="border p-2">Time</th>
                </tr>
              </thead>
              <tbody>
                {pacingStrategy.strategy.map((point, idx) => (
                  <tr key={idx} className={idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
                    <td className="border p-2 text-center">{point.distance.toFixed(1)}</td>
                    <td className="border p-2 text-center">{point.elevation.toFixed(0)}</td>
                    <td className="border p-2 text-center">{point.grade.toFixed(1)}</td>
                    <td className="border p-2 text-center">{point.speed.toFixed(1)}</td>
                    <td className="border p-2 text-center">{point.cadence.toFixed(0)}</td>
                    <td className="border p-2 text-center font-semibold">{point.power.toFixed(0)}</td>
                    <td className="border p-2 text-center">{formatTime(point.time)}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>
    
        <div className="mt-6 p-4 bg-yellow-50 rounded-lg">
          <h3 className="font-semibold mb-3">Analysis: What Happened in Your Effort</h3>
          <div className="space-y-3 text-sm text-gray-700">
            <div className="bg-white p-3 rounded border-l-4 border-red-500">
              <strong>Critical Issue: Speed Collapse at 39:00 mark (around mile 2.3-2.5)</strong>
              <p className="mt-1">Your speed dropped dramatically to ~7.3 mph while HR stayed at 168 bpm and cadence dropped to 46 rpm. This is where you lost significant time - you were grinding at unsustainably low cadence on the steepest section.</p>
            </div>
    
            <div className="bg-white p-3 rounded border-l-4 border-yellow-500">
              <strong>Pacing Pattern Analysis:</strong>
              <ul className="mt-1 ml-4 list-disc space-y-1">
                <li><strong>Start (0-26:40):</strong> Consistent ~12-13 mph, 70-90 rpm cadence, ~150 bpm HR - good sustainable pace</li>
                <li><strong>Middle (26:40-35:00):</strong> Speed holding ~11-12 mph, cadence still decent 60-80 rpm, HR climbing to 160s</li>
                <li><strong>Crisis Point (35:00-39:00):</strong> Speed crashes to 7.3 mph, cadence drops to 46 rpm, HR maxed at 168 bpm - you hit the wall on the steepest pitch</li>
                <li><strong>Recovery sections (gaps in data):</strong> Appears you had to stop/soft pedal briefly, then resumed</li>
                <li><strong>Final push (40:00+):</strong> HR surged to 135+ but speed didn't recover - too fatigued</li>
              </ul>
            </div>
    
            <div className="bg-white p-3 rounded border-l-4 border-blue-500">
              <strong>The Singlespeed Problem (38/20 gearing):</strong>
              <p className="mt-1">At 46 rpm on the steep section, you're at 60% of your average cadence. This gear is simply too tall for the 7-8% gradient sections. You're forced to:</p>
              <ul className="mt-1 ml-4 list-disc">
                <li>Mash at very low cadence (high muscular force per pedal stroke)</li>
                <li>This fatigues your legs faster than your cardiovascular system</li>
                <li>Notice your HR wasn't maxed out (168 bpm vs 181 max) - your legs failed before your cardio did</li>
              </ul>
            </div>
    
            <div className="bg-white p-3 rounded border-l-4 border-green-500">
              <strong>Where Time Was Lost:</strong>
              <p className="mt-1">Comparing to target 15:08 pace:</p>
              <ul className="mt-1 ml-4 list-disc">
                <li>First half (miles 0-1.4): Relatively close to target, maybe lost 30-45 seconds</li>
                <li>Steep section (miles 1.4-2.8): Lost 2:30-3:00 here due to the speed collapse</li>
                <li>The 39:00 mark crisis point alone probably cost 1:30-2:00</li>
              </ul>
            </div>
          </div>
        </div>
    
        <div className="mt-4 p-4 bg-red-50 rounded-lg">
          <h3 className="font-semibold mb-2">⚠️ Critical Finding: Gearing is Your Biggest Limiter</h3>
          <div className="space-y-2 text-sm">
            <p>Your data clearly shows the 38/20 singlespeed forced you into an impossible situation on the steep sections. At 214W average power, you should be capable of ~10-11 mph on 7% grades with proper gearing (80-90 rpm). Instead, you were forced to 7.3 mph at 46 rpm.</p>
            <p className="font-semibold mt-2">If you had gears allowing you to maintain 70+ rpm throughout:</p>
            <ul className="ml-4 list-disc">
              <li>You could likely sustain 230-240W instead of 214W (less muscular fatigue)</li>
              <li>You wouldn't have the catastrophic slowdown at 39:00</li>
              <li>Estimated time with same 214W but proper gearing: 16:30-17:15</li>
              <li>With 230W + proper gearing: 15:30-16:00</li>
            </ul>
          </div>
        </div>
    
        <div className="mt-4 p-4 bg-green-50 rounded-lg">
          <h3 className="font-semibold mb-2">Realistic Pathways to 15:08:</h3>
          <div className="space-y-2 text-sm">
            <div className="flex justify-between items-center">
              <span><strong>Option 1:</strong> Switch to gears + maintain current 214W:</span>
              <span className="font-bold text-orange-700">~17:00 (not quite there)</span>
            </div>
            <div className="flex justify-between items-center">
              <span><strong>Option 2:</strong> Switch to gears + increase to 245W:</span>
              <span className="font-bold text-green-700">~15:08 ✓</span>
            </div>
            <div className="flex justify-between items-center">
              <span><strong>Option 3:</strong> Keep singlespeed + increase to 300W+:</span>
              <span className="font-bold text-red-700">~15:08 (extremely difficult, cadence issues persist)</span>
            </div>
            <div className="flex justify-between items-center">
              <span><strong>Option 4:</strong> Gears + lose 15 lbs + 230W:</span>
              <span className="font-bold text-green-700">~15:08 ✓</span>
            </div>
          </div>
        </div>
    
        <div className="mt-4 p-4 bg-gray-50 rounded-lg">
          <h3 className="font-semibold mb-2">Recommended Strategy:</h3>
          <p className="text-sm text-gray-700">
            <strong>Short term (next attempt):</strong> Use a geared bike. Even with the same 214W power, you'll save 1:30-2:00 by avoiding the cadence collapse. Your HR data shows you have more cardiovascular capacity - you just can't access it with that gearing.
          </p>
          <p className="text-sm text-gray-700 mt-2">
            <strong>Medium term (2-3 months):</strong> With gears + focused training to raise sustained power from 214W to 230-245W through sweet spot intervals and VO2max work, 15:08 is very achievable.
          </p>
        </div>
      </div>
    );
    };
    
    export default ClimbPacingCalculator;
    

    Climb Pacing Strategy Calculator ∙ Version 10

    import React, { useState, useMemo } from 'react';
    import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
    
    const ClimbPacingCalculator = () => {
    const [params, setParams] = useState({
      riderWeight: 203,
      bikeWeight: 22,
      CdA: 0.388, // Reasonable default for road position
      Crr: 0.004, // Good tires on smooth pavement
      drivetrainLoss: 2,
      headwind: 0,
      rho: 0.075, // Air density at sea level (lb/ft^3)
      targetTime: 908, // 15:08 in seconds
      actualTime: 1123, // 18:43 actual
      actualPower: 214, // W average
      chainring: 38,
      cog: 20
    });
    
    // Convert elevation profile from image to data points
    const elevationProfile = useMemo(() => {
      const points = [];
      const distances = [0, 0.2, 0.4, 0.6, 0.8, 1.0, 1.2, 1.4, 1.6, 1.8, 2.0, 2.2, 2.4, 2.6, 2.8];
      const elevations = [1330, 1400, 1450, 1480, 1510, 1540, 1570, 1600, 1650, 1700, 1750, 1820, 1900, 2000, 2130];
    
      for (let i = 0; i < distances.length; i++) {
        const grade = i > 0 
          ? ((elevations[i] - elevations[i-1]) / (5280 * (distances[i] - distances[i-1]))) * 100 
          : 0;
        points.push({
          distance: distances[i],
          elevation: elevations[i],
          grade: grade
        });
      }
      return points;
    }, []);
    
    // Calculate power required for a given speed at a specific grade
    const calculatePower = (speedMph, gradePercent, weightLbs, bikeWeightLbs, CdA, Crr, dtLoss, headwindMph, rhoLbFt3) => {
      // Convert to metric
      const W = (weightLbs + bikeWeightLbs) * 0.453592; // kg
      const Vgs = speedMph * 0.44704; // m/s
      const Vhw = headwindMph * 0.44704; // m/s
      const G = gradePercent;
      const rho = rhoLbFt3 * 16.0185; // kg/m^3
      const CdAMetric = CdA * 0.092903; // m^2
    
      // Calculate forces
      const Fgravity = 9.8067 * Math.sin(Math.atan(G / 100)) * W;
      const Frolling = 9.8067 * Math.cos(Math.atan(G / 100)) * W * Crr;
      const Vas = Vgs + Vhw;
      const Fdrag = 0.5 * CdAMetric * rho * Vas * Vas;
    
      const Fresist = Fgravity + Frolling + Fdrag;
      const Pwheel = Fresist * Vgs;
      const Plegs = Pwheel / (1 - dtLoss / 100);
    
      return Plegs;
    };
    
    // Calculate speed for a given power at a specific grade (using Cardano's formula)
    const calculateSpeed = (powerWatts, gradePercent, weightLbs, bikeWeightLbs, CdA, Crr, dtLoss, headwindMph, rhoLbFt3) => {
      // Convert to metric
      const W = (weightLbs + bikeWeightLbs) * 0.453592; // kg
      const Vhw = headwindMph * 0.44704; // m/s
      const G = gradePercent;
      const rho = rhoLbFt3 * 16.0185; // kg/m^3
      const CdAMetric = CdA * 0.092903; // m^2
      const P = powerWatts;
    
      // Cubic equation coefficients
      const a = 0.5 * CdAMetric * rho;
      const b = Vhw * CdAMetric * rho;
      const c = (9.8067 * W * (Math.sin(Math.atan(G / 100)) + Crr * Math.cos(Math.atan(G / 100)))) + 
                (0.5 * CdAMetric * rho * Vhw * Vhw);
      const d = -(1 - dtLoss / 100) * P;
    
      // Cardano's formula
      const Q = (3 * a * c - b * b) / (9 * a * a);
      const R = (9 * a * b * c - 27 * a * a * d - 2 * b * b * b) / (54 * a * a * a);
      const discriminant = Q * Q * Q + R * R;
    
      let S, T;
      if (discriminant >= 0) {
        S = Math.cbrt(R + Math.sqrt(discriminant));
        T = Math.cbrt(R - Math.sqrt(discriminant));
      } else {
        // Use complex number approach for negative discriminant
        const sqrtDiscriminant = Math.sqrt(-discriminant);
        S = Math.cbrt(Math.sqrt(R * R + discriminant));
        T = S;
      }
    
      const Vgs = S + T - (b / (3 * a));
      const speedMph = Vgs * 2.23694; // m/s to mph
    
      return speedMph > 0 ? speedMph : 0;
    };
    
    // Generate pacing strategy
    const pacingStrategy = useMemo(() => {
      const { riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho, targetTime } = params;
    
      // Binary search for average power to hit target time
      let powerLow = 100;
      let powerHigh = 600;
      let bestPower = 250;
      let iterations = 0;
    
      while (powerHigh - powerLow > 0.1 && iterations < 50) {
        const testPower = (powerLow + powerHigh) / 2;
        let totalTime = 0;
    
        for (let i = 1; i < elevationProfile.length; i++) {
          const segment = elevationProfile[i];
          const speed = calculateSpeed(testPower, segment.grade, riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho);
          const distance = segment.distance - elevationProfile[i-1].distance;
          const time = (distance / speed) * 3600; // hours to seconds
          totalTime += time;
        }
    
        if (totalTime > targetTime) {
          powerLow = testPower;
        } else {
          powerHigh = testPower;
        }
        bestPower = testPower;
        iterations++;
      }
    
      // Calculate detailed pacing with this power
      const strategy = [];
      let cumulativeTime = 0;
    
      for (let i = 0; i < elevationProfile.length; i++) {
        const segment = elevationProfile[i];
        const speed = i > 0 
          ? calculateSpeed(bestPower, segment.grade, riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho)
          : 0;
    
        const distance = i > 0 ? segment.distance - elevationProfile[i-1].distance : 0;
        const segmentTime = speed > 0 ? (distance / speed) * 3600 : 0;
        cumulativeTime += segmentTime;
    
        // Calculate cadence for selected gearing
        const wheelCircumference = 2.1; // meters (700c wheel)
        const gearRatio = params.chainring / params.cog;
        const cadence = speed > 0 ? (speed * 0.44704 * 60) / (wheelCircumference * gearRatio) : 0;
    
        strategy.push({
          distance: segment.distance,
          elevation: segment.elevation,
          grade: segment.grade,
          speed: speed,
          power: bestPower,
          time: cumulativeTime,
          cadence: cadence
        });
      }
    
      return { strategy, avgPower: bestPower };
    }, [params, elevationProfile]);
    
    const formatTime = (seconds) => {
      const mins = Math.floor(seconds / 60);
      const secs = Math.floor(seconds % 60);
      return `${mins}:${secs.toString().padStart(2, '0')}`;
    };
    
    // Generate gear comparison analysis
    const gearComparison = useMemo(() => {
      const cogs = [14, 16, 18, 20, 22];
      const { riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho, actualPower, chainring } = params;
    
      return cogs.map(cog => {
        const gearRatio = chainring / cog;
        const wheelCircumference = 2.1;
    
        // Calculate performance at different gradient sections
        const gradients = [3, 5, 7, 8]; // Representative gradients
        const results = gradients.map(grade => {
          const speed = calculateSpeed(actualPower, grade, riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho);
          const cadence = (speed * 0.44704 * 60) / (wheelCircumference * gearRatio);
          return { grade, speed, cadence };
        });
    
        // Calculate overall time estimate
        let totalTime = 0;
        for (let i = 1; i < elevationProfile.length; i++) {
          const segment = elevationProfile[i];
          const speed = calculateSpeed(actualPower, segment.grade, riderWeight, bikeWeight, CdA, Crr, drivetrainLoss, headwind, rho);
          const distance = segment.distance - elevationProfile[i-1].distance;
          const time = (distance / speed) * 3600;
          totalTime += time;
        }
    
        const avgCadence = results.reduce((sum, r) => sum + r.cadence, 0) / results.length;
        const minCadence = Math.min(...results.map(r => r.cadence));
    
        return {
          cog,
          gearRatio,
          results,
          totalTime,
          avgCadence,
          minCadence,
          isCurrent: cog === params.cog
        };
      });
    }, [params, elevationProfile, calculateSpeed]);
    
    return (
      <div className="w-full max-w-7xl mx-auto p-6 bg-white">
        <h1 className="text-3xl font-bold mb-6 text-gray-800">Climb Pacing Strategy</h1>
    
        {/* Gear comparison section */}
        <div className="mb-8 p-4 bg-blue-50 rounded-lg">
          <h2 className="text-2xl font-semibold mb-4">Gear Comparison (38t chainring with your available cogs)</h2>
          <p className="text-sm text-gray-700 mb-4">All calculated at your actual 214W power. Shows cadence at different gradients and estimated total time.</p>
    
          <div className="overflow-x-auto">
            <table className="w-full border-collapse border bg-white">
              <thead className="bg-gray-100">
                <tr>
                  <th className="border p-2">Cog</th>
                  <th className="border p-2">Ratio</th>
                  <th className="border p-2">Cadence @ 3%</th>
                  <th className="border p-2">Cadence @ 5%</th>
                  <th className="border p-2">Cadence @ 7%</th>
                  <th className="border p-2">Cadence @ 8%</th>
                  <th className="border p-2">Min Cadence</th>
                  <th className="border p-2">Est. Time</th>
                  <th className="border p-2">vs Current</th>
                </tr>
              </thead>
              <tbody>
                {gearComparison.map((gear) => (
                  <tr key={gear.cog} className={gear.isCurrent ? 'bg-yellow-100 font-semibold' : gear.minCadence >= 60 && gear.minCadence <= 90 ? 'bg-green-50' : 'bg-white'}>
                    <td className="border p-2 text-center">
                      {gear.cog}t {gear.isCurrent && '(current)'}
                    </td>
                    <td className="border p-2 text-center">{gear.gearRatio.toFixed(2)}</td>
                    <td className="border p-2 text-center">{gear.results[0].cadence.toFixed(0)} rpm</td>
                    <td className="border p-2 text-center">{gear.results[1].cadence.toFixed(0)} rpm</td>
                    <td className="border p-2 text-center">{gear.results[2].cadence.toFixed(0)} rpm</td>
                    <td className="border p-2 text-center">{gear.results[3].cadence.toFixed(0)} rpm</td>
                    <td className="border p-2 text-center font-semibold">{gear.minCadence.toFixed(0)} rpm</td>
                    <td className="border p-2 text-center">{formatTime(gear.totalTime)}</td>
                    <td className="border p-2 text-center">
                      {gear.isCurrent ? '—' : (
                        <span className={gear.totalTime < params.actualTime ? 'text-green-600' : 'text-red-600'}>
                          {gear.totalTime < params.actualTime ? '-' : '+'}{formatTime(Math.abs(gear.totalTime - params.actualTime))}
                        </span>
                      )}
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
    
          <div className="mt-4 p-3 bg-white rounded border-l-4 border-green-500">
            <p className="text-sm font-semibold"> Green highlighted rows = optimal cadence range (60-90 rpm minimum)</p>
            <p className="text-sm mt-1">The gear that keeps you in this range on the steepest sections will be most efficient.</p>
          </div>
        </div>
    
        <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
          <div className="bg-gray-50 p-4 rounded-lg">
            <h2 className="text-xl font-semibold mb-4">Parameters</h2>
            <div className="space-y-3">
              <div>
                <label className="block text-sm font-medium mb-1">Rider Weight (lbs)</label>
                <input 
                  type="number" 
                  value={params.riderWeight}
                  onChange={(e) => setParams({...params, riderWeight: parseFloat(e.target.value)})}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
              <div>
                <label className="block text-sm font-medium mb-1">Bike Weight (lbs)</label>
                <input 
                  type="number" 
                  value={params.bikeWeight}
                  onChange={(e) => setParams({...params, bikeWeight: parseFloat(e.target.value)})}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
              <div>
                <label className="block text-sm font-medium mb-1">Chainring</label>
                <input 
                  type="number" 
                  value={params.chainring}
                  onChange={(e) => setParams({...params, chainring: parseFloat(e.target.value)})}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
              <div>
                <label className="block text-sm font-medium mb-1">Rear Cog</label>
                <select 
                  value={params.cog}
                  onChange={(e) => setParams({...params, cog: parseFloat(e.target.value)})}
                  className="w-full px-3 py-2 border rounded"
                >
                  <option value={14}>14t (38/14 = 2.71)</option>
                  <option value={16}>16t (38/16 = 2.38)</option>
                  <option value={18}>18t (38/18 = 2.11)</option>
                  <option value={20}>20t (38/20 = 1.90) - Current</option>
                  <option value={22}>22t (38/22 = 1.73)</option>
                </select>
              </div>
              <div>
                <label className="block text-sm font-medium mb-1">Target Time (mm:ss)</label>
                <input 
                  type="text" 
                  value={formatTime(params.targetTime)}
                  onChange={(e) => {
                    const [m, s] = e.target.value.split(':').map(Number);
                    if (!isNaN(m) && !isNaN(s)) {
                      setParams({...params, targetTime: m * 60 + s});
                    }
                  }}
                  className="w-full px-3 py-2 border rounded"
                />
              </div>
            </div>
          </div>
    
          <div className="bg-blue-50 p-4 rounded-lg">
            <h2 className="text-xl font-semibold mb-4">Target vs Actual</h2>
            <div className="space-y-3">
              <div className="border-b pb-2">
                <div className="text-sm font-medium text-gray-600 mb-1">YOUR BEST EFFORT</div>
                <div className="flex justify-between">
                  <span>Time:</span>
                  <span className="font-bold">18:43</span>
                </div>
                <div className="flex justify-between">
                  <span>Avg Power:</span>
                  <span className="font-bold">214W</span>
                </div>
                <div className="flex justify-between">
                  <span>Avg Speed:</span>
                  <span className="font-bold">9.1 mph</span>
                </div>
              </div>
              <div className="border-b pb-2">
                <div className="text-sm font-medium text-gray-600 mb-1">TARGET (15:08)</div>
                <div className="flex justify-between">
                  <span>Required Power:</span>
                  <span className="text-blue-600 font-bold">{pacingStrategy.avgPower.toFixed(0)}W</span>
                </div>
                <div className="flex justify-between">
                  <span>Avg Speed:</span>
                  <span className="text-blue-600 font-bold">
                    {((2.8 / (pacingStrategy.strategy[pacingStrategy.strategy.length - 1]?.time / 3600)) || 0).toFixed(1)} mph
                  </span>
                </div>
              </div>
              <div className="bg-red-50 p-2 rounded">
                <div className="text-sm font-medium text-gray-600 mb-1">GAP TO CLOSE</div>
                <div className="flex justify-between text-red-600">
                  <span>Time:</span>
                  <span className="font-bold">-3:35 ({((params.actualTime - params.targetTime) / params.actualTime * 100).toFixed(1)}%)</span>
                </div>
                <div className="flex justify-between text-red-600">
                  <span>Power:</span>
                  <span className="font-bold">+{(pacingStrategy.avgPower - params.actualPower).toFixed(0)}W ({((pacingStrategy.avgPower - params.actualPower) / params.actualPower * 100).toFixed(1)}%)</span>
                </div>
              </div>
            </div>
          </div>
        </div>
    
        <div className="mb-8">
          <h2 className="text-xl font-semibold mb-4">Speed Profile</h2>
          <ResponsiveContainer width="100%" height={300}>
            <LineChart data={pacingStrategy.strategy}>
              <CartesianGrid strokeDasharray="3 3" />
              <XAxis dataKey="distance" label={{ value: 'Distance (miles)', position: 'insideBottom', offset: -5 }} />
              <YAxis label={{ value: 'Speed (mph)', angle: -90, position: 'insideLeft' }} />
              <Tooltip />
              <Line type="monotone" dataKey="speed" stroke="#2563eb" strokeWidth={2} name="Speed" />
            </LineChart>
          </ResponsiveContainer>
        </div>
    
        <div className="mb-8">
          <h2 className="text-xl font-semibold mb-4">Elevation & Gradient</h2>
          <ResponsiveContainer width="100%" height={300}>
            <LineChart data={pacingStrategy.strategy}>
              <CartesianGrid strokeDasharray="3 3" />
              <XAxis dataKey="distance" label={{ value: 'Distance (miles)', position: 'insideBottom', offset: -5 }} />
              <YAxis yAxisId="left" label={{ value: 'Elevation (ft)', angle: -90, position: 'insideLeft' }} />
              <YAxis yAxisId="right" orientation="right" label={{ value: 'Gradient (%)', angle: 90, position: 'insideRight' }} />
              <Tooltip />
              <Line yAxisId="left" type="monotone" dataKey="elevation" stroke="#10b981" strokeWidth={2} name="Elevation" />
              <Line yAxisId="right" type="monotone" dataKey="grade" stroke="#ef4444" strokeWidth={2} name="Gradient" />
            </LineChart>
          </ResponsiveContainer>
        </div>
    
        <div>
          <h2 className="text-xl font-semibold mb-4">Detailed Pacing Table</h2>
          <div className="overflow-x-auto">
            <table className="w-full border-collapse border">
              <thead className="bg-gray-100">
                <tr>
                  <th className="border p-2">Distance (mi)</th>
                  <th className="border p-2">Elevation (ft)</th>
                  <th className="border p-2">Grade (%)</th>
                  <th className="border p-2">Speed (mph)</th>
                  <th className="border p-2">Cadence (rpm)</th>
                  <th className="border p-2">Power (W)</th>
                  <th className="border p-2">Time</th>
                </tr>
              </thead>
              <tbody>
                {pacingStrategy.strategy.map((point, idx) => (
                  <tr key={idx} className={idx % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
                    <td className="border p-2 text-center">{point.distance.toFixed(1)}</td>
                    <td className="border p-2 text-center">{point.elevation.toFixed(0)}</td>
                    <td className="border p-2 text-center">{point.grade.toFixed(1)}</td>
                    <td className="border p-2 text-center">{point.speed.toFixed(1)}</td>
                    <td className="border p-2 text-center">{point.cadence.toFixed(0)}</td>
                    <td className="border p-2 text-center font-semibold">{point.power.toFixed(0)}</td>
                    <td className="border p-2 text-center">{formatTime(point.time)}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        </div>
    
        <div className="mt-6 p-4 bg-yellow-50 rounded-lg">
          <h3 className="font-semibold mb-3">Analysis: What Happened in Your Effort</h3>
          <div className="space-y-3 text-sm text-gray-700">
            <div className="bg-white p-3 rounded border-l-4 border-red-500">
              <strong>Critical Issue: Speed Collapse at 39:00 mark (around mile 2.3-2.5)</strong>
              <p className="mt-1">Your speed dropped dramatically to ~7.3 mph while HR stayed at 168 bpm and cadence dropped to 46 rpm. This is where you lost significant time - you were grinding at unsustainably low cadence on the steepest section.</p>
            </div>
    
            <div className="bg-white p-3 rounded border-l-4 border-yellow-500">
              <strong>Pacing Pattern Analysis:</strong>
              <ul className="mt-1 ml-4 list-disc space-y-1">
                <li><strong>Start (0-26:40):</strong> Consistent ~12-13 mph, 70-90 rpm cadence, ~150 bpm HR - good sustainable pace</li>
                <li><strong>Middle (26:40-35:00):</strong> Speed holding ~11-12 mph, cadence still decent 60-80 rpm, HR climbing to 160s</li>
                <li><strong>Crisis Point (35:00-39:00):</strong> Speed crashes to 7.3 mph, cadence drops to 46 rpm, HR maxed at 168 bpm - you hit the wall on the steepest pitch</li>
                <li><strong>Recovery sections (gaps in data):</strong> Appears you had to stop/soft pedal briefly, then resumed</li>
                <li><strong>Final push (40:00+):</strong> HR surged to 135+ but speed didn't recover - too fatigued</li>
              </ul>
            </div>
    
            <div className="bg-white p-3 rounded border-l-4 border-blue-500">
              <strong>The Singlespeed Problem (38/20 gearing):</strong>
              <p className="mt-1">At 46 rpm on the steep section, you're at 60% of your average cadence. This gear is simply too tall for the 7-8% gradient sections. You're forced to:</p>
              <ul className="mt-1 ml-4 list-disc">
                <li>Mash at very low cadence (high muscular force per pedal stroke)</li>
                <li>This fatigues your legs faster than your cardiovascular system</li>
                <li>Notice your HR wasn't maxed out (168 bpm vs 181 max) - your legs failed before your cardio did</li>
              </ul>
            </div>
    
            <div className="bg-white p-3 rounded border-l-4 border-green-500">
              <strong>Where Time Was Lost:</strong>
              <p className="mt-1">Comparing to target 15:08 pace:</p>
              <ul className="mt-1 ml-4 list-disc">
                <li>First half (miles 0-1.4): Relatively close to target, maybe lost 30-45 seconds</li>
                <li>Steep section (miles 1.4-2.8): Lost 2:30-3:00 here due to the speed collapse</li>
                <li>The 39:00 mark crisis point alone probably cost 1:30-2:00</li>
              </ul>
            </div>
          </div>
        </div>
    
        <div className="mt-4 p-4 bg-red-50 rounded-lg">
          <h3 className="font-semibold mb-2">⚠️ Critical Finding: Gearing is Your Biggest Limiter</h3>
          <div className="space-y-2 text-sm">
            <p>Your data clearly shows the 38/20 singlespeed forced you into an impossible situation on the steep sections. At 214W average power, you should be capable of ~10-11 mph on 7% grades with proper gearing (80-90 rpm). Instead, you were forced to 7.3 mph at 46 rpm.</p>
            <p className="font-semibold mt-2">If you had gears allowing you to maintain 70+ rpm throughout:</p>
            <ul className="ml-4 list-disc">
              <li>You could likely sustain 230-240W instead of 214W (less muscular fatigue)</li>
              <li>You wouldn't have the catastrophic slowdown at 39:00</li>
              <li>Estimated time with same 214W but proper gearing: 16:30-17:15</li>
              <li>With 230W + proper gearing: 15:30-16:00</li>
            </ul>
          </div>
        </div>
    
        <div className="mt-4 p-4 bg-green-50 rounded-lg">
          <h3 className="font-semibold mb-2">Realistic Pathways to 15:08:</h3>
          <div className="space-y-2 text-sm">
            <div className="flex justify-between items-center">
              <span><strong>Option 1:</strong> Switch to gears + maintain current 214W:</span>
              <span className="font-bold text-orange-700">~17:00 (not quite there)</span>
            </div>
            <div className="flex justify-between items-center">
              <span><strong>Option 2:</strong> Switch to gears + increase to 245W:</span>
              <span className="font-bold text-green-700">~15:08 ✓</span>
            </div>
            <div className="flex justify-between items-center">
              <span><strong>Option 3:</strong> Keep singlespeed + increase to 300W+:</span>
              <span className="font-bold text-red-700">~15:08 (extremely difficult, cadence issues persist)</span>
            </div>
            <div className="flex justify-between items-center">
              <span><strong>Option 4:</strong> Gears + lose 15 lbs + 230W:</span>
              <span className="font-bold text-green-700">~15:08 ✓</span>
            </div>
          </div>
        </div>
    
        <div className="mt-4 p-4 bg-gray-50 rounded-lg">
          <h3 className="font-semibold mb-2">Gearing Recommendation Based on Your Data:</h3>
          <p className="text-sm text-gray-700 mb-2">
            Looking at the gear comparison above with your actual 214W power output:
          </p>
          <ul className="text-sm space-y-2 ml-4">
            <li><strong className="text-red-600">38/14 or 38/16: TOO HARD</strong> - You'd be spinning 90-110+ rpm on easier sections (leg speed limited, wastes energy)</li>
            <li><strong className="text-red-600">38/18: MARGINAL</strong> - Better than 20t but still drops below 60 rpm on steep sections</li>
            <li><strong className="text-yellow-600">38/20: YOUR CURRENT</strong> - Forces 46 rpm on steepest sections = catastrophic muscular fatigue</li>
            <li><strong className="text-green-600">38/22: BEST OPTION</strong> - Keeps you in 60-70 rpm range on steep sections, 75-85 rpm on moderate sections</li>
          </ul>
          <p className="text-sm text-gray-700 mt-3 font-semibold">
            Recommendation: Switch to 38/22 (1.73 ratio). This should save you 45-75 seconds and feel far more sustainable. You'll spin a bit faster on easy sections (85-90 rpm) but maintain viable cadence (60-65 rpm) on the steepest pitches.
          </p>
        </div>
    
        <div className="mt-4 p-4 bg-green-50 rounded-lg">
          <h3 className="font-semibold mb-2">Realistic Pathways to 15:08 (Updated with Cog Options):</h3>
          <div className="space-y-2 text-sm">
            <div className="flex justify-between items-center">
              <span><strong>Option 1:</strong> Switch to 38/22 + maintain current 214W:</span>
              <span className="font-bold text-orange-700">~17:45-18:00 (better but not enough)</span>
            </div>
            <div className="flex justify-between items-center">
              <span><strong>Option 2:</strong> 38/22 + increase to 245W:</span>
              <span className="font-bold text-green-700">~15:15 (very close!) </span>
            </div>
            <div className="flex justify-between items-center">
              <span><strong>Option 3:</strong> 38/22 + increase to 260W:</span>
              <span className="font-bold text-green-700">~14:50 (beats target!) ✓✓</span>
            </div>
            <div className="flex justify-between items-center">
              <span><strong>Option 4:</strong> Full geared bike with optimal ratios:</span>
              <span className="font-bold text-blue-700">Even better, but 38/22 gets you close</span>
            </div>
          </div>
        </div>
    
        <div className="mt-4 p-4 bg-yellow-50 rounded-lg">
          <h3 className="font-semibold mb-2"> Quick Win Strategy:</h3>
          <p className="text-sm text-gray-700">
            <strong>Next attempt: Use the 38/22 cog.</strong> This single change will:
          </p>
          <ul className="text-sm ml-4 mt-2 space-y-1">
            <li> Prevent the catastrophic slowdown you had at 39:00 (from 46 rpm  60-65 rpm)</li>
            <li> Let you sustain power more evenly throughout the climb</li>
            <li> Save 45-75 seconds immediately with no fitness change</li>
            <li> Allow you to access more of your cardiovascular capacity (your HR showed room to push harder)</li>
          </ul>
          <p className="text-sm text-gray-700 mt-2">
            Then focus training on raising your sustained power from 214W to 240-250W over the next 8-12 weeks, and 15:08 becomes very achievable.
          </p>
        </div>
      </div>
    );
    };
    
    export default ClimbPacingCalculator;
    

    Perfect! Now you can see exactly how each cog would perform. Here's my recommendation:

  • Use the 38/22 cog (1.73 ratio)

    Why 38/22 is your best option:

    1. Cadence on steep sections (7-8%): ~60-65 rpm instead of your current 46 rpm
    • This is the critical difference - you stay in a sustainable cadence range instead of grinding

      1. Cadence on moderate sections (5%): ~70-75 rpm instead of 57 rpm
    • More efficient, less muscular fatigue

      1. Estimated time savings: 45-75 seconds at your current 214W power
    • Gets you to ~17:45-18:00 (still not 15:08, but much better)

      Why not the other cogs:

  • 38/14 or 38/16: Too hard - you'd be spinning 100+ rpm on easier sections, which wastes energy and your legs can't turn over that fast efficiently

  • 38/18: Better than 20t but still drops below 60 rpm on the steepest parts

  • 38/20 (current): Forces unsustainable 46 rpm grinding that killed you at the 39:00 mark

    The path to 15:08:

    1. Immediate: Switch to 38/22 → saves ~60 seconds
    2. 2-3 months: Train to raise sustained power from 214W to 245W → closes remaining ~2:30 gap
    3. Result: 38/22 at 245W = ~15:15 (you're there!)

    The 38/22 won't get you to 15:08 alone, but it removes the gearing handicap and lets you actually use your fitness. Your HR data shows you have more to give - you just couldn't access it grinding at 46 rpm.

    claudeoutout-64289705-d0c1-4f03-9139-a13815bc3969


Powered by Claude Exporter