Perception and Graphics

Data 304

A bit about perception

We may see perceive more or less than is there.

We may percieve things differently from how they are

Assign numbers to squares A and B. (0 = white, 1 = black)

We may percieve things differently from how they are

Assign numbers to squares A and B. (0 = white, 1 = black)

We may percieve things differently from how they are

The Checker shadow illusion by Edward H. Adelson

We may percieve things differently from how they are

  • We are attracted to edges

  • We asses contrast, brightness, color, etc. in terms of relative rather than absolute scales

    • what’s nearby matters
    • computer color space may not be uniform in perception space
  • We are tuned to represent surfaces and shapes of 3-d objects, not data representations

Design choices matter

For all these reasons and more, data graphics should be designed with at least some knowledge of how human perception works.

  • You can learn far more about this topic than we have time for.

  • Even just understanding that there are perception issues is a start down the right path.

Perception and Scales

Code
set.seed(12345)

Quiz1 <- RandomData(8)
Quiz1sorted <- Quiz1 |> 
  mutate(letter = LETTERS[rank(value)])

Quiz2 <- 
  RandomData(8) 
Quiz2sorted <- Quiz2 |> 
  mutate(letter = LETTERS[rank(value)])

Quiz3 <- RandomData(8)
Quiz3sorted <- Quiz3 |> 
  mutate(letter = LETTERS[rank(value)])

Quiz4 <- RandomData(8)
Quiz4sorted <- Quiz4 |> 
  mutate(letter = LETTERS[rank(value)])

circles <- 
  vl_chart() |>
  vl_mark_circle(size = 3600) |>
  vl_encode_x('letter:O') |>
  vl_encode_color('value:Q') |>
  vl_scale_color(domain = 1:100, range = red_blue_pal(100)) 

circles2 <- 
  vl_chart() |>
  vl_mark_circle() |>
  vl_encode_x('letter:O') |>
  vl_encode_size('value:Q') |>
  vl_scale_size(rangeMin = 1000, rangeMax = 3600)

circles3 <- 
  vl_chart() |>
  vl_mark_circle() |>
  vl_encode_x('x:Q') |>
  vl_encode_y('y:Q') |>
  vl_encode_size('value:Q') |>
  vl_scale_size(rangeMin = 1000, rangeMax = 3600)

bars <- 
  vl_chart() |>
  vl_mark_bar() |>
  vl_encode_x('letter:O') |>
  vl_encode_y('value:Q') 

bars2 <- 
  vl_chart() |>
  vl_mark_bar() |>
  vl_encode_x('letter:O') |>
  vl_encode_y('value1:Q') |>
  vl_encode_y2('value2:Q') 

wedges <- 
  vl_chart() |>
  vl_mark_arc() |>
  vl_encode_fill('letter:N') |>
  vl_encode_theta('value:Q') |>
  vl_encode_radius(value = 120) 
  
wedges_text <- 
  vl_chart() |>
  vl_mark_text(color = "#666666", size = 20) |>
  vl_encode_text('label:N') |>
  vl_encode_theta('mid_cum_value:Q') |>
  vl_encode_radius(value = 60) 

wedges2 <- 
  vl_chart() |>
  vl_mark_arc() |>
  vl_encode_x('letter:O') |>
  vl_encode_theta('value:Q') |>
  vl_encode_radius(value = 60) 

text <- 
  vl_chart() |>
  vl_mark_text(size = 23, color = "#cccccc") |>
  vl_encode_x('letter:O') |>
  vl_encode_text("label:N") 

text3 <- 
  vl_chart() |>
  vl_mark_text(size = 23, color = "#cccccc") |>
  vl_encode_x('x:Q') |>
  vl_encode_y('y:Q') |>
  vl_encode_text("label:N") 

bar_text <-
  vl_chart() |>
  vl_mark_text(size = 23) |>
  vl_encode_x('letter:O') |>
  vl_encode_y("value_shifted:Q") |>
  # vl_encode_yOffset(datum = -20) |>
  vl_encode_text("label:N", ) 

bar_text_sol <-
  vl_chart() |>
  vl_mark_text(size = 23) |>
  vl_encode_x('letter:O') |>
  vl_encode_y("value_shifted:Q") |>
  # vl_encode_yOffset(datum = -20) |>
  vl_encode_text("value:N", ) 


 bar_text2 <-
  vl_chart() |>
  vl_mark_text(size = 23) |>
  vl_encode_x('letter:O') |>
  vl_encode_y("value2_shifted:Q") |>
  # vl_encode_yOffset(datum = -20) |>
  vl_encode_text("label:N") 
  
quiz_color <- 
  (circles + text) |>   
  vl_add_data(values = Quiz1) |>
  vl_config_legend(disable = TRUE) |>
  vl_config_axisBottom(disable = TRUE) |>
  vl_add_properties(width = 1000, height = 100) 

quiz_color_sol <- 
  (circles + text |> vl_encode_text("value:N")) |>   
  vl_add_data(values = Quiz1) |>
  vl_config_legend(disable = TRUE) |>
  vl_config_axisBottom(disable = TRUE) |>
  vl_add_properties(width = 1000, height = 100) 

quiz_color2 <-
  ((circles |> vl_scale_color(scheme = "viridis")) + text) |>   
  vl_add_data(values = Quiz3) |>
  vl_config_legend(disable = TRUE) |>
  vl_config_axisBottom(disable = TRUE) |>
  vl_add_properties(width = 1000, height = 100) 
  
quiz_color2_sol <- 
  ((circles |> vl_scale_color(scheme = "viridis")) + 
     (text |> vl_encode_text("value:N"))) |>   
  vl_add_data(values = Quiz3) |>
  vl_config_legend(disable = TRUE) |>
  vl_config_axisBottom(disable = TRUE) |>
  vl_add_properties(width = 1000, height = 100) 

quiz_angle <- 
  (wedges + wedges_text) |>   
  vl_add_data(values = Quiz1) |>
  vl_config_legend(disable = TRUE) |>
  vl_config_axisBottom(disable = TRUE) |>
  vl_add_properties(width = 1000, height = 100) 

quiz_angle_sol <- 
  (wedges + wedges_text |> vl_encode_text('value:N')) |>   
  vl_add_data(values = Quiz1) |>
  vl_config_legend(disable = TRUE) |>
  vl_config_axisBottom(disable = TRUE) |>
  vl_add_properties(width = 1000, height = 100) 

quiz_color_sorted <- 
  quiz_color |>
  vl_add_data(values = Quiz2sorted) 

quiz_color_sorted_sol <- quiz_color_sol |>
  vl_add_data(values = Quiz2sorted) 

quiz_color2_sorted <- 
  quiz_color2 |>
  vl_add_data(values = Quiz4sorted) 

quiz_color2_sorted_sol <- quiz_color2_sol |>
  vl_add_data(values = Quiz4sorted) 

quiz_size <- (circles2 + text) |>
  vl_add_data(values = Quiz1) |>
  vl_config_legend(disable = TRUE) |>
  vl_config_axisBottom(disable = TRUE) |>
  vl_config_axisLeft(disable = TRUE) |>
  vl_add_properties(width = 800, height = 100) 

print(quiz_size)

quiz_size_sol <- 
  (circles2 + text |> vl_encode_text("value:N")) |>   
  vl_add_data(values = Quiz1) |>
  vl_config_legend(disable = TRUE) |>
  vl_config_axisLeft(disable = TRUE) |>
  vl_config_axisBottom(disable = TRUE) |>
  vl_add_properties(width = 800, height = 100) 

quiz_size_random <-
  (circles3 + text3) |>
  vl_add_data(values = Quiz2) |>
  vl_config_legend(disable = TRUE) |>
  vl_config_axisBottom(disable = TRUE) |>
  vl_config_axisLeft(disable = TRUE) |>
  vl_add_properties(width = 500, height = 400) 

quiz_size_random_sol <-
  (circles3 + text3 |> vl_encode_text("value:N")) |>   
  vl_add_data(values = Quiz2) |>
  vl_config_legend(disable = TRUE) |>
  vl_config_axisBottom(disable = TRUE) |>
  vl_config_axisLeft(disable = TRUE) |>
  vl_add_properties(width = 500, height = 400) 

quiz_bars <- (bars + bar_text) |>
  vl_add_data(Quiz1) |>
  vl_config_legend(disable = TRUE) |>
  vl_config_axisBottom(disable = TRUE) |>
  vl_config_axisLeft(disable = TRUE) |>
  vl_add_properties(width = 1000, height = 300) 

quiz_bars_sol <- (bars + bar_text_sol) |>
  vl_add_data(Quiz1) |>
  vl_config_legend(disable = TRUE) |>
  vl_config_axisBottom(disable = TRUE) |>
  vl_config_axisLeft(disable = TRUE) |>
  vl_add_properties(width = 1000, height = 300) 


  # (bars + bar_text |> vl_encode_text("value:N")) |>   
  # vl_add_data(values = Quiz1) |>
  # vl_config_legend(disable = TRUE) |>
  # vl_config_axisLeft(disable = TRUE) |>
  # vl_config_axisBottom(disable = TRUE) |>
  # vl_add_properties(width = 1000, height = 300) 

quiz_bars_sorted <- quiz_bars |>
  vl_add_data(values = Quiz2sorted) 
  
quiz_bars_sorted_sol <- quiz_bars_sol |>
  vl_add_data(values = Quiz2sorted) 

quiz_bars_shifted <- (bars2 + bar_text2) |>
  vl_add_data(values = Quiz3) |>
  vl_config_legend(disable = TRUE) |>
  vl_config_axisBottom(disable = TRUE) |>
  vl_config_axisLeft(disable = TRUE) |>
  vl_add_properties(width = 1000, height = 300) 

quiz_bars_shifted_sol <- 
  (bars2 + bar_text2 |> vl_encode_text('value:N')) |>
  vl_add_data(values = Quiz3) |>
  vl_config_legend(disable = TRUE) |>
  vl_config_axisBottom(disable = TRUE) |>
  vl_config_axisLeft(disable = TRUE) |>
  vl_add_properties(width = 1000, height = 300) 

Some Quizzes

Quiz 1

Assign a numeric value to each dot.

Code
quiz_color

Quiz 2

Assign a numeric value to each dot.

Code
quiz_color_sorted

Quiz 1a

Assign a numeric value to each dot.

Code
quiz_color2

Quiz 2a

Assign a numeric value to each dot.

Code
quiz_color2_sorted

How’d you do?

Let’s check.

Quiz 1 (solution)

Code
quiz_color_sol

Quiz 2 (solution)

Code
quiz_color_sorted_sol

Quiz 1a (solution)

Code
quiz_color2_sol

Quiz 2a (solution)

Code
quiz_color2_sorted_sol

Quiz 3

Code
quiz_size

Quiz 4

Code
quiz_size_random

More Solutions

Quiz 3 (solution)

Code
quiz_size_sol

Quiz 4 (solution)

Code
quiz_size_random_sol

Quiz 5

Code
quiz_bars

Quiz 6

Code
quiz_bars_sorted

Quiz 7

Code
quiz_bars_shifted

How are you with bars?

Quiz 5 (solution)

Code
quiz_bars_sol

Quiz 6 (solution)

Code
quiz_bars_sorted_sol

Quiz 7 (solution)

Code
quiz_bars_shifted_sol

Quiz 8

Code
quiz_angle

Quiz 8 (solution)

Code
quiz_angle_sol

Target Practice

In each of the following slides, how fast can you find the blue circle?

Code
TargetData <- function(n = 20) {
  tibble(
    id = 1:n,
    x = runif(n),
    y = runif(n),
    target = 2 - (id == 1),
    distractor2 = sample(1:2, n, replace = TRUE),
    distractor3 = sample(1:3, n, replace = TRUE)
  ) |>
    mutate(
      distractor2 = ifelse(target == 1, 1, distractor2),
      distractor3 = ifelse(target == 1, 1, distractor3),
      distractor4 = ifelse(target == 1, 1, 1 + distractor3)
    )
}
Code
target_practice <- 
  vl_chart() |>
  vl_mark_point(size = 180, filled = TRUE, opacity = 0.5) |>
  vl_encode_x("x:Q") |>
  vl_encode_y("y:Q") |>
  vl_encode_color(datum = 1) |>
  vl_encode_shape(datum = 1) |>
  vl_scale_color(domain = 1:3, range = brewer_pal('qual')(3)) |>
  vl_scale_shape(
    domain = 1:3, range = c('circle', 'triangle-up', 'square')) |>
  vl_config_legend(disable = TRUE) |>
  vl_config_axisBottom(disable = TRUE) |>
  vl_config_axisLeft(disable = TRUE) |>
  vl_add_properties(width = 800, height = 400)

Target Practice #1

Find the blue circle.

Code
target_practice |>
  vl_add_data(TargetData(20)) |>
  vl_encode_color("target:N") 

Target Practice #2

Find the blue circle.

Code
target_practice |>
  vl_add_data(TargetData(100)) |>
  vl_encode_color("target:N")

Target Practice #3

Find the blue circle.

Code
target_practice |>
  vl_add_data(TargetData(100)) |>
  vl_encode_color("target:N") |>
  vl_encode_shape("distractor2:N")

Target Practice #4

Find the blue circle.

Code
target_practice |>
  vl_add_data(TargetData(100)) |>
  vl_encode_shape("target:N") |>
  vl_encode_color("distractor2:N")

Target Practice #5

Find the blue circle.

Code
target_practice |>
  vl_add_data(TargetData(100)) |>
  vl_encode_color("distractor4:N") |>
  vl_encode_shape("distractor3:N")

Target Practice #6

Find the blue circle.

Code
target_practice |>
  vl_add_data(TargetData(100)) |>
  vl_encode_shape("distractor4:N") |>
  vl_encode_color("distractor3:N")

Target Practice #7

New instructions: Find the triangle that is pointing up.

Code
t7 <-
  target_practice |>
  vl_add_data(TargetData(100)) |>
  vl_encode_shape("distractor4:N") |>
  vl_encode_color(datum = 1) |>
  vl_scale_shape(
    domain = 1:4, 
    range = c('triangle-up', 'triangle-down', 
              'triangle-left', 'triangle-right')) 
t7
Code
t7 |> 
  vl_encode_color("target:N") 

Gestalt Rules

How do these grids compare?

Gestalt Principles

Our brains tend to infer relationships between objects in a way that goes beyond what is strictly visible

How are these grouped?

Images from Figure 1.21 from Healy (2019)

What does adding lines do here?

What other gestalt inference is happening in the second plot?

Gestalt inferences

  • Proximity
  • Similarity
  • Connection
  • Continuity
  • Closure
  • Figure/Ground
  • Common Fate

A Heierarchy of visual channels

Fig 1.22 from Healy (2019)

Some data

Here are the results of some more carefully constructed “quizzes”.

Want more?

https://michaelbach.de/ot/

Healy, K. 2019. Data Visualization: A Practical Introduction. Princeton University Press. https://socviz.co/.