Skip to content
advanced Phase 14 · Visualization Principles

Auditing Misleading Charts

Catch truncated axes, cherry-picked ranges, and dual-axis tricks before publishing.

45m
0 problems
Topic Progress 0%

Truncated Axes: The Most Common Deception

Truncated Axes: The Most Common Deception

A truncated axis does not start at zero. In a bar chart, this is always misleading because bar length encodes value—when the baseline is not zero, the relative lengths of bars no longer represent the relative magnitudes of the values. A bar that is twice as tall as another no longer represents a value that is twice as large.

The most egregious truncated axes appear in bar charts. Consider two products with revenues of $100K and $105K. On a zero-based axis, the bars are nearly the same height—the 5% difference is barely visible. On an axis truncated to $90K, the second bar appears twice as tall as the first, suggesting a 100% difference. The data is identical; the visual impression is wildly different.

import matplotlib.pyplot as plt
import numpy as np

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))

products = ['Product A', 'Product B']
revenue = [100, 105]

ax1.bar(products, revenue, color='#2563eb', width=0.5)
ax1.set_ylim(0, 120)
ax1.set_title('Zero-Based Axis (Honest)')
ax1.set_ylabel('Revenue (thousands USD)')
for i, v in enumerate(revenue):
    ax1.text(i, v + 2, f'${v}K', ha='center', fontsize=11)

ax2.bar(products, revenue, color='#dc2626', width=0.5)
ax2.set_ylim(90, 110)
ax2.set_title('Truncated Axis (Misleading)')
ax2.set_ylabel('Revenue (thousands USD)')
for i, v in enumerate(revenue):
    ax2.text(i, v + 1, f'${v}K', ha='center', fontsize=11)

plt.tight_layout()
plt.show()

Line charts are the exception where truncation is acceptable. Because line charts encode value through position (not length), the viewer can read exact values from the y-axis. Truncating the y-axis in a line chart makes small changes visible without distorting the data—the position of each point is still accurate relative to the axis scale. However, you must clearly label the axis so the viewer knows it does not start at zero.

The audit question for any bar chart is simple: does the y-axis start at zero? If not, the chart is misleading by default. The burden of proof is on the chart creator to justify why truncation is necessary and to ensure the viewer is not deceived. In practice, this means bar charts should always use a zero baseline, and line charts should label their axis range clearly.

Dual-Axis Charts: Correlation Without Causation

Dual-Axis Charts: Correlation Without Causation

A dual-axis chart plots two variables on the same x-axis but with separate y-axes—one on the left, one on the right. The two variables are visually overlaid, which invites the viewer to interpret their relationship as causal. This is the chart type most frequently used to manufacture spurious correlations.

The fundamental problem is that the scales are independent. You can stretch or compress either axis to make the two lines align as closely as you want. If you want to show that ice cream sales cause drowning incidents, you can adjust the scales so the two lines track each other. The chart looks convincing, but the visual alignment is an artifact of scale choice, not evidence of a relationship.

import matplotlib.pyplot as plt
import numpy as np

fig, ax1 = plt.subplots(figsize=(10, 5))

months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
revenue = [100, 120, 115, 140, 160, 155]
marketing_spend = [20, 25, 22, 30, 35, 32]

color1 = '#2563eb'
ax1.set_xlabel('Month')
ax1.set_ylabel('Revenue (K USD)', color=color1)
line1 = ax1.plot(months, revenue, color=color1, marker='o', linewidth=2, label='Revenue')
ax1.tick_params(axis='y', labelcolor=color1)

ax2 = ax1.twinx()
color2 = '#dc2626'
ax2.set_ylabel('Marketing Spend (K USD)', color=color2)
line2 = ax2.plot(months, marketing_spend, color=color2, marker='s', linewidth=2, label='Marketing')
ax2.tick_params(axis='y', labelcolor=color2)

lines = line1 + line2
labels = [l.get_label() for l in lines]
ax1.legend(lines, labels, frameon=False, loc='upper left')
ax1.set_title('Revenue vs. Marketing Spend (Dual Axis)')
ax1.spines['top'].set_visible(False)
plt.tight_layout()
plt.show()

The honest alternative is to normalize both variables to a common scale. Plot the percentage change from a baseline period, or index both series to 100 at the start. This removes the scale manipulation problem entirely—both lines share the same axis, and their relationship (or lack thereof) is visible without any adjustment.

If you must show two variables with different units, use two separate charts stacked vertically with aligned x-axes. The viewer can still compare the patterns, but the visual alignment that implies causation is broken. Each chart has its own scale, and the viewer must consciously compare rather than passively perceive a manufactured correlation.

The audit question for dual-axis charts is: what is the correlation coefficient between the two variables? If the chart creator cannot provide this number, the chart is decorative, not analytical. A correlation coefficient quantifies the relationship; a dual-axis chart merely suggests one visually.

Cherry-Picked Ranges and Selective Data

Cherry-Picked Ranges and Selective Data

Cherry-picking is the practice of selecting a time range, subset of categories, or aggregation level that supports a predetermined conclusion while hiding the broader context. The chart is technically accurate—the data it shows is real—but the selection is manipulative.

The most common form is the cherry-picked time range. A company's stock price might be up 15% over the past month, but down 30% over the past year. A chart showing only the past month tells a story of growth; a chart showing the past year tells a story of decline. Both charts display real data. Neither tells the full story.

import matplotlib.pyplot as plt
import numpy as np

np.random.seed(42)
full_year = np.random.normal(100, 15, 12).cumsum() + 1000
cherry_picked = full_year[9:]  # Last 3 months only

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))

months_full = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
ax1.plot(months_full, full_year, marker='o', color='#2563eb', linewidth=2)
ax1.set_title('Full Year View')
ax1.set_ylabel('Stock Price ($)')
ax1.spines['top'].set_visible(False)
ax1.spines['right'].set_visible(False)

months_short = ['Oct', 'Nov', 'Dec']
ax2.plot(months_short, cherry_picked, marker='o', color='#16a34a', linewidth=2)
ax2.set_title('Cherry-Picked: Last 3 Months Only')
ax2.set_ylabel('Stock Price ($)')
ax2.spines['top'].set_visible(False)
ax2.spines['right'].set_visible(False)

plt.tight_layout()
plt.show()

The second form is category cherry-picking. A company might show revenue growth for its three fastest-growing product lines while omitting the five declining lines. The chart is accurate for the selected categories, but it misrepresents the overall business performance.

The third form is aggregation cherry-picking. Showing average income instead of median income in a region with high income inequality makes the population look wealthier than it is. Showing total revenue instead of revenue per customer makes growth look better when customer acquisition costs are rising.

The audit framework for cherry-picking asks three questions: What time range is shown, and what is hidden? What categories are included, and what is excluded? What aggregation is used, and what alternative would tell a different story? If the chart creator cannot answer these questions, the chart is suspect.

Other Manipulation Techniques and the Audit Framework

Other Manipulation Techniques and the Audit Framework

Beyond truncated axes, dual-axis charts, and cherry-picking, several other techniques distort data visualization. The 3D chart is the most notorious: adding a third dimension to a bar or pie chart makes the front elements appear larger than the back elements due to perspective, introducing a systematic bias that has nothing to do with the data. 3D charts should never be used for quantitative comparison.

The inverted axis is a subtle manipulation where the y-axis increases downward, making a declining trend appear to rise. This is sometimes done accidentally by sorting categories in descending order without adjusting the axis. The audit check is simple: verify that the axis direction matches the expected direction (higher values at the top).

Area-based encodings are frequently manipulated. When a bubble chart uses radius to encode value, the area of the bubble grows with the square of the radius, making differences appear larger than they are. A bubble with twice the radius has four times the area, but the viewer perceives only a doubling. The honest approach is to scale the area (not the radius) proportionally to the value.

import matplotlib.pyplot as plt
import numpy as np

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))

categories = ['A', 'B', 'C', 'D']
values = [10, 20, 30, 40]

ax1.bar(categories, values, color='#2563eb')
ax1.set_title('Honest: Bar Length Encodes Value')
ax1.set_ylabel('Value')
ax1.set_ylim(0, 50)

sizes = [v**2 * 10 for v in values]
ax2.scatter(range(len(categories)), [1]*4, s=sizes, color='#dc2626', alpha=0.6)
ax2.set_xticks(range(len(categories)))
ax2.set_xticklabels(categories)
ax2.set_title('Misleading: Radius Encodes Value (Area Distorted)')
ax2.set_yticks([])
ax2.set_ylim(0.5, 1.5)

plt.tight_layout()
plt.show()

The systematic audit framework for any chart consists of five checks. First, check the axis: does it start at zero (for bar charts)? Is the range appropriate? Second, check the encoding: does the visual channel match the data type? Are area encodings scaled correctly? Third, check the range: is the time period or category selection representative? Fourth, check the aggregation: is the summary statistic (mean, median, total) appropriate for the question? Fifth, check the context: are there comparison baselines, reference lines, or benchmarks that help the viewer interpret the absolute values?

A chart that passes all five checks is not necessarily truthful—it could still be based on flawed data—but it is visually honest. A chart that fails any check requires justification from the creator. The absence of justification is a red flag.

Practice Problems

0 / 2 solved
Fix a Truncated Bar Chart

You encounter a bar chart comparing customer satisfaction scores across four teams. The y-axis ranges from 7.0 to 9.0, making small differences look dramatic. Rewrite the chart to be visually honest.

Solution
import matplotlib.pyplot as plt

teams = ['Alpha', 'Beta', 'Gamma', 'Delta']
scores = [8.2, 8.5, 7.9, 8.7]

fig, ax = plt.subplots(figsize=(8, 5))
bars = ax.bar(teams, scores, color='#2563eb')
ax.set_ylim(0, 10)
ax.set_ylabel('Satisfaction Score (0-10)')
ax.set_title('Customer Satisfaction by Team')
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)

for bar, score in zip(bars, scores):
    ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.15,
            f'{score}', ha='center', fontsize=11, fontweight='bold')

plt.tight_layout()
plt.show()
Identify the Manipulation

A chart shows two lines on dual axes: 'Revenue' on the left axis (ranging $0-$500K) and 'Marketing Spend' on the right axis (ranging $0-$50K). The lines track each other closely. What is the manipulation risk and how would you present this data honestly?

Solution
import matplotlib.pyplot as plt
import numpy as np

months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
revenue = [100, 120, 115, 140, 160, 155]
marketing = [10, 12, 11, 14, 16, 15]

rev_index = [r / revenue[0] * 100 for r in revenue]
mkt_index = [m / marketing[0] * 100 for m in marketing]

fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(months, rev_index, marker='o', color='#2563eb', linewidth=2, label='Revenue (indexed)')
ax.plot(months, mkt_index, marker='s', color='#dc2626', linewidth=2, label='Marketing (indexed)')
ax.set_ylabel('Index (Jan = 100)')
ax.set_title('Revenue vs. Marketing: Indexed Comparison')
ax.legend(frameon=False)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
plt.tight_layout()
plt.show()

correlation = np.corrcoef(revenue, marketing)[0, 1]
print(f'Correlation coefficient: {correlation:.3f}')

Quiz

1. Why is a truncated y-axis always misleading in a bar chart?

Question 1 options

2. What is the primary problem with dual-axis charts?

Question 2 options

3. What are the three audit questions for detecting cherry-picked data?

Question 3 options

Flashcards

Question

Why must bar chart y-axes start at zero?

Answer

Bar length encodes value. A non-zero baseline distorts relative lengths, making small differences appear large. A bar twice as tall should represent a value twice as large—this only holds when the baseline is zero.

Question

What is the honest alternative to a dual-axis chart?

Answer

Index both variables to a common baseline (e.g., 100 at the start period) and plot them on the same axis. This eliminates scale manipulation. Alternatively, use two stacked charts with aligned x-axes but separate y-axes.

Question

What are the three forms of cherry-picked data?

Answer

1) Time range cherry-picking: showing a short period that supports the conclusion while hiding the longer trend. 2) Category cherry-picking: showing only the categories that support the conclusion. 3) Aggregation cherry-picking: using a summary statistic (mean instead of median, total instead of per-unit) that flatters the narrative.

Question

What are the five checks in the chart audit framework?

Answer

1) Axis: does it start at zero (for bars)? 2) Encoding: does the visual channel match the data type? 3) Range: is the selection representative? 4) Aggregation: is the summary statistic appropriate? 5) Context: are there baselines or benchmarks for interpretation?

Revision Notes

Key Takeaways

  • 1. Bar chart y-axes must start at zero—truncation always distorts magnitude perception
  • 2. Dual-axis charts allow scale manipulation that manufactures spurious correlations
  • 3. Cherry-picking selects data that supports a conclusion while hiding contradictory context
  • 4. The five-check audit framework: axis, encoding, range, aggregation, context
  • 5. Honest alternatives exist for every misleading technique—use them proactively

Interview Tips

  • • When shown a chart, immediately check the axis baseline and range
  • • Ask for the correlation coefficient when a chart implies a relationship
  • • Suggest honest alternatives (indexing, stacking, full-range display) rather than just identifying problems
  • • Demonstrate that you understand why each technique misleads, not just that it does

Cheat Sheet

Cheat Sheet: Auditing Misleading Charts

Manipulation Techniques

  • Truncated axis: y-axis does not start at zero (always misleading for bar charts)
  • Dual-axis: independent scales allow manufactured correlation
  • Cherry-picked range: selective time period or category subset
  • 3D charts: perspective distorts bar/pie sizes
  • Inverted axis: higher values at the bottom
  • Area encoding: radius scaling makes area grow quadratically

Audit Framework (5 Checks)

  1. Axis: zero-based for bars? Appropriate range?
  2. Encoding: visual channel matches data type?
  3. Range: representative selection?
  4. Aggregation: appropriate summary statistic?
  5. Context: baselines and benchmarks present?

Honest Alternatives

  • Bar chart: always zero-based y-axis
  • Dual-axis: index to common baseline or use stacked charts
  • Cherry-picked: show full range with the selected period highlighted
  • Area encoding: scale area (not radius) proportionally to value

Red Flags

  • No axis labels or units
  • No data source cited
  • Correlation implied without a coefficient
  • 3D or decorative elements that obscure data

Continue Learning

Before You Start

Related Reading