Skip to content

Turn static matplotlib charts into interactive web visualizations

plotjs logo

plotjs is a Python package that transform matplotlib plots into interactive charts with minimum user inputs. You can:

  • control tooltip labels and grouping
  • add CSS
  • add JavaScript
  • and many more
Warning

plotjs is in very early stage: expect regular breaking changes.

Get started

Matplotlib is great1: you can draw anything with it.

But Matplotlib's graphics are static2, unlike those of plotly or altair, for example.

For instance, a chart made with matplotlib looks like this:

import matplotlib.pyplot as plt
from plotjs import data

df = data.load_iris()

fig, ax = plt.subplots()
ax.scatter(
    df["sepal_length"],
    df["sepal_width"],
    c=df["species"].astype("category").cat.codes,
    s=300,
    alpha=0.5,
    ec="black",
)

This is just a png file, nothing crazy.

Wouldn't it be cool if we could, for example, have hover effects? Like, if I put my mouse on a point, it displays something?

Introducting ✨plotjs✨

from plotjs import PlotJS

# fmt: off
(
    PlotJS(fig=fig)                     # create a PlotJS instance
    .add_tooltip(labels=df["species"])  # add a tooltip (specie name)
    .save("iframes/quickstart.html")    # save to an HTML file
)

plotjs convert any matplotlib Figure to an HTML file that contains an interactive version of your plot. By default, it will highlight the hovered point and fade other points.

What if we want to highlight all points from a specie for example?

from plotjs import PlotJS

(
    PlotJS(fig=fig)
    .add_tooltip(
        labels=df["species"],
        groups=df["species"],
    )
    .save("iframes/quickstart2.html")
)

CSS

Now, let's say we want to a finer control over the hover effects.

That's easily possible with some basic CSS:

  • we select .hovered to control CSS for the hovered points
  • we select .not-hovered to control CSS for the un-hovered points
from plotjs import PlotJS

(
    PlotJS(fig=fig)
    .add_tooltip(
        labels=df["species"],
        groups=df["species"],
    )
    .add_css(".hovered{fill: blue !important;}")
    .save("iframes/quickstart3.html")
)

Learn more about CSS customization

Label customization

Now let's setup better labels than the current ones.

The tooltip argument just requires an iterable, and will use this for the labels. That means we can do pretty much whatever we want. For instance, with pandas, we can do:

custom_tooltip = df.apply(
    lambda row: f"Sepal length = {row['sepal_length']}<br>"
    f"Sepal width = {row['sepal_width']}<br>"
    f"{row['species'].upper()}",
    axis=1,
)

Then we use this as the new tooltip:

from plotjs import PlotJS

(
    PlotJS(fig=fig)
    .add_tooltip(
        labels=custom_tooltip,
        groups=df["species"],
    )
    .add_css(
        from_dict={
            ".tooltip": {
                "width": "200px",
                "text-align": "center",
                "opacity": "0.7",
                "font-size": "1.1em",
            }
        }
    )
    .save("iframes/quickstart4.html")
)

Now that you understand the core components of plotjs, let's see how it looks with a line chart.

Line chart

import numpy as np
import matplotlib.pyplot as plt
from plotjs import PlotJS

walk1 = np.cumsum(np.random.choice([-1, 1], size=500))
walk2 = np.cumsum(np.random.choice([-1, 1], size=500))
walk3 = np.cumsum(np.random.choice([-1, 1], size=500))

fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(walk1, linewidth=8, color="#264653")
ax.plot(walk2, linewidth=8, color="#2a9d8f")
ax.plot(walk3, linewidth=8, color="#e9c46a")

(
    PlotJS(fig=fig)
    .add_tooltip(labels=["S&P500", "CAC40", "Bitcoin"])
    .save("iframes/quickstart5.html")
)

Barplot

import matplotlib.pyplot as plt
from plotjs import PlotJS

fig, ax = plt.subplots()
ax.barh(
    ["Fries", "Cake", "Apple", "Cheese", "Broccoli"],
    [10, 30, 40, 50, 35],
    height=0.6,
    color=["#06d6a0", "#06d6a0", "#ef476f", "#06d6a0", "#ef476f"],
)

(
    PlotJS(fig=fig)
    .add_tooltip(
        labels=["Fries", "Cake", "Apple", "Cheese", "Broccoli"],
        groups=["Good", "Good", "Bad", "Good", "Bad"],
    )
    .save("iframes/quickstart6.html")
)

Connect legend and plot elements:

  • Scatter plot
import matplotlib.pyplot as plt
from plotjs import PlotJS

fig, ax = plt.subplots()
for specie in df["species"].unique():
    specie_df = df[df["species"] == specie]
    ax.scatter(
        specie_df["sepal_length"],
        specie_df["sepal_width"],
        s=200,
        ec="black",
        label=specie,
    )
ax.legend()

(
    PlotJS(fig=fig)
    .add_tooltip(
        groups=df["species"],
    )
    .save("iframes/quickstart7.html")
)
  • Line chart
import matplotlib.pyplot as plt
import numpy as np
from plotjs import PlotJS

np.random.seed(0)

length = 500
walk1 = np.cumsum(np.random.choice([-1, 1], size=length))
walk2 = np.cumsum(np.random.choice([-1, 1], size=length))
walk3 = np.cumsum(np.random.choice([-1, 1], size=length))

labels = ["S&P500", "CAC40", "Bitcoin"]

fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(walk1, linewidth=8, color="#264653", label=labels[0])
ax.plot(walk2, linewidth=8, color="#2a9d8f", label=labels[1])
ax.plot(walk3, linewidth=8, color="#e9c46a", label=labels[2])
ax.legend()

(
    PlotJS(fig=fig)
    .add_tooltip(
        labels=labels,
        groups=labels,
    )
    .save("iframes/quickstart8.html")
)

Multiple Axes

import matplotlib.pyplot as plt
from plotjs import PlotJS, data

df = data.load_iris()

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))

args = dict(
    c=df["species"].astype("category").cat.codes,
    s=300,
    alpha=0.5,
    ec="black",
)
ax1.scatter(df["sepal_width"], df["sepal_length"], **args)
ax2.scatter(df["petal_width"], df["petal_length"], **args)

(
    PlotJS(fig)
    .add_tooltip(
        groups=df["species"],
        ax=ax1,  # left Axes
    )
    .add_tooltip(
        labels=df["species"],
        ax=ax2,  # right Axes
    )
    .save("iframes/quickstart9.html")
)

Right now all Axes are 100% independent. But it's planned to support "connecting" Axes. See this issue.

Hover nearest element

plotjs has a great option to even more easily activate hover effects: the hover_nearest argument in add_tooltip().

In short, if set to True, plotjs will hover the closest plot element it can found!

import matplotlib.pyplot as plt
from plotjs import data
from plotjs import PlotJS

df = data.load_iris()

fig, ax = plt.subplots()
for specie in df["species"].unique():
    specie_df = df[df["species"] == specie]
    ax.scatter(
        specie_df["sepal_length"],
        specie_df["sepal_width"],
        s=200,
        ec="black",
    )

PlotJS(fig=fig).add_tooltip(
    labels=df["species"],
    groups=df["species"],
    hover_nearest=True,
).save("iframes/quickstart10.html")

And it works with multiple Axes too:

import matplotlib.pyplot as plt
from plotjs import PlotJS, data

df = data.load_iris()

fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(10, 4))
args = dict(
    x=df["petal_width"],
    y=df["petal_length"],
    c=df["species"].astype("category").cat.codes,
    s=300,
    ec="black",
)
ax1.scatter(**args)
ax2.scatter(**args)
ax3.scatter(**args)

(
    PlotJS(fig)
    .add_tooltip(
        groups=df["species"],
        hover_nearest=True,
        ax=ax1,
    )
    .add_tooltip(
        labels=df["species"],
        hover_nearest=True,
        ax=ax2,
    )
    .add_tooltip(
        labels=df["species"],
        groups=df["species"],
        hover_nearest=True,
        ax=ax3,
    )
    .save("iframes/quickstart11.html")
)

Installation

  • From PyPI (recommended):
pip install plotjs
  • Latest dev version:
pip install git+https://github.com/y-sunflower/plotjs.git

Dependencies

Important limitation

Plotting order

Due to the way plotjs currently works, you need to make sure the order you plot elements and the order of the labels/groups arguments is the same. For instance, the following leads to weird results because we plot the points by the specie order but labels and groups follow the order in the dataframe. See this issue.

import matplotlib.pyplot as plt
from plotjs import data
from plotjs import PlotJS

df = data.load_iris().sample(150)  # randomize the dataset

fig, ax = plt.subplots()
for specie in df["species"].unique():
    specie_df = df[df["species"] == specie]
    ax.scatter(
        specie_df["sepal_length"],
        specie_df["sepal_width"],
        s=200,
        ec="black",
    )

PlotJS(fig=fig).add_tooltip(
    labels=df["species"],
    groups=df["species"],
).save("iframes/bug.html")

One way to fix this is to not use (or avoid) for loops when plotting.

for specie in df["species"].unique():
    specie_df = df[df["species"] == specie]
    ax.scatter(
        specie_df["sepal_length"],
        specie_df["sepal_width"],
        s=200,
        ec="black",
    )

You can use for loops, assuming you sort your dataframe (see point below).

ax.scatter(
    df["sepal_length"],
    df["sepal_width"],
    c=df["species"].astype("category").cat.codes,
    s=200,
    ec="black",
)

Here the order of your dataframe does not matter.

Another way to fix this is to sort labels/groups argument by the same order you created your plot. In this previous case, this would mean to sort them by the species column. Before plotting, you do:

df = df.sort_values("species")

Q&A

Question

Why not just use plotly/bokeh/altair/other?

There are many reasons to use or not use a specific visualization framework. If you already use one and it meets your needs, you probably don't need to change. The main reasons for plotjs are as follows:

  • Matplotlib is the leading framework in terms of usage. Even with modern libraries, for reasons such as legacy and user habits, matplotlib remains by far the most downloaded data visualization tool in Python.
  • Many people are already familiar with matplotlib. And since plotjs is designed to be very easy to use, users have virtually no additional code to add to bring their graphs to life.


Question

What is the difference between this and mpld3?

mpld3 is an older project created with the goal of "bringing matplotlib to the browser". The maintainers currently do not have enough time to maintain the project, which means that many issues and feature requests are currently being ignored.

The idea behind plotjs is to create a modern version of mpld3, with a very different API, internal logic, and default behaviors. I (Joseph) personally find mpld3 very impressive, but far too complicated to use, so I decided to create the alternative of my dreams.

Appendix


  1. It really is. 

  2. To be fair, you can perfectly create interactive charts natively in Matplotlib. It requires to use its interactive mode and GUI backends to allow actions like zooming and panning in desktop windows. For instance, this differs from Plotly or Altair, which offer richer, browser-based interactivity like tooltips and filtering. Matplotlib’s interactivity is more limited and environment-dependent, while Plotly and Altair provide higher-level, web-friendly features.