Wind direction and speed timeline

Hello,

I’m looking for a Plotly.js plot like so

ie windspeed as y, time as x.

Each data point should be an arrow whom direction is wind direction (North at top / South at bottom).

Color of arrow is dependant of windspeed value (using a colorscale/colormap).

Each arrow should have exact same length.

Is there any way to have such a plot with Plotly ?

Best regards,
Sébastien

PS : this feature is also discussed on Grafana forum https://community.grafana.com/t/wind-direction-speed-timeline/67168 and on python-windrose (Matplotlib) python-windrose/windrose#302

Hi @scelles! We had something similar in the past.

Does that help?

Sorry but it doesn’t help that much.

Let’s be more precise.

Here is an attempt I did

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Wind Speed and Direction Visualization</title>
  </head>
  <body>
    <div id="myDiv" style="width: 100%; height: 600px"></div>
    <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
    <script>
      // Wait for Plotly to load before executing
      window.addEventListener("load", function () {
        // Wind data from CSV
        const windData = [
          { datetime: "2025-09-01T00:00Z", ws: 8, wd: 175 },
          { datetime: "2025-09-01T01:00Z", ws: 6, wd: 170 },
          { datetime: "2025-09-01T02:00Z", ws: 4, wd: 170 },
          { datetime: "2025-09-01T03:00Z", ws: 5.4, wd: 120 },
          { datetime: "2025-09-01T04:00Z", ws: 8.3, wd: 60 },
          { datetime: "2025-09-01T05:00Z", ws: 12.6, wd: 20 },
          { datetime: "2025-09-01T06:00Z", ws: 21.1, wd: 20 },
          { datetime: "2025-09-01T07:00Z", ws: 30.8, wd: 20 },
          { datetime: "2025-09-01T08:00Z", ws: 39.1, wd: 0 },
          { datetime: "2025-09-01T09:00Z", ws: 43.8, wd: 0 },
          { datetime: "2025-09-01T10:00Z", ws: 46.2, wd: 0 },
          { datetime: "2025-09-01T11:00Z", ws: 46.8, wd: 0 },
          { datetime: "2025-09-01T12:00Z", ws: 46.3, wd: 0 },
          { datetime: "2025-09-01T13:00Z", ws: 44.6, wd: 350 },
          { datetime: "2025-09-01T14:00Z", ws: 42.7, wd: 350 },
          { datetime: "2025-09-01T15:00Z", ws: 41.8, wd: 350 },
          { datetime: "2025-09-01T16:00Z", ws: 40.8, wd: 340 },
          { datetime: "2025-09-01T17:00Z", ws: 39.7, wd: 340 },
          { datetime: "2025-09-01T18:00Z", ws: 37.7, wd: 340 },
          { datetime: "2025-09-01T19:00Z", ws: 35.1, wd: 330 },
          { datetime: "2025-09-01T20:00Z", ws: 32.8, wd: 280 },
          { datetime: "2025-09-01T21:00Z", ws: 30.7, wd: 290 },
          { datetime: "2025-09-01T22:00Z", ws: 28.7, wd: 290 },
          { datetime: "2025-09-01T23:00Z", ws: 26.8, wd: 280 },
          { datetime: "2025-09-02T00:00Z", ws: 25.8, wd: 270 },
        ];

        // Convert wind direction to u,v components
        function windToUV(direction, speed) {
          // Convert degrees to radians
          // Add 180 to show where wind is going (not coming from)
          const radians = (((direction + 180) % 360) * Math.PI) / 180;
          const u = Math.sin(radians);
          const v = Math.cos(radians);
          return { u, v };
        }

        // Prepare data arrays
        const times = windData.map((d) => d.datetime);
        const speeds = windData.map((d) => d.ws);
        const directions = windData.map((d) => d.wd);
        const minSpeed = Math.min(...speeds);
        const maxSpeed = Math.max(...speeds);

        // Create wind direction arrows using annotations with RdYlGn colorscale
        const annotations = [];
        windData.forEach((point, i) => {
          // Calculate arrow end position
          const scale = 0.8; // Scale factor for arrow length
          const { u: uComp, v: vComp } = windToUV(point.wd, 1); // Normalized direction

          // Create time offset for horizontal component (scaled by wind speed)
          const timeMs = new Date(point.datetime).getTime();
          const speedScale = point.ws / maxSpeed; // Normalize speed
          const arrowEndTime = new Date(
            timeMs + uComp * scale * speedScale * 3600000
          ); // 1 hour offset max
          const arrowEndSpeed = point.ws + vComp * scale * speedScale * 10; // Speed offset

          // Get color based on wind speed using RdYlGn colorscale
          const normalizedSpeed = (point.ws - minSpeed) / (maxSpeed - minSpeed);
          let arrowColor;
          if (normalizedSpeed < 0.33) {
            arrowColor = '#1a9850'; // Green for low speeds
          } else if (normalizedSpeed < 0.66) {
            arrowColor = '#ffffbf'; // Yellow for medium speeds  
          } else {
            arrowColor = '#d73027'; // Red for high speeds
          }

          annotations.push({
            x: arrowEndTime,
            y: arrowEndSpeed,
            ax: point.datetime,
            ay: point.ws,
            xref: "x",
            yref: "y",
            axref: "x",
            ayref: "y",
            arrowhead: 2,
            arrowsize: 1.8,
            arrowwidth: 4,
            arrowcolor: arrowColor,
            showarrow: true,
            hovertext: `Time: ${point.datetime}<br>Speed: ${point.ws} km/h<br>Direction: ${point.wd}°`,
          });
        });

        // Create scatter points for proper hover information with RdYlGn colorscale
        const hoverTrace = {
          x: times,
          y: speeds,
          type: "scatter",
          mode: "markers",
          marker: {
            size: 12,
            color: speeds,
            colorscale: "RdYlGn",
            reversescale: true,
            colorbar: {
              title: "Wind Speed (km/h)",
              titleside: "right",
            },
            opacity: 0.7,
          },
          name: "Wind Data",
          hovertemplate:
            "Time: %{x}<br>Speed: %{y} km/h<br>Direction: %{text}°<extra></extra>",
          text: directions,
        };

        // Layout configuration
        const layout = {
          title: {
            text: "Wind Speed and Direction Over Time",
            font: { size: 18 },
          },
          xaxis: {
            title: "Time",
            type: "date",
            tickformat: "%H:%M",
          },
          yaxis: {
            title: "Wind Speed (km/h)",
            range: [0, Math.max(...speeds) * 1.2],
          },
          annotations: annotations,
          hovermode: "closest",
          showlegend: false,
          plot_bgcolor: "rgba(240,240,240,0.1)",
        };

        // Plot configuration
        const config = {
          responsive: true,
          displayModeBar: true,
        };

        // Create the plot
        Plotly.newPlot("myDiv", [hoverTrace], layout, config);

        // Add legend below the plot
        const legendDiv = document.createElement("div");
        legendDiv.innerHTML = `
        <div style="margin-top: 20px; padding: 15px; background-color: #f8f9fa; border-radius: 8px; border-left: 4px solid #007bff;">
          <h4 style="margin-top: 0; color: #333;">Chart Legend:</h4>
          <ul style="margin: 0; padding-left: 20px;">
            <li><strong>Arrows:</strong> Wind direction (pointing where wind is going)</li>
            <li><strong>Arrow colors:</strong> RdYlGn colorscale (green = low, yellow = medium, red = high)</li>
            <li><strong>Arrow length:</strong> Proportional to wind speed</li>
            <li><strong>Colorbar:</strong> Shows wind speed scale (green = low, yellow = medium, red = high)</li>
            <li><strong>Hover:</strong> Mouse over markers for detailed information</li>
          </ul>
        </div>
      `;
        document.body.appendChild(legendDiv);
      }); // End of window load event listener
    </script>
  </body>
</html>

but result is not visually satisfying

  • arrows don’t use colorscale
  • legend is not showing colorscale for green to yellow and red
  • arrows are not centered on points (which shouldn’t appears)
  • arrows direction seems to be buggy.

Any idea how to fix that?