Polar Charts in Python
A polar chart (also called a polar plot or polar graph) places each observation by an angle (theta, or θ) and a distance from the center (radius, or r). It is a natural fit for cyclic measurements, directional observations, antenna patterns, radar comparisons, and wind distributions.
Jump to a polar line chart, the angular axis, or supported marks and limits.
XY uses the same composition model as its Cartesian charts. Put line,
scatter, area, bar, column, heatmap, contour, or errorbar marks
inside polar_chart(). Focused helpers build
radar,
radial bar, and
wind rose charts on the same coordinate system.
Create a Polar Line Chart
The first mark channel becomes θ and the second becomes r. Angles use radians by default:
Without an authored sector, polar_chart() draws a full circular frame. Its
angular axis defaults to 0..2π, and a linear radial axis starts at zero and
ends at the largest radius. Pass domain= to r_axis() when the radial view
must stay fixed.
Map a Polar Field
Heatmap cells follow rings and spokes rather than stretching a Cartesian image into a circle. Contours share that projection, so they can be layered over the field with one colorbar:
The polar heatmap path inverse-samples the source grid at the requested browser or export resolution. Pixels inside a hole or outside an authored sector stay transparent. Contours remain projected vector geometry.
Choose a Polar Chart Type
The overview owns numeric theta/r data and shared coordinate-system behavior. Use a focused page when the data already matches one of these higher-level compositions.
Compare Categories with a Radar Chart
Radar charts space named dimensions evenly around the frame and close filled or outlined profiles automatically.
Draw Radial Bars
Radial bar charts turn bars into annular sectors with scalar or per-item angular widths and configurable inner radii.
Compose a Pie or Donut
The pie and donut guide turns unequal-width
sectors and base= into polished share, progress-ring, revenue-mix, and gauge
blocks.
Summarize Wind with a Wind Rose
Wind rose charts bin raw compass bearings into directional sectors and stack their counts by speed band.
Configure the Angular Axis
theta_axis() accepts the ordinary x_axis() options for labels, ticks,
formatting, and styling, plus five polar settings:
The compass combination
xy.theta_axis(unit="degrees", zero="N", direction="clockwise") makes 0° point
north, 90° east, 180° south, and 270° west.
A numeric zero is always an offset in radians counterclockwise from east,
independent of the data unit. Exact tick_values take priority over automatic
angular ticks. When tick_labels are omitted, authored fractional degree
values retain their precision (for example, 22.5 renders as 22.5°).
sector=(start, end) clips marks and ticks to that angular interval and fits
the chart to the visible arc's bounding box. On theta_axis(), domain= is a
compatibility alias for sector=; pass one or the other, not both. Numeric
theta keeps its independent full-turn data and tick range. Set
grid_shape="linear" to join spoke intersections into polygonal radial rings.
Category strings also work as theta coordinates. They are spaced evenly around
a full turn or across an authored sector, and their labels take priority over
numeric angle formatting. radar_chart() remains the convenient composition
when every series shares one category list and should close automatically.
Configure the Radial Axis
r_axis() accepts the same options as y_axis(). Use label= for the measured
quantity, domain=(minimum, maximum) for a fixed radial range, and
tick_values= when rings must land at exact values. Radial axes support
type_="linear", "log", or "symlog".
The automatic linear range begins at zero so the center keeps its usual
meaning; log autorange remains strictly positive. An explicit domain can choose
a different inner value. hole= reserves a shared display-space inner-radius
fraction from 0 up to (but not including) 1. origin= places the shared radial
origin in data space, so an origin below the visible minimum creates an
annulus. hole and origin are mutually exclusive, and a log-axis origin must
be positive.
Filled areas and annular sectors are clipped to the visible radial interval. For example, a bar extending past the outer ring draws up to that ring instead of disappearing, and a filled area crossing the radial minimum is trimmed instead of reflecting through the center. A sector wholly outside the interval disappears. Scatter points and line vertices outside the interval are culled; an out-of-range vertex splits a line into visible runs instead of connecting through its mirrored polar position.
Combine a Sector, Hole, and Error Bars
Both angular (xerr) and radial (yerr) uncertainty project through the polar
coordinate system. This example fits the plot to a 220° compass sector and
clips the marks to a shared inner hole:
Hover and Zoom
Interactive polar charts deliberately expose a smaller gesture set than Cartesian charts:
- Hover reports the nearest point or field cell: its series name, radial value,
and any color or size encoding. The numeric angle is left out — the cursor is
already on it — while an authored spoke label survives and
xy.tooltip(labels={"x": ...})opts the angle back in. A hole or excluded part of a sector is not hit-testable. - Wheel and modebar zoom scale the radial maximum while keeping the radial minimum fixed. Reset restores the original radial range.
- Theta rotation/panning, interactive sector zoom, box zoom, rectangular or lasso selection, brushing, and crosshairs are not currently available.
Keeping the radial minimum fixed prevents an ordinary zoom from unexpectedly turning a disc into an annulus. See Interactions and selections for the general interaction configuration surface.
Lay Out and Annotate Polar Charts
Explicit padding=(top, right, bottom, left) is preserved by the polar layout.
Increase the bottom or side value to reserve a stable band for a legend,
caption, or other surrounding content; the disc stays centered in the
remaining plot box.
Point-anchored text, label, marker, arrow, and callout annotations
interpret data coordinates as (theta, r) consistently in the browser, SVG,
and native raster output. Their dx and dy offsets remain screen-space
pixels. Polar rules and bands remain deferred because they require spoke/ring
and sector/annulus geometry instead of Cartesian lines and rectangles; using
one on a polar chart raises at payload build instead of drawing a Cartesian
approximation.
Use xy.pyplot
The Matplotlib-style compatibility layer routes a polar subplot through the same renderer:
Polar routing works through plt.subplot(projection="polar"),
fig.add_subplot(..., projection="polar"),
plt.axes(projection="polar"), and
plt.subplots(subplot_kw={"projection": "polar"}). The polar=True alias is
accepted by subplot(), add_subplot(), and axes(), or inside
subplot_kw; it is not a direct plt.subplots(polar=True) argument. Ordinary
plot, scatter, fill, bar, heatmap/image, contour, and errorbar calls
share the core polar renderer. fill() maps its boundary to a radial area
against r=0, which matches a full-turn filled profile but not every arbitrary
closed Matplotlib polygon. The shim preserves theta zero, direction, offset,
and authored theta grids. Degree-based set_thetamin()/set_thetamax() share
view state with radian set_xlim(), while set_rorigin() authors the
data-space radial origin. Their corresponding getters, radial limits, ticks,
grids, categorical theta, and log/symlog radial scales use the same core axes.
The stateful plt.polar(), plt.thetagrids(), and plt.rgrids() convenience
wrappers are not part of this increment; call the corresponding methods on the
polar axes. Keep and reuse the returned axes handle: passing
projection="polar" again while reactivating an existing plt.subplot() is
not supported. See the
Matplotlib compatibility guide for the
full boundary.
Supported Marks and Current Limits
The supported polar primitives are line, scatter, area, bar, column,
heatmap, contour, and errorbar. The
radar,
radial bar, and
wind rose helpers compose those primitives; they
do not add separate renderers.
Current limits:
- Histograms, box plots, hexbin, density grids, generic segments, and meshes
are rejected instead of being drawn with incorrect geometry. Polar
errorbarand contour use narrowly allowlisted projected segments; they do not make every segment-backed mark legal. - Polar lines and filled-area boundaries connect observations with straight chords. Repeat the first observation at one full turn when manually closing a line; the radar helper handles closure itself.
- Polar traces use direct rendering rather than Cartesian line decimation or
scatter-density aggregation.
line,scatter, andareaare limited to 200,000 points per trace; a larger point trace raisesValueError. Heatmap/contour grids are not rejected merely because their cell count exceeds that point ceiling. - Polar rules and bands need dedicated spoke, ring, annulus, or sector geometry and raise instead of falling back to Cartesian geometry.
- Polar LOD, facets/animation, interactive theta rotation/pan, sector zoom, and annulus/sector selection remain deferred.
Supported marks and point-anchored annotations use the same polar projection in the browser, SVG, PDF, and native raster exporters, so the chart can be displayed live or exported through the usual chart methods.
Related Charts
- Radar charts — compare named dimensions with closed profiles.
- Radial bar charts — annular sectors, pies, and donuts.
- Wind rose charts — directional frequency split into magnitude bands.
- Line charts — trends on Cartesian axes.
- Scatter charts — relationships and multichannel points on Cartesian axes.
- Bar charts — rectangular categorical and numeric bars.
- Axes and scales — shared labels, domains, ticks, and styling options.
- Display and export — notebooks, HTML, PNG, SVG, PDF, JPEG, and WebP.
API Reference
xy.polar_chart
A polar chart: the same marks, rendered through polar coordinates.
Props
| Prop | Type | Description |
|---|---|---|
*children | Component | Marks, axes, annotations, and chart chrome. |
title | Optional[str] | Title shown above the plot. |
width | int | str | Chart width in pixels or a CSS size such as ``"100%"``. |
height | int | str | Chart height in pixels or a CSS size such as ``"100%"``. |
padding | Union[float, Sequence[float], None] | Plot margins, as one value or a sequence of side values. Use zero for an edge-to-edge sparkline. |
data | TableLike | Chart-level data used by marks that omit their own ``data``. |
class_name | Optional[str] | CSS class applied to the chart container. |
class_names | Optional[dict[str, str]] | CSS classes keyed by stable chart DOM slot. |
style | Optional[dict[str, StyleValue]] | Inline style overrides for the chart container. |
styles | Optional[dict[str, dict[str, StyleValue]]] | Inline style mappings keyed by stable chart DOM slot. |
on_hover | Optional[Callable[[dict], None]] | Callback receiving hover event payloads. |
on_click | Optional[Callable[[dict], None]] | Callback receiving picked-mark click payloads. |
on_brush | Optional[Callable[[dict], None]] | Callback receiving brush event payloads. |
on_select | Optional[Callable[[Selection], None]] | Callback receiving data-space selections. |
on_view_change | Optional[Callable[[dict], None]] | Callback receiving viewport change payloads. |
hover | Optional[bool] | Whether pointer movement emits hover events. |
click | Optional[bool] | Whether picked marks emit click events. |
select | Optional[bool] | Whether shift-drag box selection is enabled. |
brush | Optional[bool] | Whether brush selection is enabled. |
crosshair | Optional[bool] | Whether plot-aligned hover guides are shown. |
navigation | Optional[bool] | Whether browser pan and zoom navigation is enabled. |
pan | Optional[bool] | Whether plain-drag panning is enabled. |
pan_axes | Optional[tuple[str, ...]] | Declared axis IDs translated by pan gestures. |
zoom | Optional[bool] | Whether viewport zoom is enabled. |
default_drag_action | Optional[DefaultDragAction] | Initial action performed by a plain plot drag. |
zoom_axes | Optional[tuple[str, ...]] | Declared axis IDs changed by zoom gestures and controls. |
zoom_limits | Optional[ZoomLimits] | Minimum and maximum magnification globally or by axis. |
wheel_zoom | Optional[bool] | Whether wheel and trackpad zoom is available. |
box_zoom | Optional[bool] | Whether box zoom is available as a drag action. |
zoom_buttons | Optional[bool] | Whether modebar Zoom In/Out commands are available. |
double_click_reset | Optional[bool] | Whether double-click restores ``reset_axes``. |
reset_axes | Optional[tuple[str, ...]] | Declared axis IDs restored by reset. |
link_group | Optional[str] | Identifier used to synchronize charts in the browser. |
link_axes | Optional[tuple[str, ...]] | Axes synchronized within the link group. |
coords | str | Coordinate system, ``"cartesian"`` (default) or ``"polar"``. Under ``"polar"`` each mark's first channel is the angle and its second is the radius. Prefer ``xy.polar_chart(...)``, which sets this for you. |
xy.theta_axis
Configure the angular axis of an `xy.polar_chart`.
Props
| Prop | Type | Description |
|---|---|---|
unit | Optional[str] | Angular unit of the data, ``"radians"`` (default) or ``"degrees"``. |
zero | Union[str, float, None] | Direction that angle 0 points — ``"E"`` (default), ``"N"``, ``"W"``, ``"S"``, or an angle in radians counterclockwise from east. |
direction | Optional[str] | ``"counterclockwise"`` (default) or ``"clockwise"``. Compass work usually wants ``zero="N"`` with ``"clockwise"``, which puts 90° at east and 180° at south. |
sector | Optional[tuple[float, float]] | Visible angular interval in the declared ``unit``. The sweep must be increasing and no wider than one full turn. |
grid_shape | Optional[str] | ``"circular"`` (default) for arc rings or ``"linear"`` for polygonal rings joining the angular spokes. |
id | str | Axis identifier referenced by marks. |
label | Optional[str] | Axis label. |
label_position | Optional[AxisLabelPosition] | Named or structured label placement. |
label_offset | Optional[float] | Label offset in pixels. |
label_angle | Optional[float] | Label rotation in degrees. |
type_ | Optional[str] | Scale type, such as ``linear``, ``time``, ``log``, or ``symlog``. |
constant | Optional[float] | Width of the linear region around zero for ``symlog``. |
domain | Optional[tuple[float, float]] | Explicit minimum and maximum scale values. |
margin | Optional[float] | Fractional padding around an automatic domain. |
bounds | Union[tuple[float, float], Literal['data'], None] | Hard navigation limits, or ``"data"`` to use the data range. Pan and zoom are clamped within these limits; ``None`` leaves navigation unrestricted. |
format | Optional[str] | Tick-label format string. |
tick_count | Optional[int] | Requested number of ticks. |
tick_values | Union[Sequence[float], np.ndarray, None] | Explicit tick positions. |
tick_labels | Optional[Sequence[str]] | Labels corresponding to explicit tick positions. |
tick_label_angle | Optional[float] | Tick-label rotation in degrees. |
tick_label_strategy | Optional[AxisTickLabelStrategy] | Collision-handling strategy for tick labels. |
side | Optional[str] | Side of the plot where the axis is drawn. |
tick_sides | Optional[Sequence[str]] | Plot sides where tick marks are drawn. Defaults to ``side``; supplying both draws mirrored ticks without moving the axis labels. |
tick_label_sides | Optional[Sequence[str]] | Plot sides where tick labels are drawn. Defaults to ``side`` and remains independent of ``tick_sides``. |
show | Optional[bool] | Draw this axis at all. ``False`` hides its baseline, tick marks, tick labels, title, and grid lines in one switch; the four narrower switches below override it either way, so ``show=False, grid=True`` leaves only the grid. |
line | Optional[bool] | Draw the axis baseline. |
ticks | Optional[bool] | Draw the tick marks. |
grid | Optional[bool] | Draw this axis's grid lines (the y axis owns the horizontal guides, the x axis the vertical ones). |
text | Optional[bool] | Draw this axis's text — its tick labels and its title. (Unlike ``tick_labels``, which supplies the label *strings*.) |
style | Optional[dict[str, StyleValue]] | Axis style overrides. An explicit property here always wins over the switches above. |
nonpositive | Optional[Literal['clip', 'mask']] | Log-axis handling for non-positive mark coordinates: ``"clip"`` or ``"mask"``. |
xy.r_axis
Configure the radial axis of an `xy.polar_chart`.
Props
| Prop | Type | Description |
|---|---|---|
hole | Optional[float] | Display-space inner-radius fraction, from 0 (no hole) up to but excluding 1. |
origin | Optional[float] | Data-space radial origin. An origin below the visible radial minimum creates an annulus. Mutually exclusive with ``hole``. |
id | str | Axis identifier referenced by marks. |
label | Optional[str] | Axis label. |
label_position | Optional[AxisLabelPosition] | Named or structured label placement. |
label_offset | Optional[float] | Label offset in pixels. |
label_angle | Optional[float] | Label rotation in degrees. |
type_ | Optional[str] | Scale type, such as ``linear``, ``time``, ``log``, or ``symlog``. |
constant | Optional[float] | Width of the linear region around zero for ``symlog``. |
domain | Optional[tuple[float, float]] | Explicit minimum and maximum scale values. |
margin | Optional[float] | Fractional padding around an automatic domain. |
bounds | Union[tuple[float, float], Literal['data'], None] | Hard navigation limits, or ``"data"`` to use the data range. Pan and zoom are clamped within these limits; ``None`` leaves navigation unrestricted. |
reverse | bool | Whether to reverse the scale direction. |
format | Optional[str] | Tick-label format string. |
tick_count | Optional[int] | Requested number of ticks. |
tick_values | Union[Sequence[float], np.ndarray, None] | Explicit tick positions. |
tick_labels | Optional[Sequence[str]] | Labels corresponding to explicit tick positions. |
tick_label_angle | Optional[float] | Tick-label rotation in degrees. |
tick_label_strategy | Optional[AxisTickLabelStrategy] | Collision-handling strategy for tick labels. |
side | Optional[str] | Side of the plot where the axis is drawn. |
tick_sides | Optional[Sequence[str]] | Plot sides where tick marks are drawn. Defaults to ``side``; supplying both draws mirrored ticks without moving the axis labels. |
tick_label_sides | Optional[Sequence[str]] | Plot sides where tick labels are drawn. Defaults to ``side`` and remains independent of ``tick_sides``. |
show | Optional[bool] | Draw this axis at all. ``False`` hides its baseline, tick marks, tick labels, title, and grid lines in one switch; the four narrower switches below override it either way, so ``show=False, grid=True`` leaves only the grid. |
line | Optional[bool] | Draw the axis baseline. |
ticks | Optional[bool] | Draw the tick marks. |
grid | Optional[bool] | Draw this axis's grid lines (the y axis owns the horizontal guides, the x axis the vertical ones). |
text | Optional[bool] | Draw this axis's text — its tick labels and its title. (Unlike ``tick_labels``, which supplies the label *strings*.) |
style | Optional[dict[str, StyleValue]] | Axis style overrides. An explicit property here always wins over the switches above. |
nonpositive | Optional[Literal['clip', 'mask']] | Log-axis handling for non-positive mark coordinates: ``"clip"`` or ``"mask"``. |
FAQ
How do I create a polar chart in Python?
Put a supported mark such as xy.line(theta, radius),
xy.heatmap(z, x=theta, y=radius), or
xy.errorbar(theta, radius, yerr=...) inside xy.polar_chart(...). Angles are
radians by default.
How do I create a radar chart?
See the radar chart guide for filled and outlined profiles, the category/value contract, and shared-scale configuration.
How do I create a wind rose?
See the wind rose guide for directional bins, speed bands, input validation, and compass conventions.
How do I create a pie or donut chart?
See Pie and Donut Charts for four live unequal-width sector compositions with center metrics and Reflex legends.
How do I use degrees instead of radians?
Add xy.theta_axis(unit="degrees"). The angle data and generated tick labels
will both use degrees.
How do I make zero degrees point north?
Use xy.theta_axis(unit="degrees", zero="N", direction="clockwise") for the
standard compass convention. The
wind rose helper applies it automatically.
Can I migrate a Matplotlib polar plot?
Yes. Create the axes with plt.subplot(projection="polar") or
plt.subplots(subplot_kw={"projection": "polar"}). Lines, scatter,
radial-to-zero fills, bars, heatmaps/images, contours, and error bars route
through the polar renderer. Theta zero/direction/offset and min/max, authored
theta grids, radial origin, radial limits/ticks/grids, categorical theta, and
log/symlog radius are supported. Arbitrary closed-polygon fills remain a
documented approximation.