latest newsbuy art
Drive, driven. Gave, given.YelloGive me a number of games.more quotes
very clickable
visualization + design
If you are interested in color, explore my other color tools, Brewer palettes resources, color blindness palettes and math and an exhausting list of 10,000 color names for all those times you couldn't distinguish between tan hide, sea buckthorn, orange peel, west side, sunshade, california and pizzaz.

Designing for Color blindness

Color choices and transformations for deuteranopia and other afflictions

Here, I help you understand color blindness and describe a process by which you can make good color choices when designing for accessibility.

The opposite of color blindness is seeing all the colors and I can help you find 1,000 (or more) maximally distinct colors.

You can also delve into the mathematics behind the color blindness simulations and learn about copunctal points (the invisible color!) and lines of confusion.

Color blindness R code

R code for converting an RGB color for color blindness. For details see the math tab and the resources section for background reading.

---
title: 'RGB color correction for color blindess: protanopia, deuteranopia, tritanopia'
author: 'Martin Krzywinski'
web: http://mkweb.bcgsc.ca/colorblind
---

```{r}
gamma = 2.4
###############################################
# Linear RGB to XYZ
# https://en.wikipedia.org/wiki/SRGB
XYZ = matrix(c(0.4124564, 0.3575761, 0.1804375,
               0.2126729, 0.7151522, 0.0721750,
               0.0193339, 0.1191920, 0.9503041),
               byrow=TRUE,nrow=3)

SA = matrix(c(0.2126,0.7152,0.0722,
              0.2126,0.7152,0.0722,
              0.2126,0.7152,0.0722),byrow=TRUE,nrow=3)

###############################################
# XYZ to LMS, normalized to D65
# https://en.wikipedia.org/wiki/LMS_color_space
# Hunt, Normalized to D65
LMSD65 = matrix(c( 0.4002, 0.7076, -0.0808,
                   -0.2263, 1.1653,  0.0457,
                    0     , 0     ,  0.9182),
                   byrow=TRUE,nrow=3)
# Hunt, equal-energy illuminants
LMSEQ = matrix(c( 0.38971, 0.68898,-0.07868,
                 -0.22981, 1.18340, 0.04641,
                  0      , 0      , 1      ),
                byrow=TRUE,nrow=3)
# CIECAM97
SMSCAM97 = matrix(c(  0.8951,  0.2664, -0.1614,
                     -0.7502,  1.7135,  0.0367,
                      0.0389, -0.0685,  1.0296),
                  byrow=TRUE,nrow=3)
# CIECAM02
LMSCAM02 = matrix(c( 0.7328, 0.4296, -0.1624,
                    -0.7036, 1.6975,  0.0061,
                     0.0030, 0.0136,  0.9834),
                  byrow=TRUE,nrow=3)

###############################################
# Determine the color blindness correction in LMS space
# under the condition that the correction does not
# alter the appearance of white as well as 
# blue (for protanopia/deuteranopia) or red (for tritanopia).
# For achromatopsia, greyscale conversion is applied
# to the linear RGB values.
getcorrection = function(LMS,type="p",g=gamma) {
  red = matrix(c(255,0,0),nrow=3)
  blue = matrix(c(0,0,255),nrow=3)
  white = matrix(c(255,255,255),nrow=3)
  LMSr = LMS %*% XYZ %*% apply(red,1:2,linearize,g)
  LMSb = LMS %*% XYZ %*% apply(blue,1:2,linearize,g)
  LMSw = LMS %*% XYZ %*% apply(white,1:2,linearize,g)
  if(type == "p") {
    x = matrix(c(LMSb[2,1],LMSb[3,1],
                  LMSw[2,1],LMSw[3,1]),byrow=T,nrow=2)
    y = matrix(c(LMSb[1,1],LMSw[1,1]),nrow=2)
    ab = solve(x) %*% y
    C = matrix(c(0,ab[1,1],ab[2,1],0,1,0,0,0,1),byrow=T,nrow=3)
  } else if (type == "d") {
    x = matrix(c(LMSb[1,1],LMSb[3,1],
                  LMSw[1,1],LMSw[3,1]),byrow=T,nrow=2)
    y = matrix(c(LMSb[2,1],LMSw[2,1]),nrow=2)
    ab = solve(x) %*% y
    C = matrix(c(1,0,0,ab[1,1],0,ab[2,1],0,0,1),byrow=T,nrow=3)
  } else if (type == "t") {
    x = matrix(c(LMSr[1,1],LMSr[2,1],
                  LMSw[1,1],LMSw[2,1]),byrow=T,nrow=2)
    y = matrix(c(LMSr[3,1],LMSw[3,1]),nrow=2)
    ab = solve(x) %*% y
    C = matrix(c(1,0,0,0,1,0,ab[1,1],ab[2,1],0),byrow=T,nrow=3)
  } else if (type == "a" | type == "g") {
    C = matrix(c(0.2126,0.7152,0.0722,
                 0.2126,0.7152,0.0722,
                 0.2126,0.7152,0.0722),byrow=TRUE,nrow=3)
  }
  return(C)
}

# rgb is a column vector
convertcolor = function(rgb,LMS=LMSD65,type="d",g=gamma) {
  C = getcorrection(LMS,type)
  if(type == "a" | type == "g") {
    T = SA
  } else {
    M = LMS %*% XYZ
    Minv = solve(M)
    T = Minv %*% C %*% M
  }
  print(T)
  rgb_converted = T %*% apply(rgb,1:2,linearize,g)
  return(apply(rgb_converted,1:2,delinearize,g))
}

# This function implements the method by Vienot, Brettel, Mollon 1999.
# The approach is the same, just the values are different.
# http://vision.psychol.cam.ac.uk/jdmollon/papers/colourmaps.pdf
convertcolor2 = function(rgb,type="d",g=2.2) {
  xyz = matrix(c(40.9568, 35.5041, 17.9167,
                 21.3389, 70.6743, 7.98680,
                 1.86297, 11.4620, 91.2367),byrow=T,nrow=3)
  lms = matrix(c(0.15514, 0.54312, -0.03286,
                 -0.15514, 0.45684,0.03286,
                 0,0,0.01608),byrow=T,nrow=3)
  rgb = (rgb/255)**g
  if(type=="p") {
    S = matrix(c(0,2.02344,-2.52581,0,1,0,0,0,1),byrow=T,nrow=3)
    rgb = 0.992052*rgb+0.003974
  } else if(type=="d") {
    S = matrix(c(1,0,0,0.494207,0,1.24827,0,0,1),byrow=T,nrow=3)
    rgb = 0.957237*rgb+0.0213814
  } else {
    stop("Only type p,d defined for this function.")
  }
  M = lms %*% xyz
  T = solve(M) %*% S %*% M
  print(T)
  rgb = T %*% rgb
  rgb = 255*rgb**(1/g)
  return(rgb)
}

###############################################
# RGB to Lab
rgb2lab = function(rgb,g=gamma) {
  rgb = apply(rgb,1:2,linearize,g)
  xyz = XYZ %*% rgb
  delta = 6/29
  xyz = xyz / (c(95.0489,100,108.8840)/100)
  f = function(t) {
    if(t > delta**3) {
      return(t**(1/3))
    } else {
      return (t/(3*delta**2) + 4/29)
    }
  }
  L = 116*f(xyz[2]) - 16
  a = 500*(f(xyz[1]) - f(xyz[2]))
  b = 200*(f(xyz[2]) - f(xyz[3]))
  return(matrix(c(L,a,b),nrow=3))
}

# CIE76 (https://en.wikipedia.org/wiki/Color_difference)
deltaE = function(rgb1,rgb2) {
  lab1 = rgb2lab(rgb1)
  lab2 = rgb2lab(rgb2)
  return(sqrt(sum((lab1-lab2)**2)))
}

clip = function(v) {
  return(max(min(v,1),0))
}

###############################################
# RGB to/from linear RGB
#https://en.wikipedia.org/wiki/SRGB
linearize = function(v,g=gamma) {
  if(v <= 0.04045) {
    return(v/255/12.92)
  } else {
    return(((v/255 + 0.055)/1.055)**g)
  }
}

delinearize = function(v,g=gamma) {
  if(v <= 0.003130805) {
    return(255*12.92*clip(v))
  } else {
    return(255*clip(1.055*(clip(v)**(1/g))-0.055))
  }
}
pretty = function(x) {
  noquote(formatC(x,digits=10,format="f",width=9))
}

# a dark red
rgb1 = matrix(c(0,209,253),nrow=3)
# dark green
rgb2 = matrix(c(60,135,0),nrow=3)
# simulate deuteranopia
convertcolor(rgb1,type="d")
convertcolor(rgb2,type="d")
# get color distance before and after simulation
deltaE(rgb1,rgb2)
deltaE(convertcolor(rgb1,type="d"),convertcolor(rgb2,type="d"))
# transformation matrices for each color blindness type
M = LMSD65 %*% XYZ
pretty(solve(M) %*% getcorrection(LMSD65,"p") %*% M)
pretty(solve(M) %*% getcorrection(LMSD65,"d") %*% M)
pretty(solve(M) %*% getcorrection(LMSD65,"t") %*% M)
pretty(SA)
# method by Vienot, Brettel, Mollon, 1999
convertcolor2(rgb1,type="d",g=2.2)
convertcolor2(rgb2,type="d",g=2.2)
```

# a dark red
rgb1 = matrix(c(225,0,30),nrow=3)

# dark green
rgb2 = matrix(c(60,135,0),nrow=3)

# simulate deuteranopia
convertcolor(rgb1,type="d")
         [,1]
[1,] 136.7002
[2,] 136.7002
[3,]   0.0000
convertcolor(rgb2,type="d")
          [,1]
[1,] 116.76071
[2,] 116.76071
[3,]  16.73263
# get color distance before and after simulation
deltaE(rgb1,rgb2)
[1] 116.9496
deltaE(convertcolor(rgb1,type="d"),convertcolor(rgb2,type="d"))
[1] 12.72204
# transformation matrices for each color blindness type
M = LMSD65 %*% XYZ

pretty(solve(M) %*% getcorrection(LMSD65,"p") %*% M)
     [,1]          [,2]         [,3]         
[1,] 0.1705569911  0.8294430089 0.0000000000 
[2,] 0.1705569911  0.8294430089 -0.0000000000
[3,] -0.0045171442 0.0045171442 1.0000000000 

pretty(solve(M) %*% getcorrection(LMSD65,"d") %*% M)
     [,1]          [,2]         [,3]         
[1,] 0.3306600735  0.6693399265 -0.0000000000
[2,] 0.3306600735  0.6693399265 0.0000000000 
[3,] -0.0278553826 0.0278553826 1.0000000000 

pretty(solve(M) %*% getcorrection(LMSD65,"t") %*% M)
     [,1]          [,2]         [,3]         
[1,] 1.0000000000  0.1273988634 -0.1273988634
[2,] -0.0000000000 0.8739092990 0.1260907010 
[3,] 0.0000000000  0.8739092990 0.1260907010 

pretty(SA)
     [,1]         [,2]         [,3]        
[1,] 0.2126000000 0.7152000000 0.0722000000
[2,] 0.2126000000 0.7152000000 0.0722000000
[3,] 0.2126000000 0.7152000000 0.0722000000
# method by Vienot, Brettel, Mollon, 1999
convertcolor2(rgb1,type="d",g=2.2)
            [,1]       [,2]          [,3]
[1,]  0.29275003 0.70724967 -2.978356e-08
[2,]  0.29275015 0.70724997  1.232823e-08
[3,] -0.02233659 0.02233658  1.000000e+00
          [,1]
[1,] 131.81223
[2,] 131.81226
[3,]  36.37274
convertcolor2(rgb2,type="d",g=2.2)
            [,1]       [,2]          [,3]
[1,]  0.29275003 0.70724967 -2.978356e-08
[2,]  0.29275015 0.70724997  1.232823e-08
[3,] -0.02233659 0.02233658  1.000000e+00
          [,1]
[1,] 122.71798
[2,] 122.71801
[3,]  48.34316
news + thoughts

Annals of Oncology cover

Wed 14-09-2022

My cover design on the 1 September 2022 Annals of Oncology issue shows 570 individual cases of difficult-to-treat cancers. Each case shows the number and type of actionable genomic alterations that were detected and the length of therapies that resulted from the analysis.

Martin Krzywinski @MKrzywinski mkweb.bcgsc.ca
An organic arrangement of 570 individual cases of difficult-to-treat cancers showing genomic changes and therapies. Apperas on Annals of Oncology cover (volume 33, issue 9, 1 September 2022).

Pleasance E et al. Whole-genome and transcriptome analysis enhances precision cancer treatment options (2022) Annals of Oncology 33:939–949.

Martin Krzywinski @MKrzywinski mkweb.bcgsc.ca
My Annals of Oncology 570 cancer cohort cover (volume 33, issue 9, 1 September 2022). (more)

Browse my gallery of cover designs.

Martin Krzywinski @MKrzywinski mkweb.bcgsc.ca
A catalogue of my journal and magazine cover designs. (more)

Survival analysis—time-to-event data and censoring

Fri 05-08-2022

Love's the only engine of survival. —L. Cohen

We begin a series on survival analysis in the context of its two key complications: skew (which calls for the use of probability distributions, such as the Weibull, that can accomodate skew) and censoring (required because we almost always fail to observe the event in question for all subjects).

We discuss right, left and interval censoring and how mishandling censoring can lead to bias and loss of sensitivity in tests that probe for differences in survival times.

Martin Krzywinski @MKrzywinski mkweb.bcgsc.ca
Nature Methods Points of Significance column: Survival analysis—time-to-event data and censoring. (read)

Dey, T., Lipsitz, S.R., Cooper, Z., Trinh, Q., Krzywinski, M & Altman, N. (2022) Points of significance: Survival analysis—time-to-event data and censoring. Nature Methods 19:906–908.

3,117,275,501 Bases, 0 Gaps

Sun 21-08-2022

See How Scientists Put Together the Complete Human Genome.

My graphic in Scientific American's Graphic Science section in the August 2022 issue shows the full history of the human genome assembly — from its humble shotgun beginnings to the gapless telomere-to-telomere assembly.

Read about the process and methods behind the creation of the graphic.

Martin Krzywinski @MKrzywinski mkweb.bcgsc.ca
3,117,275,501 Bases, 0 Gaps. Text by Clara Moskowitz (Senior Editor), art direction by Jen Christiansen (Senior Graphics Editor), source: UCSC Genome Browser.

See all my Scientific American Graphic Science visualizations.

Anatomy of SARS-Cov-2

Tue 31-05-2022

My poster showing the genome structure and position of mutations on all SARS-CoV-2 variants appears in the March/April 2022 issue of American Scientist.

Martin Krzywinski @MKrzywinski mkweb.bcgsc.ca
Deadly Genomes: Genome Structure and Size of Harmful Bacteria and Viruses (zoom)

An accompanying piece breaks down the anatomy of each genome — by gene and ORF, oriented to emphasize relative differences that are caused by mutations.

Martin Krzywinski @MKrzywinski mkweb.bcgsc.ca
Deadly Genomes: Genome Structure and Size of Harmful Bacteria and Viruses (zoom)

Cancer Cell cover

Sat 23-04-2022

My cover design on the 11 April 2022 Cancer Cell issue depicts depicts cellular heterogeneity as a kaleidoscope generated from immunofluorescence staining of the glial and neuronal markers MBP and NeuN (respectively) in a GBM patient-derived explant.

LeBlanc VG et al. Single-cell landscapes of primary glioblastomas and matched explants and cell lines show variable retention of inter- and intratumor heterogeneity (2022) Cancer Cell 40:379–392.E9.

Martin Krzywinski @MKrzywinski mkweb.bcgsc.ca
My Cancer Cell kaleidoscope cover (volume 40, issue 4, 11 April 2022). (more)

Browse my gallery of cover designs.

Martin Krzywinski @MKrzywinski mkweb.bcgsc.ca
A catalogue of my journal and magazine cover designs. (more)

© 1999–2022 Martin Krzywinski | contact | Canada's Michael Smith Genome Sciences CentreBC Cancer Research CenterBC CancerPHSA