Skip to content
design
Components

Polar chart

Compare directional observations and performance curves using their actual angular spacing.

On this page

Sailing performance

Illustrative observations · true wind speed 12 kn

True wind from the bow · mirrored port and starboard curves

Illustrative sailing speed by true wind angleTrue wind from the bow · mirrored port and starboard curves2 kn4 kn6 kn8 kn0°45°90°135°180°225°270°315°Target speed: 35°, 4 knTarget speed: 45°, 5.2 knTarget speed: 70°, 6 knTarget speed: 100°, 6.4 knTarget speed: 130°, 5.6 knTarget speed: 170°, 4.5 knTarget speed: 190°, 4.5 knTarget speed: 230°, 5.6 knTarget speed: 260°, 6.4 knTarget speed: 290°, 6 knTarget speed: 315°, 5.2 knTarget speed: 325°, 4 knObserved speed: 35°, 3.8 knObserved speed: 45°, 4.9 knObserved speed: 70°, 5.5 knObserved speed: 130°, 5.4 knObserved speed: 170°, 4.2 knObserved speed: 190°, 4.2 knObserved speed: 230°, 5.4 knObserved speed: 290°, 5.5 knObserved speed: 315°, 4.9 knObserved speed: 325°, 3.8 knBow
  • Target speed
  • Observed speed
Compare directional observations and performance curves using their actual angular spacing.
Code
import type { ReactElement } from "react";
import { PolarChart } from "@bosunkit/components/polar-chart";
import { ChartLegend } from "@bosunkit/components/chart-legend";
import { ChartPanel } from "@bosunkit/components/chart-panel";
// Fixed illustrative display strings; consumers supply their quantity formatter.
export const description: string =
  "Illustrative sailing performance curves preserve true wind angle spacing and a missing observed speed.";
const angleLabels: Readonly<Record<number, string>> = {
  "0": "0°",
  "35": "35°",
  "45": "45°",
  "70": "70°",
  "90": "90°",
  "100": "100°",
  "130": "130°",
  "135": "135°",
  "170": "170°",
  "180": "180°",
  "190": "190°",
  "230": "230°",
  "260": "260°",
  "290": "290°",
  "325": "325°",
  "225": "225°",
  "270": "270°",
  "315": "315°",
};
const speedLabels: Readonly<Record<number, string>> = {
  "0": "0 kn",
  "2": "2 kn",
  "3.8": "3.8 kn",
  "4": "4 kn",
  "4.2": "4.2 kn",
  "4.5": "4.5 kn",
  "4.9": "4.9 kn",
  "5.2": "5.2 kn",
  "5.4": "5.4 kn",
  "5.5": "5.5 kn",
  "5.6": "5.6 kn",
  "6": "6 kn",
  "6.4": "6.4 kn",
  "8": "8 kn",
};
const formatAngle: (value: string | number) => string = (value: string | number): string =>
  angleLabels[Number(value)] ?? "";
const formatSpeed: (value: number) => string = (value: number): string => speedLabels[value] ?? "";
const config = {
  target: { label: "Target speed", formatValue: formatSpeed },
  actual: { label: "Observed speed", formatValue: formatSpeed },
};
export default function SailingPerformance(): ReactElement {
  return (
    <ChartPanel
      title="Sailing performance"
      description="Illustrative observations · true wind speed 12 kn"
      legend={<ChartLegend config={config} label="Sailing performance series" />}
    >
      <PolarChart
        data={[
          { angle: 35, target: 4, actual: 3.8 },
          { angle: 45, target: 5.2, actual: 4.9 },
          { angle: 70, target: 6, actual: 5.5 },
          { angle: 100, target: 6.4, actual: null },
          { angle: 130, target: 5.6, actual: 5.4 },
          { angle: 170, target: 4.5, actual: 4.2 },
          { angle: 190, target: 4.5, actual: 4.2 },
          { angle: 230, target: 5.6, actual: 5.4 },
          { angle: 260, target: 6.4, actual: null },
          { angle: 290, target: 6, actual: 5.5 },
          { angle: 315, target: 5.2, actual: 4.9 },
          { angle: 325, target: 4, actual: 3.8 },
        ]}
        categoryKey="angle"
        config={config}
        series={["target", "actual"]}
        valueDomain={[0, 8]}
        formatCategory={formatAngle}
        formatAxisValue={formatSpeed}
        label="Illustrative sailing speed by true wind angle"
        angleReference="True wind from the bow · mirrored port and starboard curves"
        categoryLabel="True wind angle"
        dataTableLabel="View sailing observations"
        missingValueText="Unavailable"
      >
        <text x={300} y={320} textAnchor="middle" fill="currentColor">
          Bow
        </text>
      </PolarChart>
    </ChartPanel>
  );
}

Use polar plots for sailing performance and directional observations. Zero degrees is north in the drawing and angles increase clockwise; angleReference names the physical reference and from/toward convention. Prepare mirrored sailing curves in the caller. Radius values are nonnegative, zero is valid, and missing values break connected curves. connect={false} gives a point-only sky plot; the caller supplies the elevation-to-radius transform. closed closes only a complete curve. Numeric angular spacing is preserved by SVG geometry. Children are SVG annotations; direct Recharts composition belongs in ChartContainer.

01Preserve meaningObservation spacing, validity and comparison basis remain explicit.
02Caller display textLabels and formatted values come from the consumer.
03Inspect without hoverNamed controls and source values remain accessible with a keyboard.

Examples

Composition

Sailing performance

Illustrative observations · true wind speed 12 kn

True wind from the bow · mirrored port and starboard curves

Illustrative sailing speed by true wind angleTrue wind from the bow · mirrored port and starboard curves2 kn4 kn6 kn8 kn0°45°90°135°180°225°270°315°Target speed: 35°, 4 knTarget speed: 45°, 5.2 knTarget speed: 70°, 6 knTarget speed: 100°, 6.4 knTarget speed: 130°, 5.6 knTarget speed: 170°, 4.5 knTarget speed: 190°, 4.5 knTarget speed: 230°, 5.6 knTarget speed: 260°, 6.4 knTarget speed: 290°, 6 knTarget speed: 315°, 5.2 knTarget speed: 325°, 4 knObserved speed: 35°, 3.8 knObserved speed: 45°, 4.9 knObserved speed: 70°, 5.5 knObserved speed: 130°, 5.4 knObserved speed: 170°, 4.2 knObserved speed: 190°, 4.2 knObserved speed: 230°, 5.4 knObserved speed: 290°, 5.5 knObserved speed: 315°, 4.9 knObserved speed: 325°, 3.8 knBow
  • Target speed
  • Observed speed
Illustrative sailing performance curves preserve true wind angle spacing and a missing observed speed.
Code
import type { ReactElement } from "react";
import { PolarChart } from "@bosunkit/components/polar-chart";
import { ChartLegend } from "@bosunkit/components/chart-legend";
import { ChartPanel } from "@bosunkit/components/chart-panel";
// Fixed illustrative display strings; consumers supply their quantity formatter.
export const description: string =
  "Illustrative sailing performance curves preserve true wind angle spacing and a missing observed speed.";
const angleLabels: Readonly<Record<number, string>> = {
  "0": "0°",
  "35": "35°",
  "45": "45°",
  "70": "70°",
  "90": "90°",
  "100": "100°",
  "130": "130°",
  "135": "135°",
  "170": "170°",
  "180": "180°",
  "190": "190°",
  "230": "230°",
  "260": "260°",
  "290": "290°",
  "325": "325°",
  "225": "225°",
  "270": "270°",
  "315": "315°",
};
const speedLabels: Readonly<Record<number, string>> = {
  "0": "0 kn",
  "2": "2 kn",
  "3.8": "3.8 kn",
  "4": "4 kn",
  "4.2": "4.2 kn",
  "4.5": "4.5 kn",
  "4.9": "4.9 kn",
  "5.2": "5.2 kn",
  "5.4": "5.4 kn",
  "5.5": "5.5 kn",
  "5.6": "5.6 kn",
  "6": "6 kn",
  "6.4": "6.4 kn",
  "8": "8 kn",
};
const formatAngle: (value: string | number) => string = (value: string | number): string =>
  angleLabels[Number(value)] ?? "";
const formatSpeed: (value: number) => string = (value: number): string => speedLabels[value] ?? "";
const config = {
  target: { label: "Target speed", formatValue: formatSpeed },
  actual: { label: "Observed speed", formatValue: formatSpeed },
};
export default function SailingPerformance(): ReactElement {
  return (
    <ChartPanel
      title="Sailing performance"
      description="Illustrative observations · true wind speed 12 kn"
      legend={<ChartLegend config={config} label="Sailing performance series" />}
    >
      <PolarChart
        data={[
          { angle: 35, target: 4, actual: 3.8 },
          { angle: 45, target: 5.2, actual: 4.9 },
          { angle: 70, target: 6, actual: 5.5 },
          { angle: 100, target: 6.4, actual: null },
          { angle: 130, target: 5.6, actual: 5.4 },
          { angle: 170, target: 4.5, actual: 4.2 },
          { angle: 190, target: 4.5, actual: 4.2 },
          { angle: 230, target: 5.6, actual: 5.4 },
          { angle: 260, target: 6.4, actual: null },
          { angle: 290, target: 6, actual: 5.5 },
          { angle: 315, target: 5.2, actual: 4.9 },
          { angle: 325, target: 4, actual: 3.8 },
        ]}
        categoryKey="angle"
        config={config}
        series={["target", "actual"]}
        valueDomain={[0, 8]}
        formatCategory={formatAngle}
        formatAxisValue={formatSpeed}
        label="Illustrative sailing speed by true wind angle"
        angleReference="True wind from the bow · mirrored port and starboard curves"
        categoryLabel="True wind angle"
        dataTableLabel="View sailing observations"
        missingValueText="Unavailable"
      >
        <text x={300} y={320} textAnchor="middle" fill="currentColor">
          Bow
        </text>
      </PolarChart>
    </ChartPanel>
  );
}

Props

PropTypeDefaultDescription
calloutChartCallout | undefined–A visible summary bubble with caller-formatted value and context.
onCategoryDomainChange((range: readonly [number, number]) => void) | undefined–Receives a numeric viewport after drag-to-zoom or Shift-drag pan; requires categoryDomain.
maxGapnumber | undefined–Maximum elapsed distance connected by a line; numeric categories only.
categoryDomainreadonly [number, number] | undefined–Controlled numeric/time viewport; emits no requests by itself.
syncIdstring | undefined–Charts sharing this identifier synchronize inspection by category value or nearest time.
syncTolerancenumber | undefined–Maximum distance for synchronized numeric categories; zero requires exact matches.
interpolation"linear" | "stepAfter" | undefined–Linear segments or step changes; linear by default. Missing values remain gaps.
describeObservation((row: T) => string) | undefined–Caller-formatted source, processing stage or quality shown alongside every source-table row.
datarequiredreadonly T[]–Rows in display order; numeric time categories must be increasing.
categoryKeyrequiredChartCategoryKey<NoInfer<T>>–The row field naming a category or locating a sample on a numeric axis.
configrequiredChartConfig<NoInfer<T>>–Definitions for numeric series, shared with legends and controls.
seriesrequiredreadonly ChartValueKey<NoInfer<T>>[]–The visible numeric fields, in display order. Filtering preserves the complete registry's domain.
formatCategoryrequired(value: string | number) => string–Writes category ticks, tooltip headings and the first table column.
formatAxisValuerequired(value: number) => string–Writes value-axis ticks; separate from each series' observation formatter.
labelrequiredstring–The drawing's accessible name, naming the quantity and time window.
categoryLabelrequiredstring–The first table column's heading, translated by the caller.
dataTableLabelrequiredstring–The disclosure button and source table's accessible name, translated by the caller.
missingValueTextrequiredstring–Display text for missing values in the tooltip and source table.
heightstring | undefined"var(--bk-chart-height)"The plot height as a CSS length; the chart height token by default.
valueDomainreadonly [number, number] | undefined–An explicit increasing value domain that clips outlying values; bar domains always include zero.
showGridboolean | undefinedtrueDraw the grid; on by default.
showAxesboolean | undefinedtrueDraw axes; on by default. Values remain available in the table when off.
motionMotionProp | undefinedfalseEntrance and exit motion; off by default. Measurements never interpolate.
childrenReactNode | undefined–Recharts annotations appended inside the plot, such as ReferenceLine. Use ChartContainer for a custom root.
classNamestring | undefined–Classes added to the root.
testIdstring | undefined–Becomes data-testid on the root, with --plot and --table parts.
refRef<HTMLDivElement> | undefined–The chart root element.
angleReferencerequiredstring–Caller display text naming the angle reference and direction convention.
connectboolean | undefinedtrueConnect supplied observations in their given order; missing values break the curve.
closedboolean | undefinedfalseClose complete curves; off by default and never closes across a missing observation.
Do

Sailing performance

Illustrative observations · true wind speed 12 kn

True wind from the bow · mirrored port and starboard curves

Illustrative sailing speed by true wind angleTrue wind from the bow · mirrored port and starboard curves2 kn4 kn6 kn8 kn0°45°90°135°180°225°270°315°Target speed: 35°, 4 knTarget speed: 45°, 5.2 knTarget speed: 70°, 6 knTarget speed: 100°, 6.4 knTarget speed: 130°, 5.6 knTarget speed: 170°, 4.5 knTarget speed: 190°, 4.5 knTarget speed: 230°, 5.6 knTarget speed: 260°, 6.4 knTarget speed: 290°, 6 knTarget speed: 315°, 5.2 knTarget speed: 325°, 4 knObserved speed: 35°, 3.8 knObserved speed: 45°, 4.9 knObserved speed: 70°, 5.5 knObserved speed: 130°, 5.4 knObserved speed: 170°, 4.2 knObserved speed: 190°, 4.2 knObserved speed: 230°, 5.4 knObserved speed: 290°, 5.5 knObserved speed: 315°, 4.9 knObserved speed: 325°, 3.8 knBow
  • Target speed
  • Observed speed
Keep the comparison basis and missing information visible.
Don't

Preserve gaps and supply the source or validity information needed to interpret the observations.

Missing history must not appear to be a measured zero or a known state.