dreamRs / apexcharter

:bar_chart: R Htmlwidget for ApexCharts.js
https://dreamrs.github.io/apexcharter
Other
138 stars 15 forks source link

Understanding offset in color gradient for area charts #45

Closed bklingen closed 2 years ago

bklingen commented 3 years ago

Supposing I'm trying to shade the 10% lower tail in a normal distribution. I though I could do that using type=area-spline and specifying appropriate stops in the color gradient in ax_fill. When shading exactly 50% of the distribution, this seems to work, using offset=50:

library(apexcharter)
x <- seq(-3.5,3.5,length.out=100)
df <- data.frame(x=x, y=dnorm(x))
apex(
  data = df,
  mapping = aes(x=x, y=y),
  type = 'area-spline'
) %>%
ax_xaxis(
  type = 'numeric',
  min = -4,
  max = 4,
  tickAmount=8
) %>%
ax_fill(
  type='gradient',
  gradient=list(
    type='horizontal',
    opacityFrom=1,
    opacityTo=1,
    colorStops = list(
      list(offset=50, color="#FFFF00"), #yellow
      list(offset=0, color="#FF9900") #orange
    )
  )
)

image

But how do I shade the lower 10%? Using list(offset=10, color="#FFFF00"), #yellow does shade a smaller portion, but it doesn't correspond to 10% of the area. What is the relationship between the offset and the area that is colored? Probably something much simpler than I'm imagining. Finally, what if I want to shade three areas, i.e., the lower 10%, the middle 70%, and the upper 20%?

In a different vein, I also tried this approach using a fill aesthetic (I was unsuccessful using fillColor):

df <- data.frame(x=x, y=dnorm(x), z=1)
df$z[df$x>qnorm(0.025)] <- 2
apex(
  data=df,
  mapping=aes(x=x, y=y, fill=z),
  type='area-spline'
) %>%
ax_xaxis(type='numeric') %>%
ax_fill(type="solid", colors=c("#FFFF00","#FF9900"))

image

This is close enough (although there is a white space), but in fact the x-axis got messed up (the graph is now centered at 1)! There is a warning to use complete, but using it as in df <- complete(df, x, z) didn't help either. I must be doing something wrong.

pvictor commented 3 years ago

Hi,

Offset seems to be a percentage between min & max values of the data not the axis, so here [-3.5, 3.5] not [-4, 4], so you can do something like this to find the percentage you want:

scales::rescale(-2.58, from = c(-3.5, 3.5), to = c(0, 100))

If you use fill, you have a duplicate a point to connect the two series, something like this:

x <- seq(-3.5,3.5, length.out = 100)
df <- data.frame(x = x, y = dnorm(x), z=1)
df$z[df$x > qnorm(0.025)] <- 2
df <- rbind(
  df,
  transform(df[df$z == 1, ][which.max(df[df$z == 1, "x"]), ], z = 2)
)

apex(
  data=df,
  mapping=aes(x=x, y=y, fill=z),
  type='area-spline'
) %>%
  ax_xaxis(type='numeric') %>%
  ax_fill(type="solid", colors=c("#FFFF00","#FF9900"))

image

Victor

bklingen commented 3 years ago

Wonderful, thank you very much! The solution with mapping the offset to the data series works fine. Just for completeness, here is how to shade 100*alpha percent of the area:

library(apexcharter)

alpha <- 0.025
x <- seq(-3.5,3.5,0.01)
df <- data.frame(x=x, y=dnorm(x))

myoffset <- (qnorm(alpha) - min(x))/diff(range(x)) * 100
if(myoffset<0) myoffset <- 0
if(myoffset>100) myoffset <- 100

apex(
  data = df,
  mapping = aes(x=x, y=y),
  type = 'area-spline'
) %>%
ax_xaxis(
  type = 'numeric',
  min = -4,
  max = 4,
  tickAmount = 8
) %>%
ax_fill(
  type='gradient',
  gradient=list(
    type='horizontal',
    opacityFrom=1,
    opacityTo=1,
    colorStops = list(
      list(offset=myoffset, color="#FFFF00"), #yellow
      list(offset=0, color="#FF9900") #orange
    )
  )
) 

image

Regarding using the fill/color aesthetic: The duplication of the datapoint works great. When using splines, it is better to re-order the data after appending the duplicate data point at the end. However, one issue remains. When defining type='numerical' in ax_xaxis, the x-axis tick marks seemed to get messed up. Here is an example just adding a second series:

## using color aesthetics
alpha <- 0.025
x <- seq(-3.5,3.5,0.01)
df <- data.frame(x=x, y=dnorm(x), z=1)
df$z[df$x>qnorm(alpha)] <- 2

##duplicate data point where two curves meet and sort the dataframe:
df <- rbind(
  df,
  transform(df[df$z == 1, ][which.max(df[df$z == 1, "x"]), ], z = 2)
)
df <- df[order(df$z, df$x),]

## add second series:
apex(
  data = df[df$z==1,],
  mapping = aes(x=x, y=y),
  type = 'line'
) %>%
add_line(
  data = df[df$z==2,],
  mapping = aes(x=x, y=y),
  type = 'line'
) %>%
ax_xaxis(
  type = 'numeric',
  labels = list(
    formatter = htmlwidgets::JS("function(value) {return value.toFixed(2);}")
  )
)

image

Without the entire ax_xaxis statement, the axis looks a bit messy (but seems to be correct): image

I tried a toy example, and there using type='numerical' in ax_xaxis seems to work just fine:

## toy example
df <- data.frame(x=c(1,2,3,3,4,5,6), y=c(1,2,3,3,3,1,2), z=c(1,1,1,2,2,2,2))
## add second series:
apex(
  data = df[df$z==1,],
  mapping = aes(x=x, y=y),
  type = 'line'
) %>%
add_line(
  data = df[df$z==2,],
  mapping = aes(x=x, y=y),
  type = 'line'
) %>%
ax_xaxis(type="numeric")

image

In this toy example, the ax_xaxis(type="numeric") is essential. Without it, one gets:

image