Usage

For a quick usage introduction, see getting started.

To create a plot we can either use Plot.plot() to generate plots with the default settings, or create a plot generator object to be able to customize things further. This is the way we will use here.

So we start by importing the needed classes and create a plot generator object:

from pyobsplot import Obsplot, Plot

op = Obsplot()

From JavaScript to Python

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 Obsplot, Plot, d3

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

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 Obsplot, 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"],
})

op({
    "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")
        })
    ]
})

Renderers

pyobsplot provides two renderers : widget and jsdom. The widget renderer outputs plots as Jupyter widgets. The jsdom renderer uses an external npm package to generate plots as either SVG or HTML.

The following table lists the differences between the two renderers.

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

To use the jsdom renderer, 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 select which renderer to use when creating a plot generator object:

# Switch to widget renderer (the default one)
opw = Obsplot(renderer="widget")
# Switch to jsdom renderer
opj = Obsplot(renderer="jsdom")

Alternative syntaxes

As we’ve already seen, the most common syntax for a plot specification is to pass it as a dictionary :

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

But it is also possible to pass top level arguments as kwargs to the generator object, which would give the following:

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

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

import random
op(
    Plot.tickX(
        [random.gauss(0,1) for i in range(1000)], 
        {"stroke": "steelblue", "opacity": 0.2}
    )
)

Default specification values

When creating a plot generator objects, 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', 'aspectRatio', 'style']

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

op_colors = Obsplot(default={
    "width": 400, 
    "margin": 50,
    "style": {"color": "white", "background-color": "#004"}
})
op_colors(
    Plot.dot(data, {"x": "x", "y": "y"})
)

Themes

When using a plot generator object, it is possible to specify one of three output 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:

op_dark = Obsplot(theme="dark")

You can see output examples in the themes gallery

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]
})

op({
    "x": {"grid": True},
    "y": {"domain": [0, 5]},
    "marks": [Plot.lineY(df, {"x": "Date", "y": "Value", "stroke": "steelblue"})]
})

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")

op(
    Plot.tickX(value, {"x": "Value", "stroke": "darkviolet"})
)

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")

op({
  "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",
    })
  ]
})

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:

op({
    "x": {"domain": [js("new Date('2021-01-01')"), js("new Date('2022-01-01')")]}, 
    "grid": True
})

from datetime import date
op({
    "x": {"domain": [date(2021,1,1), date(2022,1,1)]}, 
    "grid": True
})

As well as the two following ones, using datetime:

op({
    "x": {"domain": [js("new Date('2021-01-01T07:00:00')"), js("new Date('2021-01-01T08:00:00')")]}, 
    "grid": True
})

from datetime import datetime
op({
    "x": {"domain": [datetime(2021,1,1,7,0,0), datetime(2021,1,1,8,0,0)]}, 
    "grid": True
})

Quarto

pyobsplot plots are compatible with quarto HTML formats. If you use the jsdom renderer, plots without legends are generated as SVG files and are also compatible with other formats such as PDF or docx.

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

Interactivity

When using the widget renderer, 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 = op(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:

Saving plots to file

Generated plots can be saved to a file. To do this, just add a path argument to your generator call:

op = Obsplot(renderer="jsdom")
op(Plot.lineY([1,2,3,2]), path="path_to/file.svg")

jsdom renderer

When using the jsdom renderer, charts can be saved either as SVG or HTML files : Plot generates charts as SVG, but if a legend, title, subtitle or caption is present, the SVG is wrapped in a <figure> HTML tag. When using the path option, pyobsplot will warn you if you try to save an HTML output to a SVG file.

In any case, you can also use Plot’s figure option to force the output to be wrapped in <figure>:

op = Obsplot(renderer="jsdom")
op({
        "marks": Plot.lineY([1, 2, 3, 2]), 
        "figure": True
    }, 
    path="/tmp/out.html"
)

widget renderer

When using the widget renderer, plots can only be saved to full HTML files. These files retain interactivity features such as tooltips. To embed widgets into an HTML website or document, Quarto documents can be more practical.