Line graph has too many lines, is there a better solution? I'm trying to graph the number of actions by users (in this case, "likes") over time.
So I have "Number of actions" as my y-axis, my x-axis is time (weeks), and each line represents one user.
My problem is that I want to look at this data for a set of about 100 users. A line graph quickly becomes a jumbled mess with 100 lines. Is there a better type of graph I can use to display this information? Or should I look at being able to toggle individual lines on/off?
I'd like to see all the data at once, but being able to discern the number of actions with high precision isn't terribly important.
Why I'm doing this
For a subset of my users (top users), I want to find out which ones may not have liked a new version of the application that was rolled out on a certain date. I'm looking for significant drops in the number of actions by individual users.
 A: Sure. First, sort by average number of actions. Then make (say) 4 graphs, each with 25 lines, one for each quartile. That means you can shrink the y-axes (but make the y axis label clear). And with 25 lines, you can vary them by line type and color and perhaps plotting symbol and get some clarity
Then stack the graphs vertically with a single time axis.
This would be pretty easy in R or SAS (at least if you have v. 9 of SAS). 
A: One of the easiest things to is a boxplot. You can immediately see how your sample medians move and what days have the most outliers.
day <- rep(1:10, 100)
likes <- rpois(1000, 10)
d <- data.frame(day, likes)
library(ggplot2)
qplot(x=day, y=likes, data=d, geom="boxplot", group=day)


For individual analysis I suggest taking a small random sample from your data and analysing separate time series.
A: I would like to suggest a (standard) preliminary analysis to remove the principal effects of (a) variation among users, (b) the typical response among all users to the change, and (c) typical variation from one time period to the next.
A simple (but by no means the best) way to do this is to perform a few iterations of "median polish" on the data to sweep out user medians and time period medians, then smooth the residuals over time.  Identify the smooths that change a lot: they are the users you want to emphasize in the graphic.
Because these are count data, it's a good idea to re-express them using a square root.
As an example of what can result, here is a simulated 60-week dataset of 240 users who typically undertake 10 to 20 actions per week.  A change in all users occurred after week 40.  Three of these were "told" to respond negatively to the change.  The left plot shows the raw data: counts of action by user (with users distinguished by color) over time.  As asserted in the question, it's a mess. The right plot shows the results of this EDA--in the same colors as before--with the unusually responsive users automatically identified and highlighted.  The identification--although it is somewhat ad hoc--is complete and correct (in this example).

Here is the R code that produced these data and carried out the analysis.  It could be improved in several ways, including


*

*Using a full median polish to find the residuals, rather than just one iteration.

*Smoothing the residuals separately before and after the change point.

*Perhaps using a more sophisticated outlier detection algorithm.  The current one merely flags all users whose range of residuals is more than twice the median range.  Albeit simple, it is robust and appears to work well.  (A user-settable value, threshold, can be adjusted to make this identification more or less stringent.)
Testing nevertheless suggests this solution works well for a wide range of user counts, 12 - 240 or more.
n.users <- 240        # Number of users (here limited to 657, the number of colors)
n.periods <- 60       # Number of time periods
i.break <- 40         # Period after which change occurs
n.outliers <- 3       # Number of greatly changed users
window <- 1/5         # Temporal smoothing window, fraction of total period
response.all <- 1.1   # Overall response to the change
threshold <- 2        # Outlier detection threshold

# Create a simulated dataset
set.seed(17)
base <- exp(rnorm(n.users, log(10), 1/2))
response <- c(rbeta(n.users - n.outliers, 9, 1),
              rbeta(n.outliers, 5, 45)) * response.all
actual <- cbind(base %o% rep(1, i.break), 
                base * response %o% rep(response.all, n.periods-i.break))
observed <- matrix(rpois(n.users * n.periods, actual), nrow=n.users)

# ---------------------------- The analysis begins here ----------------------------#
# Plot the raw data as lines
set.seed(17)
colors = sample(colors(), n.users) # (Use a different method when n.users > 657)
par(mfrow=c(1,2))
plot(c(1,n.periods), c(min(observed), max(observed)), type="n",
     xlab="Time period", ylab="Number of actions", main="Raw data")
i <- 0
apply(observed, 1, function(a) {i <<- i+1; lines(a, col=colors[i])})
abline(v = i.break, col="Gray")  # Mark the last period before a change

# Analyze the data by time period and user by sweeping out medians and smoothing
x <- sqrt(observed + 1/6)                        # Re-express the counts
mean.per.period <- apply(x, 2, median)
residuals <- sweep(x, 2, mean.per.period)
mean.per.user <- apply(residuals, 1, median)
residuals <- sweep(residuals, 1, mean.per.user)

smooth <- apply(residuals, 1, lowess, f=window)  # Smooth the residuals
smooth.y <- sapply(smooth, function(s) s$y)      # Extract the smoothed values
ends <- ceiling(window * n.periods / 4)          # Prepare to drop near-end values
range <- apply(smooth.y[-(1:ends), ], 2, function(x) max(x) - min(x))

# Mark the apparent outlying users
thick <- rep(1, n.users)
thick[outliers <- which(range >= threshold * median(range))] <- 3
type <- ifelse(thick==1, 3, 1)

cat(outliers) # Print the outlier identifiers (ideally, the last `n.outliers`)

# Plot the residuals
plot(c(1,n.periods), c(min(smooth.y), max(smooth.y)), type="n",
     xlab="Time period", ylab="Smoothed residual root", main="Residuals")
i <- 0
tmp <- lapply(smooth, 
       function(a) {i <<- i+1; lines(a, lwd=thick[i], lty=type[i], col=colors[i])})
abline(v = i.break, col="Gray")

A: Generally I find more than two or three lines on a single facet of a plot starts to be hard to read (although I still do it all the time).  So this is an interesting example of what to do when you have something that conceptually could be a 100 facet plot.  One possible way is to draw all 100 facets but instead of trying to get them all on the page at once, looking at them one at a time in an animation.  
We've actually used this technique at my work - we originally made the animation showing 60 different line plots as background for an event (the launch of a new data series), then found that doing so we actually picked up some features of the data that hadn't been visible in faceted plots with 15 or 30 facets per page.
So here is an alternative way of presenting the raw data, before you start removing the user and typical time effects as recommended by @whuber.  This is presented just as an additional alternative to his presentation of the raw data - I fully recommend that you then proceed with analysis along lines such as those he suggests.
One way of getting around this problem is to produce the 100 (or 240 in @whuber's example) time series plots separately and knit them together into an animation.  The code below will produce 240 separate images of this sort and then you can use free movie making software to turn that into a movie.  Unfortunately the only way I could do this and keep acceptable quality was a 9MB file, but if you don't need to send it across the internet that may not be a problem and anyway I'm sure there's ways around that with a bit more animation savvy.  The animation package in R could be useful here (lets you do it all in a call from R) but I've kept it simple for this illustration.
I have made the animation such that it draws each line in heavy black then leaves a pale semi-transparent green shadow behind so the eye gets a gradual picture of the accumulating data.  There are both risks and opportunities in this - the order that the lines are added will leave a different impression, so you should consider making it meaningful in some way.
Here are some of the stills from the movie, which uses the same data that @whuber generated:





# ---------------------------- Data generation - by @whuber ----------------------------#

n.users <- 240        # Number of users (here limited to 657, the number of colors)
n.periods <- 60       # Number of time periods
i.break <- 40         # Period after which change occurs
n.outliers <- 3       # Number of greatly changed users
window <- 1/5         # Temporal smoothing window, fraction of total period
response.all <- 1.1   # Overall response to the change
threshold <- 2        # Outlier detection threshold

# Create a simulated dataset
set.seed(17)
base <- exp(rnorm(n.users, log(10), 1/2))
response <- c(rbeta(n.users - n.outliers, 9, 1),
              rbeta(n.outliers, 5, 45)) * response.all
actual <- cbind(base %o% rep(1, i.break), 
                base * response %o% rep(response.all, n.periods-i.break))
observed <- matrix(rpois(n.users * n.periods, actual), nrow=n.users)

# ---------------------------- The analysis begins here ----------------------------#

# Alternative presentation of original data 
# 
setwd("eg animation")

for (i in 1:n.users){
    png(paste("line plot", i, ".png"),600,600,res=60)
    plot(c(1,n.periods), c(min(observed), max(observed)), 
        xlab="Time period", ylab="Number of actions", 
        main="Raw data", bty="l", type="n")
    if(i>1){apply(observed[1:i,], 1, function(a) {lines(a, col=rgb(0,100,0,50,maxColorValue=255))})}
    lines(observed[i,], col="black", lwd=2)
    abline(v = i.break, col="Gray")  # Mark the last period before a change
    text(1,60,i)
    dev.off()
}

##
# Then proceed to further analysis eg as set out by @whuber

A: I find that when your running out if options regarding type if graph and graph settings introduction of time through animation is the best way to display because it gives you an extra dimension to work with and allows you to display more information in an easy to follow way. Your primary focus must be on the end user experience.
A: If you're most interested in the change for individual users, maybe this is a good situation for a collection of Sparklines (like this example from The Pudding):

These are pretty detailed, but you could show a lot more charts at once by removing axis labels and units.
Many data tools have them built in (Microsoft Excel has sparklines), but I'm guessing you'd want to pull in a package to build them in R.
