Animal | Legs | Plot |
---|---|---|
Ostrich | 2 | 2 |
Spider | 8 | 8 |
Lion | 4 | 4 |
Adding Plots to Great Tables
While working on gt-extras, I’ve been exploring how to add small plots to Great Tables. These can go by many names, like spark lines, nanoplots, and so on. In this post, I’ll look at three approaches I tried: adding plots with plotnine
, svg.py
, or adding HTML directly. In the first two cases, the plots are SVGs, while the latter entails a collection of composed HTML div elements.
Here are the pieces I’ll cover:
- svg.py: creating your own tiny chart directly for a row.
- direct HTML: adding HTML divs directly.
- plotnine: adding a full, stripped-down chart to a row.
In the end, it’s often simplest to use svg.py
, since you can create basic charts with minimal overhead. Building elements with HTML has even less overhead, but it is also slightly less user-friendly. At the other end of the spectrum, as your charts become more complex, using existing packages like the more exhaustive plotnine
is a good alternative.
Here is the final result:
Code
import polars as pl
from great_tables import GT
from svg import SVG, Rect, Line
= pl.DataFrame({"Animal": ["Ostrich", "Spider", "Lion"], "Legs": [2, 8, 4], "Plot": [2, 8, 4]})
df
= 50
width = 30
height = df["Legs"].max()
max_legs_value
def create_plot_svg_py(val: int) -> str:
= SVG(
canvas =width,
width=height,
height=[
elements
Rect(=0,
x=height / 4,
y=width * (val / max_legs_value),
width=height / 2,
height="blue",
fill
),=0, x2=0, y1=0, y2=height, stroke="black"),
Line(x1
],
)
= f"<div>{canvas}</div>"
html return html
=create_plot_svg_py, columns=["Plot"]) GT(df).fmt(fns
Animal | Legs | Plot |
---|---|---|
Ostrich | 2 | |
Spider | 8 | |
Lion | 4 |
Setup
Here is the code to start:
import polars as pl
from great_tables import GT
= pl.DataFrame(
df
{"Animal": ["Ostrich", "Spider", "Lion"],
"Legs": [2, 8, 4],
"Plot": [2, 8, 4],
}
)
= GT(df) gt
The Binding Component: GT.fmt()
Let’s take advantage of the fmt()
method to apply a plotting function that formats our row values into plots. To see how we might use fmt()
, we first need to define a formatting function to apply to each cell in a column. It will take as input the value in the cell, and should return whatever you want in that cell. Before plotting, let’s imagine we wanted to replace the number with a tally of the number of legs:
def create_leg_tally(value: int) -> str:
return "|" * value
=create_leg_tally, columns="Plot") gt.fmt(fns
Animal | Legs | Plot |
---|---|---|
Ostrich | 2 | || |
Spider | 8 | |||||||| |
Lion | 4 | |||| |
A Lightweight Approach: Svg.py
Now we can apply that same logic to making our plots. Let’s start with the function that will eventually be passed into fmt()
:
from svg import SVG, Rect, Line
= 30
height = 50
width
def create_plot_svg_py(val: int) -> str:
= SVG(
canvas =width,
width=height,
height=[
elements
Rect(=0,
x=height / 4,
y=width * (val / max_legs_value),
width=height / 2,
height="blue",
fill
),=0, x2=0, y1=0, y2=height, stroke="black"),
Line(x1
],
)
= f"<div>{canvas}</div>"
html return html
Here you get to call fmt()
to modify the column you want to apply the plotting function to.
=create_plot_svg_py, columns="Plot") gt.fmt(fns
Animal | Legs | Plot |
---|---|---|
Ostrich | 2 | |
Spider | 8 | |
Lion | 4 |
This was very direct, we didn’t have save to a buffer or import heavy duty plotting functions. We built the string with the help of svg.py
and were able to insert into the table. See the string below:
'<div><svg xmlns="http://www.w3.org/2000/svg" width="50" height="30"><rect x="0" y="7.5" width="25.0" height="15.0" fill="blue"/><line stroke="black" x1="0" y1="0" x2="0" y2="30"/></svg></div>'
Even in its outputted form the string is still easily readable, which is another upside of using an SVG generation package.
Extreme Minimalism: Adding HTML directly
In the previous section, note that svg.py
simply generated a string of HTML. You can do the same thing directly.
def create_plot_html(val: int) -> str:
= f"""
bar_element <div style="position: absolute;
width: {width * val / max_legs_value}px;
height: {height / 2}px;
background-color: purple;
margin-top: {height / 4}px;
"></div>"""
= """
line_element <div style="position: absolute;
top: 0;
bottom: 0;
width: 1px;
background-color: black;
"></div>"""
= f"""
html <div style="position: relative; width: {width}px; height: {height}px;">
{bar_element}
{line_element}
</div>
"""
return html
Now that we’ve defined our create_plot_*
formatting function, the call to fmt()
is identical to the one above.
=create_plot_html, columns="Plot") gt.fmt(fns
Animal | Legs | Plot |
---|---|---|
Ostrich | 2 |
|
Spider | 8 |
|
Lion | 4 |
|
At first glance, encoding HTML in multi-line strings may not be aesthetically pleasing, nor is it particularly more lightweight than svg.py
. Still, it provides a good alternative if you are like me and insist on being as close to the output as possible. Separately, I have found the inclusion of text to be simpler with HTML on account of the default text handling behavior that comes along with it.
A Comprehensive Package: Plotnine
from io import StringIO
from plotnine import (
ggplot,
aes,
coord_flip,
geom_col,
scale_y_continuous,
scale_x_continuous,
theme_void,
geom_hline,
)
= df["Legs"].max()
max_legs_value
def create_plot_plotnine(val: int) -> str:
= (
plot
ggplot()+ aes(x=1, y=val)
+ geom_col(width=0.5, fill="green", show_legend=False)
+ scale_y_continuous(limits=(0, max_legs_value))
+ scale_x_continuous(limits=(0.5, 1.5))
+ coord_flip()
+ theme_void()
+ geom_hline(yintercept=0)
)
= StringIO()
buf format="svg", width=0.5, height=0.3, verbose=False)
plot.save(buf, = buf.getvalue()
svg_content
buf.close()
= f"<div>{svg_content}</div>"
html return html
# This might be familiar by now
=create_plot_plotnine, columns="Plot") gt.fmt(fns
Animal | Legs | Plot |
---|---|---|
Ostrich | 2 | |
Spider | 8 | |
Lion | 4 |
Nice! But that was a sizable chunk of code just to create plots comprised of one bar each. If you’re like me, you’ll find it’s not at all trivial to do, especially without experience using the plotting package.
However, this isn’t the only graphic you might want to have on display – when you come across a use case that necessitates more detailed plots, a comprehensive plotting package like plotnine
could very well be your best bet. Imagine we are passing in a list of tuples and want to generate a scatterplot, writing all of those as svg.py
elements or direct HTML would be quite cumbersome.
Conclusion
How you choose to add plots to Great Tables is up to you. In writing graphical plotting functions for gt-extras, I’ve personally turned towards an HTML-only approach that I’ve felt comfortable with in other settings. With that said, I do believe converting table values to graphic output is a task best done with a little bit of help (whether it be svg-py
or another plotting package will depend on how detailed your plots are).
The choice ultimately depends on your specific needs: simplicity and directness, versus abstraction and power. By understanding the trade-offs, you will be able to tailor your approach to the needs of your project.