The CRAFT framework provides a structured and systematic approach to data visualization. This framework is platform-independent — it is not tied to any specific tool or language. The same underlying logic can be applied using Python, R or any other visualization tool, since the framework focuses on the concept of variable-based plot selection rather than tool-specific syntax.
In this section, we look at how the CRAFT framework can be implemented using Python’s Plotly library. Plotly is chosen for its interactivity and ease of use, but the same steps and logic can be replicated in any other platform of choice.
Data Visualization: Variable Types & Approaches
1. Nature of a Variable
Visualization approach depends on variable type:
- Numeric – continuous/discrete values (e.g., age, income)
- Categorical – distinct groups/labels (e.g., gender, region)
2. Univariate Analysis
Study of a single variable in isolation (e.g., histogram for numeric, bar chart for categorical).
3. Bivariate Analysis
Study of relationship between two variables. Three cases:
- Numeric vs Numeric → scatter plot
- Numeric vs Categorical → box plot, violin plot
- Categorical vs Categorical → stacked bar, heatmap
4. Multivariate Analysis
Extension of bivariate analysis by adding more variables. Extra dimensions are added using CSS — Color, Shape, Size on top of a base 2D plot.
Types of Multivariate Layouts
- Grid :Multiple plots arranged in a matrix layout (e.g., pair plots)
- Trellis (Facet): Uses a categorical variable to subset data, generating a grid of plots — one per category level
Key Difference:
- Grid = arrangement of multiple (often different) plots in matrix form.
- Trellis = subsetting one plot type by category, arranged in grid form.
Python Code block
Show code
“”” ========================================================================= CRAFT — Plotly Visualization (Single Function) Context · Reference · Aesthetic · Framing · Telling (Storytelling) ========================================================================= FUNCTION SIGNATURE craft_plot(data, numeric, categorical, chart, title, label, color, shape) ARGUMENTS data pd.DataFrame numeric str | list[str] | None categorical str | list[str] | None chart str — see CHART MENU title str label dict — {col: ‘display name’} color str → column name : encodes that variable by color → palette name : applies that palette (e.g. ‘Set2’) → hex / named : single color (e.g. ‘#636EFA’, ‘red’) tuple → (col, palette): encode col + apply palette e.g. color=(‘sex’, ‘Set2’) shape str → column name : encodes that variable by marker symbol → symbol name : fixed symbol for all markers e.g. ‘circle’,’square’,’diamond’,’cross’,’x’, ‘triangle-up’,’triangle-down’,’star’,’pentagon’ tuple → (col, [sym1, sym2, …]): encode col + map symbols list length must match number of unique values in col e.g. shape=(‘smoker’, [‘diamond’, ‘circle’]) applies to: scatter, facet_scatter only CHART MENU Univariate numeric numeric=col histogram | box | violin | ecdf | kde Univariate categorical categorical=col bar | pie | donut Bivariate num × num numeric=[x, y] scatter | line | hexbin Bivariate num × cat numeric=col, categorical=col box | violin | strip | bar | stacked_bar | grouped_bar Bivariate cat × cat categorical=[c1, c2] heatmap | bar | stacked_bar | grouped_bar Multivariate (3+ variables, any mix) facet_histogram | facet_box | facet_scatter facet_bar | facet_violin | corr_heatmap ================================================================ “”” import math import numpy as np import pandas as pd import plotly.express as px import plotly.graph_objects as go from plotly.subplots import make_subplots # ── registries ──────────────────────────────────────────────────────────────── _QUALITATIVE = { p.lower(): p for p in dir(px.colors.qualitative) if not p.startswith(“_”) and isinstance(getattr(px.colors.qualitative, p), list) } _MARKER_SYMBOLS = { “circle”, “square”, “diamond”, “cross”, “x”, “triangle-up”, “triangle-down”, “triangle-left”, “triangle-right”, “pentagon”, “hexagon”, “star”, “hourglass”, “bowtie”, “asterisk”, “hash”, } # ── helpers ─────────────────────────────────────────────────────────────────── def _to_palette(val): “””Resolve a palette name string to a color list, or return None.””” if isinstance(val, str) and val.lower() in _QUALITATIVE: return getattr(px.colors.qualitative, _QUALITATIVE[val.lower()]) return None def _parse_color(color, df): “”” Parse color arg (str or tuple) into (color_col, disc_seq, cont_scale). color_col : column name to encode by color disc_seq : list of colors to apply cont_scale : continuous colorscale name “”” if color is None: return None, None, None # ── tuple: (col, palette) ───────────────────────────────────────────────── if isinstance(color, tuple): col, style = color pal = _to_palette(style) or (style if isinstance(style, list) else [style]) return col, pal, None # ── str: column name ────────────────────────────────────────────────────── if color in df.columns: return color, None, None # ── str: qualitative palette name ───────────────────────────────────────── pal = _to_palette(color) if pal: return None, pal, None # ── str: continuous colorscale ──────────────────────────────────────────── try: px.colors.get_colorscale(color) return None, None, color except Exception: pass # ── str: plain hex / named color ───────────────────────────────────────── return None, [color], None def _parse_shape(shape, df): “”” Parse shape arg (str or tuple) into (shape_col, symbol_map). shape_col : column name to encode by marker symbol symbol_map : fixed symbol str OR list of symbols per group “”” if shape is None: return None, None # ── tuple: (col, [sym1, sym2, …]) ────────────────────────────────────── if isinstance(shape, tuple): col, symbols = shape return col, symbols # symbols is a list matching group order # ── str: column name ────────────────────────────────────────────────────── if shape in df.columns: return shape, None # Plotly picks default symbols per group # ── str: fixed marker symbol ────────────────────────────────────────────── if shape.lower() in _MARKER_SYMBOLS: return None, shape # fixed symbol for all markers raise ValueError( f”shape='{shape}’ is not a column name or a known Plotly marker symbol.\n” f”Known symbols: {sorted(_MARKER_SYMBOLS)}” ) # ── main function ───────────────────────────────────────────────────────────── def craft_plot( data, numeric=None, categorical=None, chart=None, title=None, label=None, color=None, shape=None, ): df = data.copy() label = label or {} num_cols = [numeric] if isinstance(numeric, str) else list(numeric or []) cat_cols = [categorical] if isinstance(categorical, str) else list(categorical or []) color_col, disc_seq, cont_scale = _parse_color(color, df) shape_col, symbol_map = _parse_shape(shape, df) # convenience locals group_palette = disc_seq or px.colors.qualitative.Plotly single_clr = (disc_seq[0] if disc_seq else “#636EFA”) def _scatter_kw(x, y): “””px.scatter kwargs wiring color + shape.””” kw = dict(template=”plotly_white”, title=title, labels=label, x=x, y=y) if color_col: kw[“color”] = color_col if disc_seq: kw[“color_discrete_sequence”] = disc_seq if cont_scale: kw[“color_continuous_scale”] = cont_scale if shape_col: kw[“symbol”] = shape_col return kw def _apply_symbol(fig): “””Post-apply fixed or mapped symbols after px builds the figure.””” if symbol_map is None: return if isinstance(symbol_map, list): # map each trace (group) to its symbol in order for i, trace in enumerate(fig.data): trace.marker.symbol = symbol_map[i % len(symbol_map)] else: # fixed symbol for all traces fig.update_traces(marker_symbol=symbol_map) # ══════════════════════════════════════════════════════════════════════════ # UNIVARIATE NUMERIC # ══════════════════════════════════════════════════════════════════════════ if len(num_cols) == 1 and not cat_cols: col = num_cols[0] chart = chart or “histogram” if chart == “histogram”: fig = px.histogram(df, x=col, nbins=30, color_discrete_sequence=[single_clr], template=”plotly_white”, title=title, labels=label) elif chart == “box”: fig = px.box(df, y=col, points=”outliers”, color_discrete_sequence=[single_clr], template=”plotly_white”, title=title, labels=label) elif chart == “violin”: fig = px.violin(df, y=col, box=True, color_discrete_sequence=[single_clr], template=”plotly_white”, title=title, labels=label) elif chart == “ecdf”: fig = px.ecdf(df, x=col, markers=True, lines=True, color_discrete_sequence=[single_clr], template=”plotly_white”, title=title, labels=label) elif chart == “kde”: from scipy.stats import gaussian_kde vals = df[col].dropna().values xs = np.linspace(vals.min(), vals.max(), 300) fig = go.Figure(go.Scatter( x=xs, y=gaussian_kde(vals)(xs), mode=”lines”, line=dict(color=single_clr, width=2.5), )) fig.update_layout(template=”plotly_white”, title=title, xaxis_title=label.get(col, col), yaxis_title=”Density”) else: raise ValueError(“Univariate numeric → histogram | box | violin | ecdf | kde”) # ══════════════════════════════════════════════════════════════════════════ # UNIVARIATE CATEGORICAL # ══════════════════════════════════════════════════════════════════════════ elif not num_cols and len(cat_cols) == 1: col = cat_cols[0] counts = df[col].value_counts().reset_index() counts.columns = [col, “count”] chart = chart or “bar” if chart == “bar”: fig = px.bar(counts, x=col, y=”count”, color=col, color_discrete_sequence=group_palette, template=”plotly_white”, title=title, labels=label) elif chart in (“pie”, “donut”): fig = px.pie(counts, names=col, values=”count”, hole=(0.4 if chart == “donut” else 0), color_discrete_sequence=group_palette, template=”plotly_white”, title=title) else: raise ValueError(“Univariate categorical → bar | pie | donut”) # ══════════════════════════════════════════════════════════════════════════ # BIVARIATE NUM × NUM # ══════════════════════════════════════════════════════════════════════════ elif len(num_cols) == 2 and not cat_cols: x, y = num_cols chart = chart or “scatter” if chart == “scatter”: fig = px.scatter(df, **_scatter_kw(x, y)) _apply_symbol(fig) elif chart == “line”: fig = px.line(df.sort_values(x), x=x, y=y, template=”plotly_white”, title=title, labels=label) elif chart == “hexbin”: fig = px.density_heatmap(df, x=x, y=y, nbinsx=30, nbinsy=30, color_continuous_scale=cont_scale or “Viridis”, template=”plotly_white”, title=title, labels=label) else: raise ValueError(“Bivariate num×num → scatter | line | hexbin”) # ══════════════════════════════════════════════════════════════════════════ # BIVARIATE NUM × CAT # ══════════════════════════════════════════════════════════════════════════ elif len(num_cols) == 1 and len(cat_cols) == 1: num, cat = num_cols[0], cat_cols[0] chart = chart or “box” grp_col = color_col or cat if chart == “box”: fig = px.box(df, x=cat, y=num, color=grp_col, color_discrete_sequence=group_palette, template=”plotly_white”, title=title, labels=label) elif chart == “violin”: fig = px.violin(df, x=cat, y=num, color=grp_col, box=True, points=”outliers”, color_discrete_sequence=group_palette, template=”plotly_white”, title=title, labels=label) elif chart == “strip”: fig = px.strip(df, x=cat, y=num, color=grp_col, color_discrete_sequence=group_palette, template=”plotly_white”, title=title, labels=label) elif chart == “bar”: agg = df.groupby(cat)[num].agg([“mean”, “std”]).reset_index() agg.columns = [cat, “mean”, “std”] fig = px.bar(agg, x=cat, y=”mean”, error_y=”std”, color=cat, color_discrete_sequence=group_palette, template=”plotly_white”, title=title, labels={**label, “mean”: f”Mean of {num}”}) # ── NEW: stacked_bar (num × cat) ────────────────────────────────────── # Requires color_col to define the stack segments. # Aggregates `num` by (cat × color_col), stacks color_col within each # cat group. Falls back to a plain stacked bar if no color_col given. elif chart == “stacked_bar”: stack_by = color_col or cat # stack dimension agg = ( df.groupby([cat, stack_by])[num] .sum() .reset_index() ) fig = px.bar( agg, x=cat, y=num, color=stack_by, barmode=”stack”, color_discrete_sequence=group_palette, template=”plotly_white”, title=title, labels=label, ) # ── NEW: grouped_bar (num × cat) ───────────────────────────────────── # Requires color_col to define the sub-groups. # Aggregates `num` by (cat × color_col), places bars side-by-side. elif chart == “grouped_bar”: grp_by = color_col or cat # sub-group dimension agg = ( df.groupby([cat, grp_by])[num] .mean() .reset_index() ) fig = px.bar( agg, x=cat, y=num, color=grp_by, barmode=”group”, color_discrete_sequence=group_palette, template=”plotly_white”, title=title, labels=label, ) else: raise ValueError( “Bivariate num×cat → box | violin | strip | bar | stacked_bar | grouped_bar” ) # ══════════════════════════════════════════════════════════════════════════ # BIVARIATE CAT × CAT # ══════════════════════════════════════════════════════════════════════════ elif not num_cols and len(cat_cols) == 2: c1, c2 = cat_cols chart = chart or “heatmap” if chart == “heatmap”: ct = pd.crosstab(df[c1], df[c2]) fig = go.Figure(go.Heatmap( z=ct.values, x=list(ct.columns), y=list(ct.index), colorscale=cont_scale or “Blues”, text=ct.values, texttemplate=”%{text}”, )) fig.update_layout(template=”plotly_white”, title=title, xaxis_title=label.get(c2, c2), yaxis_title=label.get(c1, c1)) elif chart == “bar”: grp = df.groupby([c1, c2]).size().reset_index(name=”count”) fig = px.bar(grp, x=c1, y=”count”, color=c2, barmode=”group”, color_discrete_sequence=group_palette, template=”plotly_white”, title=title, labels=label) # ── NEW: stacked_bar (cat × cat) ───────────────────────────────────── # Cross-tabulates c1 × c2, stacks c2 categories within each c1 group. elif chart == “stacked_bar”: grp = df.groupby([c1, c2]).size().reset_index(name=”count”) fig = px.bar( grp, x=c1, y=”count”, color=c2, barmode=”stack”, color_discrete_sequence=group_palette, template=”plotly_white”, title=title, labels=label, ) # ── NEW: grouped_bar (cat × cat) ───────────────────────────────────── # Cross-tabulates c1 × c2, places c2 bars side-by-side within c1. elif chart == “grouped_bar”: grp = df.groupby([c1, c2]).size().reset_index(name=”count”) fig = px.bar( grp, x=c1, y=”count”, color=c2, barmode=”group”, color_discrete_sequence=group_palette, template=”plotly_white”, title=title, labels=label, ) else: raise ValueError( “Bivariate cat×cat → heatmap | bar | stacked_bar | grouped_bar” ) # ══════════════════════════════════════════════════════════════════════════ # MULTIVARIATE — T: Telling through facet grids # ══════════════════════════════════════════════════════════════════════════ elif len(num_cols) + len(cat_cols) >= 3: chart = chart or ( “facet_scatter” if len(num_cols) >= 3 and not cat_cols else “facet_violin” if num_cols and cat_cols else “facet_bar” ) def _grid(n): ncols = min(3, n) return math.ceil(n / ncols), ncols if chart == “facet_histogram”: nrows, ncols = _grid(len(num_cols)) fig = make_subplots(rows=nrows, cols=ncols, subplot_titles=[label.get(c, c) for c in num_cols]) for i, col in enumerate(num_cols): r, c = divmod(i, ncols) fig.add_trace(go.Histogram( x=df[col], nbinsx=25, name=label.get(col, col), marker_color=group_palette[i % len(group_palette)], showlegend=False, ), row=r + 1, col=c + 1) fig.update_layout(template=”plotly_white”, title=title) elif chart == “facet_box”: nrows, ncols = _grid(len(num_cols)) fig = make_subplots(rows=nrows, cols=ncols, subplot_titles=[label.get(c, c) for c in num_cols]) for i, col in enumerate(num_cols): r, c = divmod(i, ncols) fig.add_trace(go.Box( y=df[col], name=label.get(col, col), boxpoints=”outliers”, marker_color=group_palette[i % len(group_palette)], showlegend=False, ), row=r + 1, col=c + 1) fig.update_layout(template=”plotly_white”, title=title) elif chart == “facet_scatter”: kw = dict(template=”plotly_white”, title=title, labels=label) if color_col: kw[“color”] = color_col if disc_seq: kw[“color_discrete_sequence”] = disc_seq if cont_scale: kw[“color_continuous_scale”] = cont_scale if shape_col: kw[“symbol”] = shape_col fig = px.scatter_matrix(df, dimensions=num_cols, **kw) fig.update_traces(diagonal_visible=False, showupperhalf=False, marker=dict(size=4, opacity=0.65)) _apply_symbol(fig) elif chart == “facet_bar”: nrows, ncols = _grid(len(cat_cols)) fig = make_subplots(rows=nrows, cols=ncols, subplot_titles=[label.get(c, c) for c in cat_cols]) for i, col in enumerate(cat_cols): r, c = divmod(i, ncols) vc = df[col].value_counts() fig.add_trace(go.Bar( x=vc.index.tolist(), y=vc.values.tolist(), name=label.get(col, col), marker_color=group_palette[i % len(group_palette)], showlegend=False, ), row=r + 1, col=c + 1) fig.update_layout(template=”plotly_white”, title=title) elif chart == “facet_violin”: pairs = [(n, c) for n in num_cols for c in cat_cols] nrows, ncols = _grid(len(pairs)) titles = [f”{label.get(n,n)} by {label.get(c,c)}” for n, c in pairs] fig = make_subplots(rows=nrows, cols=ncols, subplot_titles=titles) for i, (num, cat) in enumerate(pairs): r, c = divmod(i, ncols) for j, grp in enumerate(df[cat].unique()): vals = df.loc[df[cat] == grp, num].dropna() fig.add_trace(go.Violin( y=vals, name=str(grp), box_visible=True, points=”outliers”, showlegend=(i == 0), line_color=group_palette[j % len(group_palette)], ), row=r + 1, col=c + 1) fig.update_layout(template=”plotly_white”, title=title) elif chart == “corr_heatmap”: corr = df[num_cols].corr() fig = go.Figure(go.Heatmap( z=corr.values, x=list(corr.columns), y=list(corr.index), colorscale=”RdBu”, zmid=0, zmin=-1, zmax=1, text=corr.round(2).values, texttemplate=”%{text}”, colorbar=dict(title=”r”), )) fig.update_layout(template=”plotly_white”, title=title) else: raise ValueError( “Multivariate → facet_histogram | facet_box | facet_scatter | ” “facet_bar | facet_violin | corr_heatmap” ) else: raise ValueError( “Cannot infer situation. Pass at least one column to numeric or categorical.” ) return fig
"""
=========================================================================
CRAFT — Plotly Visualization (Single Function)
Context · Reference · Aesthetic · Framing · Telling (Storytelling)
=========================================================================
FUNCTION SIGNATURE
craft_plot(data, numeric, categorical, chart, title, label, color, shape)
ARGUMENTS
data pd.DataFrame
numeric str | list[str] | None
categorical str | list[str] | None
chart str — see CHART MENU
title str
label dict — {col: 'display name'}
color str → column name : encodes that variable by color
→ palette name : applies that palette (e.g. 'Set2')
→ hex / named : single color (e.g. '#636EFA', 'red')
tuple → (col, palette): encode col + apply palette
e.g. color=('sex', 'Set2')
shape str → column name : encodes that variable by marker symbol
→ symbol name : fixed symbol for all markers
e.g. 'circle','square','diamond','cross','x',
'triangle-up','triangle-down','star','pentagon'
tuple → (col, [sym1, sym2, ...]): encode col + map symbols
list length must match number of unique values in col
e.g. shape=('smoker', ['diamond', 'circle'])
applies to: scatter, facet_scatter only
CHART MENU
Univariate numeric numeric=col
histogram | box | violin | ecdf | kde
Univariate categorical categorical=col
bar | pie | donut
Bivariate num × num numeric=[x, y]
scatter | line | hexbin
Bivariate num × cat numeric=col, categorical=col
box | violin | strip | bar | stacked_bar | grouped_bar
Bivariate cat × cat categorical=[c1, c2]
heatmap | bar | stacked_bar | grouped_bar
Multivariate (3+ variables, any mix)
facet_histogram | facet_box | facet_scatter
facet_bar | facet_violin | corr_heatmap
================================================================
"""
import math
import numpy as np
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
# ── registries ────────────────────────────────────────────────────────────────
_QUALITATIVE = {
p.lower(): p for p in dir(px.colors.qualitative)
if not p.startswith("_")
and isinstance(getattr(px.colors.qualitative, p), list)
}
_MARKER_SYMBOLS = {
"circle", "square", "diamond", "cross", "x", "triangle-up",
"triangle-down", "triangle-left", "triangle-right",
"pentagon", "hexagon", "star", "hourglass", "bowtie", "asterisk", "hash",
}
# ── helpers ───────────────────────────────────────────────────────────────────
def _to_palette(val):
"""Resolve a palette name string to a color list, or return None."""
if isinstance(val, str) and val.lower() in _QUALITATIVE:
return getattr(px.colors.qualitative, _QUALITATIVE[val.lower()])
return None
def _parse_color(color, df):
"""
Parse color arg (str or tuple) into (color_col, disc_seq, cont_scale).
color_col : column name to encode by color
disc_seq : list of colors to apply
cont_scale : continuous colorscale name
"""
if color is None:
return None, None, None
# ── tuple: (col, palette) ─────────────────────────────────────────────────
if isinstance(color, tuple):
col, style = color
pal = _to_palette(style) or (style if isinstance(style, list) else [style])
return col, pal, None
# ── str: column name ──────────────────────────────────────────────────────
if color in df.columns:
return color, None, None
# ── str: qualitative palette name ─────────────────────────────────────────
pal = _to_palette(color)
if pal:
return None, pal, None
# ── str: continuous colorscale ────────────────────────────────────────────
try:
px.colors.get_colorscale(color)
return None, None, color
except Exception:
pass
# ── str: plain hex / named color ─────────────────────────────────────────
return None, [color], None
def _parse_shape(shape, df):
"""
Parse shape arg (str or tuple) into (shape_col, symbol_map).
shape_col : column name to encode by marker symbol
symbol_map : fixed symbol str OR list of symbols per group
"""
if shape is None:
return None, None
# ── tuple: (col, [sym1, sym2, ...]) ──────────────────────────────────────
if isinstance(shape, tuple):
col, symbols = shape
return col, symbols # symbols is a list matching group order
# ── str: column name ──────────────────────────────────────────────────────
if shape in df.columns:
return shape, None # Plotly picks default symbols per group
# ── str: fixed marker symbol ──────────────────────────────────────────────
if shape.lower() in _MARKER_SYMBOLS:
return None, shape # fixed symbol for all markers
raise ValueError(
f"shape='{shape}' is not a column name or a known Plotly marker symbol.\n"
f"Known symbols: {sorted(_MARKER_SYMBOLS)}"
)
# ── main function ─────────────────────────────────────────────────────────────
def craft_plot(
data,
numeric=None,
categorical=None,
chart=None,
title=None,
label=None,
color=None,
shape=None,
):
df = data.copy()
label = label or {}
num_cols = [numeric] if isinstance(numeric, str) else list(numeric or [])
cat_cols = [categorical] if isinstance(categorical, str) else list(categorical or [])
color_col, disc_seq, cont_scale = _parse_color(color, df)
shape_col, symbol_map = _parse_shape(shape, df)
# convenience locals
group_palette = disc_seq or px.colors.qualitative.Plotly
single_clr = (disc_seq[0] if disc_seq else "#636EFA")
def _scatter_kw(x, y):
"""px.scatter kwargs wiring color + shape."""
kw = dict(template="plotly_white", title=title, labels=label, x=x, y=y)
if color_col: kw["color"] = color_col
if disc_seq: kw["color_discrete_sequence"] = disc_seq
if cont_scale: kw["color_continuous_scale"] = cont_scale
if shape_col: kw["symbol"] = shape_col
return kw
def _apply_symbol(fig):
"""Post-apply fixed or mapped symbols after px builds the figure."""
if symbol_map is None:
return
if isinstance(symbol_map, list):
# map each trace (group) to its symbol in order
for i, trace in enumerate(fig.data):
trace.marker.symbol = symbol_map[i % len(symbol_map)]
else:
# fixed symbol for all traces
fig.update_traces(marker_symbol=symbol_map)
# ══════════════════════════════════════════════════════════════════════════
# UNIVARIATE NUMERIC
# ══════════════════════════════════════════════════════════════════════════
if len(num_cols) == 1 and not cat_cols:
col = num_cols[0]
chart = chart or "histogram"
if chart == "histogram":
fig = px.histogram(df, x=col, nbins=30,
color_discrete_sequence=[single_clr],
template="plotly_white", title=title, labels=label)
elif chart == "box":
fig = px.box(df, y=col, points="outliers",
color_discrete_sequence=[single_clr],
template="plotly_white", title=title, labels=label)
elif chart == "violin":
fig = px.violin(df, y=col, box=True,
color_discrete_sequence=[single_clr],
template="plotly_white", title=title, labels=label)
elif chart == "ecdf":
fig = px.ecdf(df, x=col, markers=True, lines=True,
color_discrete_sequence=[single_clr],
template="plotly_white", title=title, labels=label)
elif chart == "kde":
from scipy.stats import gaussian_kde
vals = df[col].dropna().values
xs = np.linspace(vals.min(), vals.max(), 300)
fig = go.Figure(go.Scatter(
x=xs, y=gaussian_kde(vals)(xs),
mode="lines", line=dict(color=single_clr, width=2.5),
))
fig.update_layout(template="plotly_white", title=title,
xaxis_title=label.get(col, col), yaxis_title="Density")
else:
raise ValueError("Univariate numeric → histogram | box | violin | ecdf | kde")
# ══════════════════════════════════════════════════════════════════════════
# UNIVARIATE CATEGORICAL
# ══════════════════════════════════════════════════════════════════════════
elif not num_cols and len(cat_cols) == 1:
col = cat_cols[0]
counts = df[col].value_counts().reset_index()
counts.columns = [col, "count"]
chart = chart or "bar"
if chart == "bar":
fig = px.bar(counts, x=col, y="count", color=col,
color_discrete_sequence=group_palette,
template="plotly_white", title=title, labels=label)
elif chart in ("pie", "donut"):
fig = px.pie(counts, names=col, values="count",
hole=(0.4 if chart == "donut" else 0),
color_discrete_sequence=group_palette,
template="plotly_white", title=title)
else:
raise ValueError("Univariate categorical → bar | pie | donut")
# ══════════════════════════════════════════════════════════════════════════
# BIVARIATE NUM × NUM
# ══════════════════════════════════════════════════════════════════════════
elif len(num_cols) == 2 and not cat_cols:
x, y = num_cols
chart = chart or "scatter"
if chart == "scatter":
fig = px.scatter(df, **_scatter_kw(x, y))
_apply_symbol(fig)
elif chart == "line":
fig = px.line(df.sort_values(x), x=x, y=y,
template="plotly_white", title=title, labels=label)
elif chart == "hexbin":
fig = px.density_heatmap(df, x=x, y=y, nbinsx=30, nbinsy=30,
color_continuous_scale=cont_scale or "Viridis",
template="plotly_white", title=title, labels=label)
else:
raise ValueError("Bivariate num×num → scatter | line | hexbin")
# ══════════════════════════════════════════════════════════════════════════
# BIVARIATE NUM × CAT
# ══════════════════════════════════════════════════════════════════════════
elif len(num_cols) == 1 and len(cat_cols) == 1:
num, cat = num_cols[0], cat_cols[0]
chart = chart or "box"
grp_col = color_col or cat
if chart == "box":
fig = px.box(df, x=cat, y=num, color=grp_col,
color_discrete_sequence=group_palette,
template="plotly_white", title=title, labels=label)
elif chart == "violin":
fig = px.violin(df, x=cat, y=num, color=grp_col,
box=True, points="outliers",
color_discrete_sequence=group_palette,
template="plotly_white", title=title, labels=label)
elif chart == "strip":
fig = px.strip(df, x=cat, y=num, color=grp_col,
color_discrete_sequence=group_palette,
template="plotly_white", title=title, labels=label)
elif chart == "bar":
agg = df.groupby(cat)[num].agg(["mean", "std"]).reset_index()
agg.columns = [cat, "mean", "std"]
fig = px.bar(agg, x=cat, y="mean", error_y="std", color=cat,
color_discrete_sequence=group_palette,
template="plotly_white", title=title,
labels={**label, "mean": f"Mean of {num}"})
# ── NEW: stacked_bar (num × cat) ──────────────────────────────────────
# Requires color_col to define the stack segments.
# Aggregates `num` by (cat × color_col), stacks color_col within each
# cat group. Falls back to a plain stacked bar if no color_col given.
elif chart == "stacked_bar":
stack_by = color_col or cat # stack dimension
agg = (
df.groupby([cat, stack_by])[num]
.sum()
.reset_index()
)
fig = px.bar(
agg, x=cat, y=num, color=stack_by,
barmode="stack",
color_discrete_sequence=group_palette,
template="plotly_white", title=title, labels=label,
)
# ── NEW: grouped_bar (num × cat) ─────────────────────────────────────
# Requires color_col to define the sub-groups.
# Aggregates `num` by (cat × color_col), places bars side-by-side.
elif chart == "grouped_bar":
grp_by = color_col or cat # sub-group dimension
agg = (
df.groupby([cat, grp_by])[num]
.mean()
.reset_index()
)
fig = px.bar(
agg, x=cat, y=num, color=grp_by,
barmode="group",
color_discrete_sequence=group_palette,
template="plotly_white", title=title, labels=label,
)
else:
raise ValueError(
"Bivariate num×cat → box | violin | strip | bar | stacked_bar | grouped_bar"
)
# ══════════════════════════════════════════════════════════════════════════
# BIVARIATE CAT × CAT
# ══════════════════════════════════════════════════════════════════════════
elif not num_cols and len(cat_cols) == 2:
c1, c2 = cat_cols
chart = chart or "heatmap"
if chart == "heatmap":
ct = pd.crosstab(df[c1], df[c2])
fig = go.Figure(go.Heatmap(
z=ct.values, x=list(ct.columns), y=list(ct.index),
colorscale=cont_scale or "Blues",
text=ct.values, texttemplate="%{text}",
))
fig.update_layout(template="plotly_white", title=title,
xaxis_title=label.get(c2, c2),
yaxis_title=label.get(c1, c1))
elif chart == "bar":
grp = df.groupby([c1, c2]).size().reset_index(name="count")
fig = px.bar(grp, x=c1, y="count", color=c2, barmode="group",
color_discrete_sequence=group_palette,
template="plotly_white", title=title, labels=label)
# ── NEW: stacked_bar (cat × cat) ─────────────────────────────────────
# Cross-tabulates c1 × c2, stacks c2 categories within each c1 group.
elif chart == "stacked_bar":
grp = df.groupby([c1, c2]).size().reset_index(name="count")
fig = px.bar(
grp, x=c1, y="count", color=c2,
barmode="stack",
color_discrete_sequence=group_palette,
template="plotly_white", title=title, labels=label,
)
# ── NEW: grouped_bar (cat × cat) ─────────────────────────────────────
# Cross-tabulates c1 × c2, places c2 bars side-by-side within c1.
elif chart == "grouped_bar":
grp = df.groupby([c1, c2]).size().reset_index(name="count")
fig = px.bar(
grp, x=c1, y="count", color=c2,
barmode="group",
color_discrete_sequence=group_palette,
template="plotly_white", title=title, labels=label,
)
else:
raise ValueError(
"Bivariate cat×cat → heatmap | bar | stacked_bar | grouped_bar"
)
# ══════════════════════════════════════════════════════════════════════════
# MULTIVARIATE — T: Telling through facet grids
# ══════════════════════════════════════════════════════════════════════════
elif len(num_cols) + len(cat_cols) >= 3:
chart = chart or (
"facet_scatter" if len(num_cols) >= 3 and not cat_cols else
"facet_violin" if num_cols and cat_cols else
"facet_bar"
)
def _grid(n):
ncols = min(3, n)
return math.ceil(n / ncols), ncols
if chart == "facet_histogram":
nrows, ncols = _grid(len(num_cols))
fig = make_subplots(rows=nrows, cols=ncols,
subplot_titles=[label.get(c, c) for c in num_cols])
for i, col in enumerate(num_cols):
r, c = divmod(i, ncols)
fig.add_trace(go.Histogram(
x=df[col], nbinsx=25, name=label.get(col, col),
marker_color=group_palette[i % len(group_palette)],
showlegend=False,
), row=r + 1, col=c + 1)
fig.update_layout(template="plotly_white", title=title)
elif chart == "facet_box":
nrows, ncols = _grid(len(num_cols))
fig = make_subplots(rows=nrows, cols=ncols,
subplot_titles=[label.get(c, c) for c in num_cols])
for i, col in enumerate(num_cols):
r, c = divmod(i, ncols)
fig.add_trace(go.Box(
y=df[col], name=label.get(col, col), boxpoints="outliers",
marker_color=group_palette[i % len(group_palette)],
showlegend=False,
), row=r + 1, col=c + 1)
fig.update_layout(template="plotly_white", title=title)
elif chart == "facet_scatter":
kw = dict(template="plotly_white", title=title, labels=label)
if color_col: kw["color"] = color_col
if disc_seq: kw["color_discrete_sequence"] = disc_seq
if cont_scale: kw["color_continuous_scale"] = cont_scale
if shape_col: kw["symbol"] = shape_col
fig = px.scatter_matrix(df, dimensions=num_cols, **kw)
fig.update_traces(diagonal_visible=False, showupperhalf=False,
marker=dict(size=4, opacity=0.65))
_apply_symbol(fig)
elif chart == "facet_bar":
nrows, ncols = _grid(len(cat_cols))
fig = make_subplots(rows=nrows, cols=ncols,
subplot_titles=[label.get(c, c) for c in cat_cols])
for i, col in enumerate(cat_cols):
r, c = divmod(i, ncols)
vc = df[col].value_counts()
fig.add_trace(go.Bar(
x=vc.index.tolist(), y=vc.values.tolist(),
name=label.get(col, col),
marker_color=group_palette[i % len(group_palette)],
showlegend=False,
), row=r + 1, col=c + 1)
fig.update_layout(template="plotly_white", title=title)
elif chart == "facet_violin":
pairs = [(n, c) for n in num_cols for c in cat_cols]
nrows, ncols = _grid(len(pairs))
titles = [f"{label.get(n,n)} by {label.get(c,c)}" for n, c in pairs]
fig = make_subplots(rows=nrows, cols=ncols, subplot_titles=titles)
for i, (num, cat) in enumerate(pairs):
r, c = divmod(i, ncols)
for j, grp in enumerate(df[cat].unique()):
vals = df.loc[df[cat] == grp, num].dropna()
fig.add_trace(go.Violin(
y=vals, name=str(grp), box_visible=True,
points="outliers", showlegend=(i == 0),
line_color=group_palette[j % len(group_palette)],
), row=r + 1, col=c + 1)
fig.update_layout(template="plotly_white", title=title)
elif chart == "corr_heatmap":
corr = df[num_cols].corr()
fig = go.Figure(go.Heatmap(
z=corr.values, x=list(corr.columns), y=list(corr.index),
colorscale="RdBu", zmid=0, zmin=-1, zmax=1,
text=corr.round(2).values, texttemplate="%{text}",
colorbar=dict(title="r"),
))
fig.update_layout(template="plotly_white", title=title)
else:
raise ValueError(
"Multivariate → facet_histogram | facet_box | facet_scatter | "
"facet_bar | facet_violin | corr_heatmap"
)
else:
raise ValueError(
"Cannot infer situation. Pass at least one column to numeric or categorical."
)
return fig
Variable declaration
Code is written so it works with any dataset, changing only a few lines at the top — the rest stays the same.
What to Change
- Data path → set to your dataset’s file location
- Numerical variable → set to the numeric column you want
- Categorical variable → set to the categorical column you want
The Usage part — no need to touch it once the above variables are set. It stays the same
It is reusable script works for different datasets by just updating the data path and variable names.
Show code
df = pd.read_csv(“\datapath.csv”) num_var1= “numeric_variable” num_var2= “numeric_variable” num_var3= “numeric_variable” cat_var1= “Categorical_variable” cat_var2= “Categorical_variable” cat_var3= “Categorical_variable” cat_var4= “Categorical_variable”
df = pd.read_csv("\datapath.csv")
num_var1= "numeric_variable"
num_var2= "numeric_variable"
num_var3= "numeric_variable"
cat_var1= "Categorical_variable"
cat_var2= "Categorical_variable"
cat_var3= "Categorical_variable"
cat_var4= "Categorical_variable"
Usage
Show code
# Univariate numeric craft_plot(df, numeric=num_var1, chart=”histogram”, color=”#63AAAA”, title=” Histogram chart”).show() # Univariate categorical craft_plot(df, categorical=cat_var1, chart=”pie”, color=”Pastel”, title=” Pie chart”).show() # Bivariate num × num craft_plot(df, numeric=[num_var1, num_var2], chart=”scatter”, color=”black”, title=” Bivariate num × num chart”).show() # Bivariate num × cat craft_plot(df, numeric=num_var2, categorical=cat_var1, chart=”box”, color=”Set2″, title=”Bivariate num × cat chart”).show() # Bivariate cat × cat craft_plot(df, categorical=[cat_var1, cat_var2], chart=”stacked_bar”, title=”Bivariate cat × cat chart”).show() # Multivariate — grid of histograms (all numeric) craft_plot(df, numeric=[num_var1, num_var2, num_var3], chart=”facet_histogram”, color=”Plotly”, title=”Grid of histograms (all numeric)”).show() # Multivariate — scatter matrix (all numeric pairs) craft_plot(df, numeric=[num_var1, num_var2, num_var3], chart=”facet_scatter”, color=cat_var2, title=”Scatter matrix (all numeric pairs)”).show() # Multivariate — box grid (numeric × categorical) craft_plot(df, numeric=[num_var1, num_var2], categorical=[cat_var1, cat_var3], chart=”facet_violin”, color=”Set2″, title=”Box grid (numeric × categorical)”).show() # Multivariate — bar grid (all categorical) craft_plot(df, categorical=[cat_var1, cat_var2, cat_var3], chart=”facet_bar”, color=”Pastel”, title=” Bar grid (all categorical)”).show() # Multivariate — correlation heatmap craft_plot(df, numeric=[num_var1, num_var2, num_var3], chart=”corr_heatmap”, title=” Correlation Matrix”).show() # multivariate # color tuple: col + palette craft_plot(df, numeric=[num_var1, num_var2], chart=”scatter”, color=(cat_var1, “Set2″), shape=”diamond”, title=”Multivariate scatter plot”).show() # shape as column (Plotly picks symbols) craft_plot(df, numeric=[num_var1, num_var2], chart=”scatter”, color=(cat_var1, “Set2”), shape=(cat_var4,[“square”, “star”]), title=”Multivariate scatter plot”).show()
# Univariate numeric
craft_plot(df, numeric=num_var1, chart="histogram",
color="#63AAAA", title=" Histogram chart").show()
# Univariate categorical
craft_plot(df, categorical=cat_var1, chart="pie",
color="Pastel", title=" Pie chart").show()
# Bivariate num × num
craft_plot(df, numeric=[num_var1, num_var2], chart="scatter",
color="black", title=" Bivariate num × num chart").show()
# Bivariate num × cat
craft_plot(df, numeric=num_var2, categorical=cat_var1, chart="box",
color="Set2", title="Bivariate num × cat chart").show()
# Bivariate cat × cat
craft_plot(df, categorical=[cat_var1, cat_var2], chart="stacked_bar",
title="Bivariate cat × cat chart").show()
# Multivariate — grid of histograms (all numeric)
craft_plot(df, numeric=[num_var1, num_var2, num_var3],
chart="facet_histogram", color="Plotly",
title="Grid of histograms (all numeric)").show()
# Multivariate — scatter matrix (all numeric pairs)
craft_plot(df, numeric=[num_var1, num_var2, num_var3],
chart="facet_scatter", color=cat_var2,
title="Scatter matrix (all numeric pairs)").show()
# Multivariate — box grid (numeric × categorical)
craft_plot(df, numeric=[num_var1, num_var2], categorical=[cat_var1, cat_var3],
chart="facet_violin", color="Set2",
title="Box grid (numeric × categorical)").show()
# Multivariate — bar grid (all categorical)
craft_plot(df, categorical=[cat_var1, cat_var2, cat_var3],
chart="facet_bar", color="Pastel",
title=" Bar grid (all categorical)").show()
# Multivariate — correlation heatmap
craft_plot(df, numeric=[num_var1, num_var2, num_var3],
chart="corr_heatmap", title=" Correlation Matrix").show()
# multivariate
# color tuple: col + palette
craft_plot(df, numeric=[num_var1, num_var2], chart="scatter",
color=(cat_var1, "Set2"), shape="diamond", title="Multivariate scatter plot").show()
# shape as column (Plotly picks symbols)
craft_plot(df, numeric=[num_var1, num_var2], chart="scatter",
color=(cat_var1, "Set2"), shape=(cat_var4,["square", "star"]),
title="Multivariate scatter plot").show()
The following contents are available in the folder
- Dataset
- The above Python code
- Numerical Analysis
- Visual analysis