diff --git a/docs/development/contributing/index.md b/docs/development/contributing/index.md
index e78517df33c7..70ef14cf8e85 100644
--- a/docs/development/contributing/index.md
+++ b/docs/development/contributing/index.md
@@ -150,9 +150,9 @@ The user guide is maintained in the `docs/user-guide` folder. Before creating a
#### Building and serving the user guide
-The user guide is built using [MkDocs](https://www.mkdocs.org/). You install the dependencies for building the user guide by running `make requirements` in the root of the repo.
+The user guide is built using [MkDocs](https://www.mkdocs.org/). You install the dependencies for building the user guide by running `make build` in the root of the repo.
-Run `mkdocs serve` to build and serve the user guide, so you can view it locally and see updates as you make changes.
+Activate the virtual environment and run `mkdocs serve` to build and serve the user guide, so you can view it locally and see updates as you make changes.
#### Creating a new user guide page
diff --git a/docs/requirements.txt b/docs/requirements.txt
index d87ce3879ae2..dccf92dd62d1 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -2,6 +2,9 @@ pandas
pyarrow
graphviz
matplotlib
+seaborn
+plotly
+altair
mkdocs-material==9.5.2
mkdocs-macros-plugin==1.0.4
diff --git a/docs/src/python/user-guide/misc/visualization.py b/docs/src/python/user-guide/misc/visualization.py
new file mode 100644
index 000000000000..f04288cb7812
--- /dev/null
+++ b/docs/src/python/user-guide/misc/visualization.py
@@ -0,0 +1,130 @@
+# --8<-- [start:dataframe]
+import polars as pl
+
+path = "docs/data/iris.csv"
+
+df = pl.scan_csv(path).group_by("species").agg(pl.col("petal_length").mean()).collect()
+print(df)
+# --8<-- [end:dataframe]
+
+"""
+# --8<-- [start:hvplot_show_plot]
+df.plot.bar(
+ x="species",
+ y="petal_length",
+ width=650,
+)
+# --8<-- [end:hvplot_show_plot]
+"""
+
+# --8<-- [start:hvplot_make_plot]
+import hvplot
+
+plot = df.plot.bar(
+ x="species",
+ y="petal_length",
+ width=650,
+)
+hvplot.save(plot, "docs/images/hvplot_bar.html")
+with open("docs/images/hvplot_bar.html", "r") as f:
+ chart_html = f.read()
+ print(f"{chart_html}")
+# --8<-- [end:hvplot_make_plot]
+
+"""
+# --8<-- [start:matplotlib_show_plot]
+import matplotlib.pyplot as plt
+
+plt.bar(x=df["species"], height=df["petal_length"])
+# --8<-- [end:matplotlib_show_plot]
+"""
+
+# --8<-- [start:matplotlib_make_plot]
+import base64
+
+import matplotlib.pyplot as plt
+
+plt.bar(x=df["species"], height=df["petal_length"])
+plt.savefig("docs/images/matplotlib_bar.png")
+with open("docs/images/matplotlib_bar.png", "rb") as f:
+ png = base64.b64encode(f.read()).decode()
+ print(f'')
+# --8<-- [end:matplotlib_make_plot]
+
+"""
+# --8<-- [start:seaborn_show_plot]
+import seaborn as sns
+sns.barplot(
+ df,
+ x="species",
+ y="petal_length",
+)
+# --8<-- [end:seaborn_show_plot]
+"""
+
+# --8<-- [start:seaborn_make_plot]
+import seaborn as sns
+
+sns.barplot(
+ df,
+ x="species",
+ y="petal_length",
+)
+plt.savefig("docs/images/seaborn_bar.png")
+with open("docs/images/seaborn_bar.png", "rb") as f:
+ png = base64.b64encode(f.read()).decode()
+ print(f'')
+# --8<-- [end:seaborn_make_plot]
+
+"""
+# --8<-- [start:plotly_show_plot]
+import plotly.express as px
+
+px.bar(
+ df,
+ x="species",
+ y="petal_length",
+ width=400,
+)
+# --8<-- [end:plotly_show_plot]
+"""
+
+# --8<-- [start:plotly_make_plot]
+import plotly.express as px
+
+fig = px.bar(
+ df,
+ x="species",
+ y="petal_length",
+ width=650,
+)
+fig.write_html("docs/images/plotly_bar.html", full_html=False, include_plotlyjs="cdn")
+with open("docs/images/plotly_bar.html", "r") as f:
+ chart_html = f.read()
+ print(f"{chart_html}")
+# --8<-- [end:plotly_make_plot]
+
+"""
+# --8<-- [start:altair_show_plot]
+import altair as alt
+
+alt.Chart(df, width=700).mark_bar().encode(x="species:N", y="petal_length:Q")
+# --8<-- [end:altair_show_plot]
+"""
+
+# --8<-- [start:altair_make_plot]
+import altair as alt
+
+chart = (
+ alt.Chart(df, width=600)
+ .mark_bar()
+ .encode(
+ x="species:N",
+ y="petal_length:Q",
+ )
+)
+chart.save("docs/images/altair_bar.html")
+with open("docs/images/altair_bar.html", "r") as f:
+ chart_html = f.read()
+ print(f"{chart_html}")
+# --8<-- [end:altair_make_plot]
diff --git a/docs/user-guide/misc/visualization.md b/docs/user-guide/misc/visualization.md
new file mode 100644
index 000000000000..88dcd83a18a6
--- /dev/null
+++ b/docs/user-guide/misc/visualization.md
@@ -0,0 +1,60 @@
+# Visualization
+
+Data in a Polars `DataFrame` can be visualized using common visualization libraries.
+
+We illustrate plotting capabilities using the Iris dataset. We scan a CSV and then do a group-by on the `species` column and get the mean of the `petal_length`.
+
+{{code_block('user-guide/misc/visualization','dataframe',[])}}
+
+```python exec="on" result="text" session="user-guide/misc/visualization"
+--8<-- "python/user-guide/misc/visualization.py:dataframe"
+```
+
+## Built-in plotting with hvPlot
+
+Polars has a `plot` method to create interactive plots using [hvPlot](https://hvplot.holoviz.org/).
+
+{{code_block('user-guide/misc/visualization','hvplot_show_plot',[])}}
+
+```python exec="on" session="user-guide/misc/visualization"
+--8<-- "python/user-guide/misc/visualization.py:hvplot_make_plot"
+```
+
+## Matplotlib
+
+To create a bar chart we can pass columns of a `DataFrame` directly to Matplotlib as a `Series` for each column. Matplotlib does not have explicit support for Polars objects but Matplotlib can accept a Polars `Series` because it can convert each Series to a numpy array, which is zero-copy for numeric
+data without null values.
+
+{{code_block('user-guide/misc/visualization','matplotlib_show_plot',[])}}
+
+```python exec="on" session="user-guide/misc/visualization"
+--8<-- "python/user-guide/misc/visualization.py:matplotlib_make_plot"
+```
+
+## Seaborn, Plotly & Altair
+
+[Seaborn](https://seaborn.pydata.org/), [Plotly](https://plotly.com/) & [Altair](https://altair-viz.github.io/) can accept a Polars `DataFrame` by leveraging the [dataframe interchange protocol](https://data-apis.org/dataframe-api/), which offers zero-copy conversion where possible.
+
+### Seaborn
+
+{{code_block('user-guide/misc/visualization','seaborn_show_plot',[])}}
+
+```python exec="on" session="user-guide/misc/visualization"
+--8<-- "python/user-guide/misc/visualization.py:seaborn_make_plot"
+```
+
+### Plotly
+
+{{code_block('user-guide/misc/visualization','plotly_show_plot',[])}}
+
+```python exec="on" session="user-guide/misc/visualization"
+--8<-- "python/user-guide/misc/visualization.py:plotly_make_plot"
+```
+
+### Altair
+
+{{code_block('user-guide/misc/visualization','altair_show_plot',[])}}
+
+```python exec="on" session="user-guide/misc/visualization"
+--8<-- "python/user-guide/misc/visualization.py:altair_make_plot"
+```
diff --git a/mkdocs.yml b/mkdocs.yml
index 3fe9fb6ecfdb..805c235e5734 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -80,6 +80,7 @@ nav:
- user-guide/ecosystem.md
- Misc:
- user-guide/misc/multiprocessing.md
+ - user-guide/misc/visualization.md
- user-guide/misc/comparison.md
- API reference: api/index.md
diff --git a/py-polars/tests/docs/test_user_guide.py b/py-polars/tests/docs/test_user_guide.py
index 032961dd936a..dc71ca5d8f23 100644
--- a/py-polars/tests/docs/test_user_guide.py
+++ b/py-polars/tests/docs/test_user_guide.py
@@ -15,6 +15,9 @@
python_snippets_dir = repo_root / "docs" / "src" / "python"
snippet_paths = list(python_snippets_dir.rglob("*.py"))
+# Skip visualization snippets
+snippet_paths = [p for p in snippet_paths if "visualization" not in str(p)]
+
@pytest.fixture(scope="module")
def _change_test_dir() -> Iterator[None]: