The Art of Visualization with Plotly

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.

Visualization approach depends on variable type:

  • Numeric – continuous/discrete values (e.g., age, income)
  • Categorical – distinct groups/labels (e.g., gender, region)

Study of a single variable in isolation (e.g., histogram for numeric, bar chart for categorical).

Study of relationship between two variables. Three cases:

  1. Numeric vs Numeric → scatter plot
  2. Numeric vs Categorical → box plot, violin plot
  3. Categorical vs Categorical → stacked bar, heatmap

Extension of bivariate analysis by adding more variables. Extra dimensions are added using CSS — Color, Shape, Size on top of a base 2D plot.

  • 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.
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

Code is written so it works with any dataset, changing only a few lines at the top — the rest stays the same.

  • 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"
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

  1. Dataset
  2. The above Python code
  3. Numerical Analysis
  4. Visual analysis
Scroll to Top