Data Visualisation Reference

A practical guide to chart types, their purpose, and how to build them in R

Author

Personal study deck

Published

June 17, 2026

Outline

01
What is Visualization?
From dynamic documents to the .qmd ecosystem
03
Choosing right Visualzation
By data type, Purpose, audiance
02
How to choose right visualisation?
Instead of What chart? use: what is the message?

1. What is visualisation, and how is it useful?

What is visualisation?

Visualisation is the practice of turning data into a picture so that the human eye can do what a table cannot, see structure, change, relationships and outliers at a glance.

  • A bar reveals a comparison. A line reveals a trend. A map reveals a place. A scatter reveals a relationship.
  • Visualisation is not decoration, it is encoding: numbers are mapped to position, length, area, angle, colour and texture.
  • The right chart turns a 50-row table into a 5-second insight. The wrong chart hides the message.

How visualisation is useful

For analysis

  • Reveals patterns (trends, clusters, outliers)
  • Makes errors visible (a bad row jumps out of a scatter)
  • Suggests follow-up questions (why is this region different?)
  • Compresses large data into one frame

For reporting and decisions

  • Anchors a narrative: readers remember the picture
  • Supports M&E reporting, TPM dashboards, survey briefs, humanitarian situation reports
  • Speeds up decisions, audiences digest a clean chart in seconds
  • Builds trust: clear sources, clear units, clear methodology

2. How to choose the right visualisation

The first question is not “which chart”, it is “what is the message?”

Before picking a chart, write down the one sentence the chart should make true in the reader’s mind. The chart that supports that sentence the most directly is the right chart.

  • “Region X grew the fastest” (slope, dumbbell, sorted bar of growth)
  • “Most respondents agree” (diverging bar, stacked bar)
  • “Y is correlated with X” (scatter with regression line)
  • “This site is an outlier” (box plot, beeswarm, control chart)
  • “Half the people are stuck at stage 2” (funnel)
  • “These five regions account for 80% of cases” (Pareto, ordered bar)

The encoding hierarchy

Cleveland and McGill (1984) ranked how accurately the human eye can decode different visual encodings. Position beats length, length beats angle and area, and colour intensity is the least accurate. Use the most accurate encoding for the most important comparison, this is why bars beat pies and why heatmaps should carry numbers when precision matters.

Decision tree: chart by analytical question

The reader needs to see, Reach for,
Change over time Line, area, sparkline
Composition over time Stacked area, streamgraph, 100% stacked bar
Compare categories Bar, lollipop, Cleveland dot plot
Compare two points in time Dumbbell, slope
Distribution of one variable Histogram, density, ECDF
Distribution by group Box plot, violin, ridgeline, beeswarm
Relationship between two variables Scatter, hexbin, regression plot
Three variables together Bubble, coloured scatter
Composition of a whole Bar > treemap > waffle > pie (5 slices max)
Hierarchy of composition Sunburst, treemap
Geographic value Choropleth, proportional symbol map
Movement / flow Sankey, alluvial, flow map, chord
Build-up of a total Waterfall

Colour and design fundamentals

Three palette families, three purposes

  • Sequential (one hue, light to dark): for ordered values, low to high (heat maps, choropleths)
  • Diverging (two hues through a neutral midpoint): for values around a meaningful zero (deviations, Likert)
  • Categorical (distinct hues): for unordered categories (cap at 7)

Design rules to follow every time

  • Start the y-axis at zero when length is the encoding (bars).
  • One message per chart. If it needs two sentences to explain, split it.
  • Direct labels beat legends when you have 5 series or fewer.
  • The title carries the message, axes carry units, the source line carries credibility.
  • Annotate the unusual point rather than asking the reader to find it.
  • Always show units, time period and source.
  • Use colour as a redundant cue, not the only cue (colour-blind readers).

3. Choosing the right visualisation by data type, purpose, and audience

How this section is organised

The next nine groups cover, in order:

  1. Time Series / Change Over Time
  2. Categorical Comparison
  3. Distribution
  4. Relationship / Correlation
  5. Composition / Part-to-Whole
  6. Geographic / Spatial Analysis
  7. Ranking and Performance Monitoring
  8. Multivariate and High-Dimensional Analysis
  9. Tables and Text-Based Displays

Each chart slide has the image plus a tab-set with Purpose, When to use, Avoid when, and the R code to build it.

3.1 Time series, change over time

Line chart

  • Show how a single quantity evolves on a continuous (usually time) axis.

  • Compare the trajectories of several series on the same scale.

library(ggplot2)
df <- data.frame(year = 2010:2024,
                 value = c(50,51,52,55,57,60,62,63,66,68,70,72,75,76,78))
ggplot(df, aes(year, value)) +
  geom_area(fill = "#047B77", alpha = 0.08) +
  geom_line(color = "#047B77", linewidth = 1.1) +
  geom_point(color = "#047B77", fill = "white", shape = 21, size = 2.5, stroke = 1.4) +
  labs(title = "Annual value, 2010-2024",
       x = "Year", y = "Value",
       caption = "Source: Illustrative") + theme_minimal()
library(ggplot2); library(tidyr)
df <- data.frame(year = 2010:2024,
                 A = c(50,52,53,56,58,60,62,63,66,69,71,73,76,78,80),
                 B = c(40,42,44,46,48,50,51,53,55,58,60,62,64,66,68),
                 C = c(30,33,35,38,42,45,48,51,55,57,60,63,66,68,70))
long <- pivot_longer(df, -year, names_to = "series", values_to = "value")

ggplot(long, aes(year, value, color = series)) +
  geom_line(linewidth = 1.2) +
  geom_point(size = 2, fill = "white", shape = 21, stroke = 1.4) +
  scale_color_manual(values = c(A = "#047B77", B = "#1A9490", C = "#F59E0B")) +
  labs(title = "Comparing three series over time",
       x = "Year", y = "Index") + theme_minimal()

Area chart

  • A line chart with the area under it filled, emphasising volume or cumulative magnitude.

  • Show how a total and the contribution of each component to that total evolve over time.

library(ggplot2)
df <- data.frame(year = 2010:2024,
                 value = c(50,52,53,55,57,58,60,61,63,65,68,70,72,75,78))
ggplot(df, aes(year, value)) +
  geom_area(fill = "#047B77", alpha = 0.3) +
  geom_line(color = "#047B77", linewidth = 1.5) +
  labs(title = "Area chart", x = "Year", y = "Value") + theme_minimal()
library(ggplot2); library(tidyr)
df <- data.frame(year = 2010:2024,
                 Solar = c(2,3,5,7,9,11,14,17,21,25,29,33,38,42,46),
                 Wind  = c(8,9,11,12,14,16,18,20,22,25,28,30,33,35,38),
                 Hydro = c(20,21,21,22,22,23,23,24,24,25,25,26,26,27,27))
long <- pivot_longer(df, -year, names_to = "source", values_to = "twh")

ggplot(long, aes(year, twh, fill = source)) +
  geom_area(alpha = 0.92, color = "white") +
  scale_fill_manual(values = c(Solar = "#047B77", Wind = "#1A9490", Hydro = "#F59E0B")) +
  labs(title = "Stacked area", x = "Year", y = "Generation (TWh)") + theme_minimal()

Slope chart

  • Two time points, several series, the slope of each line is the rate of change.
    • Use, before / after across 3 to 10 categories (KPIs, sub-regions, indicators).
  • Avoid when: More than 10 lines cross, switch to dumbbell or small multiples.

library(ggplot2); library(dplyr); library(tidyr)
df <- data.frame(region = c("East","West","Middle","North","South"),
                 y2018 = c(24,18,20,32,28),
                 y2024 = c(38,27,30,45,36))
long <- df |> pivot_longer(-region, names_to = "year", values_to = "value") |>
  mutate(x = if_else(year == "y2018", 0, 1))

ggplot(long, aes(x, value, group = region, color = region)) +
  geom_line(linewidth = 1.6) +
  geom_point(size = 4, fill = "white", shape = 21, stroke = 2) +
  geom_text(data = filter(long, x == 0),
            aes(label = paste0(region, ": ", value, "%")),
            hjust = 1, nudge_x = -0.04, fontface = "bold") +
  geom_text(data = filter(long, x == 1),
            aes(label = paste0(region, ": ", value, "%")),
            hjust = 0, nudge_x = 0.04, fontface = "bold") +
  scale_x_continuous(breaks = c(0,1), labels = c("2018","2024"),
                     limits = c(-0.6, 1.6)) +
  labs(title = "Slope chart", y = "Value (%)") +
  theme_minimal() + theme(legend.position = "none")

Dumbbell chart

  • Show distance and direction of change between two states for several categories. Both endpoints and the change are visible at once.

  • Can be used for any from-to comparison across categories.

library(ggplot2)
df <- data.frame(region = factor(c("East","West","Middle","North","South"),
                                 levels = c("East","West","Middle","North","South")),
                 y2018 = c(24,18,20,32,28),
                 y2024 = c(38,27,30,45,36))
df$delta <- df$y2024 - df$y2018
df$mid   <- (df$y2018 + df$y2024) / 2

ggplot(df, aes(y = region)) +
  geom_segment(aes(x = y2018, xend = y2024, yend = region),
               color = "#94a3b8", linewidth = 2, alpha = 0.5) +
  geom_point(aes(x = y2018, color = "2018"), size = 6) +
  geom_point(aes(x = y2024, color = "2024"), size = 6.5) +
  geom_label(aes(x = mid, label = paste0("+", delta, " pp")),
             vjust = -0.9, color = "#1A9490", fontface = "bold", fill = "white") +
  scale_color_manual(values = c("2018" = "#94a3b8", "2024" = "#047B77")) +
  scale_y_discrete(limits = rev(levels(df$region))) +
  labs(title = "Dumbbell chart", x = "Value (%)", y = NULL) +
  theme_minimal()

3.2 Categorical comparison

Bar chart

  • Compare a single numeric variable across discrete categories using length on a common axis (the most accurate visual encoding). Any “compare values across categories” question.

  • Mainly used 6+ categories, long category names, ranked or sorted lists.
  • Not use it when: The x-axis is continuous (histogram), or labels are too long (use horizontal bar).
library(ggplot2)
df <- data.frame(sector = c("Health","Education","Defence","Energy","Transport","Other"),
                 share  = c(42, 38, 27, 19, 16, 14))
ggplot(df, aes(reorder(sector, -share), share)) +
  geom_col(fill = "#047B77", width = 0.7) +
  geom_text(aes(label = share), vjust = -0.4, fontface = "bold") +
  labs(title = "Bar chart", x = NULL, y = "Allocation (%)") +
  theme_minimal()
library(ggplot2)
df <- data.frame(sector = c("Health","Education","Defence","Energy","Transport","Other"),
                 share  = c(42, 38, 27, 19, 16, 14))
ggplot(df, aes(share, reorder(sector, share))) +
  geom_col(fill = "#047B77") +
  geom_text(aes(label = share), hjust = -0.2, fontface = "bold") +
  scale_x_continuous(expand = expansion(mult = c(0, 0.08))) +
  labs(title = "Horizontal bar", x = "Value", y = NULL) +
  theme_minimal()

Grouped bar chart

  • Compare values across categories and sub-groups simultaneously, by placing sub-group bars side-by-side.

  • Suitable for 4 sub-groups or fewer, 6 categories or fewer.

    • But to compare totals, use stacked bar instead.

library(ggplot2); library(tidyr)
df <- data.frame(year = factor(c("2018","2021","2024")),
                 A = c(35,42,48), B = c(28,33,39), C = c(21,25,30))
long <- pivot_longer(df, -year, names_to = "region", values_to = "value")

ggplot(long, aes(year, value, fill = region)) +
  geom_col(position = position_dodge(0.7), width = 0.6) +
  scale_fill_manual(values = c(A = "#047B77", B = "#1A9490", C = "#F59E0B")) +
  labs(title = "Grouped bar") + theme_minimal()

Stacked bar chart

  • Show the total for each group and the breakdown of that total into components.

  • Good when totals matter and parts sum to something meaningful.

  • Normalise each bar to 100% so the reader compares proportions, not totals.
library(ggplot2); library(tidyr)
df <- data.frame(year = factor(c("2018","2021","2024")),
                 A = c(35,42,48), B = c(28,33,39), C = c(21,25,30))
long <- pivot_longer(df, -year, names_to = "region", values_to = "value")

ggplot(long, aes(year, value, fill = region)) +
  geom_col() +
  scale_fill_manual(values = c(A = "#047B77", B = "#1A9490", C = "#F59E0B")) +
  labs(title = "Stacked bar") + theme_minimal()
library(ggplot2); library(dplyr); library(tidyr)
df <- data.frame(year = factor(c("2018","2021","2024")),
                 A = c(35,42,48), B = c(28,33,39), C = c(21,25,30))
long <- pivot_longer(df, -year, names_to = "region", values_to = "value") |>
  group_by(year) |> mutate(share = value / sum(value) * 100)

ggplot(long, aes(year, share, fill = region)) +
  geom_col(position = "stack") +
  scale_fill_manual(values = c(A = "#047B77", B = "#1A9490", C = "#F59E0B")) +
  labs(title = "100% stacked bar", y = "Share (%)") + theme_minimal()

Lollipop chart Or Cleveland dot plot

  • A bar chart with the heavy fill replaced by a thin line and a dot, reduces ink and helps long lists breathe.
    • 8 to 30 categories, sorted by value.

library(ggplot2)
df <- data.frame(sector = c("Health","Education","Defence","Energy","Transport","Other"),
                 share  = c(42, 38, 27, 19, 16, 14))
df$sector <- reorder(df$sector, df$share)

ggplot(df, aes(share, sector)) +
  geom_segment(aes(x = 0, xend = share, yend = sector),
               color = "#94a3b8", linewidth = 1.4) +
  geom_point(color = "#047B77", size = 6) +
  geom_text(aes(label = share), hjust = -0.7,
            color = "#047B77", fontface = "bold") +
  labs(title = "Lollipop", x = "Value", y = NULL) +
  theme_minimal()
library(ggplot2)
df <- data.frame(country = paste("Country", LETTERS[1:6]),
                 value = c(62, 41, 58, 33, 49, 27))
df$country <- reorder(df$country, df$value)

ggplot(df, aes(value, country)) +
  geom_segment(aes(x = 0, xend = value, yend = country),
               color = "#94a3b8", alpha = 0.7) +
  geom_point(color = "#047B77", size = 5) +
  labs(title = "Cleveland dot plot", x = "Value", y = NULL) +
  theme_minimal()

Diverging bar chart

  • Encode both direction and magnitude around a meaningful zero, useful for Likert surveys, profit-loss, deviations from target.

  • When to use: Survey results (agree / disagree), signed values, year-on-year change.

  • But not usefull if the data has no natural zero.

library(ggplot2)
df <- data.frame(
  response = factor(c("Strongly disagree","Disagree","Neutral",
                      "Agree","Strongly agree"),
                    levels = c("Strongly disagree","Disagree","Neutral",
                               "Agree","Strongly agree")),
  share    = c(-12, -18, 10, 32, 28))

ggplot(df, aes(share, response, fill = share > 0)) +
  geom_col() +
  geom_vline(xintercept = 0, color = "#475569", linewidth = 0.6) +
  scale_fill_manual(values = c("TRUE" = "#047B77", "FALSE" = "#EF4444"),
                    guide = "none") +
  labs(title = "Diverging bar", x = "Share (%)", y = NULL) +
  theme_minimal()

Pareto chart

  • Bars (descending) combined with a cumulative line, shows the “vital few” categories that account for 80% of the value.

library(ggplot2); library(dplyr)
df <- data.frame(cat = LETTERS[1:8],
                 v   = c(20, 18, 12, 9, 7, 5, 3, 2)) |>
  arrange(desc(v)) |>
  mutate(cum_pct = cumsum(v) / sum(v) * 100)

ggplot(df, aes(reorder(cat, -v))) +
  geom_col(aes(y = v), fill = "#047B77") +
  geom_line(aes(y = cum_pct, group = 1), color = "#F59E0B", linewidth = 1.2) +
  geom_point(aes(y = cum_pct), color = "#F59E0B", size = 3) +
  geom_hline(yintercept = 80, linetype = "dashed", color = "#475569") +
  scale_y_continuous(sec.axis = sec_axis(~ ., name = "Cumulative %")) +
  labs(title = "Pareto chart", x = "Category", y = "Frequency") +
  theme_minimal()

3.3 Distribution

Histogram

  • Show the shape of one numeric variable, where mass sits, spread, skew, outliers, multi-modality.

  • A smoothed version of the histogram (KDE). Easier to overlay multiple groups.
  • Used for comparing the shapes of 2 or 3 distributions on the same axis.

library(ggplot2)
set.seed(7); df <- data.frame(height = rnorm(500, 170, 9))

ggplot(df, aes(height)) +
  geom_histogram(bins = 22, fill = "#047B77", color = "white") +
  geom_vline(xintercept = mean(df$height), color = "#F59E0B",
             linewidth = 1.1, linetype = "dashed") +
  labs(title = "Histogram", x = "Height (cm)", y = "Frequency") +
  theme_minimal()
library(ggplot2)
set.seed(7)
df <- rbind(data.frame(group = "A", x = rnorm(500, 170, 9)),
            data.frame(group = "B", x = rnorm(500, 178, 7)))

ggplot(df, aes(x, fill = group, color = group)) +
  geom_density(alpha = 0.35, linewidth = 1.2) +
  scale_fill_manual(values  = c(A = "#047B77", B = "#1A9490")) +
  scale_color_manual(values = c(A = "#047B77", B = "#1A9490")) +
  labs(title = "Density plot", x = "Height (cm)", y = "Density") +
  theme_minimal()

Box plot

  • Compact summary of a distribution: median, inter-quartile box, whiskers, individual outliers.

  • Comparing distributions across many groups, spotting outliers.

  • A box plot plus a mirrored density, reveals bimodality and skew that a box plot hides.

  • Used when non-Gaussian distributions, multi-modal data.

library(ggplot2)
set.seed(7)
df <- rbind(
  data.frame(group = "A", x = rnorm(120, 50, 10)),
  data.frame(group = "B", x = rnorm(120, 55, 8 )),
  data.frame(group = "C", x = rnorm(120, 48, 14)),
  data.frame(group = "D", x = rnorm(120, 60, 7 )))

ggplot(df, aes(group, x)) +
  geom_boxplot(fill = "#CDEAE8", color = "#047B77",
               outlier.color = "#1A9490", width = 0.55) +
  labs(title = "Box plot", x = "Group", y = "Score") +
  theme_minimal()
library(ggplot2)
set.seed(7)
df <- rbind(
  data.frame(group = "A", x = rnorm(120, 50, 10)),
  data.frame(group = "B", x = rnorm(120, 55, 8 )),
  data.frame(group = "C", x = rnorm(120, 48, 14)),
  data.frame(group = "D", x = rnorm(120, 60, 7 )))

ggplot(df, aes(group, x)) +
  geom_violin(fill = "#CDEAE8", color = "#047B77") +
  geom_boxplot(width = 0.12, fill = "white", color = "#047B77") +
  labs(title = "Violin plot", x = "Group", y = "Score") +
  theme_minimal()

QQ plot

  • Compare a sample’s quantiles to a theoretical distribution (usually normal). Points on the diagonal = good fit.

  • Used for checking the normality assumption before applying a parametric test.

library(ggplot2)
set.seed(7); df <- data.frame(x = rnorm(200) + 0.4 * rnorm(200)^3)

ggplot(df, aes(sample = x)) +
  stat_qq(color = "#047B77") +
  stat_qq_line(color = "#F59E0B", linewidth = 1.2) +
  labs(title = "QQ plot",
       x = "Theoretical quantiles", y = "Sample quantiles") +
  theme_minimal()

3.4 Relationship and correlation

Scatter plot

  • Plot each observation as a point on two numeric axes, to see the relationship: linear, non-linear, no relationship, clusters, outliers.

  • Used when your first question “do X and Y move together?” or “are there clusters?”.

library(ggplot2)
set.seed(7)
df <- data.frame(x = rnorm(200, 50, 10))
df$y <- 0.55 * df$x + rnorm(200, 0, 5) + 10

ggplot(df, aes(x, y)) +
  geom_point(color = "#047B77", alpha = 0.55, size = 2.5) +
  geom_smooth(method = "lm", color = "#F59E0B", se = FALSE, linewidth = 1.1) +
  labs(title = "Scatter plot", x = "Variable X", y = "Variable Y") +
  theme_minimal()

Correlation heat map

  • Show the correlation matrix of many numeric variables at once. Diverging palette (red to blue) makes positive vs negative correlations pop.

library(ggplot2); library(reshape2)
m <- cor(mtcars)
df <- melt(m); names(df) <- c("v1","v2","cor")

ggplot(df, aes(v1, v2, fill = cor)) +
  geom_tile(color = "white") +
  geom_text(aes(label = sprintf("%.2f", cor)), size = 3) +
  scale_fill_gradient2(low = "#EF4444", mid = "white", high = "#047B77",
                       midpoint = 0, limits = c(-1, 1)) +
  labs(title = "Correlation heat map") + theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

Pair plot (scatterplot matrix)

  • A matrix of pairwise scatter plots between every pair of numeric variables, with histograms on the diagonal.

# install.packages("GGally")
library(GGally)
ggpairs(iris, columns = 1:4, aes(color = Species, alpha = 0.6)) +
  ggplot2::labs(title = "Pair plot")

3.5 Composition, part-to-whole

Pie chart

  • Show how a single whole is split into parts.

  • When to use: 5 categories or fewer, parts sum to 100%, audience expects a pie.

  • A pie chart with the centre removed. The hole can carry a total or a KPI.

  • Same rules as a pie, plus the centre is useful for a total or label.

library(ggplot2)
df <- data.frame(region = c("North","South","East","West"),
                 share  = c(42, 25, 18, 15))

ggplot(df, aes(x = "", y = share, fill = region)) +
  geom_col(width = 1, color = "white") + coord_polar(theta = "y") +
  scale_fill_manual(values = c("#047B77","#1A9490","#0EA5E9","#F59E0B")) +
  theme_void() + labs(title = "Pie chart")
library(ggplot2)
df <- data.frame(region = c("North","South","East","West"),
                 share  = c(42, 25, 18, 15))

ggplot(df, aes(x = 2, y = share, fill = region)) +
  geom_col(width = 1, color = "white") + coord_polar(theta = "y") +
  xlim(0.2, 2.5) +
  scale_fill_manual(values = c("#047B77","#1A9490","#0EA5E9","#F59E0B")) +
  theme_void() + labs(title = "Donut chart")

Practical: choose

Out of the 60+ chart types in this deck, the following short list covers the vast majority of operational reporting:

  • Line, multi-line, area (indicator trends)
  • Bar, stacked bar (compare regions, partners, sectors)
  • Lollipop (sorted indicator rankings)
  • Histogram, box plot (data-quality checks on survey data)
  • Scatter plot (correlations, regression diagnostics)
  • Choropleth map (geographic targeting and coverage)
  • Treemap (composition of activities, funds, beneficiaries)
  • Tables and small multiples (precise comparisons, faceted by category)

Rule of thumb for operational reporting: every chart in the report should answer one explicit M&E question. If you cannot write that question, drop the chart.

Common pitfalls

Twelve mistakes to avoid

  1. Truncated y-axis on bar charts, inflates tiny differences.
  2. Dual y-axes, almost always misleading. Use two charts or index to 100.
  3. 3-D bars or 3-D pies, distort perception and add no information.
  4. Rainbow palettes for ordered data, use sequential or diverging.
  5. More than 7 categorical colours, readers cannot track them.
  6. Tiny multiples that lose their axes, keep the gridlines.
  7. No source line, invisible cost to credibility.
  8. No units, “12.4” is meaningless without context.
  9. Decorative titles, “Revenue trends” loses to “Revenue up 12% in Q4”.
  10. Pie chart with 9 slices, switch to a sorted bar.
  11. Connecting unordered categories with a line, visual misinformation.
  12. Showing every point when the message is the trend, simplify and annotate.

Chart-selection cheat sheet

Question Best chart
How has X changed over time? Line, area, sparkline
Compare categories? Bar, lollipop, dot plot
Distribution? Histogram, density, ECDF
Distribution by group? Box plot, violin, ridgeline
Relationship? Scatter, hexbin, regression
Two time points? Dumbbell, slope
Composition? Bar > treemap > waffle > pie
Hierarchy? Treemap, sunburst
Geographic value? Choropleth, symbol map
Flow? Sankey, alluvial, flow map, chord
KPI vs target? Bullet, KPI card
Conversion stages? Funnel
Project schedule? Gantt
Process monitoring? Control chart
Precise values? Table, highlight table

One-slide take-aways

What to remember

  1. Start with the message, not the chart. Write down what you want the reader to see, then choose.
  2. Use position and length before colour and area for important comparisons.
  3. For two time points and many categories, the dumbbell wins. For one continuous series, a line.
  4. Box plots hide bimodality, violins and ridgelines reveal it.
  5. Heat maps need numbers in cells when precise reading matters.
  6. Bars beat pies, treemaps beat pies with many parts, waffles beat pies for “X out of 100”.
  7. For monitoring dashboards, bullet charts and KPI cards beat gauges.
  8. Tables are visualisations too. When precision matters, a well-designed table wins.
  9. Titles carry the message, axes carry the units, the source line carries the credibility.
  10. When in doubt, simplify. A clean chart that asks one question always beats a busy one that asks five.