"""
Shiny Scatter Plot Application in Python using plotnine.
"""
import pandas as pd
import numpy as np
from shiny import App, Inputs, Outputs, Session, module, reactive, render, ui
from shinywidgets import output_widget, render_widget
import plotly.express as px
from scatter import scatter_plotnine
def make_mock_df(n: int = 200, seed: int = 42) -> pd.DataFrame:
"""Generate mock dataset for generic scatter plot app."""
np.random.seed(seed)
df = pd.DataFrame({
"subject": [f"ind_{i+1}" for i in range(n)],
"x": np.random.randn(n),
"sex": np.random.choice(["Female", "Male"], n),
"diet": np.random.choice(["Chow", "HF"], n),
"geno": np.random.choice(["AA", "AB", "BB"], n),
})
y = (
2 * df["x"]
+ np.where(df["sex"] == "Male", 1.5, 0.0)
+ np.where(df["diet"] == "HF", -1.0, 0.0)
+ np.where(df["geno"] == "AB", 0.5, np.where(df["geno"] == "BB", 1.0, 0.0))
+ np.random.randn(n) * 0.8
)
df["y"] = y
df.index = df["subject"]
return df
@module.ui
def scatter_plot_input():
return ui.output_ui("scatter_inputs")
@module.ui
def scatter_plot_output():
return ui.output_ui("plot_ui")
@module.server
def scatter_plot_server(
input: Inputs,
output: Outputs,
session: Session,
plot_df: reactive.Calc,
x_label: callable = lambda: "x",
y_label: callable = lambda: "y"
):
@output
@render.ui
def scatter_inputs():
dat = plot_df()
if dat is None or dat.empty:
return ui.TagList()
cols = list(dat.columns)
candidate_vars = [c for c in cols if c not in ["x", "y", "subject"]]
if "sex" in cols and "diet" in cols:
candidate_vars.append("sex_diet")
color_choices = {"none": "None"}
for v in candidate_vars:
color_choices[v] = v
shape_choices = {"none": "None"}
for v in candidate_vars:
if v != "sex_diet":
shape_choices[v] = v
facet_choices = {"none": "None"}
for v in candidate_vars:
facet_choices[v] = v
return ui.TagList(
ui.input_radio_buttons("static", "Plot Type:", ["Static", "Interactive"], selected="Static"),
ui.input_select("color_by", "Color by:", choices=color_choices, selected="none"),
ui.input_select("shape_by", "Shape by:", choices=shape_choices, selected="none"),
ui.input_select("facet_by", "Facet by:", choices=facet_choices, selected="none"),
ui.input_checkbox("add_line", "Add regression line?", value=True),
ui.input_slider("point_size", "Point Size:", min=1, max=10, value=3, step=0.5),
ui.input_slider("point_alpha", "Transparency (Alpha):", min=0.1, max=1, value=0.7, step=0.1),
)
@reactive.calc
def plot_obj():
dat = plot_df()
if dat is None or "x" not in dat.columns or "y" not in dat.columns:
return None
col_by = input.color_by() if "color_by" in input else "none"
shp_by = input.shape_by() if "shape_by" in input else "none"
fct_by = input.facet_by() if "facet_by" in input else "none"
add_l = input.add_line() if "add_line" in input else True
pt_sz = input.point_size() if "point_size" in input else 3
pt_al = input.point_alpha() if "point_alpha" in input else 0.7
x_lbl = x_label() if callable(x_label) else x_label
y_lbl = y_label() if callable(y_label) else y_label
return scatter_plotnine(
dat=dat,
color_by=col_by,
shape_by=shp_by,
facet_by=fct_by,
add_line=add_l,
point_size=pt_sz,
point_alpha=pt_al,
x_label=x_lbl,
y_label=y_lbl
)
@output
@render.plot
def plot_static():
p = plot_obj()
return p
@output
@render_widget
def plot_interactive():
dat = plot_df()
if dat is None or dat.empty:
return None
df = dat.copy()
if "sex" in df.columns and "diet" in df.columns:
df["sex_diet"] = df["sex"].astype(str) + "_" + df["diet"].astype(str)
col_by = input.color_by() if ("color_by" in input and input.color_by() != "none") else None
shp_by = input.shape_by() if ("shape_by" in input and input.shape_by() != "none") else None
fct_by = input.facet_by() if ("facet_by" in input and input.facet_by() != "none") else None
x_lbl = x_label() if callable(x_label) else x_label
y_lbl = y_label() if callable(y_label) else y_label
add_l = input.add_line() if "add_line" in input else True
open_symbols = ["circle-open", "square-open", "diamond-open", "triangle-up-open", "triangle-down-open", "star-open", "cross-open"]
fig = px.scatter(
df,
x="x",
y="y",
color=col_by,
symbol=shp_by,
symbol_sequence=open_symbols,
facet_col=fct_by,
hover_name="subject" if "subject" in df.columns else None,
labels={"x": x_lbl, "y": y_lbl},
trendline="ols" if add_l else None
)
if not shp_by:
fig.update_traces(marker_symbol="circle-open")
fig.update_layout(template="plotly_white")
return fig
@output
@render.ui
def plot_ui():
static_val = input.static() if "static" in input else "Static"
if static_val == "Interactive":
return output_widget("plot_interactive")
else:
return ui.output_plot("plot_static")
def scatter_plot_app():
"""Create and return the Shiny App for scatter plot."""
mock_df = make_mock_df()
app_ui = ui.page_sidebar(
ui.sidebar(
scatter_plot_input("scatter")
),
ui.card(
scatter_plot_output("scatter")
),
title="Test Generic Scatter Plot (Python / plotnine)"
)
def server(input: Inputs, output: Outputs, session: Session):
scatter_plot_server("scatter", reactive.calc(lambda: mock_df))
return App(app_ui, server)
app = scatter_plot_app()Python Scatter Plot App (Shinylive)
A serverless WebAssembly-powered Python Shiny app demonstrating plotnine and scatter_plotnine() in Python.
This section displays a Python scatter plot application using scatter_plotnine(), powered directly in your browser via Shinylive (WebAssembly).
The live application runs completely serverless in your browser. Below the application, view the source files for both the application (scatter_plot_app.py) and the plotting module (scatter.py) in separate tabs.
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| viewerHeight: 750
#| components: [viewer]
## file: requirements.txt
pandas
numpy
matplotlib
plotnine
## file: scatter.py
import pandas as pd
import numpy as np
from plotnine import (
ggplot, aes, geom_point, geom_smooth, facet_wrap, labs,
theme_minimal, scale_shape_manual, scale_color_brewer, scale_color_hue
)
def scatter_plotnine(
dat,
color_by="none",
shape_by="none",
facet_by="none",
add_line=True,
point_size=3.0,
point_alpha=0.7,
x_label="x",
y_label="y"
):
if dat is None or "x" not in dat.columns or "y" not in dat.columns:
return None
df = dat.copy()
for col in ["sex", "diet", "geno", "Genotype"]:
if col in df.columns:
df[col] = df[col].astype("category")
if "sex" in df.columns and "diet" in df.columns:
df["sex_diet"] = (df["sex"].astype(str) + "_" + df["diet"].astype(str)).astype("category")
if "subject" not in df.columns:
df["subject"] = df.index.astype(str)
mapping = {"x": "x", "y": "y"}
col_var = color_by if (color_by and color_by != "none" and color_by in df.columns) else None
shape_var = shape_by if (shape_by and shape_by != "none" and shape_by in df.columns) else None
facet_var = facet_by if (facet_by and facet_by != "none" and facet_by in df.columns) else None
if col_var:
mapping["color"] = col_var
mapping["group"] = col_var
if shape_var:
mapping["shape"] = shape_var
p = ggplot(df, aes(**mapping))
if add_line:
if col_var:
p += geom_smooth(method="lm", se=False, linetype="solid", size=1)
else:
p += geom_smooth(aes(group=1), method="lm", se=False, color="black", linetype="solid", size=1)
if not shape_var:
p += geom_point(shape="o", fill="none", size=point_size, alpha=point_alpha, stroke=1.5)
else:
p += geom_point(fill="none", size=point_size, alpha=point_alpha, stroke=1.5)
shapes = ["o", "s", "^", "D", "v", "p", "*", "+", "x", "h"]
p += scale_shape_manual(values=shapes)
if facet_var:
p += facet_wrap(f"~ {facet_var}")
p += theme_minimal(base_size=9)
p += labs(x=x_label, y=y_label)
if col_var:
num_levels = df[col_var].nunique()
if num_levels <= 8:
p += scale_color_brewer(type="qual", palette="Dark2")
else:
p += scale_color_hue()
return p
## file: app.py
from shiny import App, Inputs, Outputs, Session, module, reactive, render, ui
from scatter import scatter_plotnine
import pandas as pd
import numpy as np
def make_mock_df(n=200, seed=42):
np.random.seed(seed)
df = pd.DataFrame({
"subject": [f"ind_{i+1}" for i in range(n)],
"x": np.random.randn(n),
"sex": np.random.choice(["Female", "Male"], n),
"diet": np.random.choice(["Chow", "HF"], n),
"geno": np.random.choice(["AA", "AB", "BB"], n),
})
y = (
2 * df["x"]
+ np.where(df["sex"] == "Male", 1.5, 0.0)
+ np.where(df["diet"] == "HF", -1.0, 0.0)
+ np.where(df["geno"] == "AB", 0.5, np.where(df["geno"] == "BB", 1.0, 0.0))
+ np.random.randn(n) * 0.8
)
df["y"] = y
df.index = df["subject"]
return df
mock_df = make_mock_df()
app_ui = ui.page_sidebar(
ui.sidebar(
ui.input_select("color_by", "Color by:", choices={"none": "None", "sex": "sex", "diet": "diet", "geno": "geno", "sex_diet": "sex_diet"}, selected="none"),
ui.input_select("shape_by", "Shape by:", choices={"none": "None", "sex": "sex", "diet": "diet", "geno": "geno"}, selected="none"),
ui.input_select("facet_by", "Facet by:", choices={"none": "None", "sex": "sex", "diet": "diet", "geno": "geno", "sex_diet": "sex_diet"}, selected="none"),
ui.input_checkbox("add_line", "Add regression line?", value=True),
ui.input_slider("point_size", "Point Size:", min=1, max=10, value=3, step=0.5),
ui.input_slider("point_alpha", "Transparency (Alpha):", min=0.1, max=1, value=0.7, step=0.1),
),
ui.card(
ui.output_plot("plot_static")
),
title="Test Generic Scatter Plot (Python / plotnine)"
)
def server(input: Inputs, output: Outputs, session: Session):
@output
@render.plot
def plot_static():
col_by = input.color_by()
shp_by = input.shape_by()
fct_by = input.facet_by()
add_l = input.add_line()
pt_sz = input.point_size()
pt_al = input.point_alpha()
return scatter_plotnine(
dat=mock_df,
color_by=col_by,
shape_by=shp_by,
facet_by=fct_by,
add_line=add_l,
point_size=pt_sz,
point_alpha=pt_al
)
app = App(app_ui, server)
Source Code
"""
Scatter plot rendering using plotnine.
"""
import pandas as pd
import numpy as np
from plotnine import (
ggplot, aes, geom_point, geom_smooth, facet_wrap, labs,
theme_minimal, scale_shape_manual, scale_color_brewer, scale_color_hue
)
from typing import Optional
def scatter_plotnine(
dat: pd.DataFrame,
color_by: str = "none",
shape_by: str = "none",
facet_by: str = "none",
add_line: bool = True,
point_size: float = 3.0,
point_alpha: float = 0.7,
x_label: str = "x",
y_label: str = "y"
) -> Optional[ggplot]:
"""
Create a plotnine scatter plot mirroring scatter_ggplot in R.
Parameters
----------
dat : pd.DataFrame
DataFrame containing 'x' and 'y' columns, plus metadata variables.
color_by : str
Column name to color points by, or "none". Default is "none".
shape_by : str
Column name to set point shapes by, or "none". Default is "none".
facet_by : str
Column name to facet plot by, or "none". Default is "none".
add_line : bool
Whether to add linear regression line(s). Default is True.
point_size : float
Scatter point size. Default is 3.0.
point_alpha : float
Scatter point transparency (0.0 to 1.0). Default is 0.7.
x_label : str
X-axis label. Default is "x".
y_label : str
Y-axis label. Default is "y".
Returns
-------
plotnine.ggplot.ggplot or None
A plotnine ggplot object, or None if required columns are missing.
"""
if dat is None or not isinstance(dat, pd.DataFrame):
return None
if "x" not in dat.columns or "y" not in dat.columns:
return None
df = dat.copy()
# Convert candidates to category if present
for col in ["sex", "diet", "geno", "Genotype"]:
if col in df.columns:
df[col] = df[col].astype("category")
# Calculate sex_diet combination if both columns exist
if "sex" in df.columns and "diet" in df.columns:
df["sex_diet"] = (df["sex"].astype(str) + "_" + df["diet"].astype(str)).astype("category")
# Ensure subject column exists for labeling
if "subject" not in df.columns:
df["subject"] = df.index.astype(str)
mapping = {"x": "x", "y": "y"}
col_var = color_by if (color_by and color_by != "none" and color_by in df.columns) else None
shape_var = shape_by if (shape_by and shape_by != "none" and shape_by in df.columns) else None
facet_var = facet_by if (facet_by and facet_by != "none" and facet_by in df.columns) else None
if col_var:
mapping["color"] = col_var
mapping["group"] = col_var
if shape_var:
mapping["shape"] = shape_var
p = ggplot(df, aes(**mapping))
# Add regression lines first (so they are plotted behind/below symbols)
if add_line:
if col_var:
p += geom_smooth(method="lm", se=False, linetype="solid", size=1)
else:
p += geom_smooth(aes(group=1), method="lm", se=False, color="black", linetype="solid", size=1)
# Add scatter points (open symbols / transparent fill matching R ggplot shape=1)
if not shape_var:
p += geom_point(shape="o", fill="none", size=point_size, alpha=point_alpha, stroke=1.5)
else:
p += geom_point(fill="none", size=point_size, alpha=point_alpha, stroke=1.5)
shapes = ["o", "s", "^", "D", "v", "p", "*", "+", "x", "h", "8", "H", "d", "|", "_"]
p += scale_shape_manual(values=shapes)
# Faceting
if facet_var:
p += facet_wrap(f"~ {facet_var}")
# Theme and custom labels
p += theme_minimal(base_size=9)
p += labs(x=x_label, y=y_label)
if col_var:
num_levels = df[col_var].nunique()
if num_levels <= 8:
p += scale_color_brewer(type="qual", palette="Dark2")
else:
p += scale_color_hue()
return p