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 | 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 |
- 15.1 Histogram
- 15.2 Density Plot (KDE)
- 15.3 Swarm / Beeswarm Plot
- 15.4 Factor Loading Plot
- 15.5 Johnson-Neyman Plot
- 15.6 CONSORT Flow Diagram
- 15.7 Lollipop Chart
- 15.8 Slope / Bump Chart
- 15.9 Dumbbell Plot
- 15.10 Population Pyramid
- 15.11 Tornado / Butterfly Chart
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')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')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')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')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')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')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')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')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')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')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')- 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:
- §16 Funnel, §20 Kaplan-Meier, §21 Bland-Altman → see
chart-types-applied.md - §17 Scree, §18 Profile, §19 Growth → see
chart-types-models.md
- §16 Funnel, §20 Kaplan-Meier, §21 Bland-Altman → see
- Color selection (
get_palette,get_emphasis_pair,get_diverging_cmap,get_sequential_cmap): seecolor-system.md. All quick-reference templates here default toget_paletteorget_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.mdfor 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).