Generative AI for Data Visualisation

Data Visualisation
Machine Learning & AI
R
Can generative AI create good data visualisations? This blog post compares the performance of ChatGPT, Claude, Copilot, and Gemini when presented with a generic request to visualise a dataset.
Author

Nicola Rennie

Published

October 8, 2025

Generative AI tools have been around for a little while now, and they’re mostly advertised as being able to help you with writing, coding, and reasoning. Different AI tools are designed for different types of tasks, so I wanted to see how some of the most commonly used tools compare to each other when used for data visualisation. I’m going to compare four different generative AI tools - ChatGPT, Claude, Copilot, and Gemini. Using the free tier of each tool, I’m going to give them two different datasets with a prompt to create a chart.

Prompts

I didn’t want to use well-known datasets like penguins or titanic since there are so many examples of charts of those datasets already in existence. I wanted to see how well they would do with new(ish) data. If you’ve been following these blog posts for a little while, it’s probably not a surprise that I turned to TidyTuesday to find some data!

Prompt 1: weather forecast data

The first dataset used gives information on weather forecast accuracy from the USA National Weather Service. It contains data on the observed and predicted (at 12, 24, 36, and 48 hours before) temperature and precipitation levels for different cities in the USA, between January 2021 and June 2022.

I pre-processed the data to focus on a single city (Syracuse), single year (2021), and one type of forecast (12 hours before). This was mainly to make the data small enough for uploading to different generative AI websites. The data look like this:

Code
tuesdata <- tidytuesdayR::tt_load("2022-12-20")
weather_forecasts <- tuesdata$weather_forecasts
weather <- weather_forecasts |>
  dplyr::filter(
    city == "SYRACUSE",
    lubridate::year(date) == 2021,
    forecast_hours_before == 12
  ) |>
  dplyr::select(date, high_or_low, forecast_temp, observed_temp)
date high_or_low forecast_temp observed_temp
2021-01-30 high 18 23
2021-01-30 low -3 6
2021-01-31 high 22 22
2021-01-31 low 16 16
2021-02-01 high 30 30
2021-02-01 low 21 20
2021-02-02 high 29 27
2021-02-02 low 24 25
2021-02-03 high 30 30
2021-02-03 low 21 19
2021-02-04 high 33 37
2021-02-04 low 23 22
2021-02-05 high 38 44
2021-02-05 low 20 26
2021-02-06 high 27 30
2021-02-06 low 12 8
2021-02-07 high 29 27
2021-02-07 low 11 10
2021-02-08 high 22 23
2021-02-08 low 16 16
2021-02-09 high 30 28
2021-02-09 low 16 18
2021-02-10 high 25 26
2021-02-10 low 17 18
2021-02-11 high 24 26
2021-02-11 low 2 -1
2021-02-12 high 16 18
2021-02-12 low 2 2
2021-02-13 high 21 22
2021-02-13 low 17 20
2021-02-14 high 27 25
2021-02-14 low NA 20
2021-02-15 high 27 28
2021-02-15 low 24 24
2021-02-16 high 37 35
2021-02-16 low 11 7
2021-02-17 high 22 25
2021-02-17 low 13 16
2021-02-18 high 30 27
2021-02-18 low 22 22
2021-02-19 high 33 31
2021-02-19 low 20 20
2021-02-20 high 28 29
2021-02-20 low 14 9
2021-02-21 high 30 34
2021-02-21 low 19 20
2021-02-22 high 36 37
2021-02-22 low 28 30
2021-02-23 high 37 37
2021-02-23 low 31 31
2021-02-24 high 47 52
2021-02-24 low 27 33
2021-02-25 high 33 35
2021-02-25 low 23 25
2021-02-26 high 40 41
2021-02-26 low 28 27
2021-02-27 high 43 47
2021-02-27 low 29 27
2021-02-28 high 44 44
2021-02-28 low 38 39
2021-03-01 high 40 42
2021-03-01 low 13 10
2021-03-02 high 26 25
2021-03-02 low 22 20
2021-03-03 high 41 43
2021-03-03 low 24 27
2021-03-04 high 28 30
2021-03-04 low 15 16
2021-03-05 high 28 32
2021-03-05 low 17 17
2021-03-06 high 24 24
2021-03-06 low 15 9
2021-03-07 high 26 29
2021-03-07 low 10 8
2021-03-08 high 39 37
2021-03-08 low 34 28
2021-03-09 high 43 50
2021-03-09 low 23 24
2021-03-10 high 59 65
2021-03-10 low 44 48
2021-03-11 high 67 73
2021-03-11 low 39 39
2021-03-12 high 54 58
2021-03-12 low 23 24
2021-03-13 high 36 40
2021-03-13 low 30 32
2021-03-14 high 36 38
2021-03-14 low 13 16
2021-03-15 high 28 28
2021-03-15 low 10 17
2021-03-16 high 39 43
2021-03-16 low 32 34
2021-03-17 high 50 52
2021-03-17 low 37 33
2021-03-18 high 42 43
2021-03-18 low 20 22
2021-03-19 high 36 36
2021-03-19 low 20 20
2021-03-20 high 55 57
2021-03-20 low 26 25
2021-03-21 high 63 65
2021-03-21 low 30 29
2021-03-22 high 68 70
2021-03-22 low 36 33
2021-03-23 high 69 70
2021-03-23 low 46 48
2021-03-24 high 60 60
2021-03-24 low 50 44
2021-03-25 high 76 76
2021-03-25 low 59 60
2021-03-26 high 66 71
2021-03-26 low 37 38
2021-03-27 high 57 52
2021-03-27 low 44 43
2021-03-28 high 62 62
2021-03-28 low 33 34
2021-03-29 high 43 44
2021-03-29 low 28 30
2021-03-30 high 68 71
2021-03-30 low 51 55
2021-03-31 high 57 60
2021-03-31 low 32 33
2021-04-01 high 37 40
2021-04-01 low 22 24
2021-04-02 high 38 38
2021-04-02 low 21 25
2021-04-03 high 51 50
2021-04-03 low 33 36
2021-04-04 high 57 59
2021-04-04 low 33 33
2021-04-05 high 57 57
2021-04-05 low 30 30
2021-04-06 high 62 63
2021-04-06 low 37 44
2021-04-07 high 70 73
2021-04-07 low 44 45
2021-04-08 high 78 80
2021-04-08 low NA 57
2021-04-09 high 68 71
2021-04-09 low 50 45
2021-04-10 high 82 81
2021-04-10 low 55 58
2021-04-11 high 71 72
2021-04-11 low 49 49
2021-04-12 high 56 54
2021-04-12 low 44 39
2021-04-13 high 65 67
2021-04-13 low 45 41
2021-04-14 high 69 71
2021-04-14 low 45 48
2021-04-15 high 48 51
2021-04-15 low 38 40
2021-04-16 high 47 47
2021-04-16 low 37 38
2021-04-17 high 53 56
2021-04-17 low 40 44
2021-04-18 high 60 64
2021-04-18 low 40 40
2021-04-19 high 64 63
2021-04-19 low 43 43
2021-04-20 high 51 50
2021-04-20 low 34 34
2021-04-21 high 36 36
2021-04-21 low 29 29
2021-04-22 high 42 44
2021-04-22 low 32 33
2021-04-23 high 60 63
2021-04-23 low 41 45
2021-04-24 high 67 68
2021-04-24 low 47 51
2021-04-25 high 59 59
2021-04-25 low 33 35
2021-04-26 high 51 50
2021-04-26 low 37 36
2021-04-27 high 65 68
2021-04-27 low 52 52
2021-04-28 high 72 73
2021-04-28 low 53 52
2021-04-29 high 62 55
2021-04-29 low 48 48
2021-04-30 high 52 51
2021-04-30 low 35 35
2021-05-01 high 57 58
2021-05-01 low 48 47
2021-05-02 high 64 67
2021-05-02 low 52 48
2021-05-03 high 70 71
2021-05-03 low 56 54
2021-05-04 high 69 69
2021-05-04 low 53 51
2021-05-05 high 57 52
2021-05-05 low 42 42
2021-05-06 high 57 58
2021-05-06 low 38 34
2021-05-07 high 58 57
2021-05-07 low 43 40
2021-05-08 high 51 54
2021-05-08 low 39 41
2021-05-09 high 56 58
2021-05-09 low 42 44
2021-05-10 high 58 59
2021-05-10 low 39 39
2021-05-11 high 53 57
2021-05-11 low 40 42
2021-05-12 high 62 64
2021-05-12 low 39 39
2021-05-13 high 67 69
2021-05-13 low 42 44
2021-05-14 high 71 72
2021-05-14 low 45 43
2021-05-15 high 74 75
2021-05-15 low 45 45
2021-05-16 high 73 78
2021-05-16 low 49 50
2021-05-17 high 76 78
2021-05-17 low 51 49
2021-05-18 high 79 80
2021-05-18 low 53 52
2021-05-19 high 84 87
2021-05-19 low 57 57
2021-05-20 high 88 92
2021-05-20 low 61 60
2021-05-21 high 90 93
2021-05-21 low 67 63
2021-05-22 high 85 85
2021-05-22 low 67 69
2021-05-23 high 79 77
2021-05-23 low 49 46
2021-05-24 high 77 78
2021-05-24 low 60 60
2021-05-25 high 86 87
2021-05-25 low 69 72
2021-05-26 high 87 90
2021-05-26 low 54 54
2021-05-27 high 67 66
2021-05-27 low 45 47
2021-05-28 high 52 50
2021-05-28 low 43 44
2021-05-29 high 59 62
2021-05-29 low 46 46
2021-05-30 high 57 59
2021-05-30 low 47 49
2021-05-31 high 70 72
2021-05-31 low 52 53
2021-06-01 high 77 78
2021-06-01 low 52 50
2021-06-02 high 77 77
2021-06-02 low 62 64
2021-06-03 high 75 79
2021-06-03 low 61 64
2021-06-04 high 83 84
2021-06-04 low 64 68
2021-06-05 high 91 93
2021-06-05 low 69 69
2021-06-06 high 94 93
2021-06-06 low 69 66
2021-06-07 high 91 94
2021-06-07 low 72 72
2021-06-08 high 84 83
2021-06-08 low 68 70
2021-06-09 high 88 87
2021-06-09 low 60 56
2021-06-10 high 83 82
2021-06-10 low 59 56
2021-06-11 high 80 81
2021-06-11 low 60 63
2021-06-12 high 83 82
2021-06-12 low 59 58
2021-06-13 high 84 87
2021-06-13 low 63 64
2021-06-14 high 77 80
2021-06-14 low 58 59
2021-06-15 high 69 71
2021-06-15 low 50 52
2021-06-16 high 70 69
2021-06-16 low 49 51
2021-06-17 high 77 76
2021-06-17 low 54 51
2021-06-18 high 80 82
2021-06-18 low 65 68
2021-06-19 high 86 88
2021-06-19 low 62 60
2021-06-20 high 86 86
2021-06-20 low 71 71
2021-06-21 high 91 92
2021-06-21 low 57 58
2021-06-22 high 63 63
2021-06-22 low 48 49
2021-06-23 high 74 73
2021-06-23 low 53 50
2021-06-24 high 85 83
2021-06-24 low 60 60
2021-06-25 high 87 85
2021-06-25 low 68 73
2021-06-26 high 88 90
2021-06-26 low 74 78
2021-06-27 high 96 94
2021-06-27 low 75 81
2021-06-28 high 90 96
2021-06-28 low 74 71
2021-06-29 high 90 94
2021-06-29 low 74 73
2021-06-30 high 85 87
2021-06-30 low 67 68
2021-07-01 high 81 79
2021-07-01 low 62 65
2021-07-02 high 74 77
2021-07-02 low 59 61
2021-07-03 high 73 74
2021-07-03 low 57 58
2021-07-04 high 79 79
2021-07-04 low 56 55
2021-07-05 high 90 90
2021-07-05 low 73 71
2021-07-06 high 87 88
2021-07-06 low 68 69
2021-07-07 high 83 85
2021-07-07 low 65 65
2021-07-08 high 80 82
2021-07-08 low 68 68
2021-07-09 high 79 84
2021-07-09 low 63 65
2021-07-10 high 79 80
2021-07-10 low 60 61
2021-07-11 high 78 75
2021-07-11 low 64 64
2021-07-12 high 79 76
2021-07-12 low 70 69
2021-07-13 high 87 88
2021-07-13 low 71 70
2021-07-14 high 82 84
2021-07-14 low 66 66
2021-07-15 high 89 90
2021-07-15 low 72 73
2021-07-16 high 81 84
2021-07-16 low 66 67
2021-07-17 high 77 76
2021-07-17 low 67 66
2021-07-18 high 75 84
2021-07-18 low 64 66
2021-07-19 high 84 90
2021-07-19 low 66 69
2021-07-20 high 87 87
2021-07-20 low 65 64
2021-07-21 high 75 76
2021-07-21 low 57 59
2021-07-22 high 79 77
2021-07-22 low 59 61
2021-07-23 high 80 81
2021-07-23 low 56 57
2021-07-24 high 86 84
2021-07-24 low 69 72
2021-07-25 high 86 85
2021-07-25 low 64 67
2021-07-26 high 87 87
2021-07-26 low 67 65
2021-07-27 high 87 86
2021-07-27 low 63 59
2021-07-28 high 80 79
2021-07-28 low 60 56
2021-07-29 high 78 72
2021-07-29 low 61 65
2021-07-30 high 72 71
2021-07-30 low 52 52
2021-07-31 high 75 73
2021-07-31 low 59 57
2021-08-01 high 72 74
2021-08-01 low 59 59
2021-08-02 high 76 76
2021-08-02 low 54 52
2021-08-03 high 81 79
2021-08-03 low 59 57
2021-08-04 high 83 NA
2021-08-04 low 59 NA
2021-08-05 high 84 NA
2021-08-05 low 61 NA
2021-08-06 high 88 NA
2021-08-06 low 69 NA
2021-08-07 high 89 NA
2021-08-07 low 68 NA
2021-08-08 high 88 NA
2021-08-08 low 71 NA
2021-08-09 high 90 NA
2021-08-09 low 71 NA
2021-08-10 high 92 89
2021-08-10 low 73 76
2021-08-11 high 93 92
2021-08-11 low 77 78
2021-08-12 high 91 87
2021-08-12 low 71 71
2021-08-13 high 88 91
2021-08-13 low 67 67
2021-08-14 high 79 80
2021-08-14 low 57 56
2021-08-15 high 77 77
2021-08-15 low 57 53
2021-08-16 high 82 81
2021-08-16 low 68 69
2021-08-17 high 79 85
2021-08-17 low 71 72
2021-08-18 high 79 77
2021-08-18 low 71 72
2021-08-19 high 80 82
2021-08-19 low 68 68
2021-08-20 high 84 86
2021-08-20 low 69 73
2021-08-21 high 84 86
2021-08-21 low 70 69
2021-08-22 high 86 89
2021-08-22 low 71 73
2021-08-23 high 84 86
2021-08-23 low 71 70
2021-08-24 high 87 87
2021-08-24 low 68 67
2021-08-25 high 93 92
2021-08-25 low 72 74
2021-08-26 high 89 91
2021-08-26 low 72 73
2021-08-27 high 86 87
2021-08-27 low 65 67
2021-08-28 high 84 81
2021-08-28 low 69 69
2021-08-29 high 87 85
2021-08-29 low 73 75
2021-08-30 high 85 86
2021-08-30 low 65 66
2021-08-31 high 81 82
2021-08-31 low 61 62
2021-09-01 high 74 70
2021-09-01 low 56 52
2021-09-02 high 69 69
2021-09-02 low 57 59
2021-09-03 high 71 73
2021-09-03 low 54 56
2021-09-04 high 77 79
2021-09-04 low 63 63
2021-09-05 high 75 79
2021-09-05 low 61 60
2021-09-06 high 74 76
2021-09-06 low 56 59
2021-09-07 high 80 79
2021-09-07 low 67 65
2021-09-08 high 78 78
2021-09-08 low 59 59
2021-09-09 high 75 76
2021-09-09 low 57 55
2021-09-10 high 71 73
2021-09-10 low 54 52
2021-09-11 high 79 80
2021-09-11 low 67 69
2021-09-12 high 79 79
2021-09-12 low 62 60
2021-09-13 high 75 76
2021-09-13 low 57 56
2021-09-14 high 84 85
2021-09-14 low 68 70
2021-09-15 high 73 79
2021-09-15 low 59 56
2021-09-16 high 76 77
2021-09-16 low 64 60
2021-09-17 high 82 83
2021-09-17 low 66 65
2021-09-18 high 78 80
2021-09-18 low 52 50
2021-09-19 high 74 75
2021-09-19 low 49 49
2021-09-20 high 80 80
2021-09-20 low 59 62
2021-09-21 high 77 78
2021-09-21 low 64 66
2021-09-22 high 79 77
2021-09-22 low 69 70
2021-09-23 high 73 76
2021-09-23 low 53 51
2021-09-24 high 69 70
2021-09-24 low 49 49
2021-09-25 high 76 78
2021-09-25 low 53 54
2021-09-26 high 70 72
2021-09-26 low 58 55
2021-09-27 high 75 78
2021-09-27 low 57 59
2021-09-28 high 66 68
2021-09-28 low NA 48
2021-09-29 high 65 NA
2021-09-29 low 46 NA
2021-09-30 high NA 63
2021-09-30 low 48 48
2021-10-01 high 66 65
2021-10-01 low 49 48
2021-10-02 high 77 NA
2021-10-02 low 56 NA
2021-10-03 high NA 65
2021-10-03 low 61 61
2021-10-04 high 68 NA
2021-10-04 low 57 NA
2021-10-05 high NA 65
2021-10-05 low 57 60
2021-10-06 high 74 NA
2021-10-06 low NA NA
2021-10-07 high NA 79
2021-10-07 low 59 58
2021-10-08 high 77 79
2021-10-08 low 60 61
2021-10-09 high 72 72
2021-10-09 low 59 58
2021-10-10 high 73 74
2021-10-10 low 58 56
2021-10-11 high 78 79
2021-10-11 low 57 56
2021-10-12 high 79 79
2021-10-12 low 62 59
2021-10-13 high 75 77
2021-10-13 low 59 60
2021-10-14 high 76 77
2021-10-14 low 61 62
2021-10-15 high 77 78
2021-10-15 low 63 67
2021-10-16 high 72 73
2021-10-16 low 49 51
2021-10-17 high 57 57
2021-10-17 low 45 48
2021-10-18 high 53 51
2021-10-18 low 42 43
2021-10-19 high 67 67
2021-10-19 low 50 57
2021-10-20 high 72 75
2021-10-20 low 54 55
2021-10-21 high 72 74
2021-10-21 low 51 52
2021-10-22 high 54 52
2021-10-22 low 40 38
2021-10-23 high 54 55
2021-10-23 low 40 36
2021-10-24 high 58 59
2021-10-24 low 48 49
2021-10-25 high 64 64
2021-10-25 low 50 53
2021-10-26 high 56 56
2021-10-26 low 46 45
2021-10-27 high 58 59
2021-10-27 low 36 37
2021-10-28 high 61 59
2021-10-28 low 41 43
2021-10-29 high 56 58
2021-10-29 low 46 45
2021-10-30 high 59 57
2021-10-30 low 50 48
2021-10-31 high 57 56
2021-10-31 low 45 47
2021-11-01 high 53 55
2021-11-01 low 37 42
2021-11-02 high 50 50
2021-11-02 low 34 37
2021-11-03 high 45 47
2021-11-03 low 32 31
2021-11-04 high 48 48
2021-11-04 low 30 31
2021-11-05 high 49 51
2021-11-05 low 29 28
2021-11-06 high 54 55
2021-11-06 low 31 28
2021-11-07 high 57 61
2021-11-07 low 36 34
2021-11-08 high 61 64
2021-11-08 low 43 39
2021-11-09 high 60 63
2021-11-09 low 45 50
2021-11-10 high 55 56
2021-11-10 low 34 33
2021-11-11 high 60 58
2021-11-11 low 50 50
2021-11-12 high 61 58
2021-11-12 low 40 41
2021-11-13 high 46 48
2021-11-13 low 36 39
2021-11-14 high 45 46
2021-11-14 low 36 38
2021-11-15 high 42 44
2021-11-15 low 33 35
2021-11-16 high 42 41
2021-11-16 low 31 32
2021-11-17 high 53 56
2021-11-17 low 49 55
2021-11-18 high 55 61
2021-11-18 low 33 35
2021-11-19 high 43 41
2021-11-19 low 28 29
2021-11-20 high 45 45
2021-11-20 low 37 39
2021-11-21 high 52 51
2021-11-21 low 39 40
2021-11-22 high 40 42
2021-11-22 low 26 30
2021-11-23 high 37 33
2021-11-23 low 25 21
2021-11-24 high 44 46
2021-11-24 low 32 30
2021-11-25 high 48 50
2021-11-25 low 38 39
2021-11-26 high 41 38
2021-11-26 low 28 29
2021-11-27 high 35 34
2021-11-27 low 26 31
2021-11-28 high 36 34
2021-11-28 low 27 31
2021-11-29 high 34 35
2021-11-29 low 27 26
2021-11-30 high 37 39
2021-11-30 low 32 37
2021-12-01 high 42 43
2021-12-01 low 35 36
2021-12-02 high 53 53
2021-12-02 low 33 35
2021-12-03 high 36 38
2021-12-03 low 28 33
2021-12-04 high 42 47
2021-12-04 low 30 34
2021-12-05 high 41 41
2021-12-05 low 38 39
2021-12-06 high 58 57
2021-12-06 low 29 31
2021-12-07 high 33 35
2021-12-07 low 27 29
2021-12-08 high 34 35
2021-12-08 low 26 28
2021-12-09 high 35 32
2021-12-09 low 30 27
2021-12-10 high 47 46
2021-12-10 low 37 34
2021-12-11 high 65 64
2021-12-11 low 35 34
2021-12-12 high 45 47
2021-12-12 low 33 41
2021-12-13 high 52 54
2021-12-13 low NA 32
2021-12-14 high 44 48
2021-12-14 low 27 28
2021-12-15 high 49 49
2021-12-15 low 43 47
2021-12-16 high 65 67
2021-12-16 low 40 42
2021-12-17 high 46 50
2021-12-17 low 30 34
2021-12-18 high 34 36
2021-12-18 low 29 26
2021-12-19 high 30 28
2021-12-19 low 18 20
2021-12-20 high 39 39
2021-12-20 low 33 34
2021-12-21 high 38 40
2021-12-21 low 30 30
2021-12-22 high 38 39
2021-12-22 low 22 22
2021-12-23 high 32 32
2021-12-23 low 23 24
2021-12-24 high 39 34
2021-12-24 low 31 30
2021-12-25 high 41 38
2021-12-25 low 35 34
2021-12-26 high 39 40
2021-12-26 low 22 26
2021-12-27 high 33 33
2021-12-27 low 32 32
2021-12-28 high 38 44
2021-12-28 low 32 34
2021-12-29 high 38 42
2021-12-29 low 35 37
2021-12-30 high 43 44
2021-12-30 low 36 37
2021-12-31 high 48 52
2021-12-31 low 37 34


The prompt given to all generative AI tools was:

TipPrompt 1

Create a chart of the relationship between forecast_temp (forecasted temperature) and observed_temp (observed temperature) in the attached CSV file. The high_or_low column explains whether the forecast is for the high temperature or the low temperature for that day. The generated image should follow data visualisation best practices.

If I was to visualise this data, I might create something like this as a first draft:

Code
library(ggplot2)
library(ggtext)
library(ggh4x)
# Prep annotations
annotation1 <- annotate("text",
  label = stringr::str_wrap(
    "Predicted temperatures are higher than actual temperatures", 10
  ),
  size = 3, x = 75, y = 7, lineheight = 0.8
)
annotation2 <- annotate("text",
  label = stringr::str_wrap(
    "Predicted temperatures are lower than actual temperatures", 10
  ),
  size = 3, x = 14, y = -7.5, lineheight = 0.8
)
arrow1 <- annotate("curve",
  x = 58, xend = 47,
  y = 7.5, yend = 6.5,
  arrow = arrow(length = unit(0.1, "inches"), type = "closed"),
  curvature = 0.1
)
arrow2 <- annotate("curve",
  x = 27, xend = 38,
  y = -7.5, yend = -6.5,
  arrow = arrow(length = unit(0.1, "inches"), type = "closed"),
  curvature = 0.1
)

# Plot
weather |>
  dplyr::mutate(high_or_low = factor(high_or_low,
    levels = c("low", "high"),
    labels = c("Daily low temperature", "Daily high temperature")
  )) |>
  ggplot(
    mapping = aes(
      x = observed_temp,
      y = forecast_temp - observed_temp
    )
  ) +
  geom_hline(yintercept = 0) +
  geom_point(size = 3, alpha = 0.5) +
  at_panel(annotation1, PANEL == 1) +
  at_panel(annotation2, PANEL == 2) +
  at_panel(arrow1, PANEL == 1) +
  at_panel(arrow2, PANEL == 2) +
  scale_x_continuous(limits = c(0, 100)) +
  scale_y_continuous(limits = c(-10, 10)) +
  labs(
    title = "Temperatures are more likely to be underestimated, especially when they are low.",
    subtitle = "Data from the USA National Weather Service shows that 12 hour ahead forecasts for temperatures in Syracuse in 2021 were more likely to be below the later observed temperature than above.",
    x = "Observed temperature (°F)",
    y = stringr::str_wrap("Forecast error (°F)", 10),
    caption = "Source: USA National Weather Service"
  ) +
  facet_wrap(~high_or_low, nrow = 1) +
  coord_cartesian(expand = FALSE, clip = "off") +
  theme_bw() +
  theme(
    plot.margin = margin(5, 10, 5, 5),
    plot.title.position = "plot",
    plot.title = element_text(face = "bold"),
    plot.subtitle = element_textbox_simple(margin = margin(b = 10)),
    panel.spacing = unit(1, "lines"),
    axis.title.x.bottom = element_text(hjust = 0),
    axis.title.y.left = element_text(angle = 0, hjust = 1, size = rel(0.8))
  )

The key features of this visualisation are:

  • Narrative title that states the key takeaway.
  • Does not rely on colour to differentiate categories to improve accessibility.
  • Uses transparency to make it easy to see where there are multiple observations in the same place.
  • Symmetric y-axis, and zero line to make it easier to see if the data is is above or below zero.
  • Uses annotations to make the values shown easier to understand.

Prompt 2: CEO departures data

The second dataset gives information on reasons for CEO departures between 2000 and 2018, and is from a publication by Gentry et al. Again, to limit the file size, I filtered the data to only include data points between 2000 and 2009. Even with the more limited time frame, the data is still reasonably large at 3.5Mb.

The prompt given to all generative AI tools was:

TipPrompt 2

Create a chart of the relationship between fyear (fiscal year) and ceo_dismissal (binary variable for whether the dismissal was for involuntary, non-health related reasons) in the attached CSV file. The generated image should follow data visualisation best practices.

Evaluation

In terms of deciding whether the generated plot is good, I want to look at:

  • Accuracy: Does it accurately represent the data? Are the points or lines actually the values in the data?
  • Best practices: Is the chart type appropriate for the data? Are the axes, colours, shapes etc. well-chosen?
  • Aesthetics: Does it actually look nice? How much work would I have to do, to make this a publication quality chart?

Now on to the interesting part - the results!

Results: weather forecast data

As a reminder, the prompt used was:

TipPrompt 1

Create a chart of the relationship between forecast_temp (forecasted temperature) and observed_temp (observed temperature) in the attached CSV file. The high_or_low column explains whether the forecast is for the high temperature or the low temperature for that day. The generated image should follow data visualisation best practices.

ChatGPT

ChatGPT produced the static chart above, with the following accompanying text:

Here’s the chart showing the relationship between forecasted and observed temperatures, separated by high and low temperature forecasts. The dashed line represents perfect prediction accuracy (1:1).

Things that are reasonably good:

  • The use of crosses instead of opaque dots makes it easier to see where there are clusters of points overlapping.
  • The addition of a comparison line.

Things that are not so good:

  • It guessed at the units as being degrees Celsius rather than Fahrenheit - 100°C isn’t exactly a reasonable temperature forecast!
  • It relies on colour to differentiate high and low forecasts, and the red and green colour choice is not good for accessibility.
  • This is a static chart (with no accompanying code) which makes it harder to edit.

Claude

Claude went way over the top! It built an entire, quite complex app using React. The accompanying text was extensive, and so is omitted here. A screenshot of the home page of the app is shown above. It also created a second page of the app specifically analysing forecast accuracy:

Things that are reasonably good:

  • Correctly guessed the units as Fahrenheit.
  • Design looks clean, and the colour palette is okay - matching blue for cold, and red for hot.
  • It calculates summary statistics, and provides key insights (not quite as good as a narrative title, but still a pretty good summary).

Things that are not so good:

  • There’s no guarantee that the person creating this visualisation actually knows React, so creating an app for them using React can easily result in something that they don’t understand. It feels like a disproportionately complex result, given the prompt.
  • The choice of break points on the x- and y- axes are weird.

Copilot

Copilot produced the following text:

Here’s a chart that visualizes the relationship between forecasted and observed temperatures, with a clear distinction between high and low forecasts:

📊 Chart Highlights Scatter plot format shows how closely forecasted temperatures align with observed ones.

  • Color-coded by high_or_low to differentiate high and low temperature forecasts.
  • Consistent styling with gridlines, labeled axes, and a legend for clarity.
  • Edge-colored markers improve visibility and accessibility. If you’d like to explore trends (e.g. whether high forecasts tend to be more accurate than low ones), I can help with that too. Want to dig deeper?

Based on the first sentence, you might assume that it generated a chart - it did not! No image was actually generated, even when re-prompted. Instead, it generated some Python code to make a chart which was revealed upon clicking see my thinking. The chart created by the Python code (with some tweaking) can be seen below:

See Python code
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
import os

# Set seaborn style for better aesthetics
sns.set(style="whitegrid")

# Load the CSV file
try:
    df = pd.read_csv("temperature_data.csv")
except Exception as e:
    raise RuntimeError(f"Failed to load CSV file: {e}")

# Check required columns
required_columns = {'forecast_temp', 'observed_temp', 'high_or_low'}
if not required_columns.issubset(df.columns):
    raise ValueError(f"Missing required columns in the dataset. Found columns: {df.columns.tolist()}")

# Create output directory if it doesn't exist
output_dir = "/mnt/data"
os.makedirs(output_dir, exist_ok=True)

# Create scatter plot
plt.figure(figsize=(10, 6))
sns.scatterplot(data=df, x='forecast_temp', y='observed_temp', hue='high_or_low', palette='viridis')
plt.title('Forecasted vs Observed Temperature')
plt.xlabel('Forecasted Temperature (°C)')
plt.ylabel('Observed Temperature (°C)')
plt.legend(title='Forecast Type')
plt.tight_layout()

# Save the plot
plot_path = os.path.join(output_dir, "forecast_vs_observed_temp.png")
plt.savefig(plot_path)
plt.close()

print(f"Plot saved to {plot_path}")

Things that are reasonably good:

  • Scatter plot is a reasonable choice for comparing the two columns.
  • The code has error checking - although the built in Python errors actually give an identical error at an earlier point so it’s a lot of redundant code.
  • Tries to highlight the design choices made, which are reasonable even if they aren’t well executed.

Things that are not so good:

  • It didn’t actually generate a chart! The Python code wasn’t quite correct, and it resulted in a blank plot saved in a very obscure location. The labels also end up chopped off. This isn’t a blog post about using generative AI to write code, so I’ll be forgiving about that here.
  • Again, assumes temperatures are in Celsius.
  • Colour choices are not great, and it’s quite hard to see the points when they’re layered on top of each other.

Gemini

Gemini produced a small interactive chart (screenshot above), with the following accompanying text:

The scatter plot above illustrates the relationship between forecasted and observed temperatures. The different colors represent whether the temperature is a high or low forecast for the day. This visualization helps to identify how closely forecasted temperatures align with actual observed temperatures and whether there are any patterns in the accuracy of high versus low temperature forecasts.

Things that are reasonably good:

  • The chart was interactive, and I could customise the view, see the data as a table, and download the chart as an image.

Things that are not so good:

  • The colour choices are very poor. The points for both categories are the same colour, and they are only differentiated by the dot outline being a slightly different shade of blue.
  • Non-symmetric axis ranges make it harder to compare, especially without a diagonal comparison line.
  • The text colours look fine in dark mode, but the chart downloads with a transparent background. Against the light background of this blog post, the text is very hard to read.

Results: CEO departures data

Onto the second experiment! As a reminder, the prompt used was:

TipPrompt 2

Create a chart of the relationship between fyear (fiscal year) and ceo_dismissal (binary variable for whether the dismissal was for involuntary, non-health related reasons) in the attached CSV file. The generated image should follow data visualisation best practices.

ChatGPT

ChatGPT produced the static chart above, with the following accompanying text:

Here’s the chart showing the rate of involuntary CEO dismissals (non-health related) by fiscal year. It follows best practices by using clear labels, appropriate scaling, and a focus on the trend rather than raw counts. Let me know if you want it broken down further (e.g. by industry or company size).

Similar to the first experiment, ChatGPT produced a fairly basic chart that’s fine but doesn’t quite follow best practices - such as rotated x-axis labels. It did plot the rate rather than the absolute count given, which is probably more appropriate.

Claude

Claude was unable to produce a visualisation for the second prompt, giving the error message that the text is 2413% larger than allowed. Essentially, the data is just too big to upload.

Copilot

Copilot was also unable to produce a visualisation for the second prompt because the data was too large to upload.

Gemini

Gemini again produced a small interactive chart (screenshot above), with the following accompanying text:

Here’s a chart displaying the proportion of CEO dismissals over fiscal years, derived from the data you provided. This visualization illustrates how the rate of involuntary, non-health-related CEO dismissals has changed over time. Let me know if you would like to analyze other aspects of the data!

The styling and issues are very similar to those from Gemini in the first experiment. It also plotted the rate instead of the absolute count.

Final thoughts

TipTLDR

A very quick summary of all four AI tools:

  • Gemini: Good overall with comparable performance across the two datasets and prompts. The charts aren’t perfect, but they allow interactive data exploration and don’t over-complicate it.
  • ChatGPT: Reasonable overall with comparable performance across the two datasets and prompts. The charts look more amateurish and poorer quality compared to Gemini.
  • Claude: Good for small datasets but does way too much, unless you prompt it not to. Unable to work with larger data.
  • Copilot: Poor overall. Produced code to create a chart, but not the chart itself. Unable to work with larger data.

If I had to pick one of the four right now, I’d probably go for Gemini.

This is obviously not a full-scale research study, so there’s a limit to what conclusions I can draw here. The prompts were deliberately over-simplified, to see how different tools reacted to a generic data visualisation request. More specific prompts, or additional prompts, would give better results.

The most notable thing (across all tools) was that none of them asked questions about what relationships were important to show, who they audience was, what the main message was, preference for chart type, or indeed anything else. As a human, asking questions is the first part of the data visualisation design process. One of the things I found a little bit surprising, was that all tools tended to perform tasks I didn’t ask them to do, but completely ignored other columns in the data that I didn’t mention. Generative AI could be quite useful for prototyping and quickly drafting some initial ideas - though I’m probably going to stick to pen and paper!

Generative AI tools are naturally going to be better at writing code than they are at mimicking a human following a design-process. And so most tools produced code, even though I didn’t explicitly ask for it. That could, of course, also be influenced by their memory of what I’ve previously used them for. Most of the tools (at least on a free tier) struggled with the size of the data, which was far from big data. The code-generation approach does mean that you could feed a subset of your data in, and re-run the resulting code on your full dataset. I’d generally recommend a code-first approach to data visualisation since it makes it possible to reproduce your chart again later.

All tools, across both prompts, chose the obvious chart type - which isn’t necessarily a bad thing! Without a specific prompt to be creative, defaulting to a chart type that’s commonly used for that type of data seems sensible enough. However, they weren’t very good at applying best practices for data visualisation, especially in terms of colour choices. You still need to think about best practices yourself in order to get decent results. Remember that generative AI tools are not just trained on good data visualisations!

Further reading

Reuse

Citation

BibTeX citation:
@online{rennie2025,
  author = {Rennie, Nicola},
  title = {Generative {AI} for {Data} {Visualisation}},
  date = {2025-10-08},
  url = {https://nrennie.rbind.io/blog/gen-ai-data-viz/},
  langid = {en}
}
For attribution, please cite this work as:
Rennie, Nicola. 2025. “Generative AI for Data Visualisation.” October 8, 2025. https://nrennie.rbind.io/blog/gen-ai-data-viz/.