Migrating to v0.21
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
cairosvgdependency. - PDF/JPG export no longer works. ⚠️ Watch out: exporting to a filename with any extension other than
.pngjust writes raw SVG text to that file, with no error — sop.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. paddingkwarg removed. Usestyle.figure.paddinginstead.- Arbitrary
**kwargspassthrough removed (there's nosavefig()underneath to forward them to). - New
scalekwarg, PNG-only — multiplies the output resolution. dpiis gone entirely as a concept, for both the plot constructor andexport().
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.colorwas renamed tofill, and now also accepts aGradientStylenot just a solid color. See new documentation on gradients.- Default
symbolchanged frompointtocircle; defaultsizechanged from22to24(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 fromsuntostar_8.
LineStyle
- Default
widthhalved, from4to2. dash_capstylerenamed tocap_style.edge_width/edge_color(an outline/halo around the line itself, separate fromstroke) removed entirely — lines can no longer have their own halo stroke.
PolygonStyle
- The
colorshorthand field (which set both fill and edge at once) is gone — setfillandstrokeseparately. fill_colorrenamed tofill, and its default changed fromNone(transparent) to an opaque gray (#c2c2c2). If you callpolygon(),circle(),rectangle(), orellipse()without an explicit fill color, you'll now get a filled gray shape instead of a transparent one.
LabelStyle
border_widthrenamed tostroke_width,border_colorrenamed tostroke— both now default toNoneand fall back to the newbasestyle rather than being hardcoded.line_spacingremoved — title spacing is now controlled byTitleStyle.padding_bottom(see below).- Default
font_sizechanged from15to24; defaultfont_namechanged from"Inter"toNone(seebase).
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, andnum_columnsremoved — no more text alignment, multi-column layout control.- Flat per-entry font fields (
font_name,font_size,font_weight,font_color) are now a nestedlabels: LabelStyle. Flat title font fields are now a nestedtitle: LabelStyle. locationdefault changed frominside_bottom_righttoinside_top_right(and all location values were renamed — see the enum table above).padding_x/padding_ychanged 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 nowmargin_x/margin_y(new fields, default24).-
The legend box's appearance now lives in a nested
background: PolygonStyle, matchingstyle.axes.backgroundandstyle.figure.background. This replaces the flatbackground_color,background_alpha, andborder_colorfields:Old New background_colorbackground.fill(can now also be aGradientStyle)background_alphabackground.opacityborder_colorbackground.stroke(not configurable) background.stroke_width(default1) -
New:
border_radius(default8.0, for rounded corners). It stays a top-levelLegendStylefield, sincePolygonStylehas 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.fillPlotStyle.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 simplerstyle.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, aGradientStylecan be set as thefillof aMarkerStyle,PolygonStyle,style.axes.background, orstyle.figure.background. The gradient presetGRADIENT_TRUE_NIGHTwas renamed toGRADIENT_NIGHT;GRADIENT_LAVENDER_TWILIGHTis new.
New style sections
PlotStyle.table(an instance ofTableStyle) — styles the table plotted withOpticPlot.info()PlotStyle.groundandPlotStyle.tissot— new, for horizon-plot ground fill and the newtissot()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 explicitstyle.
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
pyprojinstead ofcartopy. If you accessedprojection.crsdirectly to get a cartopy CRS object, that property is gone — use the newprojection.get_crs(source_crs)/projection.get_transformer(source_crs)methods instead. ProjectionBase.threshold(a cartopy smoothness setting) is removed. Passingthresholdto any projection constructor will now raise a validation error.- New:
Gnomonicprojection. - 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,azimuthwhere 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/ylimproperties removed.patch(center_x, center_y, **kwargs)(returned amatplotlib.patches.Patch) removed, replaced bypolygon(center_x, center_y)(returns a Shapely polygon; no**kwargs/paddingsupport).transform(axis)(calledinvert_xaxis()/invert_yaxis()on a matplotlibAxes) removed, replaced by declarativeinvert_x: bool/invert_y: boolfields on the optic itself (e.g.Refractorsetsinvert_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 setupagain 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_typedefault 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). SetSTARPLOT_SVG_TEXT_TYPE=path(orsettings.svg_text_type = "path") to keep the old behavior.- New:
settings.precision(env varSTARPLOT_PRECISION, default4) — 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!