Pure Fortran plotting library with a pylab-style API, writing SVG, PNG, PDF and EPS. No external graphics library: the PNG rasterizer and its font are part of fplotlib.
use fplotlib
call plot(x, y, "k-", label="sin")
call title("My plot")
call xlabel("x")
call ylabel("y")
call grid(.true.)
call legend()
call savefig("out.svg") ! or "out.png", "out.pdf", "out.eps"
call show() ! Jupyter (LFortran) or writes fplotlib_show.svg
! subplots
call clf()
call subplot(2, 1, 1)
call plot(x, y, "b-", label="sin")
call title("top")
call subplot(2, 1, 2)
call plot(x, y2, "r--", label="cos")
call title("bottom")
call suptitle("figure title")plot,scatter,bar,hist,fill_between,errorbarbartakesyerr=,capsize=,align="edge"andtick_label=;errorbartakesecolor=,elinewidth=,lw=andalpha=plot(y)with a single array numbers the points0, 1, 2, ...for xxlim/ylimto set,get_xlim/get_ylimto read back what autoscaling chose, andinvert_xaxis/invert_yaxisto make an axis count downscatteralso takes per-pointsizes=and color-mappedcvals=(separate keywords because Fortran cannot overload one dummy as scalar-or-array the way matplotlib'ss=andc=do)step,stem,barh,pie,boxplot,violinplotboxplot(y)for one dataset orboxplot(y(:,:), labels=)for a row each, withvert=,notch=,showmeans=,patch_artist=andwhis=violinplotthe same way, withvert=,showmeans=,showmedians=andshowextrema=pie(explode=, startangle=, counterclock=, autopct=, pctdistance=, labeldistance=, radius=, colors=, edgecolor=, linewidth=)semilogx,semilogy,loglog,axhline,axvlinefill_betweenxfor a band along y,stackplotfor layers summed on top of each other, andaxlinefor an endless line through two points or through one with aslope=textandannotate(arrowstyle="->"draws a leader with a head at the annotated point, witharrowcolor=,arrowlw=andshrink=), withrotation=,va=, abbox_facecolor=box behind them and lines broken atachar(10);figtextplaces on the canvas instead of in the axes, andtransform="axes"places in fractions of the axes box.connectionstyle="arc3,rad=0.3"bows the leader out of the straight line, andboxstyle="round,pad=0.5"rounds the corners of the boximshowof an (row, column, channel) array paints the colours given, three channels for RGB or four for RGBAimshowwithcmap,extent,origin,vmin/vmaxand square-pixelaspect, pluscolorbar(label=, orientation=, fraction=, pad=, shrink=, aspect=), upright or lying on its sidecontourandcontourfwith automatic or explicitlevels- Colormaps:
viridis,plasma,inferno,magma,gray,coolwarm - Format strings: colors
bgrcmykw/C0–C9, linestyles---:-., markerso x . s ^ v < > * + D - Optional
label=,lw=,color=,marker=,alpha= hatch=on bars, fills and patches:/,\,|,-,+andx, repeated to pack the lines closer, ruled across the shape and clipped to it (matplotlib's dotted hatches are not drawn)- Marker and line detail on
plot:markersize=,markerfacecolor=,markeredgecolor=,markeredgewidth=,markevery=,drawstyle=and adashes=pattern of your own - Title, axis labels, grid,
xlim/ylim,clf/figure(figsize=, dpi=) set_facecolor(color, alpha=)for the background of one axesgrid(on, axis=, which=, color=, linestyle=, lw=, alpha=)for one axis only, for the minor ticks, or in a colour of your own; log axes carry minor ticks without being asked, as matplotlib's dosavefig(file, transparent=, facecolor=); the extension picks the format, one of.svg,.png,.pdfor.eps, anddpi=sizes the PNG rasteraxis("on"|"off"|"equal"|"scaled"|"tight"|"auto")andset_aspectmargins(m)ormargins(x=, y=)for the room left past the data, andautoscale(enable, axis=, tight=);autoscale(.false.)pins the limits where the data has put them so far, so nothing drawn later moves themset_xscale/set_yscalewith"linear","log"or"symlog"tick_params(axis=, direction=, length=, labelsize=, rotation=)andspineslegend(loc=, fontsize=, ncol=, frameon=, title=, bbox_to_anchor=, labels=), placed where the data leaves most room whenlocis left out, as matplotlib's"best"does; a label starting with_keeps its artist out of the legend,figlegend(loc=, fontsize=, ncol=, frameon=, title=)for one legend covering every panel,xticks/ytickswith optional labels,minorticks_on,xticks(vals, minor=.true.)to place minor ticks by hand, andlocator_params(axis=, nbins=, prune=)- Formatters and locators:
tick_format(axis, "percent"/"comma"/"fixed"),tick_locator(axis, base=, nbins=)andticklabel_format(style=, useoffset=, scilimits=). An axis whose labels would otherwise repeat themselves factors the shared offset and power of ten out into a single label at its end, as matplotlib's ScalarFormatter does - Font sizes:
fontsize=ontitle/xlabel/ylabel/suptitle, andset_fontsize(size=, title=, labels=, ticks=, legend=)to set them globally title(loc="left"|"right")andxlabel/ylabel(labelpad=)savefig(dpi=, bbox_inches="tight", pad_inches=)to crop to the drawing- Several live figures at once:
figure(num=),gcf(),close(num=, all=) subplots(m, n, axs, sharex=, sharey=)hands back axes handles, socall axs(1,2)%plot(x, y)works alongside the statefulsubplotstyle; shared axes span the union of the group and drop their inner tick labels. Every call the stateful interface offers is also a method on the handle- Subplots:
subplot(m, n, i)and figure-levelsuptitle; per-axes state (series, labels, grid, legend, scale, limits) with matplotlib's default subplot spacing subplots_adjustandtight_layoutconstrained_layout(.true.): the same fit, but made afresh at draw time, so labels added after the plotting still get their room. Ours keeps one margin for the whole grid where matplotlib works row by rowtwinxandtwinyfor a second y or x axis on the same plot- Bars:
bar/barhwithbottom=/left=for stacks,colors=for a color per bar,edgecolor=/linewidth=, andbar_label(fmt=, padding=) add_axesfor an axes placed by hand,inset_axesfor a small axes inside another, andsecondary_xaxis/secondary_yaxisfor a second scale along an edgetablefor a block of text below (or above) the axesstreamplotfor the streamlines of a vector fieldadd_frame/save_animation(file, fps=, loop=)to write an animated GIF, or a full-colour animated PNG with a.pngor.apngfile namespy(sparsity pattern of a dense matrix or COO indices),matshow,eventplotandbroken_barhhist2dandhexbinfor counting points into square or hexagonal binsimshow(interpolation="bilinear")to smooth an image instead of showing it as blocks- 3D axes:
plot3d,scatter3d,plot_surface(flat orcmap=) andplot_wireframe, withview_init,zlabelandzlim, drawn with mplot3d's camera, panes and lighting - Polar axes:
polar(theta, r), orset_polar()on an axes, with the angular grid, the degree labels and the radial labels along 22.5° - Layered like matplotlib: images below patches, the grid above the
patches and below the lines, text on top, with
set_zorderto lift the artist just drawn out of that order - Missing data: a NaN or an infinity breaks the line and the band there and takes no part in the limits, as it does in matplotlib
- Patches:
add_rectangle,add_circle,add_ellipse,add_polygon,add_arrowandadd_pathfor an arbitrary path of lines and cubics, filled and outlined in data coordinates clabelto write the level into each contour line, breaking the line at its straightest stretch to make roomquiverfor a field of arrows, sized as matplotlib sizes them, andquiverkeyfor a reference arrow drawn to the same scale- Date axes:
date_num(y, m, d)for the numbers andxaxis_date()to have the ticks land on round dates and read as dates subplot2grid: panels that span several cells of a grid, so a wide plot can sit over two narrow onessubplot_mosaic(["AB", "CC"], keys, axs): the panels drawn as a small picture of the figure, one character per cell, a panel per charactergridspec(width_ratios=, height_ratios=): columns and rows of unequal size, for a wide panel beside a narrow onepcolormeshandpcolor: a grid of cells with edges of your own choosing, which is what an unevenly sampled field needstriplotandtripcolor: scattered points joined into triangles by the same Delaunay triangulation matplotlib gets from Qhull, drawn as edges or filled by valuetricontourandtricontourf: level lines and filled bands over those same triangles, with matplotlib's rounded levels and a colorbarplot_trisurf: those triangles lifted into three dimensions and lit by the same light sourceplot_surfaceusesbar3d: boxes standing on the xy plane, six lit faces each, painted back to front with everything else in the axesquiver3d: arrows in space, a shaft and two barbs turned fifteen degrees off it, with the limits taken from where the arrows startcontour3d: level lines of a grid drawn in space, each at the height of its own level- matplotlib's tick locator and formatter: the same 1/2/2.5/5 steps, the same number of ticks for the space available, and one decimal count shared by the whole axis. A log axis writes its decades as powers, and when it spans a decade or less it labels the multiples between them
- Mathtext:
$10^{-3}$,$x_i$,$E = mc^2$,$\frac{a}{b}$,$\sqrt{x}$and the greek letters in any label, laid out once and drawn by every backend, with the letters sloped and the rest upright as TeX sets them. Text is UTF-8; PDF and EPS fill the greek as outlines, since no core font has it fontweight=andfontstyle=ontitle,suptitle,xlabel,ylabel,textandannotate: real bold and oblique faces, not a smeared or sheared regular one- Categorical axes:
plot,barandbarhtake a list of names in place of the numbers, and place them at 0, 1, 2, ... with the names as ticks - 49 matplotlib colormaps, plus the qualitative
tab10,tab20andSet1, any of them reversed with a_rsuffix imshow(norm="log")for a logarithmic color scale, andimshow(boundaries=)for matplotlib'sBoundaryNorm: one flat color per band, and a colorbar of blocks to matchnorm="centered"withvcenter=,norm="power"withgamma=andnorm="symlog"withlinthresh=, onimshowandpcolormeshset_bad,set_underandset_overfor the samples a colormap has nothing to say about, andset_cmap_colorsto build one from a list of stops; missing samples are left clear unlessset_badsays otherwiseerrorbarwithxerr=and asymmetricyerr_lo=/yerr_hi=armsfill_between(where=, interpolate=)to shade only where a condition holds, carried out to the crossing pointscatter(edgecolors=, linewidths=)to outline the markershist(bins=, bin_edges=, density=, cumulative=, histtype="step"/"stepfilled", weights=, stacked=, orientation="horizontal", log=, rwidth=)axhspan/axvspanshaded bands andhlines/vlinesline runsstyle_use("ggplot")and friends (seaborn,fivethirtyeight,dark_background,grayscale,default), orrc(figsize=, dpi=, fontsize=, linewidth=, grid=, facecolor=, axes_facecolor=, grid_color=, text_color=, color_cycle=)for one setting at a time- Colors as CSS4/X11 names,
tab:names,#rgb,#rrggbb,#rrggbbaa, single letters,C0-C9, or a greyscale fraction such as"0.5" - SVG defaults aligned with matplotlib (6.4×4.8 in, tab10 colors, subplot margins)
fplotlib is an fpm package with no dependencies
outside the Fortran standard library, so a project picks it up with one entry
in its fpm.toml:
[dependencies]
fplotlib = { git = "https://github.com/certik/fplotlib" }and then use fplotlib and call plot, title, savefig as the examples below
do. It builds with gfortran, flang and LFortran:
fpm build --compiler gfortran
fpm build --compiler flang
fpm build --compiler lfortranThe pixi tasks below are for developing fplotlib itself, with pinned compilers.
Requires pixi. Every compiler has its own environment with
nothing in it but the compiler and fpm: gfortran and LFortran come from
conda-forge on Linux and macOS, flang on Linux (on macOS provide a system
flang on PATH). A task installs its environment the first time it runs.
pixi run test-gfortran # fpm test --compiler gfortran
pixi run test-gfortran-O3 # the same, built with -O3 -march=native
pixi run test-flang
pixi run test-flang-O3
pixi run test-lfortran
pixi run demo-flang # fpm run --example demo, writes demo.svg
# Compare the flang build against matplotlib references (needs Python,
# which lives in its own environment)
pixi run compare-all # all five below, sharing one build
pixi run compare # SVG, structurally
pixi run compare-png # PNG, pixel by pixel
pixi run compare-pdf # PDF, rasterized by pdftoppm
pixi run compare-eps # EPS, rasterized by ghostscript
pixi run compare-gif # GIF, frame by framefpm test (or pixi run test-gfortran, test-flang, test-lfortran) writes every
test plot to tests/out/ and checks each PNG, and each frame of the GIF,
against a stored fingerprint in tests/fingerprints.txt. It needs nothing
outside the repository: no Python and no reference images.
A fingerprint is 4096 bits, 64 words of 64 bits, one word per tile of the
image, computed with integer arithmetic only (tests/test_fingerprint.f90).
Two builds of the same picture differ in a few bits per word, because
compilers round some antialiased edge pixels one level differently; a moved,
missing or recoloured element changes about half the bits of its tile's word.
A case fails when any word differs by more than 10 bits, and the report says
where in the image:
FAIL clabel: 36 bits differ near x=80..160, y=120..180 (limit 10)
Measured on this suite, a tick moved by 1 px, a changed tick label or a one letter typo in a title changes a word by 18 to 40 bits, while a tick moved by 0.25 px or less passes. What the fingerprint cannot tell is whether a change is right, so after an intended change look at the new pictures (and run the matplotlib comparisons below), then store the new fingerprints:
fpm test -- --updateCI also runs the gfortran and flang tests built with -O3 -march=native
(pixi run test-gfortran-O3, test-flang-O3), where fused multiply-add
changes the rounding, so a picture that depends on the last bits of a
floating point value fails there.
The fingerprints cover the raster output. SVG, PDF and EPS are written by the same layout and drawing code but checked only by the comparisons below.
The SVG comparison is structural because a viewer, not fplotlib, decides what an SVG looks like. PNG, PDF and EPS are compared as pixels, since there fplotlib decides.
CI (Linux) has one job per compiler, each with only that compiler and fpm
installed, the compiler from fortran-lang/setup-fortran: gfortran, flang
and ifx are tested twice, the second time with -O3 -march=native so that
fused multiply-add is allowed and the pictures must not change; LFortran is
tested once. Each job also builds and runs the demo. ifx is given
-fp-model precise, because by default it reassociates, which turns a
linspace into a running sum and moves its endpoint and a contour grid's
exactly-zero column by a few ulps. A separate job, on pixi, compares the
flang build against matplotlib.
src/ library modules
fplotlib_state.f90 shared types and the figure state
fplotlib_artist.f90 drawing primitives (strokes, fills, markers)
fplotlib_draw.f90 layout, axes decoration, one renderer per plot type
fplotlib.f90 the plotting API
examples/ demo.f90
tests/ Fortran test plots, fingerprints, matplotlib refs, compare scripts
MIT, in LICENSE.
The compiled-in glyph outlines in src/fplotlib_glyphs_data.f90 are derived
from DejaVu Sans, whose licence is in LICENSE_DEJAVU: the Bitstream Vera fonts
are copyright Bitstream, and the DejaVu changes are in the public domain. The
colormap tables in src/fplotlib_cmap.f90 are sampled from matplotlib, which is
BSD-licensed, and the perceptual maps (viridis, magma, inferno, plasma) are
released by their authors under CC0.
With LFortran as a Jupyter kernel, display the SVG interactively:
use fplotlib
use lfortran_display
! ... plot / title / grid / legend ...
call display_data("image/svg+xml", render_svg())show() always writes fplotlib_show.svg (works with Flang and LFortran offline).
fplotlib is measured against matplotlib rather than described as similar to it:
every feature has a case in tests/test_plots.f90 and a matplotlib reference
in tests/gen_mpl_refs.py, and the comparisons above put a number on the
difference. As of this writing, over 100 cases:
| format | cases | mean difference |
|---|---|---|
| PNG | 124 | 1.50/255 |
| 123 | 2.05/255 | |
| EPS | 124 | 3.57/255 |
| GIF | 20 frames | 0.60/255 |
What is left is mostly antialiasing along edges, and text: fplotlib draws with
its own compiled-in DejaVu Sans metrics and glyph outlines, so a stem lands
on the same pixel column as matplotlib's but not always with the same
coverage. The SVG is not matplotlib's SVG byte for byte, and is not meant to
be: matplotlib writes every glyph as a path and attaches its own metadata,
while fplotlib writes <text> and leaves the glyphs to the viewer.