Skip to content

Migrating to v0.21

< Back to Changelog

This page summarizes the major changes between version 0.20 and 0.21.

📣 The big change in v0.21 is that Starplot's rendering backend was completely replaced.

Previous versions rendered plots with matplotlib + cartopy. Starting in 0.21, Starplot has its own SVG-first rendering engine, with no matplotlib or cartopy dependency at all. This is a much bigger change than a typical release, so please read through this guide carefully before upgrading — especially if your code involves custom styling, exporting, or the ax/fig properties on a plot.

Why this change was made:

  • Creating plots and exporting to SVG is now at least 2x faster
  • Allows supporting gradient fills on markers and polygons
  • Exported SVGs are now much easier and faster to work with in design applications (e.g. Inkscape, Affinity Designer, Adobe Illustrator, etc)
  • Makes the code easier to work with and add more features (e.g. interactivity and animation)

Dependencies

As a result of the new SVG-first backend, matplotlib, cartopy, and pyogrio are no longer dependencies of Starplot. If your own code imports them directly (e.g. to post-process a plot), you'll need to add them to your own project's dependencies going forward — Starplot no longer pulls them in transitively.

New dependencies: pyproj, pyqtree, fonttools, pydantic-extra-types, and cairosvg (used for PNG export — see Exporting Plots below).

No More ax / fig / dpi

Since there's no matplotlib figure anymore, plot.ax, plot.fig, and plot.dpi are all gone. If you were using these to do custom matplotlib rendering on top of a Starplot plot, that pattern no longer works — there's no matplotlib Axes/Figure to hand off to. As a side effect of this, close_fig() is also gone.

Plots now have a plot.canvas object that handles SVG rendering internally, but it's not designed as a public extension point the way ax/fig were. Future versions of Starplot will likely have a public API to this internal canvas.

Exporting Plots

export() no longer wraps matplotlib's savefig(), so its behavior changed in a few important ways:

# Before (0.20)
p.export("map.svg", padding=0.1, transparent=True)  # any savefig() kwarg worked
p.export("map.pdf")  # matplotlib inferred format from the extension
p.export("map.png", dpi=300)

# After (0.21)
p.export("map.svg")
p.export("map.png", scale=2)  # scale multiplies PNG resolution -- there's no dpi kwarg anymore
  • Only SVG and PNG are truly supported now. PNG export goes through the new cairosvg dependency.
  • PDF/JPG export no longer works. ⚠️ Watch out: exporting to a filename with any extension other than .png just writes raw SVG text to that file, with no error — so p.export("map.pdf") will silently produce a file containing SVG markup, not a real PDF. If you need PDF/JPG output, you'll need to convert the exported SVG yourself (e.g. with a separate tool), or export PNG and convert that.
  • padding kwarg removed. Use style.figure.padding instead.
  • Arbitrary **kwargs passthrough removed (there's no savefig() underneath to forward them to).
  • New scale kwarg, PNG-only — multiplies the output resolution.
  • dpi is gone entirely as a concept, for both the plot constructor and export().

Styling Framework

The styling framework was refactored for more consistent naming across all style classes.

If you have any code that reads or sets style fields directly (as opposed to just using the built-in style extensions), you'll need to update field names.

Fields renamed

These renamed fields apply to MarkerStyle, LineStyle, PolygonStyle, and LabelStyle (not every class has every field — see the specific tables below for exact class membership):

Old field New field
color / fill_color / font_color fill
edge_color / border_color stroke
edge_width / border_width stroke_width
alpha / font_alpha opacity
line_style dash_array

These were renamed to make them more consistent with their SVG styling terms and to make them all more consistent and clear (e.g. fill is more specific and less ambiguous than color).

MarkerStyle

  • fill: FillStyleEnum (controlled which half of a marker was filled — full/left/right/top/bottom/none) is gone. Half-filled markers are no longer supported at all — this isn't just a rename, the feature was removed.
  • color was renamed to fill, and now also accepts a GradientStyle not just a solid color. See new documentation on gradients.
  • Default symbol changed from point to circle; default size changed from 22 to 24 (and is now in pixels, not points).
  • The symbol set shrank from 20 to 14 options. Removed: point, square_stripes_diagonal, sun, circle_plus, circle_dot, circle_dotted_edge, circle_dotted_rings. Added: satellite. If you used any of the removed symbols, you'll need to pick a replacement — note this is also why the Sun's default marker symbol changed from sun to star_8.

LineStyle

  • Default width halved, from 4 to 2.
  • dash_capstyle renamed to cap_style.
  • edge_width / edge_color (an outline/halo around the line itself, separate from stroke) removed entirely — lines can no longer have their own halo stroke.

PolygonStyle

  • The color shorthand field (which set both fill and edge at once) is gone — set fill and stroke separately.
  • fill_color renamed to fill, and its default changed from None (transparent) to an opaque gray (#c2c2c2). If you call polygon(), circle(), rectangle(), or ellipse() without an explicit fill color, you'll now get a filled gray shape instead of a transparent one.

LabelStyle

  • border_width renamed to stroke_width, border_color renamed to stroke — both now default to None and fall back to the new base style rather than being hardcoded.
  • line_spacing removed — title spacing is now controlled by TitleStyle.padding_bottom (see below).
  • Default font_size changed from 15 to 24; default font_name changed from "Inter" to None (see base).

Enums renamed and de-enum'd

Every style enum was renamed and converted from an Enum subclass to a plain class (except GradientType, which stayed an Enum). If you import any of these old names, it will now fail:

Old New Notes
FillStyleEnum (removed — see MarkerStyle above)
FontWeightEnum FontWeight same values
FontStyleEnum FontStyle
MarkerSymbolEnum (no longer a runtime enum — MarkerSymbol is a Literal[...] type) symbol set also changed, see above
LineStyleEnum DashArray
CapStyleEnum CapStyle
JoinStyleEnum JoinStyle
LegendLocationEnum LegendLocation values renamed, e.g. "upper center" → "inside_top_center", "outside left upper" → "outside_top_left"
AnchorPointEnum AnchorPoint values renamed from space-separated ("top left") to snake_case ("top_left")
AlignmentEnum HorizontalAlignment the field that used it on LegendStyle was removed (see below)
ZOrderEnum ZOrder same values
GradientDirection GradientType the mollweide option was dropped — only linear/radial now

LegendStyle restructured

Legend styling was consolidated and several fields changed how they work (not just their name):

  • alignment, expand, and num_columns removed — no more text alignment, multi-column layout control.
  • Flat per-entry font fields (font_name, font_size, font_weight, font_color) are now a nested labels: LabelStyle. Flat title font fields are now a nested title: LabelStyle.
  • location default changed from inside_bottom_right to inside_top_right (and all location values were renamed — see the enum table above).
  • padding_x/padding_y changed meaning: previously they controlled the legend's distance from the map edge; now they control padding inside the legend box, between its border and its content. The "distance from anchor position" concept is now margin_x/margin_y (new fields, default 24).
  • The legend box's appearance now lives in a nested background: PolygonStyle, matching style.axes.background and style.figure.background. This replaces the flat background_color, background_alpha, and border_color fields:

    Old New
    background_color background.fill (can now also be a GradientStyle)
    background_alpha background.opacity
    border_color background.stroke
    (not configurable) background.stroke_width (default 1)
  • New: border_radius (default 8.0, for rounded corners). It stays a top-level LegendStyle field, since PolygonStyle has no corner radius.

legend() also gained an additive "magnitude scale" sub-feature (magnitude_scale=True plus related kwargs) for drawing a star-size legend — not a breaking change, just new.

base style and text halos

New: PlotStyle.base (an instance of PlotBaseStyle), which provides fallback values for any LabelStyle whose font_name, font_family, stroke, or stroke_width are left as None:

base:
    font_name: str = "Inter"
    font_family: str = "sans-serif"
    text_stroke_width: float = 0
    text_stroke: Color | None = None

This replaces the old flat PlotStyle.text_border_width / text_border_color fields. Practically: if you were setting a label's border_color/border_width (now stroke/stroke_width) directly on individual styles, that still works the same way — but if you were relying on the global default text border, set style.base.text_stroke / style.base.text_stroke_width instead.

Axes, figure, and gradients

  • PlotStyle.background_color → style.axes.background.fill
  • PlotStyle.figure_background_color → style.figure.background.fill
  • The old bordered-frame fields (border_font_size, border_font_weight, border_font_color, border_line_color, border_bg_color) are all removed, replaced by a much simpler style.axes.border: LineStyle | None (default: a plain 2px black line). There's no longer a way to put custom text/labels directly on that border frame.
  • Gradients are now a general-purpose fill value. Previously, gradients were special-cased and only usable for PlotStyle.background_color (as a raw list of (stop, color) tuples). Now, a GradientStyle can be set as the fill of a MarkerStyle, PolygonStyle, style.axes.background, or style.figure.background. The gradient preset GRADIENT_TRUE_NIGHT was renamed to GRADIENT_NIGHT; GRADIENT_LAVENDER_TWILIGHT is new.

New style sections

  • PlotStyle.table (an instance of TableStyle) — styles the table plotted with OpticPlot.info()
  • PlotStyle.ground and PlotStyle.tissot — new, for horizon-plot ground fill and the new tissot() method, respectively.
  • PlotStyle.line, .polygon, .circle, .ellipse, .rectangle, .marker, .text — default styles used by the corresponding generic plotting methods when you don't pass an explicit style.

Plot Methods

alpha_fn renamed to opacity_fn

On both stars() and dsos(), the alpha_fn kwarg was renamed to opacity_fn (matching the alpha → opacity field rename above).

gridlines() kwargs renamed

MapPlot.gridlines():

Old New
ra_formatter_fn ra_label_fn
dec_formatter_fn dec_label_fn
tick_marks, ra_tick_locations, dec_tick_locations (removed — tick marks are no longer supported on map gridlines)
(n/a) ra_label_locations, dec_label_locations (new — control which sides show labels)

HorizonPlot.gridlines():

Old New
show_labels (removed, replaced by az_label_locations/alt_label_locations)
az_formatter_fn az_label_fn
alt_formatter_fn alt_label_fn
divider_line, show_ticks, tick_step (removed)

New: tissot() plot method

A new method for plotting a Tissot's indicatrix, styled via the new PlotStyle.tissot. Additive — no migration needed unless you want to use it.

Projections

  • Projections are now backed by pyproj instead of cartopy. If you accessed projection.crs directly to get a cartopy CRS object, that property is gone — use the new projection.get_crs(source_crs) / projection.get_transformer(source_crs) methods instead.
  • ProjectionBase.threshold (a cartopy smoothness setting) is removed. Passing threshold to any projection constructor will now raise a validation error.
  • New: Gnomonic projection.
  • Every other projection class (Miller, Mercator, PlateCarree, ObliqueMercator, Mollweide, Equidistant, StereoNorth, StereoSouth, Robinson, LambertAzEqArea, Orthographic, Stereographic) keeps the same name and the same constructor kwargs (center_ra, center_dec, azimuth where applicable) — no changes needed for typical usage.

Optics (Scope, Refractor, Reflector, Binoculars, Camera)

If you use the built-in optic classes normally (just passing them to optic_fov() / OpticPlot), nothing changes. But if you subclassed Optic or called its lower-level methods directly:

  • xlim / ylim properties removed.
  • patch(center_x, center_y, **kwargs) (returned a matplotlib.patches.Patch) removed, replaced by polygon(center_x, center_y) (returns a Shapely polygon; no **kwargs/padding support).
  • transform(axis) (called invert_xaxis()/invert_yaxis() on a matplotlib Axes) removed, replaced by declarative invert_x: bool / invert_y: bool fields on the optic itself (e.g. Refractor sets invert_x=True).

Ephemeris Default Changed

The default ephemeris changed from "de421.bsp" to "de440s.bsp" everywhere it's used as a default (plot constructors, and Sun.get(), Moon.get(), Planet.get()/.all(), Comet.get()/.all()/.at(), Observer.position()/.observe()). This is a more accurate/modern ephemeris, but:

  • You may need to run starplot setup again to download it.
  • Positions calculated with the new default will differ very slightly from 0.20 (both are accurate; they're just different ephemeris data). If you need historical output consistency, pass ephemeris="de421.bsp" explicitly.

Settings

  • settings.svg_text_type default changed from "path" to "element" — exported SVGs now default to real <text> elements (editable/selectable in graphic design tools, but dependent on system fonts being available wherever the SVG is later viewed) instead of vector path outlines (guaranteed visual fidelity, larger files). Set STARPLOT_SVG_TEXT_TYPE=path (or settings.svg_text_type = "path") to keep the old behavior.
  • New: settings.precision (env var STARPLOT_PRECISION, default 4) — number of decimal places used when rounding coordinates/dimensions in SVG output.

Your Plots May Look Different

Even without changing any of your own code, plots may render with different proportions than before, due to a combination of changes: the internal resolution baseline used for autoscale=True changed slightly (4096 → 4000), an internal scale *= 1.28 multiplier that used to apply to every plot was removed, and many built-in style defaults changed (marker sizes, line widths, font sizes, dash patterns — see Styling Framework above). If you'd hand-tuned a scale value to get a specific look in 0.20, you'll likely want to re-check it against 0.21.

Questions, bugs, and other issues

If you have any questions, run into bugs or other issues, please let us know by creating an issue on GitHub, chatting on Discord, or emailing Steve Berardi. Thanks for your patience and support!

Happy Starplotting!!