Visualizations#

Matplotlib settings:#

plt.rcParams controls the default styles and behaviors of plots. There are a lot of options.

The common ones are:

  • plt.rcParams[‘figure.figsize’]: Default size of the figure in inches.

  • plt.rcParams[‘figure.dpi’]: Resolution of the figure in dots per inch

  • plt.rcParams[‘axes.titlesize’]

  • plt.rcParams[‘axes.labelsize’]

  • plt.rcParams[‘font.size’]

  • plt.rcParams[‘font.family’]

  • plt.rcParams[‘lines.linewidth’]

  • plt.rcParams[‘legend.loc’]

Reset rcParams to default with: plt.rcdefaults()

import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

import warnings
warnings.filterwarnings('ignore')

plt.rcParams['figure.figsize'] = (8, 4)

rng = np.random.default_rng(42)

# Adding these here since I always use them and still forget the syntax.
plt.axvline(x=0, color='gray', linestyle='--', linewidth=1)
plt.axhline(y=0, color='gray', linestyle='--', linewidth=1)

plt.scatter(np.arange(50), rng.random(50), color='tab:blue')
plt.title('Globally setting plt.rcParams["figure.figsize"]', fontsize=16)
plt.show()
../_images/75f58d778d9453bf4261b33c951cda888e11b647f679432a672cfc0bdcffe56d.png
with plt.rc_context({'figure.figsize': [4, 2], 'axes.titlesize': 10}):
    plt.scatter(np.arange(50), rng.random(50), color='tab:blue')
    plt.title('Temporarily setting plt.rcParams')
    plt.show()
../_images/9a6cbe8289f3fd517e6fd49f8cc084fc4d8afc92420e7829f9a400832fd099e8.png

Matplotlib has a list of named colors here

Coolors is also a great website for picking color palletes

plt.scatter(np.arange(50), rng.random(50), label='color 1')
plt.scatter(np.arange(50), rng.random(50), label='color 2')
plt.scatter(np.arange(50), rng.random(50), label='color 3')
plt.title('Default Color Cycles')
plt.legend()
plt.show()
../_images/8c0392bd0e826d3604ca41635e49019178209edb7e48aa5c0cb26fc70a2b238a.png
from cycler import cycler

new_color_cycle = cycler(color=['#929591', '#bf77f6', '#a2cffe'])

with plt.rc_context({'axes.prop_cycle': new_color_cycle}):
    plt.scatter(np.arange(50), rng.random(50), label='color 1')
    plt.scatter(np.arange(50), rng.random(50), label='color 2')
    plt.scatter(np.arange(50), rng.random(50), label='color 3')
    plt.title('Overriding Color Cycles')
    plt.legend()
    plt.show()
../_images/4636a55b9cee18082b7b8540ec0810b73e1c547729408473776c9f0dd634368d.png

Subplots#

When you create a grid of subplots using plt.subplots(rows, cols), Matplotlib returns an array of Axes objects that can be accessed with axs[row, column].

rows = 2
columns = 3
fig, axs = plt.subplots(rows, columns)

for row in range(rows):
    for col in range(columns):
        axs[row, col].text(0.5, 0.5, f"ax[{row}, {col}]", va="center", ha="center")
        axs[row, col].tick_params(labelbottom=False, labelleft=False)
        axs[row, col].set_title(f"Subplot {row*columns+col}")

fig.suptitle("Main Title for the Figure")
plt.tight_layout()
../_images/a64de2a45b205ceae6d764442498302576ea16d70d4c77460e8245675eb4722c.png

sharex=True allows the subplots to use the same x-axis.

ax.set() can be used to set multiple properties at once.

fig, axs = plt.subplots(2, 1, figsize=(8, 4), sharex=True)

for ax in axs:
    ax.scatter(np.arange(50), rng.random(50))
    ax.set(xlim=(0, 50), ylim=(-1, 2), xlabel='x', ylabel='y', title='')
../_images/3fba04fc3fdb55c635a2261a0e2c5dd50e2edb5febe50d172463087594bfa3a3.png

plt versus ax:#

  • plt.xlabel() → ax.set_xlabel()

  • plt.ylabel() → ax.set_ylabel()

  • plt.xlim() → ax.set_xlim()

  • plt.ylim() → ax.set_ylim()

  • plt.title() → ax.set_title()

Gestalt’s Six Principles of Visual Perception#

Define how people interact with and create order out of visual stimuli.

Perceptual grouping#

Proximity#

We perceive objects that are physically close together as belonging to part of a group.

group_size = 5
group0 = rng.integers(low=0, high=5, size=(group_size,2))
group1 = rng.integers(low=[[10], [5]], high=[[15], [10]], size=(2, group_size)).T
group2 = rng.integers(low=[[0], [10]], high=[[5], [15]], size=(2, group_size)).T
df = pd.DataFrame(np.concatenate([group0, group1, group2]), columns=['x', 'y'])
df['group'] = df.index // group_size

sns.scatterplot(data=df, x='x', y='y', s=300)
plt.show()
../_images/2431f745120f17413b75132c97c506578ca388922d1152ceb26b1e9cf85e8f87.png
group_size = 5
group0 = rng.normal(loc=0, scale=2.0, size=(group_size, 2))
group1 = rng.normal(loc=10, scale=2.0, size=(2, group_size)).T
group2 = rng.normal(loc=10, scale=2.0, size=(2, group_size)).T
df = pd.DataFrame(np.concatenate([group0, group1, group2]), columns=['x', 'y'])
df['group'] = df.index // group_size
df.loc[df.group==1, 'x'] -= 20

sns.scatterplot(data=df, x='x', y='y', s=300)
plt.show()
../_images/a6a44116d3c5f2981aa294141e747e5848c58c06315a965782f26cb06af3048e.png

Similarity#

Objects that are of similar color, shape, size, or orientation are perceived as related or belonging to part of a group.

sns.scatterplot(data=df, x='x', y='y', hue="group", style="group", s=300)
plt.show()
../_images/9dc2608c26067f8c1b808cacbd1e55b8d08f34bcfec613eadbbb6f1467b0ffac.png

Enclosure#

objects that are physically enclosed together as belonging to part of a group.

x_min = df.loc[df.group==0, 'x'].min() - 0.5
x_max = df.loc[df.group==0, 'x'].max() + 0.5
y_min = df.loc[df.group==0, 'y'].min() - 0.5
y_max = df.loc[df.group==0, 'y'].max() + 0.5
highlight = plt.Rectangle((x_min, y_min), 
                          x_max-x_min, y_max-y_min, 
                          color='orange', alpha=0.3)
plt.gca().add_patch(highlight)
sns.scatterplot(data=df, x='x', y='y', s=300)
plt.show()
../_images/d2005f88e9fc3faf561d43829984aed8468ef5bc03395bdd9959aede565a696c.png

Closure#

People like things to be simple and to fit in the constructs that are already in our heads

edge_points = 20
left = rng.integers(low=[[0], [0]], high=[[1], [10]], size=(2, edge_points)).T
right = rng.integers(low=[[9], [0]], high=[[10], [10]], size=(2, edge_points)).T
top = rng.integers(low=[[0], [9]], high=[[10], [10]], size=(2, edge_points)).T
bottom = rng.integers(low=[[0], [0]], high=[[10], [1]], size=(2, edge_points)).T
df = pd.DataFrame(np.concatenate([left, right, top, bottom]), columns=['x', 'y'])

sns.scatterplot(data=df, x='x', y='y', s=300)
plt.show()
../_images/f5a6677b5c323fca5bec8cbe37af824475a4990f7eb404dc80c905555b6181ff.png

Continuity#

Our eyes seek the smoothest path and naturally create continuity in what we see even where it may not explicitly exist

df = pd.DataFrame({
    'y': np.concatenate((np.sin(2*np.pi*np.linspace(0, 1)), np.cos(2*np.pi*np.linspace(0, 1)))),
    'x': np.concatenate((np.linspace(0, np.pi), np.linspace(0, np.pi)))
})

sns.scatterplot(data=df, x='x', y='y', s=100)
plt.show()
../_images/6bce0eb84d69be4288fe5f2fec7f2d267d57199bfc3b739c68d72557da0f4744.png

Connection#

objects that are physically connected are thought of as part of a group

x = np.linspace(0, 10, 100)
y1 = np.sin(x)
y2 = np.cos(x)
df = pd.DataFrame({'x': x, 'y1': y1, 'y2': y2})
sns.lineplot(data=df, x='x', y='y1', linewidth=3, markers=True, dashes=False, errorbar=None)
sns.lineplot(data=df, x='x', y='y2', linewidth=3, markers=True, dashes=False, errorbar=None)
plt.show()
../_images/6ab6ae46a47bf2a3f55d52a2771b799e3417f451866811213fb136d465057fa4.png

Plot examples#

Tip

A lot of these examples come from Cole Nussbaumer Knaflic’s great book: Storytelling with Data: A Data Visualization Guide for Business Professionals

companies = ['Company D', 'Our Company', 'Company A', 'Company C', 'Company F', 'Company E', 'All Others']
market_share = [40, 25, 18, 8, 5, 4, 2]
colors = ['lightgray', 'tab:blue', 'lightgray', 'lightgray', 'lightgray', 'lightgray', 'lightgray']

plt.figure(figsize=(8, 4))
plt.barh(companies, market_share, color=colors)
plt.xlabel('Market Share (%)')
plt.title('Market Share')
plt.gca().invert_yaxis()
plt.tight_layout()
plt.show()
../_images/4619821f422507650ea94eea7dbd7a79a2d3f9388045575acab57b656da09531.png
x = rng.uniform(500, 4000, 50)
y = 3 - 2 * np.exp(-x / 1500) + rng.normal(0, 0.2, 50)  # simulated trend with noise

plt.scatter(x, y, color='lightgray', label='Data')
plt.scatter(np.mean(x), np.mean(y), color='black', s=75, label='Average')
plt.text(np.mean(x) - 500, np.mean(y) + 0.1, 'Average', color='black', fontsize=12, weight='bold',
         bbox=dict(facecolor='white', edgecolor='grey', alpha=0.75, boxstyle='round, pad=0.5'))
plt.title('Scatterplot')
plt.xlabel('x')
plt.ylabel('y')
plt.tight_layout()
plt.show()
../_images/12892a61e6ecd2e4fc583bcd18d717ae6c438ac4226eeaae770972308b080113.png
categories = ['Peers', 'Culture', 'Work environment', 'Leadership', 'Career development', 'Rewards & recognition', 'Perf management']
start_vals = [85, 80, 76, 59, 49, 41, 23]
end_vals = [91, 75, 75, 62, 39, 42, 25]
dates = [2000, 2001]
colors = ['gray', 'gray', 'gray', 'gray', 'orange', 'gray', 'gray']

fig, ax = plt.subplots(figsize=(6, 6))

for i, category in enumerate(categories):
    ax.plot(dates, [start_vals[i], end_vals[i]], marker='o', color=colors[i])
    ax.text(dates[0]-0.1, start_vals[i], f'{category} {start_vals[i]}%', va='center', ha='right', color=colors[i])
    ax.text(dates[1]+0.1, end_vals[i], f'{end_vals[i]}%', va='center', color=colors[i])

plt.title('Employee feedback over time', fontsize=14, weight='bold')
plt.xticks(dates, [str(date) for date in dates], fontsize=12)
plt.ylim(min(start_vals+end_vals)-5, max(start_vals+end_vals)+5)
ax.set_xlabel('Survey year', fontsize=12)
ax.set_ylabel('Percent favorable', fontsize=12)

# Hide the y-axis and spines for cleaner look
ax.spines['left'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
ax.get_yaxis().set_visible(False)

plt.tight_layout()
plt.show()
../_images/3f5bcdb9e0ccf3c838724dbdc51608ba77f07c15ec34127267a037268c264965.png
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
received = [180, 220, 260, 190, 160, 170, 150, 180, 200, 210, 190, 220]
processed = [v-(10*(i-4)) if i > 4 else v+rng.integers(low=-2, high=2) for i, v in enumerate(received)]

# Plot received data in gray and processed data in blue
plt.plot(months, received, color='lightgray', linewidth=3, label='Received')
plt.plot(months, processed, color='darkblue', linewidth=3, label='Processed')

# Annotate values on the plot
for i, (r, p) in enumerate(zip(received, processed)):
    plt.text(months[i], r + 5, str(r), color='gray', fontsize=10, ha='center')
    plt.text(months[i], p - 10, str(p), color='darkblue', fontsize=10, ha='center')


# Add title and labels
plt.title('Received vs Processed', fontsize=14, weight='bold')
plt.xlabel('Months')
plt.ylabel('Count')

plt.axvline(x='May', color='gray', linestyle='--', linewidth=1)
plt.text('May', 240, '  2 employees quit',
         color='gray', fontsize=10, va='top', ha='left')

plt.legend(loc='upper right')
plt.grid(True, axis='y', linestyle='--', linewidth=0.5)
plt.ylim(100, 300)
plt.tight_layout()
plt.show()
../_images/7b7968f4ae518b72381c037ceaf1c99e102bae94971e1bbbdb4430ca5c59e999.png
x = np.linspace(0, 10, 100)
y = np.sin(x)
df = pd.DataFrame({'x': x, 'y': y})

plt.plot(df['x'], df['y'].diff(), color='orange', label='y.diff()')
plt.axhline(y=0, color='gray', linestyle='--')
plt.title('Difference Over Time')
plt.ylabel('Difference from T(1) and T(0)')
plt.legend()
plt.show()
../_images/efaeaabed118389579cba216480b6c25faf64496554548418ce18dce41abdd6a.png
def golden_spiral(a=1, b=0.306349, num_points=1000):
    """
    Generate points for a golden spiral in polar coordinates and convert to Cartesian.
    
    Parameters:
    - a: scale factor
    - b: growth factor
    - num_points: number of points in the spiral
    """
    theta = np.linspace(0, 4 * np.pi, num_points)
    r = a * np.exp(b * theta)
    x = r * np.cos(theta)
    y = r * np.sin(theta)
    return x, y

x, y = golden_spiral()
fig, ax = plt.subplots(figsize=(8, 8))
ax.plot(x, y, color='gold', linewidth=200)
ax.plot(x, y, color='#a2cffe', linewidth=100)
ax.plot(x, y, color='#82cafc', linewidth=50)
ax.plot(x, y, color='#a4be5c', linewidth=5)
# ax.plot(x, y, color='gold', linewidth=200)
# ax.plot(x, y, color='gold', linewidth=200)
ax.set_aspect('equal')
ax.axis('off')
plt.title('Golden Spiral', fontsize=16)
plt.show()
../_images/bbb55d3d3488072c01244692a30a31ce9b4a34a5a0307ef57b7d4114204a6b2b.png
import random

def clamp(value, min_value=0, max_value=255):
    """Clamp the value between min_value and max_value."""
    return max(min_value, min(value, max_value))

def random_walk_color(n, step_size=30, seed=None):
    """
    Perform a random walk in RGB color space to generate n hex colors.

    Parameters:
    - n (int): Number of colors to generate.
    - step_size (int): Maximum change in each RGB channel per step.

    Returns:
    - List[str]: List of n hexadecimal color strings.
    """
    if seed is not None:
        random.seed(seed)
    print(f"seed: {seed}")

    # Initialize with a random starting color
    current_r = random.randint(0, 255)
    current_g = random.randint(0, 255)
    current_b = random.randint(0, 255)

    colors = [f'#{current_r:02X}{current_g:02X}{current_b:02X}']

    for _ in range(n - 1):
        # Random step for each channel
        step_r = random.randint(-step_size, step_size)
        step_g = random.randint(-step_size, step_size)
        step_b = random.randint(-step_size, step_size)

        # Update current color with clamping
        current_r = clamp(current_r + step_r)
        current_g = clamp(current_g + step_g)
        current_b = clamp(current_b + step_b)

        # Append the new color in hex format
        hex_color = f'#{current_r:02X}{current_g:02X}{current_b:02X}'
        colors.append(hex_color)

    return colors

num_colors = 100  # Number of colors to generate
step = 15        # Maximum step size per channel
widths = np.linspace(200, 1, num_colors)
x, y = golden_spiral()
hex_colors = random_walk_color(num_colors, step_size=step, seed=42)
# for idx, color in enumerate(hex_colors, 1):

fig, ax = plt.subplots(figsize=(8, 8))
for width, color in zip(widths, hex_colors):
    ax.plot(x, y, color=color, linewidth=width)

ax.set_aspect('equal')
ax.axis('off')
plt.show()
seed: 42
../_images/d4644933d7636e1b29a6b90400e181e307a28937a62880b09a23b53ef65eb607.png
step = 10
hex_colors = random_walk_color(num_colors, step_size=step)

fig, ax = plt.subplots(figsize=(6, 6))
for width, color in zip(widths, hex_colors):
    ax.scatter(x, y, color=color, linewidth=width*.9, marker='.')

ax.set_aspect('equal')
ax.axis('off')
plt.xlim(-20, 30)
plt.show()
seed: None
../_images/fb1af7cd584fdcd8d11473d06eed1dfb3f8fd0242bdd610f5922148e69233316.png
num_colors = 1000  # Number of colors to generate
step = 5       # Maximum step size per channel
widths = np.geomspace(200, 1, num_colors)
x, y = golden_spiral()
hex_colors = random_walk_color(num_colors, step_size=step, seed=42)
# for idx, color in enumerate(hex_colors, 1):

fig, ax = plt.subplots(figsize=(8, 8))
for width, color in zip(widths, hex_colors):
    # ax.plot(x, y, color=color, linewidth=width)
    ax.scatter(x, y, color=color, linewidth=width, marker='.')


ax.set_aspect('equal')
ax.axis('off')
plt.show()
seed: 42
../_images/45e22f03bda90638b0ede8f2ad15c9b12348bab6f6e37f64772a99a33a6fad75.png