RKolor na wykresie to nie ozdobnik, tylko kanał informacji – tak samo jak położenie punktu na osi czy długość słupka. Dobrze dobrany kolor pomaga odbiorcy zobaczyć strukturę danych w ułamku sekundy, źle dobrany potrafi ją ukryć albo wręcz zasugerować coś, czego w danych nie ma.
# install.packages(c("viridis",
# "RColorBrewer",
# "wesanderson",
# "ggsci",
# "ggplot2",
# "patchwork",
# "scales",
# "colorspace",
# "scico",
# "MetBrewer",
# "paletteer"))
library(ggplot2)
library(viridis)
library(RColorBrewer)
library(wesanderson)
library(ggsci)
library(patchwork) # łączenie kilku wykresów w jeden
library(scales) # show_col(), alpha()
library(colorspace) # przestrzeń HCL, symulacja daltonizmu
library(scico) # naukowe mapy kolorów F. Crameriego
library(MetBrewer) # palety z dzieł sztuki (Metropolitan Museum of Art)
library(paletteer) # jeden interfejs do ~2500 palet z wielu pakietów
# Przykładowe dane
set.seed(123)
df <- data.frame(
kategoria = rep(LETTERS[1:5], each = 20),
x = rnorm(100),
y = rnorm(100)
)
head(df)
## kategoria x y
## 1 A -0.56047565 -0.71040656
## 2 A -0.23017749 0.25688371
## 3 A 1.55870831 -0.24669188
## 4 A 0.07050839 -0.34754260
## 5 A 0.12928774 -0.95161857
## 6 A 1.71506499 -0.04502772
R rozumie koloryZanim przejdziemy do kodu, warto przypomnieć, skąd biorą się „trzy liczby”, którymi opisujemy kolor na ekranie.
Siatkówka człowieka zawiera trzy rodzaje czopków, najbardziej wrażliwych na fale krótkie (S, ok. 420 nm), średnie (M, ok. 530 nm) i długie (L, ok. 560 nm). Widzenie barw jest więc trójchromatyczne – i dlatego ekrany mieszają tylko trzy składowe: czerwoną, zieloną i niebieską (model RGB).
Dalsze etapy przetwarzania działają według teorii procesów przeciwstawnych (Hering): mózg porównuje sygnały w kanałach jasny–ciemny, czerwony–zielony i niebieski–żółty. Dlatego np. nie istnieje „czerwonawa zieleń”.
Wzrok jest najbardziej czuły na jasność (luminancję), a jasność różnych barw o tej samej „mocy” na ekranie jest postrzegana bardzo różnie – żółty wydaje się dużo jaśniejszy od niebieskiego.
Około 8% mężczyzn i 0,4% kobiet (w populacjach europejskich) ma wrodzone zaburzenia widzenia barw, najczęściej w osi czerwony–zielony (Birch, 2012). W grupie 30 osób (pół na pół) spodziewamy się średnio ok. 1 takiej osoby, a szansa, że jest co najmniej jedna, wynosi ok. 70%.
Źródła: Healy (2018), rozdz. 1 Look at data (fragment o percepcji koloru); Wilke (2019), rozdz. 4 Color scales; Zeileis, Hornik i Murrell (2009) Escaping RGBland; Birch (2012).
R| Sposób | Przykład (biały) | Przykład (czarny) | Zakres wartości | Typowe funkcje w R |
|---|---|---|---|---|
| Nazwa (słownie) | "white" |
"black" |
657 nazw (colors()) |
colors(), col2rgb() |
| HEX (szesnastkowo) | "#FFFFFF" |
"#000000" |
00–FF dla każdej składowej |
tekst "#RRGGBB" lub "#RRGGBBAA" |
| RGB | rgb(1, 1, 1) lub
rgb(255, 255, 255, maxColorValue = 255) |
rgb(0, 0, 0) |
domyślnie 0–1, opcjonalnie 0–255 | rgb(), col2rgb() |
Najważniejsza obserwacja: wszystkie te zapisy prowadzą do
tego samego – R wewnętrznie i tak przechowuje
kolor jako tekst w formacie HEX. Funkcja rgb() nie zwraca
„obiektu koloru”, tylko napis typu "#FF0000".
To najwygodniejszy sposób na start: wpisujemy po prostu
"red", "steelblue" czy
"tomato".
R zna 657 nazw:
length(colors())
## [1] 657
head(colors(), 20)
## [1] "white" "aliceblue" "antiquewhite" "antiquewhite1"
## [5] "antiquewhite2" "antiquewhite3" "antiquewhite4" "aquamarine"
## [9] "aquamarine1" "aquamarine2" "aquamarine3" "aquamarine4"
## [13] "azure" "azure1" "azure2" "azure3"
## [17] "azure4" "beige" "bisque" "bisque1"
Nazwy można przeszukiwać jak każdy wektor tekstowy:
# wszystkie odcienie, które w nazwie mają "blue"
grep("blue", colors(), value = TRUE)[1:15]
## [1] "aliceblue" "blue" "blue1" "blue2"
## [5] "blue3" "blue4" "blueviolet" "cadetblue"
## [9] "cadetblue1" "cadetblue2" "cadetblue3" "cadetblue4"
## [13] "cornflowerblue" "darkblue" "darkslateblue"
Funkcja col2rgb() „tłumaczy” nazwę na trzy składowe RGB
(w skali 0–255):
col2rgb(c("tomato", "steelblue", "gold"))
## [,1] [,2] [,3]
## red 255 70 255
## green 99 130 215
## blue 71 180 0
Szybki podgląd wybranych kolorów daje scales::show_col()
– pokazuje kafelki wraz z kodem HEX:
show_col(c("tomato", "steelblue", "gold", "orchid", "seagreen", "chocolate"))
Do pokazania losowej próbki nazwanych kolorów przyda się mała
funkcja, która dobiera kolor napisu (czarny/biały) do jasności tła. Wagi
0.299, 0.587 i 0.114 to klasyczne
współczynniki luminancji – odzwierciedlają to, że oko jest
najbardziej czułe na zieleń, a najmniej na błękit.
# Kolor tekstu dopasowany do jasności tła
kontrastowy_tekst <- function(kolory) {
rgb_mat <- col2rgb(kolory)
luminancja <- 0.299 * rgb_mat["red", ] + 0.587 * rgb_mat["green", ] + 0.114 * rgb_mat["blue", ]
ifelse(luminancja > 140, "black", "white")
}
set.seed(2026)
probka <- sample(colors(distinct = TRUE), 24)
kafelki <- data.frame(
nazwa = probka,
x = rep(1:6, times = 4),
y = rep(4:1, each = 6)
)
ggplot(kafelki,
aes(x,
y,
fill = nazwa)) +
geom_tile(color = "white",
linewidth = 1) +
geom_text(aes(label = nazwa),
color = kontrastowy_tekst(kafelki$nazwa),
size = 3) +
scale_fill_identity() + # kolor brany wprost z kolumny 'nazwa'
labs(title = "Losowe 24 nazwane kolory w R") +
theme_void()
Ciekawostka: istnieją też całe skale szarości gray0 …
gray100 (lub grey0 … grey100 –
R akceptuje obie pisownie):
show_col(paste0("gray", seq(0, 100, by = 10)))
R ≠ nazwy w HTML/CSSW HTML i CSS również używa się nazw kolorów (red,
blue, green…), ale R korzysta z innej
listy – pochodzącej z systemu okien X11 (plik
rgb.txt z lat 80.). Większość nazw się pokrywa, ale nie
wszystkie oznaczają ten sam kolor:
nazwy <- c("gray", "green", "maroon", "purple")
hex_R <- rgb(t(col2rgb(nazwy)), maxColorValue = 255)
hex_CSS <- c("#808080", "#008000", "#800000", "#800080") # wartości ze specyfikacji CSS
porownanie <- data.frame(
nazwa = rep(nazwy, 2),
zrodlo = rep(c("R (X11)", "HTML / CSS"), each = 4),
hex = c(hex_R, hex_CSS)
)
ggplot(porownanie, aes(nazwa, zrodlo, fill = hex)) +
geom_tile(color = "white", linewidth = 2) +
geom_text(aes(label = hex), color = kontrastowy_tekst(porownanie$hex)) +
scale_fill_identity() +
labs(title = "Ta sama nazwa, inny kolor", x = NULL, y = NULL) +
theme_minimal(base_size = 13)
Wniosek: nazwy są świetne do szybkich szkiców, ale
gdy kolor ma być dokładnie taki sam w R, na stronie WWW i w
prezentacji – używaj kodów HEX.
Źródła: Tian Zheng, Colors in R – klasyczna ściąga ze wszystkimi nazwami kolorów w R; hasło X11 color names w Wikipedii (porównanie z kolorami webowymi).
Kolor zapisujemy jako "#RRGGBB": po znaku #
następują trzy pary znaków – intensywność czerwonego
(RR), zielonego (GG) i niebieskiego (BB).
W systemie szesnastkowym używamy cyfr 0–9 oraz liter
A–F (A = 10, …, F = 15). Para znaków to liczba od
00 (= 0) do FF (= 15·16 + 15 = 255). Daje to
256 × 256 × 256 ≈ 16,7 mln kolorów.
strtoi("FF", base = 16) # szesnastkowo -> dziesiętnie
## [1] 255
strtoi("80", base = 16) # 8 * 16 = 128, czyli "połowa"
## [1] 128
sprintf("%02X", 200) # dziesiętnie -> szesnastkowo
## [1] "C8"
col2rgb("#FF8000") # pomarańczowy: dużo czerwieni, połowa zieleni, zero niebieskiego
## [,1]
## red 255
## green 128
## blue 0
Możemy napisać własną funkcję składającą kod HEX – to dobre ćwiczenie, żeby zrozumieć, że HEX to tylko inny zapis RGB:
hex_z_rgb <- function(r, g, b) sprintf("#%02X%02X%02X", r, g, b)
hex_z_rgb(255, 128, 0)
## [1] "#FF8000"
rgb(255, 128, 0, maxColorValue = 255) # wbudowana funkcja daje to samo
## [1] "#FF8000"
Kilka praktycznych uwag:
Wielkość liter nie ma znaczenia: "#ff8000" i
"#FF8000" to ten sam kolor.
Krótki zapis "#FFF" znany z CSS w R (co najmniej do
wersji 4.3) nie działa – zawsze podawaj 6 znaków:
try(col2rgb("#FFF"))
## [,1]
## red 255
## green 255
## blue 255
"#RRGGBBAA", gdzie 00 to kolor
całkowicie przezroczysty, a FF – w pełni kryjący. Np.
"#FF000080" to czerwień w ok. 50% przezroczysta (więcej w
punkcie 4).col2rgb("#FF000080", alpha = TRUE)
## [,1]
## red 255
## green 0
## blue 0
## alpha 128
Model RGB jest addytywny: mieszamy światło, więc
zero wszystkich składowych daje czerń, a maksimum – biel. W
R kolor RGB tworzy funkcja rgb().
Pułapka: w CSS piszemy
rgb(255, 255, 255), ale w R funkcja
rgb() domyślnie przyjmuje wartości z przedziału
0–1:
rgb(1, 1, 1) # biały
## [1] "#FFFFFF"
rgb(0, 0, 0) # czarny
## [1] "#000000"
rgb(1, 0, 0) # czerwony
## [1] "#FF0000"
try(rgb(255, 0, 0)) # błąd! wartość spoza [0, 1]
## Error in rgb(255, 0, 0) : color intensity 255, not in [0,1]
rgb(255, 0, 0, maxColorValue = 255) # tak podajemy wartości 0–255
## [1] "#FF0000"
Zwróć uwagę, że rgb() zwraca napis HEX
– to dlatego wynik można od razu przekazać do dowolnej funkcji
graficznej.
Addytywne mieszanie najłatwiej zrozumieć, oglądając je. Poniżej siatka kolorów, w której zmieniamy ilość czerwieni (oś X) i zieleni (oś Y), a niebieski ustawiamy na trzech poziomach:
siatka <- expand.grid(
r = seq(0, 1, by = 0.1),
g = seq(0, 1, by = 0.1),
b = c(0, 0.5, 1)
)
siatka$kolor <- rgb(siatka$r, siatka$g, siatka$b)
ggplot(siatka, aes(r,
g,
fill = kolor)) +
geom_tile() +
scale_fill_identity() +
facet_wrap(~ b,
labeller = label_both) +
coord_fixed() +
labs(title = "Mieszanie addytywne w modelu RGB",
x = "czerwony (R)",
y = "zielony (G)") +
theme_minimal()
Czerwony + zielony daje żółty (prawy górny róg pierwszego panelu), a wszystkie trzy na maksimum – biel. To dokładnie to, co dzieje się w pikselach Twojego ekranu.
Funkcja odwrotna, col2rgb(), zamienia dowolny kolor
(nazwę lub HEX) na macierz składowych 0–255.
Przezroczystość przydaje się przede wszystkim przy nakładaniu się punktów (ang. overplotting) – gdy punktów jest dużo, pełne kolory zlewają się w jedną plamę, a przezroczyste pokazują, gdzie danych jest najwięcej.
set.seed(1)
duzo <- data.frame(x = rnorm(5000))
duzo$y <- 0.6 * duzo$x + rnorm(5000, sd = 0.8)
p_pelne <- ggplot(duzo, aes(x, y)) +
geom_point(color = "steelblue") +
labs(title = "alpha = 1") +
theme_minimal()
p_przezr <- ggplot(duzo, aes(x, y)) +
geom_point(color = "steelblue", alpha = 0.1) +
labs(title = "alpha = 0.1") +
theme_minimal()
p_pelne | p_przezr
Przezroczystość można podać na kilka sposobów:
rgb(70, 130, 180,
alpha = 128,
maxColorValue = 255) # w funkcji rgb()
## [1] "#4682B480"
adjustcolor("steelblue", alpha.f = 0.5) # bazowy R
## [1] "#4682B480"
scales::alpha("steelblue", 0.5) # pakiet scales
## [1] "#4682B480"
W ggplot2 najprościej użyć argumentu
alpha = w warstwie
(geom_point(alpha = 0.1)).
RGB opisuje, jak ekran wytwarza kolor, ale nie to, jak człowiek go postrzega. Dlatego istnieją przestrzenie bliższe ludzkiej intuicji:
hsv(h, s, v), wszystkie wartości
0–1. Intuicyjna, ale nadal techniczna.hcl(h, c, l). To przestrzeń
percepcyjnie jednorodna: ta sama zmiana liczby oznacza podobną
zmianę odczuwaną przez oko.hsv(0, 1, 1) # "#FF0000" – czerwony w HSV
## [1] "#FF0000"
hcl(h = 0, c = 80, l = 60) # "#E16A86" – czerwień o kontrolowanej jasności (HCL)
## [1] "#E16A86"
Dlaczego to ważne? Porównajmy klasyczną „tęczę”
(rainbow(), tworzoną w HSV) z paletą HCL – a potem obie
przekształćmy do odcieni szarości, czyli zostawmy tylko
informację o jasności:
swatchplot(list(
"rainbow()" = rainbow(7),
"rainbow() – w szarości" = desaturate(rainbow(7)),
"qualitative_hcl()" = qualitative_hcl(7),
"qualitative_hcl() – w szarości" = desaturate(qualitative_hcl(7))
))
W tęczy żółty i błękit „wybijają się” jasnością, a niebieski jest dużo ciemniejszy – oko odczyta to jako ważniejsze/mniej ważne kategorie, choć dane tego nie mówią. W palecie HCL wszystkie kolory mają podobną jasność, więc żaden nie dominuje.
Funkcja colorspace::specplot() pokazuje, jak w palecie
zmieniają się składowe – zwróć uwagę na krzywą jasności
(Luminance): w tęczy skacze w górę i w dół, w
viridis rośnie równomiernie.
specplot(rainbow(100, end = 0.7),
main = "rainbow()")
specplot(hcl.colors(100, "viridis"),
main = "hcl.colors('viridis')")
Na koniec – ten sam kolor w różnych zapisach:
(kol <- "steelblue")
## [1] "steelblue"
col2rgb(kol) # RGB 0–255
## [,1]
## red 70
## green 130
## blue 180
rgb(t(col2rgb(kol)),
maxColorValue = 255) # HEX
## [1] "#4682B4"
rgb2hsv(col2rgb(kol)) # HSV
## [,1]
## h 0.5757576
## s 0.6111111
## v 0.7058824
as(hex2RGB("#4682B4"),
"polarLUV") # HCL (L, C, H)
## L C H
## [1,] 52.46539 54.48801 242.5573
Źródła: Zeileis, Hornik i Murrell (2009)
Escaping RGBland – artykuł, który spopularyzował przestrzeń HCL
w R; Zeileis i in. (2020) Journal of Statistical Software
colorspace; Ihaka (2003) Colour for presentation
graphics.
ggplot2: najczęstsza pułapka i rodziny
skalPułapka: w ggplot2 jest różnica między
kolorem zmapowanym na zmienną (wewnątrz
aes()) a kolorem ustawionym na stałe (poza
aes()):
p_zle <- ggplot(df,
aes(x,
y,
color = "blue")) +
geom_point(size = 2) +
labs(title = 'aes(color = "blue")',
subtitle = "Źle: 'blue' potraktowane jak kategoria") +
theme_minimal()
p_dobrze <- ggplot(df,
aes(x,
y)) +
geom_point(size = 2,
color = "blue") +
labs(title = 'geom_point(color = "blue")',
subtitle = "Dobrze: kolor ustawiony na stałe") +
theme_minimal()
p_zle | p_dobrze
Zasada: w aes() piszemy nazwy zmiennych, poza
aes() – konkretne wartości.
Druga ważna rzecz: color (colour) dotyczy
punktów, linii i obramowań, a fill – wypełnienia (słupków,
pudełek, kafelków). Każdej z tych estetyk odpowiada rodzina funkcji
scale_color_*() i scale_fill_*():
| Funkcja | Typ zmiennej | Do czego |
|---|---|---|
scale_*_manual(values = ...) |
dyskretna | ręcznie wybrane kolory |
scale_*_identity() |
dowolna | kolor zapisany wprost w danych |
scale_*_gradient(low, high) |
ciągła | gradient między dwoma kolorami |
scale_*_gradient2(low, mid, high, midpoint) |
ciągła | gradient dywergentny (z punktem środkowym) |
scale_*_gradientn(colours = ...) |
ciągła | gradient przez wiele kolorów |
scale_*_brewer() / _distiller() /
_fermenter() |
dyskretna / ciągła / przedziały | palety ColorBrewer |
scale_*_viridis_d() / _c() /
_b() |
dyskretna / ciągła / przedziały | palety viridis |
W scale_*_manual() warto podawać kolory jako
wektor nazwany – wtedy każda kategoria dostaje „swój”
kolor niezależnie od kolejności poziomów (przydatne, gdy ta sama grupa
występuje na wielu wykresach):
moje_kolory <- c(A = "#E69F00",
B = "#56B4E9",
C = "#009E73",
D = "#F0E442",
E = "#0072B2")
p_manual <- ggplot(df,
aes(x, y,
color = kategoria)) +
geom_point(size = 3) +
scale_color_manual(values = moje_kolory) +
labs(title = "scale_color_manual()") +
theme_minimal()
p_grad2 <- ggplot(df,
aes(x, y,
color = y)) +
geom_point(size = 3) +
scale_color_gradient2(low = "#2166AC",
mid = "grey90",
high = "#B2182B",
midpoint = 0) +
labs(title = "scale_color_gradient2()") +
theme_minimal()
p_manual | p_grad2
Źródła: Wickham, Navarro i Pedersen, ggplot2:
Elegant Graphics for Data Analysis (3. wyd.), rozdział o skalach
kolorów; dokumentacja ggplot2
(?scale_colour_manual).
R (bez dodatkowych pakietów)Bazowy R ma całkiem bogaty zestaw palet. Od wersji
R 4.0.0 domyślna paleta (palette())
została zmieniona na bardziej wyważoną, a dodano funkcje
palette.colors() i hcl.colors():
palette() # obecna domyślna paleta
## [1] "black" "#DF536B" "#61D04F" "#2297E6" "#28E2E5" "#CD0BBC" "#F5C710"
## [8] "gray62"
palette.pals() # palety jakościowe dostępne w palette.colors()
## [1] "R3" "R4" "ggplot2" "Okabe-Ito"
## [5] "Accent" "Dark 2" "Paired" "Pastel 1"
## [9] "Pastel 2" "Set 1" "Set 2" "Set 3"
## [13] "Tableau 10" "Classic Tableau" "Polychrome 36" "Alphabet"
Szczególnie warta zapamiętania jest paleta Okabe-Ito – zaprojektowana tak, by była czytelna dla osób z zaburzeniami widzenia barw:
show_col(palette.colors(palette = "Okabe-Ito"))
hcl.colors() daje dostęp do ponad 100 palet HCL – w tym
wersji "Viridis", "Cividis", a nawet
"Zissou 1" (inspirowanej… filmem Wesa Andersona):
head(hcl.pals(type = "sequential"), 12)
## [1] "Grays" "Light Grays" "Blues 2" "Blues 3" "Purples 2"
## [6] "Purples 3" "Reds 2" "Reds 3" "Greens 2" "Greens 3"
## [11] "Oslo" "Purple-Blue"
show_col(hcl.colors(8, "Zissou 1"))
Własny gradient z dowolnych kolorów tworzy
colorRampPalette() – zwraca funkcję, która
generuje tyle kolorów, ile potrzebujemy:
bialo_czerwona <- colorRampPalette(c("white", "#DC143C"))
show_col(bialo_czerwona(7))
Te same palety działają oczywiście w grafice bazowej:
par(mfrow = c(1, 2))
barplot(table(df$kategoria), col = hcl.colors(5, "Zissou 1"),
main = "barplot() + hcl.colors()")
plot(df$x, df$y, pch = 19,
col = palette.colors(6, "Okabe-Ito")[-1][factor(df$kategoria)],
main = "plot() + Okabe-Ito", xlab = "x", ylab = "y")
par(mfrow = c(1, 1))
Źródła: Zeileis i Murrell (2019), wpis na blogu R Core A New palette() for R, https://blog.r-project.org/2019/11/21/a-new-palette-for-r/; Zeileis i Murrell (2023) Coloring in R’s Blind Spot (The R Journal); Okabe i Ito (2008) Color Universal Design.
Zanim sięgniemy po konkretne pakiety – najważniejsza decyzja to typ palety, który musi pasować do typu danych. Ten podział zaproponowała kartografka Cynthia Brewer:
swatchplot(list(
"Sekwencyjna (Blues)" = brewer.pal(7, "Blues"),
"Dywergentna (RdBu)" = brewer.pal(7, "RdBu"),
"Jakościowa (Set2)" = brewer.pal(7, "Set2")
))
{viridis} - palety przyjazne dla osób z zaburzeniami
widzenia kolorówPalety viridis powstały w 2015 roku. Od 2017 jako nowa
domyślna mapa kolorów biblioteki matplotlib (Python), w odpowiedzi na
krytykę „tęczowej” mapy jet. Są percepcyjnie
jednorodne, zachowują porządek po konwersji do skali szarości i
są czytelne przy najczęstszych zaburzeniach widzenia barw. Wariant
cividis zoptymalizowano specjalnie pod kątem osób z
deuteranopią i protanopią.
Źródła: Smith i van der Walt (2015), wystąpienie A Better Default Colormap for Matplotlib (SciPy 2015); winieta pakietu Introduction to the viridis color maps (Garnier i in.); Nuñez, Anderton i Renslow (2018) – paleta cividis.
# a) scale_color_viridis_d() - dla zmiennych dyskretnych
ggplot(df,
aes(x = x,
y = y,
color = kategoria)) +
geom_point(size = 3) +
scale_color_viridis_d() + # domyślnie opcja "viridis"
labs(title = "Viridis - zmienna dyskretna") +
theme_minimal()
# b) scale_fill_viridis_c() - dla zmiennych ciągłych
# kolor pudełka = mediana w grupie (after_stat() sięga po wartości policzone przez geom_boxplot)
ggplot(df,
aes(x = kategoria,
y = y,
fill = after_stat(middle))) +
geom_boxplot() +
scale_fill_viridis_c(option = "plasma") + # opcje: viridis, magma, plasma, inferno, cividis
labs(title = "Viridis - zmienna ciągła (plasma)",
fill = "mediana") +
theme_minimal()
# c) Różne opcje viridis
p1 <- ggplot(df,
aes(x = x,
y = y,
color = kategoria)) +
geom_point(size = 3) +
scale_color_viridis_d(option = "magma") +
labs(title = "Magma") +
theme_minimal()
p2 <- ggplot(df,
aes(x = x,
y = y,
color = kategoria)) +
geom_point(size = 3) +
scale_color_viridis_d(option = "inferno") +
labs(title = "Inferno") +
theme_minimal()
p1 | p2 # wyświetlenie obu wykresów obok siebie (patchwork)
{RColorBrewer} - klasyczne palety kolorystycznePalety ColorBrewer zaprojektowała Cynthia Brewer (Penn State) z myślą
o mapach tematycznych. Każda paleta była testowana pod kątem
czytelności, a strona colorbrewer2.org pozwala filtrować palety
„bezpieczne” dla osób z daltonizmem, do druku i do kserowania. W
ggplot2 palety te są wbudowane:
scale_*_brewer() (dane dyskretne),
scale_*_distiller() (ciągłe) i
scale_*_fermenter() (ciągłe pocięte na przedziały).
Źródła: Harrower i Brewer (2003) ColorBrewer.org: An Online Tool for Selecting Colour Schemes for Maps; strona colorbrewer2.org.
# a) Palety sekwencyjne (dla danych uporządkowanych)
ggplot(df,
aes(x = kategoria,
fill = kategoria)) +
geom_bar() +
scale_fill_brewer(palette = "Blues") +
labs(title = "RColorBrewer - paleta Blues (sekwencyjna)") +
theme_minimal()
# b) Palety dywergentne (dla danych z punktem centralnym)
ggplot(df,
aes(x = x,
y = y,
color = y)) +
geom_point(size = 3) +
scale_color_distiller(palette = "RdYlBu") +
labs(title = "RColorBrewer - paleta RdYlBu (dywergentna)") +
theme_minimal()
# c) Palety jakościowe (dla kategorii)
ggplot(df,
aes(x = x,
y = y,
color = kategoria)) +
geom_point(size = 3) +
scale_color_brewer(palette = "Set2") +
labs(title = "RColorBrewer - paleta Set2 (jakościowa)") +
theme_minimal()
# Podgląd dostępnych palet
display.brewer.all()
Tylko palety przyjazne dla osób z zaburzeniami widzenia barw:
display.brewer.all(colorblindFriendly = TRUE)
{wesanderson} - palety inspirowane filmami Wesa
AndersonaPakiet Karthika Rama powstał z inspiracji blogiem Wes Anderson Palettes na Tumblrze, gdzie autorzy wycinali palety z kadrów filmów. To przykład palet estetycznych, a nie projektowanych pod kątem percepcji – świetnie wyglądają w prezentacji, ale przed publikacją warto sprawdzić je pod kątem czytelności.
Źródła: repozytorium github.com/karthik/wesanderson; blog wesandersonpalettes.tumblr.com.
# a) Podstawowe użycie - sprawdzamy liczbę dostępnych kolorów
ggplot(df,
aes(x = x,
y = y,
color = kategoria)) +
geom_point(size = 3) +
scale_color_manual(values = wes_palette("Darjeeling1",
n = 5,
type = "continuous")) +
labs(title = "Wes Anderson - Darjeeling1") +
theme_minimal()
# b) Różne palety
ggplot(df,
aes(x = kategoria,
fill = kategoria)) +
geom_bar() +
scale_fill_manual(values = wes_palette("GrandBudapest1",
n = 5,
type = "continuous")) +
labs(title = "Wes Anderson - Grand Budapest Hotel") +
theme_minimal()
# c) Paleta z większą liczbą kolorów (interpolacja)
# Niektóre palety mają mniej kolorów - trzeba użyć type = "continuous" do interpolacji
ggplot(df,
aes(x = x,
y = y,
color = kategoria)) +
geom_point(size = 3) +
scale_color_manual(values = wes_palette("Zissou1",
n = 5,
type = "continuous")) +
labs(title = "Wes Anderson - Zissou (interpolacja)") +
theme_minimal()
Dostępne palety:
Nowsze wersje pakietu dodają kolejne palety (np. z The French Dispatch i Asteroid City) – aktualną listę zawsze sprawdzisz poleceniem:
names(wes_palettes)
## [1] "BottleRocket1" "BottleRocket2" "Rushmore1"
## [4] "Rushmore" "Royal1" "Royal2"
## [7] "Zissou1" "Zissou1Continuous" "Darjeeling1"
## [10] "Darjeeling2" "Chevalier1" "FantasticFox1"
## [13] "Moonrise1" "Moonrise2" "Moonrise3"
## [16] "Cavalcanti1" "GrandBudapest1" "GrandBudapest2"
## [19] "IsleofDogs1" "IsleofDogs2" "FrenchDispatch"
## [22] "AsteroidCity1" "AsteroidCity2" "AsteroidCity3"
{ggsci} - palety z czasopism naukowych i science
fictionPakiet Nana Xiao zbiera palety inspirowane stylem graficznym czasopism naukowych (Nature, Science, Lancet, NEJM…) oraz popkultury. Uwaga: to nie są oficjalne wymagania redakcji – raczej sposób, by wykres „wyglądał jak z Nature”.
Źródło: dokumentacja pakietu nanx.me/ggsci.
# a) Nature Publishing Group
ggplot(df,
aes(x = x,
y = y,
color = kategoria)) +
geom_point(size = 3) +
scale_color_npg() +
labs(title = "ggsci - paleta NPG (Nature)") +
theme_minimal()
# b) AAAS (Science)
ggplot(df,
aes(x = kategoria,
fill = kategoria)) +
geom_bar() +
scale_fill_aaas() +
labs(title = "ggsci - paleta AAAS (Science)") +
theme_minimal()
# c) Lancet
ggplot(df,
aes(x = x,
y = y,
color = kategoria)) +
geom_point(size = 3) +
scale_color_lancet() +
labs(title = "ggsci - paleta Lancet") +
theme_minimal()
# d) Star Trek
ggplot(df,
aes(x = x,
y = y,
color = kategoria)) +
geom_point(size = 3) +
scale_color_startrek() +
labs(title = "ggsci - paleta Star Trek") +
theme_minimal()
# e) Rick and Morty
ggplot(df,
aes(x = kategoria,
fill = kategoria)) +
geom_bar() +
scale_fill_rickandmorty() +
labs(title = "ggsci - paleta Rick and Morty") +
theme_minimal()
Inne dostępne palety:
scale_color_jco() (Journal of Clinical Oncology),scale_color_nejm() (New England Journal of
Medicine),scale_color_gsea() (GSEA GenePattern),scale_color_d3(), scale_color_igv(),
scale_color_simpsons(), etc.
p_viridis <- ggplot(df,
aes(x = x,
y = y,
color = kategoria)) +
geom_point(size = 2) +
scale_color_viridis_d() +
labs(title = "viridis") +
theme_minimal() +
theme(legend.position = "none")
p_brewer <- ggplot(df,
aes(x = x,
y = y,
color = kategoria)) +
geom_point(size = 2) +
scale_color_brewer(palette = "Set2") +
labs(title = "RColorBrewer") +
theme_minimal() +
theme(legend.position = "none")
p_wes <- ggplot(df,
aes(x = x,
y = y,
color = kategoria)) +
geom_point(size = 2) +
scale_color_manual(values = wes_palette("Darjeeling1",
n = 5,
type = "continuous")) +
labs(title = "wesanderson") +
theme_minimal() +
theme(legend.position = "none")
p_ggsci <- ggplot(df,
aes(x = x,
y = y,
color = kategoria)) +
geom_point(size = 2) +
scale_color_npg() +
labs(title = "ggsci") +
theme_minimal() +
theme(legend.position = "none")
(p_viridis | p_brewer) / (p_wes | p_ggsci)
Tęczowa mapa kolorów (w Matlabie i dawnym matplotlib znana jako
jet) przez lata była domyślna w wielu programach. Problem:
jasność zmienia się w niej nieregularnie, więc oko
widzi „granice” i „pasma”, których w danych nie ma, a ciągła zmiana bywa
ukryta. Zobaczmy to na wbudowanym zbiorze volcano (mapa
wysokości wulkanu Maungawhau w Auckland):
wulkan <- expand.grid(x = 1:nrow(volcano),
y = 1:ncol(volcano))
wulkan$wysokosc <- as.vector(volcano)
rysuj_wulkan <- function(kolory, tytul) {
ggplot(wulkan, aes(x, y, fill = wysokosc)) +
geom_raster() +
scale_fill_gradientn(colours = kolory) +
coord_fixed() +
labs(title = tytul) +
theme_void() +
theme(legend.position = "none")
}
(rysuj_wulkan(rainbow(100, end = 0.7), "Tęcza") |
rysuj_wulkan(desaturate(rainbow(100, end = 0.7)), "Tęcza – w szarości")) /
(rysuj_wulkan(viridis(100), "viridis") |
rysuj_wulkan(desaturate(viridis(100)), "viridis – w szarości"))
W wersji tęczowej po przejściu na szarość nie wiadomo już, gdzie jest szczyt – jasne pasmo żółci i błękitu leży w środku skali. Viridis w skali szarości nadal czytelnie pokazuje „niżej–wyżej”.
Liu i Heer (2018) sprawdzili to eksperymentalnie: badani oceniali,
który z dwóch kolorów leży bliżej trzeciego na skali. Tęcza wypadła
słabo (wolniej i mniej trafnie), a viridis dawał
konsekwentnie niski odsetek błędów.
Źródła: Borland i Taylor (2007) Rainbow Color Map (Still) Considered Harmful; Liu i Heer (2018) Somewhere Over the Rainbow: An Empirical Assessment of Quantitative Colormaps; Crameri, Shephard i Heron (2020) The misuse of colour in science communication.
Pakiet colorspace ma funkcje symulujące najczęstsze
zaburzenia widzenia barw: deutan() (deuteranopia –
brak/osłabienie czopków M), protan() (protanopia – czopki
L) i tritan() (tritanopia – czopki S). Symulacja opiera się
na fizjologicznym modelu Machado, Oliveiry i Fernandesa (2009).
klasyczna <- brewer.pal(5, "Set1") # czerwony, niebieski, zielony, fioletowy, pomarańczowy
okabe <- palette.colors(6, "Okabe-Ito")[-1]
swatchplot(list(
"Set1" = klasyczna,
"Set1 – deuteranopia" = deutan(klasyczna),
"Set1 – protanopia" = protan(klasyczna),
"Okabe-Ito" = okabe,
"Okabe-Ito – deuteranopia" = deutan(okabe),
"Okabe-Ito – protanopia" = protan(okabe)
))
Klasyczny problem z badań: grupa kontrolna na zielono, eksperymentalna na czerwono.
set.seed(7)
badanie <- data.frame(
grupa = rep(c("kontrolna",
"eksperymentalna"),
each = 60),
x = rnorm(120),
y = c(rnorm(60, 0),
rnorm(60, 0.8))
)
czerw_ziel <- c(kontrolna = "#4DAF4A", eksperymentalna = "#E41A1C")
wykres_grup <- function(kolory, tytul) {
ggplot(badanie,
aes(x,
y,
color = grupa)) +
geom_point(size = 2.5) +
scale_color_manual(values = kolory) +
labs(title = tytul,
color = NULL) +
theme_minimal() +
theme(legend.position = "bottom")
}
wykres_grup(czerw_ziel, "Czerwony vs zielony") |
wykres_grup(setNames(deutan(czerw_ziel),
names(czerw_ziel)),
"…tak samo w deuteranopii")
Rozwiązania: paleta Okabe-Ito albo viridis;
dodatkowe kodowanie kształtem
(shape = grupa) lub podpisy bezpośrednio przy danych
zamiast legendy.
Do interaktywnego sprawdzania własnych wykresów przydadzą się
colorspace::cvd_emulator() (aplikacja Shiny, do której
wgrywa się obrazek) oraz pakiet colorblindr Clausa Wilkego
(dostępny na GitHubie), z funkcją cvd_grid() pokazującą
wykres w czterech wariantach naraz.
Źródła: Okabe i Ito (2008); Machado, Oliveira i Fernandes (2009); Birch (2012); Wilke (2019), rozdz. 19 Common pitfalls of color use.
Klasyczne eksperymenty Anne Treisman pokazały, że cecha taka jak
kolor jest przetwarzana przeduwagowo (ang.
preattentive): pojedynczy czerwony element wśród niebieskich
„wyskakuje” (pop-out) niezależnie od liczby dystraktorów. Jeśli
jednak cel różni się dopiero połączeniem cech (czerwone
kółko wśród czerwonych trójkątów i niebieskich kółek), szukanie staje
się powolne i sekwencyjne. Możemy odtworzyć oba warunki w
ggplot2:
set.seed(42)
n <- 40
dystr <- data.frame(x = runif(n), y = runif(n))
cel <- data.frame(x = 0.62, y = 0.41)
popout <- rbind(
data.frame(dystr, kolor = "steelblue", ksztalt = 16),
data.frame(cel, kolor = "firebrick", ksztalt = 16)
)
koniunkcja <- rbind(
data.frame(dystr[1:20, ], kolor = "firebrick", ksztalt = 17), # czerwone trójkąty
data.frame(dystr[21:40, ], kolor = "steelblue", ksztalt = 16), # niebieskie kółka
data.frame(cel, kolor = "firebrick", ksztalt = 16) # cel: czerwone kółko
)
rysuj_szukanie <- function(dane, tytul) {
ggplot(dane, aes(x, y, color = kolor, shape = ksztalt)) +
geom_point(size = 4) +
scale_color_identity() +
scale_shape_identity() +
coord_fixed() +
labs(title = tytul) +
theme_void() +
theme(panel.border = element_rect(fill = NA, color = "grey70"))
}
rysuj_szukanie(popout, "Pop-out: znajdź czerwone kółko") |
rysuj_szukanie(koniunkcja, "Koniunkcja: znajdź czerwone kółko")
Ta sama zasada działa na wykresach: jeśli chcesz, żeby odbiorca zobaczył jedną grupę, pomaluj ją wyrazistym kolorem, a resztę – na szaro.
df_wyr <- df[order(df$kategoria == "C"), ] # grupa C rysowana na wierzchu
ggplot(df_wyr, aes(x, y, color = kategoria == "C")) +
geom_point(size = 3) +
scale_color_manual(values = c(`FALSE` = "grey80", `TRUE` = "firebrick"),
labels = c("pozostałe kategorie", "kategoria C"), name = NULL) +
labs(title = "Wyróżnienie jednej grupy: kolor jako „wskaźnik laserowy\"") +
theme_minimal()
Gotowe rozwiązanie tego typu oferuje pakiet
gghighlight.
Źródła: Treisman i Gelade (1980) A feature-integration theory of attention; Healey i Enns (2012) Attention and Visual Memory in Visualization and Computer Graphics.
ggplot2Kolor to także materiał eksperymentalny. Słynne zadanie Stroopa (1935): nazwij kolor czcionki, ignorując znaczenie słowa. W warunku niezgodnym (słowo „CZERWONY” napisane na niebiesko) czas reakcji rośnie – automatyczne czytanie interferuje z nazywaniem koloru. Bodziec możemy wygenerować w kilku linijkach:
slowa <- c("CZERWONY", "ZIELONY", "NIEBIESKI", "ŻÓŁTY")
kolory <- c("#D7191C", "#1A9641", "#2B83BA", "#E6AB02")
set.seed(1935)
stroop <- data.frame(
x = rep(1:4, times = 4) * 1.6,
y = rep(4:1, each = 4),
slowo = sample(slowa, 16, replace = TRUE)
)
stroop$zgodny <- kolory[match(stroop$slowo, slowa)]
stroop$niezgodny <- sapply(stroop$slowo, function(s) sample(kolory[slowa != s], 1))
rysuj_stroop <- function(kolumna, tytul) {
ggplot(stroop, aes(x, y, label = slowo, color = .data[[kolumna]])) +
geom_text(size = 4.5, fontface = "bold") +
scale_color_identity() +
xlim(0.6, 7.4) +
labs(title = tytul) +
theme_void()
}
rysuj_stroop("zgodny", "Warunek zgodny") / rysuj_stroop("niezgodny", "Warunek niezgodny")
Źródła / inspiracje: Stroop (1935) Studies of interference in serial verbal reactions; MacLeod (1991) Half a century of research on the Stroop effect: An integrative review.
{scico} – naukowe mapy kolorów (np. macierz
korelacji)Fabio Crameri opracował zestaw Scientific Colour Maps –
palet percepcyjnie jednorodnych i czytelnych dla osób z daltonizmem,
szeroko stosowanych w naukach o Ziemi. Pakiet scico
udostępnia je w R. Dobre zastosowanie statystyczne: macierz
korelacji, gdzie zero jest naturalnym środkiem, więc potrzebna
jest paleta dywergentna.
kor <- cor(mtcars[, c("mpg", "cyl", "disp", "hp", "wt", "qsec")])
kor_df <- as.data.frame(as.table(kor))
names(kor_df) <- c("zm1", "zm2", "r")
wykres_kor <- function(skala, tytul) {
ggplot(kor_df, aes(zm1, zm2, fill = r)) +
geom_tile(color = "white") +
geom_text(aes(label = sprintf("%.2f", r)), size = 3) +
skala +
coord_fixed() +
labs(title = tytul, x = NULL, y = NULL) +
theme_minimal()
}
wykres_kor(scale_fill_viridis_c(limits = c(-1, 1)), "Sekwencyjna (gorzej)") |
wykres_kor(scale_fill_scico(palette = "vik", limits = c(-1, 1)), "Dywergentna scico 'vik' (lepiej)")
Na lewym wykresie trudno od razu stwierdzić, które korelacje są ujemne. Na prawym znak widać od razu (niebieski vs brąz), a siła związku odpowiada nasyceniu.
scico_palette_show() # podgląd wszystkich palet scico
Źródła: Crameri, Shephard i Heron (2020); strona
fabiocrameri.ch/colourmaps; dokumentacja pakietu scico
(Thomas Lin Pedersen).
{MetBrewer} – palety z obrazów w Metropolitan
Museum of ArtBlake Robert Mills stworzył palety na podstawie dzieł z kolekcji nowojorskiego Met – od Hiroshige i Hokusaia po Van Gogha. Pakiet oznacza też, które palety są przyjazne dla osób z daltonizmem.
p_hiro <- ggplot(df, aes(x = kategoria, fill = kategoria)) +
geom_bar() +
scale_fill_met_d("Hiroshige") +
labs(title = "MetBrewer – Hiroshige") +
theme_minimal() +
theme(legend.position = "none")
p_hok <- ggplot(df, aes(x = x, y = y, color = y)) +
geom_point(size = 3) +
scale_color_met_c("Hokusai2") +
labs(title = "MetBrewer – Hokusai2 (ciągła)") +
theme_minimal()
p_hiro | p_hok
# Palety oznaczone jako przyjazne dla daltonistów.
# Nazwa obiektu zależy od wersji pakietu (CRAN: colorblind_palettes,
# GitHub: colorblind_met_palettes), więc wyszukujemy go po wzorcu:
obiekt_cb <- grep("^colorblind.*palettes$", ls("package:MetBrewer"), value = TRUE)[1]
get(obiekt_cb, envir = asNamespace("MetBrewer"))
## [1] "Archambault" "Cassatt1" "Cassatt2" "Demuth" "Derain"
## [6] "Egypt" "Greek" "Hiroshige" "Hokusai2" "Hokusai3"
## [11] "Ingres" "Isfahan1" "Isfahan2" "Java" "Johnson"
## [16] "Kandinsky" "Morgenstern" "OKeeffe1" "OKeeffe2" "Pillement"
## [21] "Tam" "Troy" "VanGogh3" "Veronese"
Źródło: github.com/BlakeRMills/MetBrewer.
{paletteer} – jeden interfejs do tysięcy paletZamiast uczyć się składni kilkudziesięciu pakietów, można skorzystać
z paletteer (Emil Hvitfeldt), który zbiera ponad 2500 palet
dyskretnych i kilkaset ciągłych. Paletę wskazujemy jako
"pakiet::paleta":
nrow(palettes_d_names) # liczba palet dyskretnych
## [1] 2545
nrow(palettes_c_names) # liczba palet ciągłych
## [1] 323
paletteer_d("nord::aurora")
## <colors>
## #BF616AFF #D08770FF #EBCB8BFF #A3BE8CFF #B48EADFF
p_nord <- ggplot(df,
aes(x = x,
y = y,
color = kategoria)) +
geom_point(size = 3) +
scale_color_manual(values = as.character(paletteer_d("nord::aurora"))) +
# skrót: scale_color_paletteer_d("nord::aurora") – wymaga aktualnych wersji ggplot2 i paletteer
labs(title = "paletteer – nord::aurora") +
theme_minimal()
p_tab <- ggplot(df,
aes(x = kategoria,
fill = kategoria)) +
geom_bar() +
scale_fill_manual(values = as.character(paletteer_d("ggthemes::Tableau_10"))) + # paleta jako zwykły wektor
labs(title = "paletteer – ggthemes::Tableau_10") +
theme_minimal() +
theme(legend.position = "none")
p_nord | p_tab
Przeglądanie palet wygodnie robi się w repozytorium
r-color-palettes tego samego autora – to najpełniejszy katalog
palet dostępnych w R.
Źródła: github.com/EmilHvitfeldt/paletteer; github.com/EmilHvitfeldt/r-color-palettes.
Gdy przygotowujemy pracę, plakat lub serię wykresów, warto mieć jedną, spójną paletę i zdefiniować ją raz. Poniższe kolory pochodzą z popularnej palety z serwisu coolors.co:
kogni_kolory <- c(
granat = "#264653",
turkus = "#2A9D8F",
piasek = "#E9C46A",
pomarancz = "#F4A261",
ceglasty = "#E76F51"
)
# skala dyskretna
scale_color_kogni_d <- function(...) {
scale_color_manual(values = unname(kogni_kolory), ...)
}
# skala ciągła (gradient przez trzy kolory)
scale_fill_kogni_c <- function(...) {
scale_fill_gradientn(colours = kogni_kolory[c("granat", "turkus", "piasek")], ...)
}
p_wlasna_d <- ggplot(df,
aes(x = x, y = y,
color = kategoria)) +
geom_point(size = 3) +
scale_color_kogni_d() +
labs(title = "Własna paleta – dyskretna") +
theme_minimal()
p_wlasna_c <- ggplot(wulkan,
aes(x, y,
fill = wysokosc)) +
geom_raster() +
scale_fill_kogni_c() +
coord_fixed() +
labs(title = "Własna paleta – ciągła") +
theme_void()
p_wlasna_d | p_wlasna_c
Przed użyciem sprawdźmy, czy paleta przechodzi test daltonizmu i skali szarości:
swatchplot(list(
"oryginał" = kogni_kolory,
"deuteranopia" = deutan(kogni_kolory),
"szarość" = desaturate(kogni_kolory)
))
Źródło: Simon Jackson (2018) Creating corporate colour palettes for ggplot2 (blog drsimonj.svbtle.com).
colorspace::hcl_wizard() /
choose_palette() – interaktywny kreator palet HCL w R.
Typ palety pasuje do danych: sekwencyjna dla wartości uporządkowanych, dywergentna dla danych z punktem środkowym, jakościowa dla kategorii.
Nie za dużo kategorii: powyżej ok. 7–8 kolorów
oko przestaje je niezawodnie rozróżniać – rozważ grupowanie, panele
(facet_wrap()) lub wyróżnienie tylko kluczowych
grup.
Kolor nie jest jedynym nośnikiem informacji: dodaj kształt, typ linii lub etykiety bezpośrednio przy danych.
Test szarości i daltonizmu:
desaturate() i deutan() z pakietu
colorspace.
Spójność: ta sama grupa ma ten sam kolor na
wszystkich wykresach (nazwany wektor w
scale_*_manual()).
Unikaj „tęczy” dla danych ciągłych.
Uważaj na konotacje: czerwony bywa odczytywany jako „źle/niebezpiecznie”, zielony jako „dobrze” – nie przypisuj ich przypadkowo.
Systemy zapisu kolorów w R:
"steelblue") – wygodne do
szkiców; 657 nazw z listy X11, nie wszystkie zgodne z CSS."#4682B4") – uniwersalny,
precyzyjny; działa tak samo w R, HTML, PowerPoincie; opcjonalnie z alfą
"#4682B480".rgb(70, 130, 180, maxColorValue = 255)) – liczby zamiast
kodu; pamiętaj, że w R domyślna skala to 0–1.hcl(), pakiet
colorspace) – przestrzeń zgodna z ludzką percepcją;
podstawa dobrych palet.{viridis} - najlepsze dla:
{RColorBrewer} - najlepsze dla:
{wesanderson} - najlepsze dla:
{ggsci} - najlepsze dla:
{colorspace} - najlepsze dla:
{scico} - najlepsze dla:
{MetBrewer} i {paletteer} - najlepsze
dla:
Podręczniki i przewodniki (dostępne bezpłatnie online)
Kolor w R i przestrzeń HCL
Palety i ich twórcy
Percepcja koloru i uwaga (perspektywa kognitywistyczna)
Materiał przygotowano z pomocą asystenta AI Claude (Anthropic); treści i źródła zweryfikował autor.