Wind
The wind dial, with the direction the wind comes from, its speed as a barb count and the vessel at its heading.
On this page
Code
import {
Priority,
VesselImage,
Wind,
WindVariant,
WindVisualization,
type WindHistogramData,
} from "@bosunkit/instruments";
import { useEffect, useState, type CSSProperties, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const description: string =
"A squall line coming through on a live feed: the wind veers from west-northwest to north-northwest and gusts " +
"from 12 to 26 knots, so the barbs change and the histogram of the last minute fills in, on a large dial with the " +
"vessel, a medium dial of force streaks and a small cockpit dial.";
const HISTORY_SECONDS: number = 60;
const BIN_DEGREES: number = 10;
const PANEL: CSSProperties = {
display: "flex",
flexWrap: "wrap",
alignItems: "flex-end",
justifyContent: "center",
gap: 28,
width: "100%",
maxWidth: 660,
};
const FIGURE: CSSProperties = { display: "flex", flexDirection: "column", alignItems: "center", gap: 6, margin: 0 };
const CAPTION: CSSProperties = { fontSize: 13, color: "var(--text-dim)", textAlign: "center", maxWidth: 220 };
/** Direction the wind comes from, in degrees, at a time in seconds: a slow veer with shifts in the gusts. */
function directionAt(time: number): number {
return 305 + 20 * ((1 - Math.cos(time / 25)) / 2) + 8 * Math.sin(time / 3.7);
}
/** Wind speed in knots at a time in seconds: a building breeze with gusts. */
function speedAt(time: number): number {
const gust: number = Math.max(0, Math.sin(time / 5)) ** 3;
return 12 + 6 * ((1 - Math.cos(time / 25)) / 2) + 8 * gust;
}
/** How often the wind came from each 10 degree sector over the last minute. */
function histogramAt(time: number): WindHistogramData[] {
const counts: Map<number, number> = new Map<number, number>();
for (let past: number = 0; past < HISTORY_SECONDS; past += 1) {
const sector: number = (Math.round(directionAt(time - past) / BIN_DEGREES) * BIN_DEGREES) % 360;
counts.set(sector, (counts.get(sector) ?? 0) + 1);
}
return Array.from(counts, ([direction, occurrences]: [number, number]): WindHistogramData => ({
direction,
occurrences,
}));
}
export default function Showcase(): ReactElement {
const [second, setSecond]: [number, Dispatch<SetStateAction<number>>] = useState<number>(0);
useEffect((): (() => void) => {
const interval: number = window.setInterval((): void => {
setSecond((previous: number): number => previous + 1);
}, 1000);
return (): void => {
window.clearInterval(interval);
};
}, []);
const direction: number = directionAt(second);
const speed: number = speedAt(second);
const heading: number = 47 + 4 * Math.sin(second / 6);
return (
<div style={PANEL}>
<figure style={FIGURE}>
<div style={{ width: 240, height: 240 }}>
<Wind
variant={WindVariant.large}
visualization={WindVisualization.histogram}
currentWindFromDirection={direction}
currentWindSpeedKnots={speed}
windHistogramData={histogramAt(second)}
vesselImage={VesselImage.genericTop}
vesselHeadingDeg={heading}
priority={Priority.enhanced}
/>
</div>
<figcaption style={CAPTION}>Large: the double ring, the last minute's directions and the vessel</figcaption>
</figure>
<figure style={FIGURE}>
<div style={{ width: 160, height: 160 }}>
<Wind
variant={WindVariant.medium}
visualization={WindVisualization.forceGraphics}
currentWindFromDirection={direction}
currentWindSpeedKnots={speed}
vesselImage={VesselImage.genericTop}
vesselHeadingDeg={heading}
/>
</div>
<figcaption style={CAPTION}>Medium: force streaks for the strength</figcaption>
</figure>
<figure style={FIGURE}>
<div style={{ width: 90, height: 90 }}>
<Wind
variant={WindVariant.auto}
smallVariantMaxPx={100}
mediumVariantMaxPx={220}
currentWindFromDirection={direction}
currentWindSpeedKnots={speed}
priority={Priority.enhanced}
/>
</div>
<figcaption style={CAPTION}>Cockpit: the small layout, picked by size</figcaption>
</figure>
</div>
);
}
The wind dial shows where the wind comes from, as an arrow on the ring, and how hard it blows, as the number of barbs
on the arrow. The speed is snapped to the barb icons, so a small change in speed does not change the arrow. The center
layer is either a histogram of the directions the wind has come from or a field of force streaks, and the vessel
silhouette turns to the heading. It ports obc-wind; the overview lists the other instruments.
Examples
Wind
Code
import { Wind, WindVariant } from "@bosunkit/instruments";
import type { ReactElement } from "react";
export const description: string =
"Wind from the northwest at 14 knots, with the direction histogram behind the arrow and the vessel at its heading.";
export default function WindExample(): ReactElement {
return (
<div style={{ width: 200, height: 200 }}>
<Wind
variant={WindVariant.medium}
currentWindSpeedKnots={14}
currentWindFromDirection={310}
vesselHeadingDeg={47}
/>
</div>
);
}
Force
Code
import { Wind, WindVariant, WindVisualization } from "@bosunkit/instruments";
import type { ReactElement } from "react";
export const description: string = "The same wind with the force-graphics streak field in the center layer.";
export default function Force(): ReactElement {
return (
<div style={{ width: 200, height: 200 }}>
<Wind
variant={WindVariant.medium}
visualization={WindVisualization.forceGraphics}
currentWindSpeedKnots={14}
currentWindFromDirection={310}
vesselHeadingDeg={47}
/>
</div>
);
}
Enhanced
Code
import { Priority, Wind, WindVariant, WindVisualization } from "@bosunkit/instruments";
import type { ReactElement } from "react";
export const description: string = "The streak field and the wind arrow in the enhanced palette.";
export default function Enhanced(): ReactElement {
return (
<div style={{ width: 200, height: 200 }}>
<Wind
variant={WindVariant.medium}
visualization={WindVisualization.forceGraphics}
priority={Priority.enhanced}
currentWindSpeedKnots={14}
currentWindFromDirection={310}
vesselHeadingDeg={47}
/>
</div>
);
}
No reading
Code
import { Wind } from "@bosunkit/instruments";
import type { ReactElement } from "react";
export const description: string =
"A wind speed that is not a number draws no wind arrow, so the dial never shows a calm it did not measure.";
export default function NoReading(): ReactElement {
return (
<div style={{ width: 200, height: 200 }}>
<Wind currentWindSpeedKnots={Number.NaN} currentWindFromDirection={310} />
</div>
);
}
True, apparent and historical wind
State whether the display represents true or apparent wind, and declare the direction reference. The input is the direction the wind comes from; converting a downstream flow vector requires the application to resolve that convention before rendering. Keep vessel heading in the compatible reference.
Barbs are discrete speed buckets, so pair them with a numeric reading when small changes matter. Give the histogram a stated time window and derive its occurrences from that window. A non-finite speed hides the wind glyph, but the surrounding panel must explain why data is unavailable; an empty glyph is not a source-health message. Do not mix a fresh speed with an old direction without exposing the difference.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| currentWindFromDirection | number | undefined | 0 | The direction the wind is coming from, in degrees. |
| currentWindSpeedKnots | number | undefined | 25 | The wind speed in knots (snapped to the authored barb-icon buckets). |
| windHistogramData | WindHistogramData[] | undefined | [] | Wind-direction histogram; occurrence density grows inward from the ring. |
| vesselImage | VesselImage | undefined | VesselImage.genericTop | Vessel silhouette at the center (hidden in the small variant). |
| vesselHeadingDeg | number | undefined | 0 | Heading of the vessel silhouette in degrees, clockwise from the top of the dial. The dial does not turn, so the
heading and the wind direction must use the same reference. Default 0. |
| variant | WindVariant | undefined | WindVariant.auto | Layout variant. auto (default) resolves from the rendered size against
smallVariantMaxPx / mediumVariantMaxPx; large adds the double ring. |
| smallVariantMaxPx | number | undefined | WIND_SMALL_MAX_PX_DEFAULT | Size in CSS pixels, taken as the smaller side of the dial, below which variant auto picks the small layout.
Default 96. |
| mediumVariantMaxPx | number | undefined | WIND_MEDIUM_MAX_PX_DEFAULT | Size in CSS pixels, taken as the smaller side of the dial, from which variant auto picks the large layout;
sizes between smallVariantMaxPx and this one get the medium layout. Default 200. |
| visualization | WindVisualization | undefined | WindVisualization.histogram | Center visualization: wind histogram (default) or the force-graphics streak field. |
| priority | Priority | undefined | Priority.regular | Color priority: Priority.enhanced uses the blue/enhanced palette. |
| className | string | undefined | – | Class names added to the instrument's root element. |
Related
- Compass: the wind arrow on the heading card.
- Data integrity: the states a value can be in, and what the crew does.