Usage

For a quick usage introduction, see getting started.

Plot specification

Converting a plot specification from JavaScript to Python should be straightforward most of the time:

  • all dictionary keys must be quoted (so x: becomes "x":)
  • JavaScript true and false must be replaced by True and False
  • JavaScript null must be replaced by None

So the following JavaScript code:

Plot.plot(
    {
        color: {legend: true},
        grid: false,
        marks: [Plot.dot(data, {x: "x", y: "y", fill: "type", r: 5})]
    }
)

Becomes:

op({
    "color": {"legend": True},
    "grid": False,
    "marks": [Plot.dot(data, {"x": "x", "y": "y", "fill": "type", "r": 5})],
})

It is possible to replace JavaScript methods from the Plot, d3 and Math modules with Python methods, but you must first import the corresponding classes.

from pyobsplot import Plot, d3

Plot.plot({
    "x": {
        "axis": None
    },
    "marks": [
        Plot.ruleY([0], {"stroke": "steelblue"}),
        Plot.lineY(d3.cumsum({ "length": 100 }, d3.randomNormal()))
    ]
})
−4−2024681012141618

If your specification includes JavaScript code (such as anonymous functions), you can pass it as a string by using the js method (after importing it):

from pyobsplot import Plot, d3, js
import polars as pl

data = pl.DataFrame({
    "x": [1, 5, 2, 4, 6, 2, 4],
    "y": [2, 1, 3, 4, 5, 1, 2],
    "type": ["T1", "T2", "T1", "T2", "T1", "T1", "T2"],
})

Plot.plot({
    "grid": True,
    "marks": [
        Plot.dot(data, {
            "x": "x", "y": "y", "r": 5,
            "stroke": "black", "fill": "steelblue",
            "fillOpacity": js("d => d.type == 'T1' ? 0.7 : 0.1")
        })
    ]
})
1.01.52.02.53.03.54.04.55.0↑ y1.01.52.02.53.03.54.04.55.05.56.0x →

Alternative syntaxes

For the simplest cases, you can also pass a mark method directly as plot specification. The JavaScript plot() method will be called automatically to display the plot:

import random
v = [random.gauss(0,1) for i in range(1000)]

Plot.plot(
    Plot.tickX(
        v, 
        {"stroke": "steelblue", "opacity": 0.2}
    )
)
−3−2−10123

You can call also call the plot() method directly on a Plot mark method:

Plot.tickX(
        v, 
        {"stroke": "steelblue", "opacity": 0.2}
    ).plot()
−3−2−10123

Output formats

pyobsplot allows to output plots as Jupyter widgets, but also as static HTML, SVG or PNG.

The output format is determined by the format argument passed to a plot generator object or during plot creation.

Format value Output type Renderer
“widget” (default) Jupyter Widget widget
“html” Static HTML jsdom
“svg” SVG image jsdom or jsdom+typst
“png” PNG image jsdom+typst

The following table lists the differences between the widget output and the other static formats (HTML, SVG and PNG).

Widget output Other outputs
Output Jupyter Widget Static HTML, SVG, PNG. PDF is available when saving to a file.
Additional
installation
None Needs a working node.js installation and an additional npm package
Quarto Supported only in HTML format HTML output supported in HTML format, SVG and PNG outputs supported in other formats
Output size Big : includes the data and code needed to generate the plot Moderate : size of the output file
Plot interactions
(tooltips…)
Supported Not supported. Only static plots are produced.
Save plot to file Plots can be saved as embeddable HTML file Plots can be saved to static HTML, SVG, PNG or PDF files.
Jupyter
interactivity
Basic None
Persistence
between sessions
Widget state is not saved between sessions (for now) Output is saved between sessions

To use a static output format like HTML, SVG or PNG, you need a working node.js installation and you must install the npm pyobsplot package globally or locally:

# Install locally
npm install pyobsplot
# Install globally
npm install -g pyobsplot

After that, you can specifiy a format when creating the plot by adding a format argument:

# Specify format to Plot.plot()
Plot.plot(Plot.auto(penguins, {"x": "flipper_length_mm"}), format="png")

Saving plots to file

Plots can be saved to a file. To do this, just add a path argument to your generator or Plot.plot call:

Plot.plot(Plot.lineY([1,2,3,2]), path="path_to/file.svg")

Widget format

When using the widget format, plots can only be saved to HTML files. These files retain interactive features such as tooltips.

Plot.plot(Plot.lineY([1,2,3,2]), path="path_to/file.html")
Tip

To embed widgets into an HTML website or document, Quarto documents can be more practical.

Other output formats

Plots can also be saved as SVG, PNG, PDF or static HTML files. The output format is determined by the path file extension.

Plot.plot(Plot.lineY([1,2,3,2]), path="path_to/file.png")
Note

PDF format is only available when saving to a file, as PDF output cannot be embedded and displayed in a Jupyter notebook.

Note

When saving a plot to an HTML file, the result will depend on the format value. If format="widget" (the default), the HTML file will be a Jupyter widget, but if format="html", then a static HTML version will be saved.

Caution

Plot generates charts as SVG, but if a legend, title, subtitle or caption is present, the SVG is wrapped in a <figure> HTML tag. In this case, when saving to an SVG file, the plot will be converted using typst.

Themes

When using a plot generator object, it is possible to specify one of three color themes:

  • light theme (default) creates plots with a white background and a black foreground color
  • dark theme creates plots with a black background and a white foreground
  • current theme creates plots with a transparent background and a currentColor foreground

You can specify a mode when creating the plot generator object by using the theme argument:

Plot.plot(Plot.lineY([1,2,3,2]), theme="dark")

You can see output examples in the themes gallery

Note

The current theme is not available when exporting to PNG, PDF, or SVG via typst.

Plot generator

Calling Plot.plot() is the fastest way to generate a plot with the default settings, but for further customization you can import the Obsplot class and create a plot generator:

from pyobsplot import Obsplot, Plot

op = Obsplot()

By default plot generators output plots as widget format, but you can specify another one:

op = Obsplot(format="png")

You can then create plots by calling this generator with your plot specification:

op(
    {
        "grid": True,
        "marks": [Plot.dot(data, {"x": "x", "y": "y", "fill": "type", "r": 5})],
    }
)

For the simplest cases, you can also create your plot directly by passing a Plot mark method to the generator:

penguins = pl.read_csv("data/penguins.csv")

op(Plot.auto(penguins, {"x": "flipper_length_mm"}))
051015202530354045505560↑ Frequency170180190200210220230flipper_length_mm →

You can also override the default format output or save the plot to a file by adding a format or path argument when calling the generator:

# Switch to SVG output for this plot
op(Plot.auto(penguins, {"x": "flipper_length_mm"}), format="svg")
# Save widget to HTML file
op(Plot.auto(penguins, {"x": "flipper_length_mm"}), path="plot.html")

Default specification values

When creating a plot generator, it is possible to specify default specification values that will be applied to every plot created with this generator.

Only the top-level layout options can be specified as defaults. This can be useful to specify a default width, colors, margins or even style.

The complete list of available default attributes is :

['marginTop', 'marginRight', 'marginBottom', 'marginLeft', 'margin', 'width', 'height', 'grid', 'aspectRatio', 'style']

So to create a plot generator object that creates by default 400px wide plots with a 50px margin and a dark theme with blue color, you could use :

op_colors = Obsplot(
    format = "html",
    theme = "dark",
    default={
        "width": 400, 
        "margin": 50,
        "style": {"color": "#54A4C4"}
    }
)
op_colors(
    Plot.dot(data, {"x": "x", "y": "y"})
)
1.01.52.02.53.03.54.04.55.0↑ y123456x →

Data handling

DataFrames and Series

Pandas and polars DataFrames can be passed directly in a plot specification. They will be converted to JavaScript objects via Arrow IPC serialization, to ensure speed and data types conversion.

import polars as pl
from datetime import date

df = pl.DataFrame({
    "Date": [date(2023, 1, 1), date(2023, 1, 2), date(2023, 1, 3), date(2023, 1, 4)],
    "Value": [4.2, 3.8, 4.5, 4.7]
})

Plot.plot({
    "x": {"grid": True},
    "y": {"domain": [0, 5]},
    "marks": [Plot.lineY(df, {"x": "Date", "y": "Value", "stroke": "steelblue"})]
})
0.00.51.01.52.02.53.03.54.04.55.0↑ Value12 AMJan 112 PM12 AMJan 212 PM12 AMJan 312 PM12 AMJan 4

If you pass a pandas or polars Series object, it will be automatically converted to a DataFrame with one column:

value = df.get_column("Value")

Plot.plot(
    Plot.tickX(value, {"x": "Value", "stroke": "darkviolet"})
)
3.83.94.04.14.24.34.44.54.64.7Value →

pyobsplot implements a simple caching mechanism for some data objects (it currently works for DataFrames and for GeoJson data). Sometimes the same data object is used several times in a plot specification:

penguins = pl.read_csv("data/penguins.csv")

Plot.plot({
  "height": 600,
  "grid": True,
  "facet": {
    "marginRight": 80
  },
  "marks": [
    Plot.frame({"facet": False}),
    Plot.dot(penguins, {
      "x": "culmen_depth_mm",
      "y": "culmen_length_mm",
      "r": 1.5,
      "fill": "#ccc",
      "facet": "exclude",
      "fx": "sex",
      "fy": "species",
    }),
    Plot.dot(penguins, {
       "x": "culmen_depth_mm",
       "y": "culmen_length_mm",
       "fx": "sex",
       "fy": "species",
    })
  ]
})
AdelieChinstrapGentoospeciesFEMALEMALEsex354045505535404550553540455055↑ culmen_length_mm1520152015201520culmen_depth_mm →

In this case, caching ensures that the penguins DataFrame is only serialized and transmitted once instead of twice.

datetime objects

datetime.date and datetime.datetime Python objects are automatically serialized and converted to JavaScript Date objects.

That makes the following two specifications equivalent:

Plot.plot({
    "x": {"domain": [js("new Date('2021-01-01')"), js("new Date('2022-01-01')")]}, 
    "grid": True
})
Jan2021FebMarAprMayJunJulAugSepOctNovDecJan2022
from datetime import date
Plot.plot({
    "x": {"domain": [date(2021,1,1), date(2022,1,1)]}, 
    "grid": True
})
Jan2021FebMarAprMayJunJulAugSepOctNovDecJan2022

As well as the two following ones, using datetime:

Plot.plot({
    "x": {"domain": [js("new Date('2021-01-01T07:00:00')"), js("new Date('2021-01-01T08:00:00')")]}, 
    "grid": True
})
7:00AM7:057:107:157:207:257:307:357:407:457:507:558:00
from datetime import datetime
Plot.plot({
    "x": {"domain": [datetime(2021,1,1,7,0,0), datetime(2021,1,1,8,0,0)]}, 
    "grid": True
})
7:00AM7:057:107:157:207:257:307:357:407:457:507:558:00

Quarto

pyobsplot plots are compatible with quarto HTML formats. If you use static output formats such as SVG, PNG or PDF, they may work in PDF or docx documents as well.

If your source document is a jupyter notebook (and not a .qmd file), then you have to use the --execute argument to force plot computation and to make them visible in the output:

quarto render test.ipynb --execute --to html

Jupyter Interactivity

When using the default widget format, the fact that plots are generated as Jupyter widgets allow for basic interactivity. More specifically, you can set the spec attribute of an existing Obsplot to another plot specification and it will update it.

This allows to do things like the following, where a plot is updated depending on the value of a Jupyter IntSlider widget:

def generate_plot_spec(opacity):
    return {
      "grid": True,
      "marks": [
            Plot.rectY(penguins, Plot.binX({"y": "count"}, {"x": "body_mass_g", "fill": "steelblue", "fillOpacity": opacity})),
            Plot.ruleY([0])
            ]
    }

plot = Plot.plot(generate_plot_spec(1))

def update_plot(change):
    new = change['new']
    plot.spec = generate_plot_spec(new / 100)


w = IntSlider(value = 100, min = 0, max = 100)
w.observe(update_plot, names='value')

display(w)
display(plot)

You can see a live version of this example in the following Colab notebook: