Skip to content

Latest commit

 

History

History
545 lines (429 loc) · 18.3 KB

File metadata and controls

545 lines (429 loc) · 18.3 KB

Chart Types — Quick Reference (P3)

Compact 100-300 word entries with minimal runnable code snippets for secondary chart types. For full 6-paragraph treatment of upgraded charts (Funnel → §16, Scree → §17, Profile → §18, Growth → §19, Kaplan-Meier → §20, Bland-Altman → §21), see the chart-family files linked from chart-type-guide.md.

All colors, figure sizes, and helpers are defined in scripts/ssci_style.py (SSOT). Reference this file by helper name and registry key — never copy HEX, DPI, or millimeter values into this document.

Discipline-by-quick-reference matrix

Discipline Quick-reference charts most often used Companion primary
Psychology — descriptive §15.1 Histogram, §15.2 Density, §15.3 Swarm §5 Box, §6 Violin
Psychology — measurement / factor analysis §15.4 Factor Loading §17 Scree, §18 Profile
Psychology — moderation §15.5 Johnson-Neyman §11.2 Continuous marginal effect, §12 Simple slopes
Medicine / Clinical trials §15.6 CONSORT §20 Kaplan-Meier, §29 ROC + Calibration
Public-policy comparisons §15.7 Lollipop, §15.8 Slope, §15.9 Dumbbell §1 Grouped bar
Demography §15.10 Population pyramid §27 Ridgeline, §30 Mobility matrix
Decision / Risk analysis §15.11 Tornado / Butterfly §31 Marginal effects

Table of Contents


15.1 Histogram

Single-variable frequency distribution. 15–30 bins with edgecolor='black', optional normal-curve or KDE overlay. Use for raw distribution shape, sample-size diagnostic, or a pre-modeling normality check. For multi-group distribution comparison prefer §15.2 Density or §6 Violin (raw histograms become hard to overlay). For dense distributions with raw-data and box summary, see §14 Raincloud.

APA note: state the bin width or count. Frequency on the y-axis is fine; for cross-sample comparison use density (area = 1) with density=True.

from ssci_style import (
    apply_style, FIGURE_SIZES, NEUTRAL, get_palette, save_figure,
)
import numpy as np
from scipy.stats import norm

apply_style()
focal = get_palette(1)[0]

fig, ax = plt.subplots(figsize=FIGURE_SIZES['single_column'])
ax.hist(data, bins=25, color=focal, alpha=0.7,
        edgecolor='black', linewidth=0.5)

# Optional normal curve overlay
x = np.linspace(data.min(), data.max(), 100)
bin_width = (data.max() - data.min()) / 25
ax.plot(x, norm.pdf(x, data.mean(), data.std()) * len(data) * bin_width,
        color=NEUTRAL['reference'], linestyle='--', lw=1.0)
ax.set_xlabel('Variable')
ax.set_ylabel('Frequency')

save_figure(fig, 'figure_15_1_histogram')

15.2 Density Plot (KDE)

Smooth continuous distribution. scipy.stats.gaussian_kde(data) yields a kernel density estimate that can be filled with low alpha for multi-group overlay. For 2–3 groups, KDE overlay with alpha=0.3 is more readable than a histogram. For 4+ groups, see §27 Ridgeline (stacked KDEs) for cleaner separation.

APA note: state the bandwidth selection method (Silverman, Scott, or fixed value). Bandwidth choice changes the smoothing materially — report it.

from ssci_style import (
    apply_style, FIGURE_SIZES, get_palette, save_figure,
)
from scipy.stats import gaussian_kde
import numpy as np

apply_style()
colors = get_palette(n_groups)

fig, ax = plt.subplots(figsize=FIGURE_SIZES['single_column'])
for i, (data, label) in enumerate(groups):
    kde = gaussian_kde(data)
    x = np.linspace(data.min(), data.max(), 200)
    ax.fill_between(x, kde(x), color=colors[i], alpha=0.3, label=label)
    ax.plot(x, kde(x), color=colors[i], lw=1.0)
ax.set_xlabel('Variable')
ax.set_ylabel('Density')
ax.legend(frameon=False)

save_figure(fig, 'figure_15_2_density')

15.3 Swarm / Beeswarm Plot

Individual data points without overlap. Manual jitter (np.random.uniform) or seaborn.swarmplot. Point size 3–5, alpha=0.5. Often layered on §5 Box or §6 Violin (forming a near- raincloud). The pure-matplotlib manual-jitter implementation below adds zero dependencies; seaborn.swarmplot is recommended for collision-free placement when sample size and rendering speed permit.

APA note: state the jitter band width. Swarm shows raw n exactly; this is its primary virtue over box / violin.

from ssci_style import (
    apply_style, FIGURE_SIZES, get_palette, save_figure,
)
import numpy as np

apply_style()
colors = get_palette(n_groups)

fig, ax = plt.subplots(figsize=FIGURE_SIZES['box_plot'])
for i, (data, label) in enumerate(groups):
    jitter = np.random.uniform(-0.15, 0.15, len(data))
    ax.scatter(np.full(len(data), i) + jitter, data,
               color=colors[i], alpha=0.5, s=15, edgecolors='none')

ax.set_xticks(range(n_groups))
ax.set_xticklabels(group_labels)
ax.set_ylabel('Outcome')

save_figure(fig, 'figure_15_3_swarm')

15.4 Factor Loading Plot

CFA/EFA loadings as a horizontal bar chart or heatmap. Reference line at the inclusion threshold (|λ| ≥ .30 conservative; ≥ .40 strict; discipline-dependent). Color-encode by factor assignment so each factor's items group visually. For a full CFA correlation pattern across many items × many factors, switch to a heatmap (ax.imshow) with a diverging colormap.

APA note: state the threshold convention and the rotation method (Promax, Oblimin, Varimax). λ (Greek loading) is not italic per APA 7 Table 6.5.

from ssci_style import (
    apply_style, FIGURE_SIZES, get_palette,
    add_reference_line, save_figure,
)

apply_style()
focal = get_palette(1)[0]

fig, ax = plt.subplots(figsize=FIGURE_SIZES['bar_chart'])
ax.barh(range(n_items), loadings, color=focal,
        edgecolor='black', linewidth=0.4)

add_reference_line(ax, 0.30, 'v', label='Threshold |λ| ≥ .30')

ax.set_yticks(range(n_items))
ax.set_yticklabels(item_names)
ax.set_xlabel('Standardized loading')

save_figure(fig, 'figure_15_4_factor_loading')

15.5 Johnson-Neyman Plot

Significance region for continuous moderation. X-axis: moderator W. Y-axis: conditional effect of X on Y with 95% CI band. Vertical dashed lines mark Johnson-Neyman critical points where the CI crosses zero; shade the significant region. For categorical moderation see §11.1; for the predicted-Y view of a moderation see §31 Marginal effects. The Johnson-Neyman approach is the continuous- moderator complement to simple-slopes (§12) and is preferred when W has no natural splitting threshold.

APA note: report the critical W values and the proportion of the W-range in the significant region.

from ssci_style import (
    apply_style, FIGURE_SIZES, NEUTRAL, get_palette, save_figure,
)

apply_style()
focal = get_palette(1)[0]

fig, ax = plt.subplots(figsize=FIGURE_SIZES['interaction'])
ax.plot(w_grid, cond_effect, color=focal, lw=1.5)
ax.fill_between(w_grid, ci_lo, ci_hi, color=focal, alpha=0.18)

ax.axhline(0, color=NEUTRAL['reference'], linestyle='--', lw=0.6)

# J-N critical values
for w_jn in jn_critical_points:
    ax.axvline(w_jn, color=NEUTRAL['axis'], linestyle=':', lw=0.6)

ax.set_xlabel('Moderator $W$')
ax.set_ylabel(r'Conditional effect of $X$ on $Y$  ($b_1 + b_3 W$)')

save_figure(fig, 'figure_15_5_johnson_neyman')

15.6 CONSORT Flow Diagram

RCT participant flow per CONSORT 2010. Four stages: Enrollment → Allocation → Follow-up → Analysis. FancyBboxPatch for boxes, FancyArrowPatch for arrows, exclusion-counts in side annotations. For complex multi-arm trials with multiple exclusion paths, an external drawing tool (Excalidraw, draw.io, or the CONSORT R package) is often faster than matplotlib. The matplotlib implementation works and is reproducible from data, but a 5-arm trial with multiple exclusion-reason callouts becomes labor-intensive.

APA note: CONSORT-compliant diagrams must include all four stages with n values; exclusion reasons at each stage are required per the CONSORT 2010 statement (Schulz et al., 2010, BMJ).

from ssci_style import (
    apply_style, FIGURE_SIZES, save_figure,
)
from matplotlib.patches import FancyBboxPatch
import matplotlib.pyplot as plt

apply_style()
fig, ax = plt.subplots(figsize=FIGURE_SIZES['full_width'])
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
ax.axis('off')

stages = [
    ('Assessed for eligibility (n = 200)',     0.5, 0.90),
    ('Randomized (n = 180)',                   0.5, 0.65),
    ('Allocated to intervention (n = 90)',     0.3, 0.40),
    ('Allocated to control (n = 90)',          0.7, 0.40),
    ('Analyzed (n = 85)',                      0.3, 0.15),
    ('Analyzed (n = 87)',                      0.7, 0.15),
]
for label, x, y in stages:
    box = FancyBboxPatch(
        (x - 0.18, y - 0.04), 0.36, 0.08,
        boxstyle='round,pad=0.02', facecolor='white',
        edgecolor='black', linewidth=1.0, transform=ax.transAxes,
    )
    ax.add_patch(box)
    ax.text(x, y, label, ha='center', va='center', fontsize=7,
            transform=ax.transAxes)

# Connect with arrows (simplified — full CONSORT has exclusion callouts)
arrow_kw = dict(arrowstyle='->', mutation_scale=12, lw=0.8, color='black')
from matplotlib.patches import FancyArrowPatch
for (x1, y1), (x2, y2) in [
    ((0.5, 0.86), (0.5, 0.69)),
    ((0.5, 0.61), (0.3, 0.44)),
    ((0.5, 0.61), (0.7, 0.44)),
    ((0.3, 0.36), (0.3, 0.19)),
    ((0.7, 0.36), (0.7, 0.19)),
]:
    arr = FancyArrowPatch((x1, y1), (x2, y2),
                          transform=ax.transAxes, **arrow_kw)
    ax.add_patch(arr)

save_figure(fig, 'figure_15_6_consort')

15.7 Lollipop Chart

Cleaner alternative to a bar chart. A marker at the category value plus a thin line back to zero (or a baseline). Less visual weight than full bars; preferred when bar area would obscure value precision (typically n_categories > 12 or when bars would compress into a wall of color). Identical message to §1 Grouped bar, different ink-to-data ratio.

APA note: lollipops follow the same Y-axis baseline rule as bars (zero baseline required when ranks or counts are encoded by line length).

from ssci_style import (
    apply_style, FIGURE_SIZES, NEUTRAL, get_palette,
    add_reference_line, save_figure,
)
import numpy as np

apply_style()
focal = get_palette(1)[0]

fig, ax = plt.subplots(figsize=FIGURE_SIZES['bar_chart'])
y = np.arange(len(categories))
ax.hlines(y, 0, values, color=NEUTRAL['reference'], lw=0.6)
ax.plot(values, y, 'o', color=focal, markersize=8,
        markeredgecolor='black', markeredgewidth=0.5)

ax.set_yticks(y)
ax.set_yticklabels(categories)
ax.set_xlabel('Value')

add_reference_line(ax, 0, 'v')

save_figure(fig, 'figure_15_7_lollipop')

15.8 Slope / Bump Chart

Before-after comparison across categories. Two y-positions per category, lines connect. The slope encodes the magnitude of change; the rank order at each timepoint is the "bump" variant. Standard "how did the league standings change after the trade deadline" visualization; widely adopted in business / policy for pre-post treatment displays at small n.

APA note: when slope encodes change, the y-axis units must match at both timepoints. Color-encode direction of change (focal = increase, gray = decrease) for visual scan-ability.

from ssci_style import (
    apply_style, FIGURE_SIZES, get_emphasis_pair,
    remove_spines, save_figure,
)

apply_style()
focal, ref = get_emphasis_pair()

fig, ax = plt.subplots(figsize=FIGURE_SIZES['line_chart'])
for cat, val_pre, val_post in categories:
    color = focal if (val_post - val_pre) > 0 else ref
    ax.plot([0, 1], [val_pre, val_post],
            color=color, lw=1.4, marker='o', ms=6)
    ax.text(-0.05, val_pre, cat, ha='right', va='center', fontsize=8)
    ax.text(1.05, val_post, f'{val_post:.0f}',
            ha='left', va='center', fontsize=8)

ax.set_xlim(-0.3, 1.3)
ax.set_xticks([0, 1])
ax.set_xticklabels(['Time 1', 'Time 2'])
remove_spines(ax, keep=('bottom',))

save_figure(fig, 'figure_15_8_slope')

15.9 Dumbbell Plot

Paired comparison: gap between two values per category. Two markers per row connected by a line — emphasizes the gap size and direction. Standard "wage gap by industry" or "before-after by demographic" visualization. Distinct from §15.8 Slope: slope shows change for a single category over time; dumbbell shows a between- group gap at a single timepoint (or pre-post within a category).

APA note: order rows by gap magnitude (largest at top, descending) for visual scan-ability. Label both markers with their values to remove decoding load.

from ssci_style import (
    apply_style, FIGURE_SIZES, NEUTRAL, get_palette, save_figure,
)
import numpy as np

apply_style()
c_a, c_b = get_palette(2)

fig, ax = plt.subplots(figsize=FIGURE_SIZES['bar_chart'])
y = np.arange(len(categories))
ax.hlines(y, val_low, val_high, color=NEUTRAL['reference'], lw=1.2)
ax.scatter(val_low,  y, color=c_a, s=60, zorder=3, label='Group A')
ax.scatter(val_high, y, color=c_b, s=60, zorder=3, label='Group B')

ax.set_yticks(y)
ax.set_yticklabels(categories)
ax.set_xlabel('Value')
ax.legend(frameon=False, loc='upper right')

save_figure(fig, 'figure_15_9_dumbbell')

15.10 Population Pyramid

Age × sex distribution. Horizontal bars in mirrored layout — males left (negative direction), females right (positive direction). Standard in demography (the "pyramid" or "rectangle" shape diagnoses fertility-mortality regime). The conventional male-left / female-right orientation is from demographic publications; reverse it only if you state the convention explicitly.

APA note: include the total n per sex in the Note. Label the x-axis with absolute counts (the negative axis must show absolute values, not negatives, to avoid misreading).

from ssci_style import (
    apply_style, FIGURE_SIZES, get_palette, save_figure,
)
import numpy as np

apply_style()
c_male, c_female = get_palette(2)

fig, ax = plt.subplots(figsize=FIGURE_SIZES['bar_chart'])
y = np.arange(len(age_groups))

ax.barh(y, -np.asarray(male_counts), color=c_male,
        edgecolor='black', linewidth=0.4)
ax.barh(y,  np.asarray(female_counts), color=c_female,
        edgecolor='black', linewidth=0.4)

ax.set_yticks(y)
ax.set_yticklabels(age_groups)

# Symmetric x-axis tick labels (show absolute values on both sides)
xtick_locs = ax.get_xticks()
ax.set_xticks(xtick_locs)
ax.set_xticklabels([f'{abs(int(t))}' for t in xtick_locs])
ax.axvline(0, color='black', lw=0.8)

ax.text(0.05, 1.01, 'Male', transform=ax.transAxes, fontsize=8,
        color=c_male)
ax.text(0.95, 1.01, 'Female', transform=ax.transAxes, fontsize=8,
        ha='right', color=c_female)
ax.set_xlabel('Count')

save_figure(fig, 'figure_15_10_pyramid')

15.11 Tornado / Butterfly Chart

Sensitivity analysis: factors ranked by absolute effect. Horizontal bars centered on zero, ordered by absolute effect from largest (top) to smallest (bottom). Standard in decision analysis and economic-evaluation sensitivity tables; the visual ranking maps directly to which model inputs drive the outcome. Distinct from §1 Grouped bar (categories not signed) and §8 Forest (effect estimates with CI).

APA note: state the sensitivity range explicitly (e.g., "Factors varied across their plausible range; effect = ΔOutcome at factor max minus ΔOutcome at factor min, all other factors at base case."). Bars must be ordered by |effect|.

from ssci_style import (
    apply_style, FIGURE_SIZES, get_palette, save_figure,
)
import numpy as np

apply_style()
focal_pos, focal_neg = get_palette(2)

# Order by |effect|, largest at top
effects = np.asarray(raw_effects)
order   = np.argsort(np.abs(effects))[::-1]
sorted_effects = effects[order]
sorted_labels  = [factors[i] for i in order]

fig, ax = plt.subplots(figsize=FIGURE_SIZES['bar_chart'])
y = np.arange(len(sorted_labels))
colors = [focal_pos if e > 0 else focal_neg for e in sorted_effects]

ax.barh(y, sorted_effects, color=colors,
        edgecolor='black', linewidth=0.4)

ax.set_yticks(y)
ax.set_yticklabels(sorted_labels)
ax.invert_yaxis()   # Largest |effect| at top
ax.axvline(0, color='black', lw=0.8)
ax.set_xlabel('Effect on outcome (sensitivity range)')

save_figure(fig, 'figure_15_11_tornado')

Cross-references and shared concepts

  • Full 6-paragraph treatment for charts upgraded out of the quick-reference (Funnel, Scree, Profile, Growth, Kaplan-Meier, Bland-Altman) is in the discipline-specific chart family files:
  • Color selection (get_palette, get_emphasis_pair, get_diverging_cmap, get_sequential_cmap): see color-system.md. All quick-reference templates here default to get_palette or get_emphasis_pair.
  • APA 7 conventions (italic Latin vs. non-italic Greek, p reporting, threshold conventions): see apa-figure-standards.md. §15.4 uses λ (Greek, non-italic); §15.5 uses W (Latin algebraic, italic).
  • Y-axis baseline rule: §15.7 Lollipop and §15.9 Dumbbell follow the bar-chart zero-baseline convention because their line length encodes value. §15.1–§15.3 (distribution displays) do not require zero baseline. See the Index in chart-type-guide.md for the full rule.
  • When to upgrade from quick-reference to main: a quick-reference entry is sufficient when (a) the chart is a one-off in your paper, (b) the parameters are stable across applications, and (c) the reader does not need to compare across multiple datasets. When the chart is the centerpiece of a result, see the main-section counterpart (e.g., §15.1 Histogram → §6 Violin for distribution comparison; §15.4 Factor Loading → §17 Scree + §18 Profile for factor retention + profile interpretation).